From 999be68eff62c76c10d30507f98adee854d8ba58 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 1 Jul 2026 11:12:28 -0700 Subject: [PATCH 1/7] Update protocol --- package.json | 2 +- pnpm-lock.yaml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index b1192e9e36..b9c20a3766 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ }, "dependencies": { "@livekit/mutex": "1.1.1", - "@livekit/protocol": "1.46.6", + "@livekit/protocol": "1.48.2", "events": "^3.3.0", "jose": "^6.1.0", "loglevel": "^1.9.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e02bbc55ac..74fb402167 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,8 +12,8 @@ importers: specifier: 1.1.1 version: 1.1.1 '@livekit/protocol': - specifier: 1.46.6 - version: 1.46.6 + specifier: 1.48.2 + version: 1.48.2 '@types/dom-mediacapture-record': specifier: ^1 version: 1.0.22 @@ -1203,8 +1203,8 @@ packages: '@livekit/mutex@1.1.1': resolution: {integrity: sha512-EsshAucklmpuUAfkABPxJNhzj9v2sG7JuzFDL4ML1oJQSV14sqrpTYnsaOudMAw9yOaW53NU3QQTlUQoRs4czw==} - '@livekit/protocol@1.46.6': - resolution: {integrity: sha512-upzlHP1vi/kZ/QqALZTFskQ0ifqc2f15RKucHYOsIHJsaXvEYanG75mAb7o+Yomfs4XhQ4BaRsdY+TFHXpaqrg==} + '@livekit/protocol@1.48.2': + resolution: {integrity: sha512-xFcZdiVa4LpykarDZwdXbnS17qq+qbNKIT9v5/peNFNTjnMalmJkyo+XIIYfix6T9pG4LumjNzNBvxPEkIOrBg==} '@livekit/throws-transformer@0.1.3': resolution: {integrity: sha512-PBttE6W6g/2ALGu6kWOunZ5qdrXwP9Ge1An2/62OfE6Rhc0Abd4yp6ex2pWhwUfGxDsSZvFgoB1Ia/5mWAMuKQ==} @@ -5058,7 +5058,7 @@ snapshots: '@livekit/mutex@1.1.1': {} - '@livekit/protocol@1.46.6': + '@livekit/protocol@1.48.2': dependencies: '@bufbuild/protobuf': 1.10.1 From 09a14734afcd099adc94e0959099f3c6719d2dbe Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 1 Jul 2026 11:51:24 -0700 Subject: [PATCH 2/7] Schema metadata support --- src/api/SignalClient.ts | 12 +- src/index.ts | 5 + src/room/Room.ts | 8 +- .../outgoing/OutgoingDataTrackManager.ts | 4 + src/room/data-track/outgoing/types.ts | 15 +- src/room/data-track/schema.ts | 197 ++++++++++++++++++ src/room/data-track/types.ts | 23 ++ 7 files changed, 260 insertions(+), 4 deletions(-) create mode 100644 src/room/data-track/schema.ts diff --git a/src/api/SignalClient.ts b/src/api/SignalClient.ts index eb7bdb0419..88c160e6e1 100644 --- a/src/api/SignalClient.ts +++ b/src/api/SignalClient.ts @@ -56,7 +56,7 @@ import { } from '@livekit/protocol'; import log, { LoggerNames, getLogger } from '../logger'; import type { DataTrackHandle } from '../room/data-track/handle'; -import { type DataTrackSid } from '../room/data-track/types'; +import { DataTrackFrameEncoding, DataTrackSchemaId, type DataTrackSid } from '../room/data-track/types'; import { ConnectionError } from '../room/errors'; import CriticalTimers from '../room/timers'; import type { LoggerOptions } from '../room/types'; @@ -711,13 +711,21 @@ export class SignalClient { }); } - sendPublishDataTrackRequest(handle: DataTrackHandle, name: string, usesE2ee: boolean) { + sendPublishDataTrackRequest( + handle: DataTrackHandle, + name: string, + usesE2ee: boolean, + schema?: DataTrackSchemaId, + frameEncoding?: DataTrackFrameEncoding, + ) { return this.sendRequest({ case: 'publishDataTrackRequest', value: new PublishDataTrackRequest({ pubHandle: handle, name: name, encryption: usesE2ee ? Encryption_Type.GCM : Encryption_Type.NONE, + schema: schema ? DataTrackSchemaId.toProtobuf(schema) : undefined, + frameEncoding: frameEncoding ? DataTrackFrameEncoding.toProtobuf(frameEncoding) : undefined, }), }); } diff --git a/src/index.ts b/src/index.ts index 1d218cd89d..1e618c8737 100644 --- a/src/index.ts +++ b/src/index.ts @@ -178,6 +178,11 @@ export type { DataTrackSubscribeOptions, RemoteDataTrackPipelineOptions, }; +export type { + DataTrackSchemaId, + DataTrackSchemaEncoding, + DataTrackFrameEncoding, +} from './room/data-track/types'; export { DataTrackPacket, type DataTrackPacketHeader } from './room/data-track/packet'; export { type DataTrackExtensions, diff --git a/src/room/Room.ts b/src/room/Room.ts index 169fb1b237..2a785ad351 100644 --- a/src/room/Room.ts +++ b/src/room/Room.ts @@ -294,7 +294,13 @@ class Room extends (EventEmitter as new () => TypedEmitter) this.outgoingDataTrackManager = new OutgoingDataTrackManager({ e2eeManager: this.e2eeManager }); this.outgoingDataTrackManager .on('sfuPublishRequest', (event) => { - this.engine.client.sendPublishDataTrackRequest(event.handle, event.name, event.usesE2ee); + this.engine.client.sendPublishDataTrackRequest( + event.handle, + event.name, + event.usesE2ee, + event.schema, + event.frameEncoding, + ); }) .on('sfuUnpublishRequest', (event) => { this.engine.client.sendUnPublishDataTrackRequest(event.handle); diff --git a/src/room/data-track/outgoing/OutgoingDataTrackManager.ts b/src/room/data-track/outgoing/OutgoingDataTrackManager.ts index 4685d5cadd..99d3212049 100644 --- a/src/room/data-track/outgoing/OutgoingDataTrackManager.ts +++ b/src/room/data-track/outgoing/OutgoingDataTrackManager.ts @@ -263,6 +263,8 @@ export default class OutgoingDataTrackManager extends (EventEmitter as new () => handle, name: options.name, usesE2ee: this.e2eeManager !== null, + schema: options.schema, + frameEncoding: options.frameEncoding, }); await descriptor.completionFuture.promise; @@ -395,6 +397,8 @@ export default class OutgoingDataTrackManager extends (EventEmitter as new () => handle: descriptor.info.pubHandle, name: descriptor.info.name, usesE2ee: descriptor.info.usesE2ee, + schema: descriptor.info.schema, + frameEncoding: descriptor.info.frameEncoding, }); } } diff --git a/src/room/data-track/outgoing/types.ts b/src/room/data-track/outgoing/types.ts index 1b70f7a599..31c4a9d74b 100644 --- a/src/room/data-track/outgoing/types.ts +++ b/src/room/data-track/outgoing/types.ts @@ -1,11 +1,22 @@ import type LocalDataTrack from '../LocalDataTrack'; import { type DataTrackHandle } from '../handle'; -import { type DataTrackInfo, type DataTrackSid } from '../types'; +import { + type DataTrackFrameEncoding, + type DataTrackInfo, + type DataTrackSchemaId, + type DataTrackSid, +} from '../types'; import { type DataTrackPublishError, type DataTrackPublishErrorReason } from './errors'; /** Options for publishing a data track. */ export type DataTrackOptions = { name: string; + + /** Schema describing frames sent on the track. */ + schema?: DataTrackSchemaId; + + /** Encoding of frames sent on the track. */ + frameEncoding?: DataTrackFrameEncoding; }; /** Encodes whether a data track publish request to the SFU has been successful or not. */ @@ -26,6 +37,8 @@ export type EventSfuPublishRequest = { handle: DataTrackHandle; name: string; usesE2ee: boolean; + schema?: DataTrackSchemaId; + frameEncoding?: DataTrackFrameEncoding; }; /** Request sent to the SFU to unpublish a track. */ diff --git a/src/room/data-track/schema.ts b/src/room/data-track/schema.ts new file mode 100644 index 0000000000..8b9f5b590d --- /dev/null +++ b/src/room/data-track/schema.ts @@ -0,0 +1,197 @@ +import { + DataTrackFrameEncoding as ProtocolDataTrackFrameEncoding, + DataTrackSchemaEncoding as ProtocolDataTrackSchemaEncoding, + DataTrackSchemaId as ProtocolDataTrackSchemaId, + DataTrackFrameEncoding_WellKnownFrameEncoding as ProtocolWellKnownFrameEncoding, + DataTrackSchemaEncoding_WellKnownSchemaEncoding as ProtocolWellKnownSchemaEncoding, +} from '@livekit/protocol'; + +/** + * Encoding used to interpret a data track schema definition. + * + * Identifies the interface definition language the schema is written in (e.g. a + * `.proto` file for `'protobuf'`). This in turn dictates the wire format of the + * frames the schema describes, captured by {@link DataTrackFrameEncoding}. + * + * The well-known encodings mirror the schema encodings from the MCAP spec: + * https://mcap.dev/spec/registry#schema-encodings. Use `{ custom }` for an + * application-specific encoding not enumerated here; prefer a well-known encoding + * where possible. The identifier must be non-empty and no longer than 32 characters. + * + * `'other'` is only produced when receiving a well-known encoding introduced after + * this SDK version; it is not meant to be sent. + */ +export type DataTrackSchemaEncoding = + /** Protocol Buffer IDL, describes `'protobuf'` encoded frames. */ + | 'protobuf' + /** FlatBuffer IDL, describes `'flatbuffer'` encoded frames. */ + | 'flatbuffer' + /** ROS 1 Message, describes `'ros1'` encoded frames. */ + | 'ros1Msg' + /** ROS 2 Message, describes `'cdr'` encoded frames. */ + | 'ros2Msg' + /** ROS 2 IDL, describes `'cdr'` encoded frames. */ + | 'ros2Idl' + /** OMG IDL, describes `'cdr'` encoded frames. */ + | 'omgIdl' + /** JSON Schema, describes `'json'` encoded frames. */ + | 'jsonSchema' + /** Another well-known encoding not known to this client version. */ + | 'other' + /** An application-specific encoding identified by the contained string. */ + | { custom: string }; + +/** + * Encoding used for frames pushed on a data track. + * + * The serialization format of the frame bytes (e.g. `'protobuf'`); the structure + * of those bytes is described by a schema, see {@link DataTrackSchemaEncoding}. + * + * Use `{ custom }` for an application-specific encoding not enumerated here; prefer + * a well-known encoding where possible. The identifier must be non-empty and no + * longer than 32 characters. + * + * `'other'` is only produced when receiving a well-known encoding introduced after + * this SDK version; it is not meant to be sent. + */ +export type DataTrackFrameEncoding = + /** ROS 1, must be described by a `'ros1Msg'` schema. */ + | 'ros1' + /** CDR, must be described by a `'ros2Msg'`, `'ros2Idl'`, or `'omgIdl'` schema. */ + | 'cdr' + /** Protocol Buffer, must be described by a `'protobuf'` schema. */ + | 'protobuf' + /** FlatBuffer, must be described by a `'flatbuffer'` schema. */ + | 'flatbuffer' + /** CBOR, self-describing. */ + | 'cbor' + /** MessagePack, self-describing. */ + | 'msgpack' + /** JSON, self-describing or described by a `'jsonSchema'` schema. */ + | 'json' + /** Another well-known encoding not known to this client version. */ + | 'other' + /** An application-specific encoding identified by the contained string. */ + | { custom: string }; + +/** + * Identifier for a data track schema. + * + * A compound identifier with two components: {@link name} and {@link encoding}. + * + * Two IDs are equal only if both components match; the same name with a different + * encoding refers to a distinct schema. + */ +export type DataTrackSchemaId = { + /** Name component of the identifier. Must be non-empty and no longer than 256 characters. */ + name: string; + /** Encoding component of the identifier. */ + encoding: DataTrackSchemaEncoding; +}; + +const SCHEMA_ENCODING_TO_WELL_KNOWN: Record = { + protobuf: ProtocolWellKnownSchemaEncoding.PROTOBUF, + flatbuffer: ProtocolWellKnownSchemaEncoding.FLATBUFFER, + ros1Msg: ProtocolWellKnownSchemaEncoding.ROS1_MSG, + ros2Msg: ProtocolWellKnownSchemaEncoding.ROS2_MSG, + ros2Idl: ProtocolWellKnownSchemaEncoding.ROS2_IDL, + omgIdl: ProtocolWellKnownSchemaEncoding.OMG_IDL, + jsonSchema: ProtocolWellKnownSchemaEncoding.JSON_SCHEMA, +}; + +const WELL_KNOWN_TO_SCHEMA_ENCODING: Partial< + Record +> = { + [ProtocolWellKnownSchemaEncoding.PROTOBUF]: 'protobuf', + [ProtocolWellKnownSchemaEncoding.FLATBUFFER]: 'flatbuffer', + [ProtocolWellKnownSchemaEncoding.ROS1_MSG]: 'ros1Msg', + [ProtocolWellKnownSchemaEncoding.ROS2_MSG]: 'ros2Msg', + [ProtocolWellKnownSchemaEncoding.ROS2_IDL]: 'ros2Idl', + [ProtocolWellKnownSchemaEncoding.OMG_IDL]: 'omgIdl', + [ProtocolWellKnownSchemaEncoding.JSON_SCHEMA]: 'jsonSchema', +}; + +const FRAME_ENCODING_TO_WELL_KNOWN: Record = { + ros1: ProtocolWellKnownFrameEncoding.ROS1, + cdr: ProtocolWellKnownFrameEncoding.CDR, + protobuf: ProtocolWellKnownFrameEncoding.PROTOBUF, + flatbuffer: ProtocolWellKnownFrameEncoding.FLATBUFFER, + cbor: ProtocolWellKnownFrameEncoding.CBOR, + msgpack: ProtocolWellKnownFrameEncoding.MSGPACK, + json: ProtocolWellKnownFrameEncoding.JSON, +}; + +const WELL_KNOWN_TO_FRAME_ENCODING: Partial< + Record +> = { + [ProtocolWellKnownFrameEncoding.ROS1]: 'ros1', + [ProtocolWellKnownFrameEncoding.CDR]: 'cdr', + [ProtocolWellKnownFrameEncoding.PROTOBUF]: 'protobuf', + [ProtocolWellKnownFrameEncoding.FLATBUFFER]: 'flatbuffer', + [ProtocolWellKnownFrameEncoding.CBOR]: 'cbor', + [ProtocolWellKnownFrameEncoding.MSGPACK]: 'msgpack', + [ProtocolWellKnownFrameEncoding.JSON]: 'json', +}; + +export const DataTrackSchemaEncoding = { + from(protocol: ProtocolDataTrackSchemaEncoding): DataTrackSchemaEncoding { + switch (protocol.value.case) { + case 'wellKnown': + // Maps unspecified or a value introduced after this client version to 'other'. + return WELL_KNOWN_TO_SCHEMA_ENCODING[protocol.value.value] ?? 'other'; + case 'custom': + return { custom: protocol.value.value }; + default: + return 'other'; + } + }, + toProtobuf(encoding: DataTrackSchemaEncoding): ProtocolDataTrackSchemaEncoding { + if (typeof encoding === 'object') { + return new ProtocolDataTrackSchemaEncoding({ + value: { case: 'custom', value: encoding.custom }, + }); + } + const wellKnown = + SCHEMA_ENCODING_TO_WELL_KNOWN[encoding] ?? ProtocolWellKnownSchemaEncoding.UNSPECIFIED; + return new ProtocolDataTrackSchemaEncoding({ value: { case: 'wellKnown', value: wellKnown } }); + }, +}; + +export const DataTrackFrameEncoding = { + from(protocol: ProtocolDataTrackFrameEncoding): DataTrackFrameEncoding { + switch (protocol.value.case) { + case 'wellKnown': + // Maps unspecified or a value introduced after this client version to 'other'. + return WELL_KNOWN_TO_FRAME_ENCODING[protocol.value.value] ?? 'other'; + case 'custom': + return { custom: protocol.value.value }; + default: + return 'other'; + } + }, + toProtobuf(encoding: DataTrackFrameEncoding): ProtocolDataTrackFrameEncoding { + if (typeof encoding === 'object') { + return new ProtocolDataTrackFrameEncoding({ + value: { case: 'custom', value: encoding.custom }, + }); + } + const wellKnown = + FRAME_ENCODING_TO_WELL_KNOWN[encoding] ?? ProtocolWellKnownFrameEncoding.UNSPECIFIED; + return new ProtocolDataTrackFrameEncoding({ value: { case: 'wellKnown', value: wellKnown } }); + }, +}; + +export const DataTrackSchemaId = { + from(protocol: ProtocolDataTrackSchemaId): DataTrackSchemaId { + return { + name: protocol.name, + encoding: protocol.encoding ? DataTrackSchemaEncoding.from(protocol.encoding) : 'other', + }; + }, + toProtobuf(schemaId: DataTrackSchemaId): ProtocolDataTrackSchemaId { + return new ProtocolDataTrackSchemaId({ + name: schemaId.name, + encoding: DataTrackSchemaEncoding.toProtobuf(schemaId.encoding), + }); + }, +}; diff --git a/src/room/data-track/types.ts b/src/room/data-track/types.ts index 3c5e2a38f3..5ee9dec05e 100644 --- a/src/room/data-track/types.ts +++ b/src/room/data-track/types.ts @@ -1,5 +1,8 @@ import { Encryption_Type, DataTrackInfo as ProtocolDataTrackInfo } from '@livekit/protocol'; import { type DataTrackHandle } from './handle'; +import { DataTrackFrameEncoding, DataTrackSchemaId } from './schema'; + +export * from './schema'; export type DataTrackSid = string; @@ -9,6 +12,18 @@ export type DataTrackInfo = { pubHandle: DataTrackHandle; name: string; usesE2ee: boolean; + + /** Schema associated with frames sent on the track. + * + * Absent if the publisher did not associate a {@link DataTrackSchemaId} with the track. + */ + schema?: DataTrackSchemaId; + + /** Encoding of frames sent on the track. + * + * Absent if the publisher did not specify a {@link DataTrackFrameEncoding} for the track. + */ + frameEncoding?: DataTrackFrameEncoding; }; export type RemoteDataTrackPipelineOptions = { @@ -26,6 +41,10 @@ export const DataTrackInfo = { pubHandle: protocolInfo.pubHandle, name: protocolInfo.name, usesE2ee: protocolInfo.encryption !== Encryption_Type.NONE, + schema: protocolInfo.schema ? DataTrackSchemaId.from(protocolInfo.schema) : undefined, + frameEncoding: protocolInfo.frameEncoding + ? DataTrackFrameEncoding.from(protocolInfo.frameEncoding) + : undefined, }; }, toProtobuf(info: DataTrackInfo): ProtocolDataTrackInfo { @@ -34,6 +53,10 @@ export const DataTrackInfo = { pubHandle: info.pubHandle, name: info.name, encryption: info.usesE2ee ? Encryption_Type.GCM : Encryption_Type.NONE, + schema: info.schema ? DataTrackSchemaId.toProtobuf(info.schema) : undefined, + frameEncoding: info.frameEncoding + ? DataTrackFrameEncoding.toProtobuf(info.frameEncoding) + : undefined, }); }, }; From 868b284bf9e2a0766f1f8fb576675e78c0762efa Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 1 Jul 2026 11:51:33 -0700 Subject: [PATCH 3/7] Testing --- .../outgoing/OutgoingDataTrackManager.test.ts | 50 +++++++++ src/room/data-track/schema.test.ts | 106 ++++++++++++++++++ 2 files changed, 156 insertions(+) create mode 100644 src/room/data-track/schema.test.ts diff --git a/src/room/data-track/outgoing/OutgoingDataTrackManager.test.ts b/src/room/data-track/outgoing/OutgoingDataTrackManager.test.ts index aac62db8c0..e42fdc019b 100644 --- a/src/room/data-track/outgoing/OutgoingDataTrackManager.test.ts +++ b/src/room/data-track/outgoing/OutgoingDataTrackManager.test.ts @@ -251,6 +251,56 @@ describe('DataTrackOutgoingManager', () => { expect(handle).not.toStrictEqual(handle2); }); + it.each([ + { + title: 'well-known encodings', + schema: { name: 'my_schema', encoding: 'jsonSchema' }, + frameEncoding: 'json', + }, + { + title: 'custom encodings', + schema: { name: 'my_schema', encoding: { custom: 'a' } }, + frameEncoding: { custom: 'b' }, + }, + ] as const)( + 'should forward schema and frame encoding on publish ($title)', + async ({ schema, frameEncoding }) => { + const manager = new OutgoingDataTrackManager(); + const managerEvents = subscribeToEvents(manager, [ + 'sfuPublishRequest', + ]); + + const localDataTrack = new LocalDataTrack({ name: 'test', schema, frameEncoding }, manager); + + // 1. Publish a data track with schema metadata + const publishRequestPromise = localDataTrack.publish(); + + // 2. The publish request sent to the SFU carries the schema and frame encoding + const sfuPublishEvent = await managerEvents.waitFor('sfuPublishRequest'); + expect(sfuPublishEvent.schema).toStrictEqual(schema); + expect(sfuPublishEvent.frameEncoding).toStrictEqual(frameEncoding); + const handle = sfuPublishEvent.handle; + + // 3. Respond as the SFU would, echoing the metadata back on the DataTrackInfo + manager.receivedSfuPublishResponse(handle, { + type: 'ok', + data: { + sid: 'bogus-sid', + pubHandle: handle, + name: 'test', + usesE2ee: false, + schema, + frameEncoding, + }, + }); + await publishRequestPromise; + + // 4. The metadata is reflected on the local track's info + expect(localDataTrack.info?.schema).toStrictEqual(schema); + expect(localDataTrack.info?.frameEncoding).toStrictEqual(frameEncoding); + }, + ); + it.each([ // Single packet payload case [ diff --git a/src/room/data-track/schema.test.ts b/src/room/data-track/schema.test.ts new file mode 100644 index 0000000000..eed79d85e5 --- /dev/null +++ b/src/room/data-track/schema.test.ts @@ -0,0 +1,106 @@ +import { + DataTrackFrameEncoding as ProtocolDataTrackFrameEncoding, + DataTrackSchemaEncoding as ProtocolDataTrackSchemaEncoding, + DataTrackSchemaId as ProtocolDataTrackSchemaId, + DataTrackFrameEncoding_WellKnownFrameEncoding as ProtocolWellKnownFrameEncoding, + DataTrackSchemaEncoding_WellKnownSchemaEncoding as ProtocolWellKnownSchemaEncoding, +} from '@livekit/protocol'; +import { describe, expect, it } from 'vitest'; +import { DataTrackFrameEncoding, DataTrackSchemaEncoding, DataTrackSchemaId } from './schema'; + +describe('DataTrackSchemaEncoding', () => { + const wellKnown: Array = [ + 'protobuf', + 'flatbuffer', + 'ros1Msg', + 'ros2Msg', + 'ros2Idl', + 'omgIdl', + 'jsonSchema', + ]; + + it.each(wellKnown)('round-trips well-known encoding %s', (encoding) => { + const protobuf = DataTrackSchemaEncoding.toProtobuf(encoding); + expect(protobuf.value.case).toEqual('wellKnown'); + expect(DataTrackSchemaEncoding.from(protobuf)).toEqual(encoding); + }); + + it('round-trips a custom encoding', () => { + const encoding: DataTrackSchemaEncoding = { custom: 'my_encoding' }; + const protobuf = DataTrackSchemaEncoding.toProtobuf(encoding); + expect(protobuf.value).toEqual({ case: 'custom', value: 'my_encoding' }); + expect(DataTrackSchemaEncoding.from(protobuf)).toEqual(encoding); + }); + + it('maps an unspecified well-known value to "other"', () => { + const protobuf = new ProtocolDataTrackSchemaEncoding({ + value: { case: 'wellKnown', value: ProtocolWellKnownSchemaEncoding.UNSPECIFIED }, + }); + expect(DataTrackSchemaEncoding.from(protobuf)).toEqual('other'); + }); + + it('maps a well-known value introduced after this version to "other"', () => { + const protobuf = new ProtocolDataTrackSchemaEncoding({ + value: { case: 'wellKnown', value: 999 as ProtocolWellKnownSchemaEncoding }, + }); + expect(DataTrackSchemaEncoding.from(protobuf)).toEqual('other'); + }); + + it('maps an absent oneof to "other"', () => { + expect(DataTrackSchemaEncoding.from(new ProtocolDataTrackSchemaEncoding())).toEqual('other'); + }); +}); + +describe('DataTrackFrameEncoding', () => { + const wellKnown: Array = [ + 'ros1', + 'cdr', + 'protobuf', + 'flatbuffer', + 'cbor', + 'msgpack', + 'json', + ]; + + it.each(wellKnown)('round-trips well-known encoding %s', (encoding) => { + const protobuf = DataTrackFrameEncoding.toProtobuf(encoding); + expect(protobuf.value.case).toEqual('wellKnown'); + expect(DataTrackFrameEncoding.from(protobuf)).toEqual(encoding); + }); + + it('round-trips a custom encoding', () => { + const encoding: DataTrackFrameEncoding = { custom: 'my_encoding' }; + const protobuf = DataTrackFrameEncoding.toProtobuf(encoding); + expect(protobuf.value).toEqual({ case: 'custom', value: 'my_encoding' }); + expect(DataTrackFrameEncoding.from(protobuf)).toEqual(encoding); + }); + + it('maps an unspecified well-known value to "other"', () => { + const protobuf = new ProtocolDataTrackFrameEncoding({ + value: { case: 'wellKnown', value: ProtocolWellKnownFrameEncoding.UNSPECIFIED }, + }); + expect(DataTrackFrameEncoding.from(protobuf)).toEqual('other'); + }); + + it('maps a well-known value introduced after this version to "other"', () => { + const protobuf = new ProtocolDataTrackFrameEncoding({ + value: { case: 'wellKnown', value: 999 as ProtocolWellKnownFrameEncoding }, + }); + expect(DataTrackFrameEncoding.from(protobuf)).toEqual('other'); + }); +}); + +describe('DataTrackSchemaId', () => { + it('round-trips name and encoding', () => { + const schemaId: DataTrackSchemaId = { name: 'rgb', encoding: 'protobuf' }; + const protobuf = DataTrackSchemaId.toProtobuf(schemaId); + expect(protobuf).toBeInstanceOf(ProtocolDataTrackSchemaId); + expect(protobuf.name).toEqual('rgb'); + expect(DataTrackSchemaId.from(protobuf)).toEqual(schemaId); + }); + + it('defaults encoding to "other" when the protobuf encoding is absent', () => { + const protobuf = new ProtocolDataTrackSchemaId({ name: 'rgb' }); + expect(DataTrackSchemaId.from(protobuf)).toEqual({ name: 'rgb', encoding: 'other' }); + }); +}); From 04eaa801888aa2650ccb6521506c4fbe1dc2958d Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Wed, 1 Jul 2026 11:59:27 -0700 Subject: [PATCH 4/7] Format --- src/api/SignalClient.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/api/SignalClient.ts b/src/api/SignalClient.ts index 88c160e6e1..e154d498d3 100644 --- a/src/api/SignalClient.ts +++ b/src/api/SignalClient.ts @@ -56,7 +56,11 @@ import { } from '@livekit/protocol'; import log, { LoggerNames, getLogger } from '../logger'; import type { DataTrackHandle } from '../room/data-track/handle'; -import { DataTrackFrameEncoding, DataTrackSchemaId, type DataTrackSid } from '../room/data-track/types'; +import { + DataTrackFrameEncoding, + DataTrackSchemaId, + type DataTrackSid, +} from '../room/data-track/types'; import { ConnectionError } from '../room/errors'; import CriticalTimers from '../room/timers'; import type { LoggerOptions } from '../room/types'; From 6bfa1d9fddcd75206c42056a68fbd4a51d5200f7 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Thu, 2 Jul 2026 09:22:12 -0700 Subject: [PATCH 5/7] Use `toStrictEqual` for tests --- src/room/data-track/schema.test.ts | 34 ++++++++++++++++-------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/src/room/data-track/schema.test.ts b/src/room/data-track/schema.test.ts index eed79d85e5..db82538405 100644 --- a/src/room/data-track/schema.test.ts +++ b/src/room/data-track/schema.test.ts @@ -21,33 +21,35 @@ describe('DataTrackSchemaEncoding', () => { it.each(wellKnown)('round-trips well-known encoding %s', (encoding) => { const protobuf = DataTrackSchemaEncoding.toProtobuf(encoding); - expect(protobuf.value.case).toEqual('wellKnown'); - expect(DataTrackSchemaEncoding.from(protobuf)).toEqual(encoding); + expect(protobuf.value.case).toStrictEqual('wellKnown'); + expect(DataTrackSchemaEncoding.from(protobuf)).toStrictEqual(encoding); }); it('round-trips a custom encoding', () => { const encoding: DataTrackSchemaEncoding = { custom: 'my_encoding' }; const protobuf = DataTrackSchemaEncoding.toProtobuf(encoding); - expect(protobuf.value).toEqual({ case: 'custom', value: 'my_encoding' }); - expect(DataTrackSchemaEncoding.from(protobuf)).toEqual(encoding); + expect(protobuf.value).toStrictEqual({ case: 'custom', value: 'my_encoding' }); + expect(DataTrackSchemaEncoding.from(protobuf)).toStrictEqual(encoding); }); it('maps an unspecified well-known value to "other"', () => { const protobuf = new ProtocolDataTrackSchemaEncoding({ value: { case: 'wellKnown', value: ProtocolWellKnownSchemaEncoding.UNSPECIFIED }, }); - expect(DataTrackSchemaEncoding.from(protobuf)).toEqual('other'); + expect(DataTrackSchemaEncoding.from(protobuf)).toStrictEqual('other'); }); it('maps a well-known value introduced after this version to "other"', () => { const protobuf = new ProtocolDataTrackSchemaEncoding({ value: { case: 'wellKnown', value: 999 as ProtocolWellKnownSchemaEncoding }, }); - expect(DataTrackSchemaEncoding.from(protobuf)).toEqual('other'); + expect(DataTrackSchemaEncoding.from(protobuf)).toStrictEqual('other'); }); it('maps an absent oneof to "other"', () => { - expect(DataTrackSchemaEncoding.from(new ProtocolDataTrackSchemaEncoding())).toEqual('other'); + expect(DataTrackSchemaEncoding.from(new ProtocolDataTrackSchemaEncoding())).toStrictEqual( + 'other', + ); }); }); @@ -64,29 +66,29 @@ describe('DataTrackFrameEncoding', () => { it.each(wellKnown)('round-trips well-known encoding %s', (encoding) => { const protobuf = DataTrackFrameEncoding.toProtobuf(encoding); - expect(protobuf.value.case).toEqual('wellKnown'); - expect(DataTrackFrameEncoding.from(protobuf)).toEqual(encoding); + expect(protobuf.value.case).toStrictEqual('wellKnown'); + expect(DataTrackFrameEncoding.from(protobuf)).toStrictEqual(encoding); }); it('round-trips a custom encoding', () => { const encoding: DataTrackFrameEncoding = { custom: 'my_encoding' }; const protobuf = DataTrackFrameEncoding.toProtobuf(encoding); - expect(protobuf.value).toEqual({ case: 'custom', value: 'my_encoding' }); - expect(DataTrackFrameEncoding.from(protobuf)).toEqual(encoding); + expect(protobuf.value).toStrictEqual({ case: 'custom', value: 'my_encoding' }); + expect(DataTrackFrameEncoding.from(protobuf)).toStrictEqual(encoding); }); it('maps an unspecified well-known value to "other"', () => { const protobuf = new ProtocolDataTrackFrameEncoding({ value: { case: 'wellKnown', value: ProtocolWellKnownFrameEncoding.UNSPECIFIED }, }); - expect(DataTrackFrameEncoding.from(protobuf)).toEqual('other'); + expect(DataTrackFrameEncoding.from(protobuf)).toStrictEqual('other'); }); it('maps a well-known value introduced after this version to "other"', () => { const protobuf = new ProtocolDataTrackFrameEncoding({ value: { case: 'wellKnown', value: 999 as ProtocolWellKnownFrameEncoding }, }); - expect(DataTrackFrameEncoding.from(protobuf)).toEqual('other'); + expect(DataTrackFrameEncoding.from(protobuf)).toStrictEqual('other'); }); }); @@ -95,12 +97,12 @@ describe('DataTrackSchemaId', () => { const schemaId: DataTrackSchemaId = { name: 'rgb', encoding: 'protobuf' }; const protobuf = DataTrackSchemaId.toProtobuf(schemaId); expect(protobuf).toBeInstanceOf(ProtocolDataTrackSchemaId); - expect(protobuf.name).toEqual('rgb'); - expect(DataTrackSchemaId.from(protobuf)).toEqual(schemaId); + expect(protobuf.name).toStrictEqual('rgb'); + expect(DataTrackSchemaId.from(protobuf)).toStrictEqual(schemaId); }); it('defaults encoding to "other" when the protobuf encoding is absent', () => { const protobuf = new ProtocolDataTrackSchemaId({ name: 'rgb' }); - expect(DataTrackSchemaId.from(protobuf)).toEqual({ name: 'rgb', encoding: 'other' }); + expect(DataTrackSchemaId.from(protobuf)).toStrictEqual({ name: 'rgb', encoding: 'other' }); }); }); From ddc00b9ad8bd8ac60f6688f2b450957fc2a031d9 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Thu, 2 Jul 2026 09:54:44 -0700 Subject: [PATCH 6/7] Simplify enum mapping --- src/room/data-track/schema.ts | 28 ++++++---------------------- 1 file changed, 6 insertions(+), 22 deletions(-) diff --git a/src/room/data-track/schema.ts b/src/room/data-track/schema.ts index 8b9f5b590d..f81d87a9fb 100644 --- a/src/room/data-track/schema.ts +++ b/src/room/data-track/schema.ts @@ -99,17 +99,9 @@ const SCHEMA_ENCODING_TO_WELL_KNOWN: Record -> = { - [ProtocolWellKnownSchemaEncoding.PROTOBUF]: 'protobuf', - [ProtocolWellKnownSchemaEncoding.FLATBUFFER]: 'flatbuffer', - [ProtocolWellKnownSchemaEncoding.ROS1_MSG]: 'ros1Msg', - [ProtocolWellKnownSchemaEncoding.ROS2_MSG]: 'ros2Msg', - [ProtocolWellKnownSchemaEncoding.ROS2_IDL]: 'ros2Idl', - [ProtocolWellKnownSchemaEncoding.OMG_IDL]: 'omgIdl', - [ProtocolWellKnownSchemaEncoding.JSON_SCHEMA]: 'jsonSchema', -}; +const WELL_KNOWN_TO_SCHEMA_ENCODING = Object.fromEntries( + Object.entries(SCHEMA_ENCODING_TO_WELL_KNOWN).map(([key, value]) => [value, key]), +) as Partial>; const FRAME_ENCODING_TO_WELL_KNOWN: Record = { ros1: ProtocolWellKnownFrameEncoding.ROS1, @@ -121,17 +113,9 @@ const FRAME_ENCODING_TO_WELL_KNOWN: Record -> = { - [ProtocolWellKnownFrameEncoding.ROS1]: 'ros1', - [ProtocolWellKnownFrameEncoding.CDR]: 'cdr', - [ProtocolWellKnownFrameEncoding.PROTOBUF]: 'protobuf', - [ProtocolWellKnownFrameEncoding.FLATBUFFER]: 'flatbuffer', - [ProtocolWellKnownFrameEncoding.CBOR]: 'cbor', - [ProtocolWellKnownFrameEncoding.MSGPACK]: 'msgpack', - [ProtocolWellKnownFrameEncoding.JSON]: 'json', -}; +const WELL_KNOWN_TO_FRAME_ENCODING = Object.fromEntries( + Object.entries(FRAME_ENCODING_TO_WELL_KNOWN).map(([key, value]) => [value, key]), +) as Partial>; export const DataTrackSchemaEncoding = { from(protocol: ProtocolDataTrackSchemaEncoding): DataTrackSchemaEncoding { From 8c5df6ab79005ba21d227182f1ddf6d192f17345 Mon Sep 17 00:00:00 2001 From: Jacob Gelman <3182119+ladvoc@users.noreply.github.com> Date: Thu, 2 Jul 2026 09:55:19 -0700 Subject: [PATCH 7/7] Don't use wildcard export Co-authored-by: Ryan Gaus --- src/room/data-track/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/room/data-track/types.ts b/src/room/data-track/types.ts index 5ee9dec05e..68e0418838 100644 --- a/src/room/data-track/types.ts +++ b/src/room/data-track/types.ts @@ -2,7 +2,7 @@ import { Encryption_Type, DataTrackInfo as ProtocolDataTrackInfo } from '@liveki import { type DataTrackHandle } from './handle'; import { DataTrackFrameEncoding, DataTrackSchemaId } from './schema'; -export * from './schema'; +export { DataTrackFrameEncoding, DataTrackSchemaEncoding, DataTrackSchemaId } from './schema'; export type DataTrackSid = string;