import type { PacketTrailerMetadata, PacketTrailerPublishOptions } from './types'; import { hasPacketTrailerPublishOptions } from './utils'; export const PACKET_TRAILER_MAGIC = Uint8Array.from([ 'L'.charCodeAt(0), 'K'.charCodeAt(0), 'T'.charCodeAt(0), 'S'.charCodeAt(0), ]); export const PACKET_TRAILER_TIMESTAMP_TAG = 0x01; export const PACKET_TRAILER_FRAME_ID_TAG = 0x02; export const PACKET_TRAILER_ENVELOPE_SIZE = 5; const TIMESTAMP_TLV_SIZE = 10; const FRAME_ID_TLV_SIZE = 6; export interface ExtractPacketTrailerResult { data: Uint8Array; metadata?: PacketTrailerMetadata; } export function appendPacketTrailer( data: Uint8Array, userTimestamp: bigint, frameId: number, ): Uint8Array { const hasTimestamp = userTimestamp !== BigInt(0); const hasFrameId = frameId !== 0; if (!hasTimestamp && !hasFrameId) { return data; } const trailerLength = (hasTimestamp ? TIMESTAMP_TLV_SIZE : 0) + (hasFrameId ? FRAME_ID_TLV_SIZE : 0) + PACKET_TRAILER_ENVELOPE_SIZE; const result = new Uint8Array(data.length + trailerLength); let offset = 0; result.set(data, offset); offset += data.length; if (hasTimestamp) { result[offset++] = PACKET_TRAILER_TIMESTAMP_TAG ^ 0xff; result[offset++] = 8 ^ 0xff; writeUint64Xor(result, offset, userTimestamp); offset += 8; } if (hasFrameId) { result[offset++] = PACKET_TRAILER_FRAME_ID_TAG ^ 0xff; result[offset++] = 4 ^ 0xff; writeUint32Xor(result, offset, frameId); offset += 4; } result[offset++] = trailerLength ^ 0xff; result.set(PACKET_TRAILER_MAGIC, offset); return result; } export function appendPacketTrailerToEncodedFrame( frame: RTCEncodedVideoFrame, options: PacketTrailerPublishOptions | undefined, frameId: number, ): boolean { if (!hasPacketTrailerPublishOptions(options) || frame.data.byteLength === 0) { return false; } const userTimestamp = options?.timestamp ? BigInt(Date.now()) * BigInt(1000) : BigInt(0); const packetTrailerFrameId = options?.frameId ? frameId : 0; const data = new Uint8Array(frame.data); const result = appendPacketTrailer(data, userTimestamp, packetTrailerFrameId); if (result.byteLength === data.byteLength) { return false; } frame.data = result.buffer.slice( result.byteOffset, result.byteOffset + result.byteLength, ) as ArrayBuffer; return true; } export function extractPacketTrailer(data: ArrayBuffer | Uint8Array): ExtractPacketTrailerResult { const bytes = data instanceof Uint8Array ? data : new Uint8Array(data); if (bytes.length < PACKET_TRAILER_ENVELOPE_SIZE) { return { data: bytes }; } const magicOffset = bytes.length - PACKET_TRAILER_MAGIC.length; if (!matchesMagic(bytes, magicOffset)) { return { data: bytes }; } const trailerLength = bytes[bytes.length - PACKET_TRAILER_ENVELOPE_SIZE] ^ 0xff; if (trailerLength < PACKET_TRAILER_ENVELOPE_SIZE || trailerLength > bytes.length) { return { data: bytes }; } const trailerStart = bytes.length - trailerLength; const trailerEnd = bytes.length - PACKET_TRAILER_ENVELOPE_SIZE; const strippedData = bytes.subarray(0, trailerStart); let offset = trailerStart; let foundAny = false; const metadata: PacketTrailerMetadata = { userTimestamp: BigInt(0), frameId: 0, }; while (offset + 2 <= trailerEnd) { const tag = bytes[offset++] ^ 0xff; const length = bytes[offset++] ^ 0xff; if (offset + length > trailerEnd) { break; } if (tag === PACKET_TRAILER_TIMESTAMP_TAG && length === 8) { metadata.userTimestamp = readUint64Xor(bytes, offset); foundAny = true; } else if (tag === PACKET_TRAILER_FRAME_ID_TAG && length === 4) { metadata.frameId = readUint32Xor(bytes, offset, length); foundAny = true; } offset += length; } if (!foundAny) { return { data: bytes }; } return { data: strippedData, metadata }; } function matchesMagic(data: Uint8Array, offset: number) { for (let index = 0; index < PACKET_TRAILER_MAGIC.length; index += 1) { if (data[offset + index] !== PACKET_TRAILER_MAGIC[index]) { return false; } } return true; } function readUint64Xor(data: Uint8Array, offset: number): bigint { const hi = BigInt( (((data[offset] ^ 0xff) << 24) | ((data[offset + 1] ^ 0xff) << 16) | ((data[offset + 2] ^ 0xff) << 8) | (data[offset + 3] ^ 0xff)) >>> 0, ); const lo = BigInt( (((data[offset + 4] ^ 0xff) << 24) | ((data[offset + 5] ^ 0xff) << 16) | ((data[offset + 6] ^ 0xff) << 8) | (data[offset + 7] ^ 0xff)) >>> 0, ); return (hi << BigInt(32)) | lo; } function readUint32Xor(data: Uint8Array, offset: number, length: number) { let value = 0; for (let index = 0; index < length; index += 1) { value = (value << 8) | (data[offset + index] ^ 0xff); } return value >>> 0; } function writeUint64Xor(target: Uint8Array, offset: number, value: bigint) { const hi = Number((value >> BigInt(32)) & BigInt(0xffffffff)); const lo = Number(value & BigInt(0xffffffff)); target[offset] = (hi >>> 24) ^ 0xff; target[offset + 1] = ((hi >>> 16) & 0xff) ^ 0xff; target[offset + 2] = ((hi >>> 8) & 0xff) ^ 0xff; target[offset + 3] = (hi & 0xff) ^ 0xff; target[offset + 4] = (lo >>> 24) ^ 0xff; target[offset + 5] = ((lo >>> 16) & 0xff) ^ 0xff; target[offset + 6] = ((lo >>> 8) & 0xff) ^ 0xff; target[offset + 7] = (lo & 0xff) ^ 0xff; } function writeUint32Xor(target: Uint8Array, offset: number, value: number) { for (let index = 3; index >= 0; index -= 1) { target[offset + (3 - index)] = ((value >> (index * 8)) & 0xff) ^ 0xff; } } export function getFrameRtpTimestamp( frame: RTCEncodedVideoFrame | RTCEncodedAudioFrame, ): number | undefined { try { const metadata = frame.getMetadata() as Record; if (typeof metadata.rtpTimestamp === 'number') { return metadata.rtpTimestamp; } if (typeof metadata.timestamp === 'number') { return metadata.timestamp; } } catch { // getMetadata() might not be available } if (typeof frame.timestamp === 'number') { return frame.timestamp; } return undefined; } export function getFrameSsrc(frame: RTCEncodedVideoFrame | RTCEncodedAudioFrame): number { try { const metadata = frame.getMetadata() as Record; if (typeof metadata.synchronizationSource === 'number') { return metadata.synchronizationSource; } } catch {} return 0; } export interface PacketTrailerFramePayload { trackId: string; rtpTimestamp: number; ssrc: number; metadata: PacketTrailerMetadata; } export interface ProcessPacketTrailerResult { data?: ArrayBuffer; payload?: PacketTrailerFramePayload; } /** * Extracts a packet trailer from an encoded frame and returns the stripped * frame data (if any) along with a ready-to-post metadata payload. Returns an * empty object when no trailer is present, an RTP timestamp can't be read, or * a trackId isn't available. */ export function processPacketTrailer( frame: RTCEncodedVideoFrame | RTCEncodedAudioFrame, trackId: string | undefined, ): ProcessPacketTrailerResult { if (frame.data.byteLength === 0) { return {}; } const result = extractPacketTrailer(frame.data); if (!result.metadata) { return {}; } const strippedData = (result.data.buffer as ArrayBuffer).slice( result.data.byteOffset, result.data.byteOffset + result.data.byteLength, ); const rtpTimestamp = getFrameRtpTimestamp(frame); if (rtpTimestamp === undefined || !trackId) { return { data: strippedData }; } return { data: strippedData, payload: { trackId, rtpTimestamp, ssrc: getFrameSsrc(frame), metadata: result.metadata, }, }; }