livekit-client 2.20.0 → 2.20.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/dist/livekit-client.e2ee.worker.js +1 -1
  2. package/dist/livekit-client.e2ee.worker.js.map +1 -1
  3. package/dist/livekit-client.e2ee.worker.mjs +41 -11
  4. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  5. package/dist/livekit-client.esm.mjs +744 -178
  6. package/dist/livekit-client.esm.mjs.map +1 -1
  7. package/dist/livekit-client.fm.worker.js +1 -1
  8. package/dist/livekit-client.fm.worker.js.map +1 -1
  9. package/dist/livekit-client.fm.worker.mjs +41 -7
  10. package/dist/livekit-client.fm.worker.mjs.map +1 -1
  11. package/dist/livekit-client.umd.js +1 -1
  12. package/dist/livekit-client.umd.js.map +1 -1
  13. package/dist/src/api/SignalClient.d.ts.map +1 -1
  14. package/dist/src/e2ee/E2eeManager.d.ts +1 -0
  15. package/dist/src/e2ee/E2eeManager.d.ts.map +1 -1
  16. package/dist/src/e2ee/types.d.ts +1 -0
  17. package/dist/src/e2ee/types.d.ts.map +1 -1
  18. package/dist/src/e2ee/utils.d.ts +1 -0
  19. package/dist/src/e2ee/utils.d.ts.map +1 -1
  20. package/dist/src/e2ee/worker/DataCryptor.d.ts +1 -0
  21. package/dist/src/e2ee/worker/DataCryptor.d.ts.map +1 -1
  22. package/dist/src/e2ee/worker/FrameCryptor.d.ts +1 -0
  23. package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -1
  24. package/dist/src/e2ee/worker/naluUtils.d.ts +1 -0
  25. package/dist/src/e2ee/worker/naluUtils.d.ts.map +1 -1
  26. package/dist/src/e2ee/worker/sifPayload.d.ts +1 -0
  27. package/dist/src/e2ee/worker/sifPayload.d.ts.map +1 -1
  28. package/dist/src/frameMetadata/frameMetadata.d.ts +1 -0
  29. package/dist/src/frameMetadata/frameMetadata.d.ts.map +1 -1
  30. package/dist/src/frameMetadata/types.d.ts +1 -0
  31. package/dist/src/frameMetadata/types.d.ts.map +1 -1
  32. package/dist/src/logger.d.ts +3 -1
  33. package/dist/src/logger.d.ts.map +1 -1
  34. package/dist/src/room/PCTransport.d.ts +42 -1
  35. package/dist/src/room/PCTransport.d.ts.map +1 -1
  36. package/dist/src/room/PCTransportManager.d.ts +1 -0
  37. package/dist/src/room/PCTransportManager.d.ts.map +1 -1
  38. package/dist/src/room/RTCEngine.d.ts +46 -4
  39. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  40. package/dist/src/room/RegionUrlProvider.d.ts.map +1 -1
  41. package/dist/src/room/Room.d.ts +1 -0
  42. package/dist/src/room/Room.d.ts.map +1 -1
  43. package/dist/src/room/data-track/depacketizer.d.ts.map +1 -1
  44. package/dist/src/room/data-track/frame.d.ts +1 -0
  45. package/dist/src/room/data-track/frame.d.ts.map +1 -1
  46. package/dist/src/room/data-track/incoming/IncomingDataTrackManager.d.ts +7 -0
  47. package/dist/src/room/data-track/incoming/IncomingDataTrackManager.d.ts.map +1 -1
  48. package/dist/src/room/data-track/outgoing/OutgoingDataTrackManager.d.ts.map +1 -1
  49. package/dist/src/room/data-track/outgoing/types.d.ts +1 -0
  50. package/dist/src/room/data-track/outgoing/types.d.ts.map +1 -1
  51. package/dist/src/room/data-track/packet/extensions.d.ts +1 -0
  52. package/dist/src/room/data-track/packet/extensions.d.ts.map +1 -1
  53. package/dist/src/room/data-track/packet/index.d.ts +1 -0
  54. package/dist/src/room/data-track/packet/index.d.ts.map +1 -1
  55. package/dist/src/room/data-track/packet/serializable.d.ts +1 -0
  56. package/dist/src/room/data-track/packet/serializable.d.ts.map +1 -1
  57. package/dist/src/room/participant/LocalParticipant.d.ts +1 -0
  58. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  59. package/dist/src/room/participant/Participant.d.ts +1 -0
  60. package/dist/src/room/participant/Participant.d.ts.map +1 -1
  61. package/dist/src/room/participant/publishUtils.d.ts +7 -0
  62. package/dist/src/room/participant/publishUtils.d.ts.map +1 -1
  63. package/dist/src/room/track/LocalAudioTrack.d.ts +1 -0
  64. package/dist/src/room/track/LocalAudioTrack.d.ts.map +1 -1
  65. package/dist/src/room/track/Track.d.ts +1 -0
  66. package/dist/src/room/track/Track.d.ts.map +1 -1
  67. package/dist/src/room/track/options.d.ts +11 -1
  68. package/dist/src/room/track/options.d.ts.map +1 -1
  69. package/dist/src/room/track/record.d.ts.map +1 -1
  70. package/dist/src/room/utils.d.ts +1 -0
  71. package/dist/src/room/utils.d.ts.map +1 -1
  72. package/dist/src/test/signalToken.d.ts +26 -0
  73. package/dist/src/test/signalToken.d.ts.map +1 -0
  74. package/dist/src/type-polyfills/non-shared-typed-arrays.d.ts +2 -0
  75. package/dist/src/type-polyfills/non-shared-typed-arrays.d.ts.map +1 -0
  76. package/dist/src/utils/dataPacketBuffer.d.ts +19 -0
  77. package/dist/src/utils/dataPacketBuffer.d.ts.map +1 -1
  78. package/dist/ts4.2/e2ee/E2eeManager.d.ts +1 -0
  79. package/dist/ts4.2/e2ee/types.d.ts +1 -0
  80. package/dist/ts4.2/e2ee/utils.d.ts +1 -0
  81. package/dist/ts4.2/e2ee/worker/DataCryptor.d.ts +1 -0
  82. package/dist/ts4.2/e2ee/worker/FrameCryptor.d.ts +1 -0
  83. package/dist/ts4.2/e2ee/worker/naluUtils.d.ts +1 -0
  84. package/dist/ts4.2/e2ee/worker/sifPayload.d.ts +1 -0
  85. package/dist/ts4.2/frameMetadata/frameMetadata.d.ts +1 -0
  86. package/dist/ts4.2/frameMetadata/types.d.ts +1 -0
  87. package/dist/ts4.2/logger.d.ts +3 -1
  88. package/dist/ts4.2/room/PCTransport.d.ts +42 -1
  89. package/dist/ts4.2/room/PCTransportManager.d.ts +1 -0
  90. package/dist/ts4.2/room/RTCEngine.d.ts +46 -4
  91. package/dist/ts4.2/room/Room.d.ts +1 -0
  92. package/dist/ts4.2/room/data-track/frame.d.ts +1 -0
  93. package/dist/ts4.2/room/data-track/incoming/IncomingDataTrackManager.d.ts +7 -0
  94. package/dist/ts4.2/room/data-track/outgoing/types.d.ts +1 -0
  95. package/dist/ts4.2/room/data-track/packet/extensions.d.ts +1 -0
  96. package/dist/ts4.2/room/data-track/packet/index.d.ts +1 -0
  97. package/dist/ts4.2/room/data-track/packet/serializable.d.ts +1 -0
  98. package/dist/ts4.2/room/participant/LocalParticipant.d.ts +1 -0
  99. package/dist/ts4.2/room/participant/Participant.d.ts +1 -0
  100. package/dist/ts4.2/room/participant/publishUtils.d.ts +7 -0
  101. package/dist/ts4.2/room/track/LocalAudioTrack.d.ts +1 -0
  102. package/dist/ts4.2/room/track/Track.d.ts +1 -0
  103. package/dist/ts4.2/room/track/options.d.ts +11 -1
  104. package/dist/ts4.2/room/utils.d.ts +1 -0
  105. package/dist/ts4.2/test/signalToken.d.ts +26 -0
  106. package/dist/ts4.2/type-polyfills/non-shared-typed-arrays.d.ts +2 -0
  107. package/dist/ts4.2/utils/dataPacketBuffer.d.ts +19 -0
  108. package/package.json +8 -4
  109. package/src/api/SignalClient.e2e.test.ts +264 -0
  110. package/src/api/SignalClient.ts +35 -2
  111. package/src/e2ee/E2eeManager.ts +3 -1
  112. package/src/e2ee/types.ts +1 -0
  113. package/src/e2ee/utils.ts +1 -0
  114. package/src/e2ee/worker/DataCryptor.ts +1 -0
  115. package/src/e2ee/worker/FrameCryptor.test.ts +1 -0
  116. package/src/e2ee/worker/FrameCryptor.ts +1 -0
  117. package/src/e2ee/worker/ParticipantKeyHandler.test.ts +1 -0
  118. package/src/e2ee/worker/e2ee.worker.ts +1 -0
  119. package/src/e2ee/worker/naluUtils.ts +1 -0
  120. package/src/e2ee/worker/sifPayload.ts +1 -0
  121. package/src/frameMetadata/frameMetadata.test.ts +83 -0
  122. package/src/frameMetadata/frameMetadata.ts +8 -0
  123. package/src/frameMetadata/types.ts +1 -0
  124. package/src/logger.ts +2 -0
  125. package/src/room/PCTransport.test.ts +150 -0
  126. package/src/room/PCTransport.ts +179 -31
  127. package/src/room/PCTransportManager.ts +10 -6
  128. package/src/room/RTCEngine.test.ts +275 -2
  129. package/src/room/RTCEngine.ts +277 -71
  130. package/src/room/RegionUrlProvider.ts +4 -2
  131. package/src/room/Room.ts +8 -1
  132. package/src/room/data-track/depacketizer.ts +1 -0
  133. package/src/room/data-track/frame.ts +1 -0
  134. package/src/room/data-track/incoming/IncomingDataTrackManager.test.ts +303 -0
  135. package/src/room/data-track/incoming/IncomingDataTrackManager.ts +70 -8
  136. package/src/room/data-track/outgoing/OutgoingDataTrackManager.ts +2 -0
  137. package/src/room/data-track/outgoing/types.ts +1 -0
  138. package/src/room/data-track/packet/extensions.ts +1 -0
  139. package/src/room/data-track/packet/index.ts +1 -0
  140. package/src/room/data-track/packet/serializable.ts +1 -0
  141. package/src/room/errors.ts +4 -4
  142. package/src/room/participant/LocalParticipant.ts +17 -6
  143. package/src/room/participant/Participant.ts +6 -0
  144. package/src/room/participant/publishUtils.ts +14 -11
  145. package/src/room/track/LocalAudioTrack.ts +16 -0
  146. package/src/room/track/Track.ts +11 -6
  147. package/src/room/track/options.ts +11 -1
  148. package/src/room/track/record.ts +1 -0
  149. package/src/room/utils.ts +1 -0
  150. package/src/test/signalServerSetup.ts +151 -0
  151. package/src/test/signalToken.ts +59 -0
  152. package/src/test/vitest-context.d.ts +10 -0
  153. package/src/type-polyfills/{non-shared-typed-arrays.d.ts → non-shared-typed-arrays.ts} +6 -1
  154. package/src/utils/dataPacketBuffer.test.ts +100 -0
  155. package/src/utils/dataPacketBuffer.ts +48 -1
