Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/user-data-frame-metadata.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"livekit-client": patch
---

Add support for user_data frame metadata trailer type
7 changes: 7 additions & 0 deletions examples/demo/demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,13 @@ const appActions = {
`\nReceive: ${fmt(receiveTime)}` +
`\nLatency: ${latencyDisplay}`;
}
if (meta.userData && meta.userData.byteLength > 0) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this just renders some info on the demo app overlay when user_data is detected.

const hex = Array.from(meta.userData.subarray(0, 6))
.map((b) => b.toString(16).padStart(2, '0'))
.join(' ');
const ellipsis = meta.userData.byteLength > 6 ? ' ...' : '';
text += `${text ? '\n' : ''}user data: ${meta.userData.byteLength} bytes [${hex}${ellipsis}]`;
}
overlayElm.textContent = text;
}
});
Expand Down
3 changes: 2 additions & 1 deletion src/e2ee/E2eeManager.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Encryption_Type, TrackInfo } from '@livekit/protocol';
import { EventEmitter } from 'events';
import type TypedEventEmitter from 'typed-emitter';
import type { FrameMetadata } from '../frameMetadata/types';
import { hasFrameMetadataPublishOptions } from '../frameMetadata/utils';
import log, { LogLevel, workerLogger } from '../logger';
import type RTCEngine from '../room/RTCEngine';
Expand Down Expand Up @@ -246,7 +247,7 @@ export class E2EEManager
trackId: string,
rtpTimestamp: number,
ssrc: number,
metadata: { userTimestamp: bigint; frameId: number },
metadata: FrameMetadata,
) {
if (!this.room) {
return;
Expand Down
83 changes: 83 additions & 0 deletions src/frameMetadata/frameMetadata.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,57 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import {
PACKET_TRAILER_ENVELOPE_SIZE,
PACKET_TRAILER_FRAME_ID_TAG,
PACKET_TRAILER_MAGIC,
PACKET_TRAILER_TIMESTAMP_TAG,
PACKET_TRAILER_USER_DATA_TAG,
appendPacketTrailer,
appendPacketTrailerToEncodedFrame,
extractPacketTrailer,
processPacketTrailer,
} from './frameMetadata';

/**
* Builds a packet trailer (XORed TLVs + envelope) for the given fields. The
* SDK only writes timestamp/frameId, so this mirrors the native sender's TLV
* encoding to exercise user_data extraction.
*/
function buildTrailer(
payload: Uint8Array,
fields: { userTimestamp?: bigint; frameId?: number; userData?: Uint8Array },
): Uint8Array {
const tlvs: number[] = [];

if (fields.userTimestamp !== undefined) {
tlvs.push(PACKET_TRAILER_TIMESTAMP_TAG ^ 0xff, 8 ^ 0xff);
for (let i = 7; i >= 0; i -= 1) {
tlvs.push(Number((fields.userTimestamp >> BigInt(i * 8)) & BigInt(0xff)) ^ 0xff);
}
}

if (fields.frameId !== undefined) {
tlvs.push(PACKET_TRAILER_FRAME_ID_TAG ^ 0xff, 4 ^ 0xff);
for (let i = 3; i >= 0; i -= 1) {
tlvs.push(((fields.frameId >> (i * 8)) & 0xff) ^ 0xff);
}
}

if (fields.userData !== undefined) {
tlvs.push(PACKET_TRAILER_USER_DATA_TAG ^ 0xff, fields.userData.length ^ 0xff);
for (const byte of fields.userData) {
tlvs.push(byte ^ 0xff);
}
}

const trailerLength = tlvs.length + PACKET_TRAILER_ENVELOPE_SIZE;
const result = new Uint8Array(payload.length + trailerLength);
result.set(payload, 0);
result.set(tlvs, payload.length);
result[payload.length + tlvs.length] = trailerLength ^ 0xff;
result.set(PACKET_TRAILER_MAGIC, payload.length + tlvs.length + 1);
return result;
}

describe('packetTrailer', () => {
afterEach(() => {
vi.useRealTimers();
Expand Down Expand Up @@ -47,6 +93,43 @@ describe('packetTrailer', () => {
});
});

it('extracts user_data from a trailer carrying only user_data', () => {
const payload = Uint8Array.from([1, 2, 3, 4]);
const userData = Uint8Array.from([0x00, 0x01, 0xfe, 0xff, 0x42]);
const trailer = buildTrailer(payload, { userData });
const extracted = extractPacketTrailer(trailer);

expect(Array.from(extracted.data)).toEqual(Array.from(payload));
expect(extracted.metadata?.userTimestamp).toBe(0n);
expect(extracted.metadata?.frameId).toBe(0);
expect(extracted.metadata?.userData).toBeInstanceOf(Uint8Array);
expect(Array.from(extracted.metadata!.userData!)).toEqual(Array.from(userData));
});

it('extracts user_data alongside timestamp and frameId', () => {
const payload = Uint8Array.from([9, 8, 7, 6, 5]);
const userData = Uint8Array.from([10, 20, 30, 40]);
const trailer = buildTrailer(payload, {
userTimestamp: 1_744_249_600_123_456n,
frameId: 42,
userData,
});
const extracted = extractPacketTrailer(trailer);

expect(Array.from(extracted.data)).toEqual(Array.from(payload));
expect(extracted.metadata?.userTimestamp).toBe(1_744_249_600_123_456n);
expect(extracted.metadata?.frameId).toBe(42);
expect(Array.from(extracted.metadata!.userData!)).toEqual(Array.from(userData));
});

it('leaves userData undefined when no user_data tag is present', () => {
const payload = Uint8Array.from([1, 2, 3, 4]);
const trailer = appendPacketTrailer(payload, 1_744_249_600_123_456n, 42);
const extracted = extractPacketTrailer(trailer);

expect(extracted.metadata?.userData).toBeUndefined();
});

it('returns data unchanged when both timestamp and frameId are 0', () => {
const payload = Uint8Array.from([1, 2, 3, 4]);
const result = appendPacketTrailer(payload, 0n, 0);
Expand Down
8 changes: 8 additions & 0 deletions src/frameMetadata/frameMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const PACKET_TRAILER_MAGIC = Uint8Array.from([

export const PACKET_TRAILER_TIMESTAMP_TAG = 0x01;
export const PACKET_TRAILER_FRAME_ID_TAG = 0x02;
export const PACKET_TRAILER_USER_DATA_TAG = 0x03;
export const PACKET_TRAILER_ENVELOPE_SIZE = 5;

const TIMESTAMP_TLV_SIZE = 10;
Expand Down Expand Up @@ -127,6 +128,13 @@ export function extractPacketTrailer(data: ArrayBuffer | Uint8Array): ExtractPac
} else if (tag === PACKET_TRAILER_FRAME_ID_TAG && length === 4) {
metadata.frameId = readUint32Xor(bytes, offset, length);
foundAny = true;
} else if (tag === PACKET_TRAILER_USER_DATA_TAG) {
const userData = new Uint8Array(length);
for (let index = 0; index < length; index += 1) {
userData[index] = bytes[offset + index] ^ 0xff;
}
metadata.userData = userData;
foundAny = true;
}

offset += length;
Expand Down
1 change: 1 addition & 0 deletions src/frameMetadata/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { FrameMetadataPayload } from './frameMetadata';
export interface FrameMetadata {
userTimestamp: bigint;
frameId: number;
userData?: Uint8Array;
}

/** @deprecated Use {@link FrameMetadata} instead. */
Expand Down
Loading