@@ -1,6 +1,7 @@
1
1
  import { Encryption_Type, TrackInfo } from '@livekit/protocol';
2
2
  import { EventEmitter } from 'events';
3
3
  import type TypedEventEmitter from 'typed-emitter';
4
+ import type { FrameMetadata } from '../frameMetadata/types';
4
5
  import { hasFrameMetadataPublishOptions } from '../frameMetadata/utils';
5
6
  import log, { LogLevel, workerLogger } from '../logger';
6
7
  import type RTCEngine from '../room/RTCEngine';
@@ -20,6 +21,7 @@ import {
20
21
  isScriptTransformSupportedForWorker,
21
22
  isVideoTrack,
22
23
  } from '../room/utils';
24
+ import type { NonSharedUint8Array } from '../type-polyfills/non-shared-typed-arrays';
23
25
  import type { BaseKeyProvider } from './KeyProvider';
24
26
  import { E2EE_FLAG } from './constants';
25
27
  import { type E2EEManagerCallbacks, EncryptionEvent, KeyProviderEvent } from './events';
@@ -246,7 +248,7 @@ export class E2EEManager
246
248
  trackId: string,
247
249
  rtpTimestamp: number,
248
250
  ssrc: number,
249
- metadata: { userTimestamp: bigint; frameId: number },
251
+ metadata: FrameMetadata,
250
252
  ) {
251
253
  if (!this.room) {
252
254
  return;
package/src/e2ee/types.ts CHANGED
@@ -2,6 +2,7 @@ import type { FrameMetadataPayload } from '../frameMetadata/frameMetadata';
2
2
  import type { FrameMetadataPublishOptions } from '../frameMetadata/types';
3
3
  import type { LogLevel } from '../logger';
4
4
  import type { VideoCodec } from '../room/track/options';
5
+ import type { NonSharedUint8Array } from '../type-polyfills/non-shared-typed-arrays';
5
6
  import type { BaseE2EEManager } from './E2eeManager';
6
7
  import type { BaseKeyProvider } from './KeyProvider';
7
8
 
package/src/e2ee/utils.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { type DataPacket, EncryptedPacketPayload } from '@livekit/protocol';
2
+ import type { NonSharedUint8Array } from '../type-polyfills/non-shared-typed-arrays';
2
3
  import { ENCRYPTION_ALGORITHM } from './constants';
3
4
  import type { KeyProviderOptions } from './types';
4
5
 
@@ -1,4 +1,5 @@
1
1
  import { workerLogger } from '../../logger';
2
+ import type { NonSharedUint8Array } from '../../type-polyfills/non-shared-typed-arrays';
2
3
  import { ENCRYPTION_ALGORITHM } from '../constants';
3
4
  import { CryptorError, CryptorErrorReason } from '../errors';
4
5
  import type { DecodeRatchetOptions, KeySet, RatchetResult } from '../types';
@@ -1,6 +1,7 @@
1
1
  import { afterEach, describe, expect, it, vitest } from 'vitest';
2
2
  import { appendPacketTrailer, extractPacketTrailer } from '../../frameMetadata/frameMetadata';
3
3
  import type { FrameMetadataPublishOptions } from '../../frameMetadata/types';
4
+ import type { NonSharedUint8Array } from '../../type-polyfills/non-shared-typed-arrays';
4
5
  import { IV_LENGTH, KEY_PROVIDER_DEFAULTS } from '../constants';
5
6
  import { CryptorEvent } from '../events';
6
7
  import type { KeyProviderOptions } from '../types';
@@ -10,6 +10,7 @@ import { hasFrameMetadataPublishOptions } from '../../frameMetadata/utils';
10
10
  import { workerLogger } from '../../logger';
11
11
  import { type VideoCodec, videoCodecs } from '../../room/track/options';
12
12
  import { mimeTypeToVideoCodecString } from '../../room/track/utils';
13
+ import type { NonSharedUint8Array } from '../../type-polyfills/non-shared-typed-arrays';
13
14
  import { ENCRYPTION_ALGORITHM, IV_LENGTH, UNENCRYPTED_BYTES } from '../constants';
14
15
  import { CryptorError, CryptorErrorReason } from '../errors';
15
16
  import { type CryptorCallbacks, CryptorEvent } from '../events';
@@ -1,4 +1,5 @@
1
1
  import { describe, expect, it, test, vitest } from 'vitest';
2
+ import type { NonSharedUint8Array } from '../../type-polyfills/non-shared-typed-arrays';
2
3
  import { ENCRYPTION_ALGORITHM, KEY_PROVIDER_DEFAULTS } from '../constants';
3
4
  import { KeyHandlerEvent } from '../events';
4
5
  import { createKeyMaterialFromString, importKey } from '../utils';
@@ -1,5 +1,6 @@
1
1
  import { workerLogger } from '../../logger';
2
2
  import type { VideoCodec } from '../../room/track/options';
3
+ import type { NonSharedUint8Array } from '../../type-polyfills/non-shared-typed-arrays';
3
4
  import { AsyncQueue } from '../../utils/AsyncQueue';
4
5
  import { KEY_PROVIDER_DEFAULTS } from '../constants';
5
6
  import { CryptorErrorReason } from '../errors';
@@ -2,6 +2,7 @@
2
2
  * NALU (Network Abstraction Layer Unit) utilities for H.264 and H.265 video processing
3
3
  * Contains functions for parsing and working with NALUs in video frames
4
4
  */
5
+ import type { NonSharedUint8Array } from '../../type-polyfills/non-shared-typed-arrays';
5
6
 
6
7
  /**
7
8
  * Mask for extracting NALU type from H.264 header byte
@@ -1,4 +1,5 @@
1
1
  import type { VideoCodec } from '../..';
2
+ import type { NonSharedUint8Array } from '../../type-polyfills/non-shared-typed-arrays';
2
3
 
3
4
  // Payload definitions taken from https://github.com/livekit/livekit/blob/master/pkg/sfu/downtrack.go#L104
4
5
 
@@ -1,11 +1,57 @@
1
1
  import { afterEach, describe, expect, it, vi } from 'vitest';
2
2
  import {
3
+ PACKET_TRAILER_ENVELOPE_SIZE,
4
+ PACKET_TRAILER_FRAME_ID_TAG,
5
+ PACKET_TRAILER_MAGIC,
6
+ PACKET_TRAILER_TIMESTAMP_TAG,
7
+ PACKET_TRAILER_USER_DATA_TAG,
3
8
  appendPacketTrailer,
4
9
  appendPacketTrailerToEncodedFrame,
5
10
  extractPacketTrailer,
6
11
  processPacketTrailer,
7
12
  } from './frameMetadata';
8
13
 
14
+ /**
15
+ * Builds a packet trailer (XORed TLVs + envelope) for the given fields. The
16
+ * SDK only writes timestamp/frameId, so this mirrors the native sender's TLV
17
+ * encoding to exercise user_data extraction.
18
+ */
19
+ function buildTrailer(
20
+ payload: Uint8Array,
21
+ fields: { userTimestamp?: bigint; frameId?: number; userData?: Uint8Array },
22
+ ): Uint8Array {
23
+ const tlvs: number[] = [];
24
+
25
+ if (fields.userTimestamp !== undefined) {
26
+ tlvs.push(PACKET_TRAILER_TIMESTAMP_TAG ^ 0xff, 8 ^ 0xff);
27
+ for (let i = 7; i >= 0; i -= 1) {
28
+ tlvs.push(Number((fields.userTimestamp >> BigInt(i * 8)) & BigInt(0xff)) ^ 0xff);
29
+ }
30
+ }
31
+
32
+ if (fields.frameId !== undefined) {
33
+ tlvs.push(PACKET_TRAILER_FRAME_ID_TAG ^ 0xff, 4 ^ 0xff);
34
+ for (let i = 3; i >= 0; i -= 1) {
35
+ tlvs.push(((fields.frameId >> (i * 8)) & 0xff) ^ 0xff);
36
+ }
37
+ }
38
+
39
+ if (fields.userData !== undefined) {
40
+ tlvs.push(PACKET_TRAILER_USER_DATA_TAG ^ 0xff, fields.userData.length ^ 0xff);
41
+ for (const byte of fields.userData) {
42
+ tlvs.push(byte ^ 0xff);
43
+ }
44
+ }
45
+
46
+ const trailerLength = tlvs.length + PACKET_TRAILER_ENVELOPE_SIZE;
47
+ const result = new Uint8Array(payload.length + trailerLength);
48
+ result.set(payload, 0);
49
+ result.set(tlvs, payload.length);
50
+ result[payload.length + tlvs.length] = trailerLength ^ 0xff;
51
+ result.set(PACKET_TRAILER_MAGIC, payload.length + tlvs.length + 1);
52
+ return result;
53
+ }
54
+
9
55
  describe('packetTrailer', () => {
10
56
  afterEach(() => {
11
57
  vi.useRealTimers();
@@ -47,6 +93,43 @@ describe('packetTrailer', () => {
47
93
  });
48
94
  });
49
95
 
96
+ it('extracts user_data from a trailer carrying only user_data', () => {
97
+ const payload = Uint8Array.from([1, 2, 3, 4]);
98
+ const userData = Uint8Array.from([0x00, 0x01, 0xfe, 0xff, 0x42]);
99
+ const trailer = buildTrailer(payload, { userData });
100
+ const extracted = extractPacketTrailer(trailer);
101
+
102
+ expect(Array.from(extracted.data)).toEqual(Array.from(payload));
103
+ expect(extracted.metadata?.userTimestamp).toBe(0n);
104
+ expect(extracted.metadata?.frameId).toBe(0);
105
+ expect(extracted.metadata?.userData).toBeInstanceOf(Uint8Array);
106
+ expect(Array.from(extracted.metadata!.userData!)).toEqual(Array.from(userData));
107
+ });
108
+
109
+ it('extracts user_data alongside timestamp and frameId', () => {
110
+ const payload = Uint8Array.from([9, 8, 7, 6, 5]);
111
+ const userData = Uint8Array.from([10, 20, 30, 40]);
112
+ const trailer = buildTrailer(payload, {
113
+ userTimestamp: 1_744_249_600_123_456n,
114
+ frameId: 42,
115
+ userData,
116
+ });
117
+ const extracted = extractPacketTrailer(trailer);
118
+
119
+ expect(Array.from(extracted.data)).toEqual(Array.from(payload));
120
+ expect(extracted.metadata?.userTimestamp).toBe(1_744_249_600_123_456n);
121
+ expect(extracted.metadata?.frameId).toBe(42);
122
+ expect(Array.from(extracted.metadata!.userData!)).toEqual(Array.from(userData));
123
+ });
124
+
125
+ it('leaves userData undefined when no user_data tag is present', () => {
126
+ const payload = Uint8Array.from([1, 2, 3, 4]);
127
+ const trailer = appendPacketTrailer(payload, 1_744_249_600_123_456n, 42);
128
+ const extracted = extractPacketTrailer(trailer);
129
+
130
+ expect(extracted.metadata?.userData).toBeUndefined();
131
+ });
132
+
50
133
  it('returns data unchanged when both timestamp and frameId are 0', () => {
51
134
  const payload = Uint8Array.from([1, 2, 3, 4]);
52
135
  const result = appendPacketTrailer(payload, 0n, 0);
@@ -10,6 +10,7 @@ export const PACKET_TRAILER_MAGIC = Uint8Array.from([
10
10
 
11
11
  export const PACKET_TRAILER_TIMESTAMP_TAG = 0x01;
12
12
  export const PACKET_TRAILER_FRAME_ID_TAG = 0x02;
13
+ export const PACKET_TRAILER_USER_DATA_TAG = 0x03;
13
14
  export const PACKET_TRAILER_ENVELOPE_SIZE = 5;
14
15
 
15
16
  const TIMESTAMP_TLV_SIZE = 10;
@@ -127,6 +128,13 @@ export function extractPacketTrailer(data: ArrayBuffer | Uint8Array): ExtractPac
127
128
  } else if (tag === PACKET_TRAILER_FRAME_ID_TAG && length === 4) {
128
129
  metadata.frameId = readUint32Xor(bytes, offset, length);
129
130
  foundAny = true;
131
+ } else if (tag === PACKET_TRAILER_USER_DATA_TAG) {
132
+ const userData = new Uint8Array(length);
133
+ for (let index = 0; index < length; index += 1) {
134
+ userData[index] = bytes[offset + index] ^ 0xff;
135
+ }
136
+ metadata.userData = userData;
137
+ foundAny = true;
130
138
  }
131
139
 
132
140
  offset += length;
@@ -3,6 +3,7 @@ import type { FrameMetadataPayload } from './frameMetadata';
3
3
  export interface FrameMetadata {
4
4
  userTimestamp: bigint;
5
5
  frameId: number;
6
+ userData?: Uint8Array;
6
7
  }
7
8
 
8
9
  /** @deprecated Use {@link FrameMetadata} instead. */
package/src/logger.ts CHANGED
@@ -22,6 +22,8 @@ export enum LoggerNames {
22
22
  PCTransport = 'livekit-pc-transport',
23
23
  E2EE = 'lk-e2ee',
24
24
  DataTracks = 'livekit-data-tracks',
25
+ Region = 'livekit-region',
26
+ ICE = 'livekit-ice',
25
27
  }
26
28
 
27
29
  type LogLevelString = keyof typeof LogLevel;
@@ -0,0 +1,150 @@
1
+ import { type MediaDescription, parse } from 'sdp-transform';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { conformBundledCodecFmtp, placeholderMidsFromTransceivers } from './PCTransport';
4
+
5
+ /** Parse the `key[=value]` pairs of an fmtp config into a comparable set. */
6
+ const paramSet = (config: string) => new Set(config.split(';').filter(Boolean));
7
+
8
+ const fmtpOf = (media: MediaDescription[], mid: string, payload: number) => {
9
+ const m = media.find((section) => `${section.mid}` === mid)!;
10
+ return m.fmtp.find((f) => f.payload === payload)?.config;
11
+ };
12
+
13
+ /** Predicate for conformBundledCodecFmtp based on an explicit set of mids. */
14
+ const placeholders = (mids: string[]) => {
15
+ const set = new Set(mids);
16
+ return (m: MediaDescription) => set.has(`${m.mid}`);
17
+ };
18
+
19
+ // One publisher bundle: a published mic + camera (real sends, with msid/ssrc)
20
+ // and their pre-populated recvonly placeholders.
21
+ const PUBLISHER_SDP = `v=0
22
+ o=- 0 0 IN IP4 127.0.0.1
23
+ s=-
24
+ t=0 0
25
+ a=group:BUNDLE 0 1 2 3
26
+ m=audio 9 UDP/TLS/RTP/SAVPF 111
27
+ c=IN IP4 0.0.0.0
28
+ a=mid:0
29
+ a=sendonly
30
+ a=msid:s mic
31
+ a=ssrc:1111 cname:a
32
+ a=rtpmap:111 opus/48000/2
33
+ a=fmtp:111 minptime=10;useinbandfec=1;usedtx=1
34
+ m=audio 9 UDP/TLS/RTP/SAVPF 111
35
+ c=IN IP4 0.0.0.0
36
+ a=mid:1
37
+ a=recvonly
38
+ a=rtpmap:111 opus/48000/2
39
+ a=fmtp:111 minptime=10;useinbandfec=1
40
+ m=video 9 UDP/TLS/RTP/SAVPF 49
41
+ c=IN IP4 0.0.0.0
42
+ a=mid:2
43
+ a=sendonly
44
+ a=msid:s cam
45
+ a=ssrc:2222 cname:b
46
+ a=rtpmap:49 H265/90000
47
+ a=fmtp:49 level-id=186;profile-id=1;tier-flag=0;tx-mode=SRST
48
+ m=video 9 UDP/TLS/RTP/SAVPF 49
49
+ c=IN IP4 0.0.0.0
50
+ a=mid:3
51
+ a=recvonly
52
+ a=rtpmap:49 H265/90000
53
+ a=fmtp:49 level-id=180;profile-id=1;tier-flag=0;tx-mode=SRST`;
54
+
55
+ describe('placeholderMidsFromTransceivers', () => {
56
+ const tr = (mid: string | null, track: unknown) =>
57
+ ({ mid, sender: { track } }) as unknown as RTCRtpTransceiver;
58
+
59
+ it('includes transceivers with a mid and no outgoing track', () => {
60
+ const mids = placeholderMidsFromTransceivers([
61
+ tr('0', {}), // real send
62
+ tr('1', null), // pre-populated placeholder
63
+ tr('2', null), // reverted after unpublish (msid may linger in SDP, but no track)
64
+ tr(null, null), // not yet negotiated — no mid, excluded
65
+ ]);
66
+ expect(mids).toEqual(new Set(['1', '2']));
67
+ });
68
+ });
69
+
70
+ describe('conformBundledCodecFmtp', () => {
71
+ it('copies opus and H.265 fmtp from real sections onto placeholders', () => {
72
+ const { media } = parse(PUBLISHER_SDP);
73
+
74
+ conformBundledCodecFmtp(media, placeholders(['1', '3']));
75
+
76
+ expect(fmtpOf(media, '1', 111)).toBe(fmtpOf(media, '0', 111));
77
+ expect(paramSet(fmtpOf(media, '1', 111)!)).toContain('usedtx=1');
78
+ expect(fmtpOf(media, '3', 49)).toBe(fmtpOf(media, '2', 49));
79
+ expect(paramSet(fmtpOf(media, '3', 49)!)).toContain('level-id=186');
80
+ });
81
+
82
+ it('never rewrites the real (non-placeholder) sections', () => {
83
+ const { media } = parse(PUBLISHER_SDP);
84
+
85
+ conformBundledCodecFmtp(media, placeholders(['1', '3']));
86
+
87
+ expect(fmtpOf(media, '0', 111)).toBe('minptime=10;useinbandfec=1;usedtx=1');
88
+ expect(paramSet(fmtpOf(media, '2', 49)!)).toContain('level-id=186');
89
+ });
90
+
91
+ it('converges divergent placeholders even when no real section declares the payload', () => {
92
+ // Two placeholders that disagree (a reverted one kept level 186, a fresh one
93
+ // has 180) with no active send present.
94
+ const { media } = parse(`v=0
95
+ o=- 0 0 IN IP4 127.0.0.1
96
+ s=-
97
+ t=0 0
98
+ a=group:BUNDLE 10 11
99
+ m=video 9 UDP/TLS/RTP/SAVPF 49
100
+ a=mid:10
101
+ a=rtpmap:49 H265/90000
102
+ a=fmtp:49 level-id=186;profile-id=1;tier-flag=0;tx-mode=SRST
103
+ m=video 9 UDP/TLS/RTP/SAVPF 49
104
+ a=mid:11
105
+ a=rtpmap:49 H265/90000
106
+ a=fmtp:49 level-id=180;profile-id=1;tier-flag=0;tx-mode=SRST`);
107
+
108
+ conformBundledCodecFmtp(media, placeholders(['10', '11']));
109
+
110
+ expect(fmtpOf(media, '10', 49)).toBe(fmtpOf(media, '11', 49));
111
+ });
112
+
113
+ it('conforms a reverted section that kept its msid (second-publish case)', () => {
114
+ // The real second publish: a live camera send (level 186), plus the section
115
+ // left over from the first (unpublished) track. Chrome keeps that section's
116
+ // `a=msid`/`a=ssrc` even though it no longer sends, so it must be identified
117
+ // as a placeholder by its transceiver (no track), not by SDP.
118
+ const { media } = parse(`v=0
119
+ o=- 0 0 IN IP4 127.0.0.1
120
+ s=-
121
+ t=0 0
122
+ a=group:BUNDLE 3 4 5
123
+ m=video 9 UDP/TLS/RTP/SAVPF 49
124
+ a=mid:3
125
+ a=sendonly
126
+ a=msid:s cam
127
+ a=ssrc:9 cname:c
128
+ a=rtpmap:49 H265/90000
129
+ a=fmtp:49 level-id=186;profile-id=1;tier-flag=0;tx-mode=SRST
130
+ m=video 9 UDP/TLS/RTP/SAVPF 49
131
+ a=mid:4
132
+ a=sendonly
133
+ a=msid:s stale
134
+ a=ssrc:8 cname:d
135
+ a=rtpmap:49 H265/90000
136
+ a=fmtp:49 level-id=180;profile-id=1;tier-flag=0;tx-mode=SRST
137
+ m=video 9 UDP/TLS/RTP/SAVPF 49
138
+ a=mid:5
139
+ a=recvonly
140
+ a=rtpmap:49 H265/90000
141
+ a=fmtp:49 level-id=180;profile-id=1;tier-flag=0;tx-mode=SRST`);
142
+
143
+ // mid 4 still has msid/ssrc in the SDP, but its transceiver has no track.
144
+ conformBundledCodecFmtp(media, placeholders(['4', '5']));
145
+
146
+ expect(paramSet(fmtpOf(media, '3', 49)!)).toContain('level-id=186'); // real send untouched
147
+ expect(fmtpOf(media, '4', 49)).toBe(fmtpOf(media, '3', 49)); // stale conformed
148
+ expect(fmtpOf(media, '5', 49)).toBe(fmtpOf(media, '3', 49)); // placeholder conformed
149
+ });
150
+ });