livekit-client 2.18.9 → 2.18.10

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 (184) 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 +5609 -644
  4. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  5. package/dist/livekit-client.esm.mjs +2870 -2420
  6. package/dist/livekit-client.esm.mjs.map +1 -1
  7. package/dist/livekit-client.pt.worker.js +2 -0
  8. package/dist/livekit-client.pt.worker.js.map +1 -0
  9. package/dist/livekit-client.pt.worker.mjs +5834 -0
  10. package/dist/livekit-client.pt.worker.mjs.map +1 -0
  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 +2 -1
  14. package/dist/src/api/SignalClient.d.ts.map +1 -1
  15. package/dist/src/e2ee/E2eeManager.d.ts +8 -7
  16. package/dist/src/e2ee/E2eeManager.d.ts.map +1 -1
  17. package/dist/src/e2ee/types.d.ts +35 -8
  18. package/dist/src/e2ee/types.d.ts.map +1 -1
  19. package/dist/src/e2ee/utils.d.ts +5 -5
  20. package/dist/src/e2ee/utils.d.ts.map +1 -1
  21. package/dist/src/e2ee/worker/DataCryptor.d.ts +5 -5
  22. package/dist/src/e2ee/worker/DataCryptor.d.ts.map +1 -1
  23. package/dist/src/e2ee/worker/FrameCryptor.d.ts +21 -4
  24. package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -1
  25. package/dist/src/e2ee/worker/naluUtils.d.ts +1 -1
  26. package/dist/src/e2ee/worker/naluUtils.d.ts.map +1 -1
  27. package/dist/src/e2ee/worker/sifPayload.d.ts +7 -7
  28. package/dist/src/e2ee/worker/sifPayload.d.ts.map +1 -1
  29. package/dist/src/index.d.ts +4 -1
  30. package/dist/src/index.d.ts.map +1 -1
  31. package/dist/src/options.d.ts +7 -0
  32. package/dist/src/options.d.ts.map +1 -1
  33. package/dist/src/packetTrailer/PacketTrailerManager.d.ts +49 -0
  34. package/dist/src/packetTrailer/PacketTrailerManager.d.ts.map +1 -0
  35. package/dist/src/packetTrailer/packetTrailer.d.ts +32 -0
  36. package/dist/src/packetTrailer/packetTrailer.d.ts.map +1 -0
  37. package/dist/src/packetTrailer/types.d.ts +57 -0
  38. package/dist/src/packetTrailer/types.d.ts.map +1 -0
  39. package/dist/src/packetTrailer/utils.d.ts +9 -0
  40. package/dist/src/packetTrailer/utils.d.ts.map +1 -0
  41. package/dist/src/packetTrailer/worker/packetTrailer.worker.d.ts +2 -0
  42. package/dist/src/packetTrailer/worker/packetTrailer.worker.d.ts.map +1 -0
  43. package/dist/src/room/RTCEngine.d.ts +2 -1
  44. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  45. package/dist/src/room/Room.d.ts +3 -1
  46. package/dist/src/room/Room.d.ts.map +1 -1
  47. package/dist/src/room/data-track/RemoteDataTrack.d.ts +5 -1
  48. package/dist/src/room/data-track/RemoteDataTrack.d.ts.map +1 -1
  49. package/dist/src/room/data-track/depacketizer.d.ts +12 -4
  50. package/dist/src/room/data-track/depacketizer.d.ts.map +1 -1
  51. package/dist/src/room/data-track/frame.d.ts +3 -3
  52. package/dist/src/room/data-track/frame.d.ts.map +1 -1
  53. package/dist/src/room/data-track/incoming/IncomingDataTrackManager.d.ts +3 -1
  54. package/dist/src/room/data-track/incoming/IncomingDataTrackManager.d.ts.map +1 -1
  55. package/dist/src/room/data-track/incoming/pipeline.d.ts +4 -1
  56. package/dist/src/room/data-track/incoming/pipeline.d.ts.map +1 -1
  57. package/dist/src/room/data-track/outgoing/types.d.ts +2 -2
  58. package/dist/src/room/data-track/outgoing/types.d.ts.map +1 -1
  59. package/dist/src/room/data-track/packet/extensions.d.ts +4 -4
  60. package/dist/src/room/data-track/packet/extensions.d.ts.map +1 -1
  61. package/dist/src/room/data-track/packet/index.d.ts +5 -5
  62. package/dist/src/room/data-track/packet/index.d.ts.map +1 -1
  63. package/dist/src/room/data-track/packet/serializable.d.ts +1 -1
  64. package/dist/src/room/data-track/packet/serializable.d.ts.map +1 -1
  65. package/dist/src/room/data-track/types.d.ts +7 -0
  66. package/dist/src/room/data-track/types.d.ts.map +1 -1
  67. package/dist/src/room/events.d.ts +2 -2
  68. package/dist/src/room/participant/LocalParticipant.d.ts +3 -1
  69. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  70. package/dist/src/room/participant/Participant.d.ts +1 -1
  71. package/dist/src/room/participant/Participant.d.ts.map +1 -1
  72. package/dist/src/room/track/PacketTrailerExtractor.d.ts +19 -0
  73. package/dist/src/room/track/PacketTrailerExtractor.d.ts.map +1 -0
  74. package/dist/src/room/track/RemoteVideoTrack.d.ts +16 -0
  75. package/dist/src/room/track/RemoteVideoTrack.d.ts.map +1 -1
  76. package/dist/src/room/track/Track.d.ts +1 -1
  77. package/dist/src/room/track/Track.d.ts.map +1 -1
  78. package/dist/src/room/track/create.d.ts.map +1 -1
  79. package/dist/src/room/track/options.d.ts +10 -0
  80. package/dist/src/room/track/options.d.ts.map +1 -1
  81. package/dist/src/room/track/utils.d.ts.map +1 -1
  82. package/dist/src/room/utils.d.ts +4 -3
  83. package/dist/src/room/utils.d.ts.map +1 -1
  84. package/dist/src/test/MockMediaStreamTrack.d.ts.map +1 -1
  85. package/dist/src/utils/dataPacketBuffer.d.ts +1 -1
  86. package/dist/src/utils/dataPacketBuffer.d.ts.map +1 -1
  87. package/dist/src/version.d.ts +1 -1
  88. package/dist/ts4.2/api/SignalClient.d.ts +2 -1
  89. package/dist/ts4.2/e2ee/E2eeManager.d.ts +8 -7
  90. package/dist/ts4.2/e2ee/types.d.ts +35 -8
  91. package/dist/ts4.2/e2ee/utils.d.ts +5 -5
  92. package/dist/ts4.2/e2ee/worker/DataCryptor.d.ts +5 -5
  93. package/dist/ts4.2/e2ee/worker/FrameCryptor.d.ts +21 -4
  94. package/dist/ts4.2/e2ee/worker/naluUtils.d.ts +1 -1
  95. package/dist/ts4.2/e2ee/worker/sifPayload.d.ts +7 -7
  96. package/dist/ts4.2/index.d.ts +5 -1
  97. package/dist/ts4.2/options.d.ts +7 -0
  98. package/dist/ts4.2/packetTrailer/PacketTrailerManager.d.ts +49 -0
  99. package/dist/ts4.2/packetTrailer/packetTrailer.d.ts +32 -0
  100. package/dist/ts4.2/packetTrailer/types.d.ts +57 -0
  101. package/dist/ts4.2/packetTrailer/utils.d.ts +9 -0
  102. package/dist/ts4.2/packetTrailer/worker/packetTrailer.worker.d.ts +2 -0
  103. package/dist/ts4.2/room/RTCEngine.d.ts +2 -1
  104. package/dist/ts4.2/room/Room.d.ts +3 -1
  105. package/dist/ts4.2/room/data-track/RemoteDataTrack.d.ts +5 -1
  106. package/dist/ts4.2/room/data-track/depacketizer.d.ts +12 -4
  107. package/dist/ts4.2/room/data-track/frame.d.ts +3 -3
  108. package/dist/ts4.2/room/data-track/incoming/IncomingDataTrackManager.d.ts +3 -1
  109. package/dist/ts4.2/room/data-track/incoming/pipeline.d.ts +4 -1
  110. package/dist/ts4.2/room/data-track/outgoing/types.d.ts +2 -2
  111. package/dist/ts4.2/room/data-track/packet/extensions.d.ts +4 -4
  112. package/dist/ts4.2/room/data-track/packet/index.d.ts +5 -5
  113. package/dist/ts4.2/room/data-track/packet/serializable.d.ts +1 -1
  114. package/dist/ts4.2/room/data-track/types.d.ts +7 -0
  115. package/dist/ts4.2/room/events.d.ts +2 -2
  116. package/dist/ts4.2/room/participant/LocalParticipant.d.ts +3 -1
  117. package/dist/ts4.2/room/participant/Participant.d.ts +1 -1
  118. package/dist/ts4.2/room/track/PacketTrailerExtractor.d.ts +19 -0
  119. package/dist/ts4.2/room/track/RemoteVideoTrack.d.ts +16 -0
  120. package/dist/ts4.2/room/track/Track.d.ts +1 -1
  121. package/dist/ts4.2/room/track/options.d.ts +10 -0
  122. package/dist/ts4.2/room/utils.d.ts +4 -3
  123. package/dist/ts4.2/utils/dataPacketBuffer.d.ts +1 -1
  124. package/dist/ts4.2/version.d.ts +1 -1
  125. package/package.json +24 -16
  126. package/src/api/SignalClient.test.ts +102 -10
  127. package/src/api/SignalClient.ts +4 -2
  128. package/src/api/WebSocketStream.test.ts +0 -1
  129. package/src/e2ee/E2eeManager.ts +82 -30
  130. package/src/e2ee/types.ts +37 -8
  131. package/src/e2ee/utils.ts +7 -6
  132. package/src/e2ee/worker/DataCryptor.ts +6 -6
  133. package/src/e2ee/worker/FrameCryptor.test.ts +177 -4
  134. package/src/e2ee/worker/FrameCryptor.ts +94 -14
  135. package/src/e2ee/worker/ParticipantKeyHandler.test.ts +4 -4
  136. package/src/e2ee/worker/e2ee.worker.ts +13 -5
  137. package/src/e2ee/worker/naluUtils.ts +4 -4
  138. package/src/e2ee/worker/sifPayload.ts +10 -8
  139. package/src/index.ts +7 -0
  140. package/src/options.ts +8 -0
  141. package/src/packetTrailer/PacketTrailerManager.test.ts +172 -0
  142. package/src/packetTrailer/PacketTrailerManager.ts +250 -0
  143. package/src/packetTrailer/packetTrailer.test.ts +174 -0
  144. package/src/packetTrailer/packetTrailer.ts +276 -0
  145. package/src/packetTrailer/types.ts +75 -0
  146. package/src/packetTrailer/utils.test.ts +105 -0
  147. package/src/packetTrailer/utils.ts +50 -0
  148. package/src/packetTrailer/worker/packetTrailer.worker.ts +155 -0
  149. package/src/packetTrailer/worker/tsconfig.json +14 -0
  150. package/src/room/BackOffStrategy.test.ts +1 -1
  151. package/src/room/RTCEngine.test.ts +219 -0
  152. package/src/room/RTCEngine.ts +86 -20
  153. package/src/room/Room.test.ts +62 -1
  154. package/src/room/Room.ts +28 -5
  155. package/src/room/data-track/RemoteDataTrack.ts +8 -1
  156. package/src/room/data-track/depacketizer.test.ts +433 -1
  157. package/src/room/data-track/depacketizer.ts +79 -61
  158. package/src/room/data-track/frame.ts +2 -2
  159. package/src/room/data-track/incoming/IncomingDataTrackManager.test.ts +194 -0
  160. package/src/room/data-track/incoming/IncomingDataTrackManager.ts +21 -1
  161. package/src/room/data-track/incoming/pipeline.ts +13 -2
  162. package/src/room/data-track/outgoing/types.ts +3 -2
  163. package/src/room/data-track/packet/extensions.ts +2 -2
  164. package/src/room/data-track/packet/index.ts +6 -6
  165. package/src/room/data-track/packet/serializable.ts +1 -1
  166. package/src/room/data-track/types.ts +8 -0
  167. package/src/room/events.ts +2 -2
  168. package/src/room/participant/LocalParticipant.test.ts +81 -0
  169. package/src/room/participant/LocalParticipant.ts +48 -7
  170. package/src/room/participant/Participant.ts +1 -1
  171. package/src/room/participant/publishUtils.ts +1 -1
  172. package/src/room/track/PacketTrailerExtractor.ts +43 -0
  173. package/src/room/track/RemoteVideoTrack.ts +23 -2
  174. package/src/room/track/Track.ts +1 -1
  175. package/src/room/track/create.ts +0 -4
  176. package/src/room/track/options.ts +11 -0
  177. package/src/room/track/record.ts +1 -1
  178. package/src/room/track/utils.ts +4 -1
  179. package/src/room/utils.test.ts +14 -1
  180. package/src/room/utils.ts +17 -3
  181. package/src/test/MockMediaStreamTrack.ts +0 -1
  182. package/src/type-polyfills/non-shared-typed-arrays.d.ts +6 -0
  183. package/src/utils/dataPacketBuffer.ts +1 -1
  184. package/src/version.ts +1 -1
@@ -0,0 +1,155 @@
1
+ import { appendPacketTrailerToEncodedFrame, processPacketTrailer } from '../packetTrailer';
2
+ import type {
3
+ PTMetadataMessage,
4
+ PTScriptTransformOptions,
5
+ PTWorkerMessage,
6
+ PacketTrailerPublishOptions,
7
+ } from '../types';
8
+ import { hasPacketTrailerPublishOptions } from '../utils';
9
+
10
+ /**
11
+ * Holds the trackId currently associated with a pipeline. A mutable
12
+ * wrapper is used so the transform closure always reads the latest
13
+ * trackId after a receiver gets re-bound to a new track.
14
+ */
15
+ interface PipelineState {
16
+ trackId: string;
17
+ hasPacketTrailer: boolean;
18
+ }
19
+
20
+ const pipelines = new Map<string, PipelineState>();
21
+
22
+ onmessage = (ev: MessageEvent<PTWorkerMessage>) => {
23
+ const msg = ev.data;
24
+
25
+ switch (msg.kind) {
26
+ case 'init':
27
+ postMessage({ kind: 'initAck' });
28
+ break;
29
+
30
+ case 'decode':
31
+ setupDecodeTransform(
32
+ msg.data.readableStream,
33
+ msg.data.writableStream,
34
+ msg.data.trackId,
35
+ msg.data.hasPacketTrailer,
36
+ );
37
+ break;
38
+
39
+ case 'encode':
40
+ setupEncodeTransform(
41
+ msg.data.readableStream,
42
+ msg.data.writableStream,
43
+ msg.data.packetTrailer,
44
+ );
45
+ break;
46
+
47
+ case 'updateTrackId':
48
+ updateTrackId(msg.data.oldTrackId, msg.data.newTrackId, msg.data.hasPacketTrailer);
49
+ break;
50
+
51
+ default:
52
+ break;
53
+ }
54
+ };
55
+
56
+ function setupDecodeTransform(
57
+ readable: ReadableStream,
58
+ writable: WritableStream,
59
+ trackId: string,
60
+ hasPacketTrailer: boolean,
61
+ ) {
62
+ const state: PipelineState = { trackId, hasPacketTrailer };
63
+ pipelines.set(trackId, state);
64
+
65
+ const transform = new TransformStream({
66
+ transform(
67
+ frame: RTCEncodedVideoFrame,
68
+ controller: TransformStreamDefaultController<RTCEncodedVideoFrame>,
69
+ ) {
70
+ try {
71
+ if (state.hasPacketTrailer) {
72
+ const result = processPacketTrailer(frame, state.trackId);
73
+ if (result.data) {
74
+ frame.data = result.data;
75
+ }
76
+ if (result.payload) {
77
+ const msg: PTMetadataMessage = { kind: 'metadata', data: result.payload };
78
+ postMessage(msg);
79
+ }
80
+ }
81
+ } catch {
82
+ // Never drop frames on trailer-extraction failure — pass through so
83
+ // video keeps decoding even if metadata is lost for this frame.
84
+ }
85
+ controller.enqueue(frame);
86
+ },
87
+ });
88
+
89
+ readable
90
+ .pipeThrough(transform)
91
+ .pipeTo(writable)
92
+ .catch(() => {
93
+ pipelines.delete(state.trackId);
94
+ });
95
+ }
96
+
97
+ function setupEncodeTransform(
98
+ readable: ReadableStream,
99
+ writable: WritableStream,
100
+ packetTrailer?: PacketTrailerPublishOptions,
101
+ ) {
102
+ if (!hasPacketTrailerPublishOptions(packetTrailer)) {
103
+ readable.pipeTo(writable).catch(() => {});
104
+ return;
105
+ }
106
+
107
+ let frameId = 0;
108
+ const transform = new TransformStream({
109
+ transform(
110
+ frame: RTCEncodedVideoFrame,
111
+ controller: TransformStreamDefaultController<RTCEncodedVideoFrame>,
112
+ ) {
113
+ try {
114
+ if (packetTrailer?.frameId) {
115
+ frameId = frameId === 0xffffffff ? 1 : frameId + 1;
116
+ }
117
+ appendPacketTrailerToEncodedFrame(frame, packetTrailer, frameId);
118
+ } catch {
119
+ // Never drop frames on trailer-write failure.
120
+ }
121
+ controller.enqueue(frame);
122
+ },
123
+ });
124
+
125
+ readable
126
+ .pipeThrough(transform)
127
+ .pipeTo(writable)
128
+ .catch(() => {});
129
+ }
130
+
131
+ function updateTrackId(oldTrackId: string, newTrackId: string, hasPacketTrailer: boolean) {
132
+ const state = pipelines.get(oldTrackId);
133
+ if (state) {
134
+ state.trackId = newTrackId;
135
+ state.hasPacketTrailer = hasPacketTrailer;
136
+ pipelines.delete(oldTrackId);
137
+ pipelines.set(newTrackId, state);
138
+ }
139
+ }
140
+
141
+ // Operations using RTCRtpScriptTransform.
142
+ // @ts-ignore
143
+ if (self.RTCTransformEvent) {
144
+ // @ts-ignore
145
+ self.onrtctransform = (event: RTCTransformEvent) => {
146
+ // @ts-ignore
147
+ const transformer = event.transformer;
148
+ const options = transformer.options as PTScriptTransformOptions;
149
+ if (options.kind === 'encode') {
150
+ setupEncodeTransform(transformer.readable, transformer.writable, options.packetTrailer);
151
+ } else {
152
+ setupDecodeTransform(transformer.readable, transformer.writable, options.trackId, true);
153
+ }
154
+ };
155
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "extends": "../../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "lib": [
5
+ "DOM",
6
+ "DOM.Iterable",
7
+ "ES2017",
8
+ "ES2018.Promise",
9
+ "WebWorker",
10
+ "ES2021.WeakRef",
11
+ "DOM.AsyncIterable"
12
+ ]
13
+ }
14
+ }
@@ -6,7 +6,7 @@ vi.mock('./utils', async () => {
6
6
  const actual = await vi.importActual('./utils');
7
7
  return {
8
8
  ...actual,
9
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
9
+
10
10
  sleep: vi.fn((ms: number) => Promise.resolve()),
11
11
  extractProjectFromUrl: vi.fn((url: URL) => {
12
12
  // @ts-ignore
@@ -0,0 +1,219 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+ import RTCEngine from './RTCEngine';
3
+ import { roomOptionDefaults } from './defaults';
4
+
5
+ describe('RTCEngine', () => {
6
+ const originalRTCRtpSender = window.RTCRtpSender;
7
+ const originalRTCRtpScriptTransform = (window as unknown as { RTCRtpScriptTransform?: unknown })
8
+ .RTCRtpScriptTransform;
9
+ const originalUserAgent = navigator.userAgent;
10
+
11
+ afterEach(() => {
12
+ Object.defineProperty(window, 'RTCRtpSender', {
13
+ configurable: true,
14
+ value: originalRTCRtpSender,
15
+ writable: true,
16
+ });
17
+ Object.defineProperty(window, 'RTCRtpScriptTransform', {
18
+ configurable: true,
19
+ value: originalRTCRtpScriptTransform,
20
+ writable: true,
21
+ });
22
+ Object.defineProperty(window.navigator, 'userAgent', {
23
+ configurable: true,
24
+ value: originalUserAgent,
25
+ });
26
+ });
27
+
28
+ function stubInsertableStreamsSupport() {
29
+ class MockRTCRtpSender {
30
+ createEncodedStreams() {}
31
+ }
32
+
33
+ Object.defineProperty(window, 'RTCRtpSender', {
34
+ configurable: true,
35
+ value: MockRTCRtpSender,
36
+ writable: true,
37
+ });
38
+ }
39
+
40
+ function stubScriptTransformSupport() {
41
+ Object.defineProperty(window, 'RTCRtpScriptTransform', {
42
+ configurable: true,
43
+ value: class MockRTCRtpScriptTransform {},
44
+ writable: true,
45
+ });
46
+ Object.defineProperty(window.navigator, 'userAgent', {
47
+ configurable: true,
48
+ value:
49
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_0) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15',
50
+ });
51
+ }
52
+
53
+ function makeRTCConfiguration(engine: RTCEngine) {
54
+ return (
55
+ engine as unknown as { makeRTCConfiguration: () => RTCConfiguration }
56
+ ).makeRTCConfiguration();
57
+ }
58
+
59
+ function setupPacketTrailerSender(engine: RTCEngine, sender: RTCRtpSender, opts = {}) {
60
+ (
61
+ engine as unknown as {
62
+ setupPacketTrailerSender: (sender: RTCRtpSender, opts?: unknown) => void;
63
+ }
64
+ ).setupPacketTrailerSender(sender, opts);
65
+ }
66
+
67
+ it('does not enable encoded insertable streams without E2EE or a packet trailer worker', () => {
68
+ stubInsertableStreamsSupport();
69
+
70
+ const engine = new RTCEngine(roomOptionDefaults);
71
+
72
+ expect(makeRTCConfiguration(engine).encodedInsertableStreams).toBeUndefined();
73
+ });
74
+
75
+ it('enables encoded insertable streams when a packet trailer worker is configured', () => {
76
+ stubInsertableStreamsSupport();
77
+
78
+ const engine = new RTCEngine({
79
+ ...roomOptionDefaults,
80
+ packetTrailer: { worker: {} as Worker },
81
+ });
82
+
83
+ expect(makeRTCConfiguration(engine).encodedInsertableStreams).toBe(true);
84
+ });
85
+
86
+ it('does not enable encoded insertable streams for packet trailers when script transforms are supported', () => {
87
+ stubInsertableStreamsSupport();
88
+ stubScriptTransformSupport();
89
+
90
+ const engine = new RTCEngine({
91
+ ...roomOptionDefaults,
92
+ packetTrailer: { worker: {} as Worker },
93
+ });
94
+
95
+ expect(makeRTCConfiguration(engine).encodedInsertableStreams).toBeUndefined();
96
+ });
97
+
98
+ it('enables encoded insertable streams for E2EE', () => {
99
+ stubInsertableStreamsSupport();
100
+
101
+ const engine = new RTCEngine(roomOptionDefaults);
102
+ (
103
+ engine as unknown as {
104
+ signalOpts: {
105
+ autoSubscribe: boolean;
106
+ maxRetries: number;
107
+ e2eeEnabled: boolean;
108
+ websocketTimeout: number;
109
+ };
110
+ }
111
+ ).signalOpts = {
112
+ autoSubscribe: true,
113
+ maxRetries: 1,
114
+ e2eeEnabled: true,
115
+ websocketTimeout: 15_000,
116
+ };
117
+
118
+ expect(makeRTCConfiguration(engine).encodedInsertableStreams).toBe(true);
119
+ });
120
+
121
+ it('does not create sender encoded streams when packetTrailer has no worker', () => {
122
+ const engine = new RTCEngine({
123
+ ...roomOptionDefaults,
124
+ packetTrailer: {} as never,
125
+ });
126
+ const createEncodedStreams = vi.fn();
127
+ const sender = {
128
+ createEncodedStreams,
129
+ } as unknown as RTCRtpSender;
130
+
131
+ setupPacketTrailerSender(engine, sender);
132
+
133
+ expect(createEncodedStreams).not.toHaveBeenCalled();
134
+ });
135
+
136
+ it('does not create sender passthrough streams for packet trailers when script transforms are supported', () => {
137
+ stubScriptTransformSupport();
138
+
139
+ const engine = new RTCEngine({
140
+ ...roomOptionDefaults,
141
+ packetTrailer: { worker: {} as Worker },
142
+ });
143
+ const createEncodedStreams = vi.fn();
144
+ const sender = {
145
+ createEncodedStreams,
146
+ } as unknown as RTCRtpSender;
147
+
148
+ setupPacketTrailerSender(engine, sender);
149
+
150
+ expect(createEncodedStreams).not.toHaveBeenCalled();
151
+ });
152
+
153
+ it('posts sender encode streams to the packet trailer worker when write features are enabled', () => {
154
+ stubInsertableStreamsSupport();
155
+
156
+ const worker = { postMessage: vi.fn() } as unknown as Worker;
157
+ const engine = new RTCEngine({
158
+ ...roomOptionDefaults,
159
+ packetTrailer: { worker },
160
+ });
161
+ const readable = {} as ReadableStream;
162
+ const writable = {} as WritableStream;
163
+ const createEncodedStreams = vi.fn(() => ({ readable, writable }));
164
+ const sender = {
165
+ createEncodedStreams,
166
+ } as unknown as RTCRtpSender;
167
+
168
+ setupPacketTrailerSender(engine, sender, { packetTrailer: { timestamp: true, frameId: true } });
169
+
170
+ expect(createEncodedStreams).toHaveBeenCalledTimes(1);
171
+ expect(worker.postMessage).toHaveBeenCalledWith(
172
+ {
173
+ kind: 'encode',
174
+ data: {
175
+ readableStream: readable,
176
+ writableStream: writable,
177
+ packetTrailer: { timestamp: true, frameId: true },
178
+ },
179
+ },
180
+ [readable, writable],
181
+ );
182
+ });
183
+
184
+ it('uses RTCRtpScriptTransform for sender packet trailer writes when supported', () => {
185
+ stubScriptTransformSupport();
186
+
187
+ const transform = {};
188
+ const RTCRtpScriptTransform = vi.fn(() => transform);
189
+ Object.defineProperty(window, 'RTCRtpScriptTransform', {
190
+ configurable: true,
191
+ value: RTCRtpScriptTransform,
192
+ writable: true,
193
+ });
194
+ Object.defineProperty(globalThis, 'RTCRtpScriptTransform', {
195
+ configurable: true,
196
+ value: RTCRtpScriptTransform,
197
+ writable: true,
198
+ });
199
+
200
+ const worker = {} as Worker;
201
+ const engine = new RTCEngine({
202
+ ...roomOptionDefaults,
203
+ packetTrailer: { worker },
204
+ });
205
+ const createEncodedStreams = vi.fn();
206
+ const sender = {
207
+ createEncodedStreams,
208
+ } as unknown as RTCRtpSender;
209
+
210
+ setupPacketTrailerSender(engine, sender, { packetTrailer: { timestamp: true } });
211
+
212
+ expect(RTCRtpScriptTransform).toHaveBeenCalledWith(worker, {
213
+ kind: 'encode',
214
+ packetTrailer: { timestamp: true },
215
+ });
216
+ expect((sender as unknown as { transform: unknown }).transform).toBe(transform);
217
+ expect(createEncodedStreams).not.toHaveBeenCalled();
218
+ });
219
+ });
@@ -54,9 +54,14 @@ import {
54
54
  toProtoSessionDescription,
55
55
  } from '../api/SignalClient';
56
56
  import type { BaseE2EEManager } from '../e2ee/E2eeManager';
57
- import { asEncryptablePacket } from '../e2ee/utils';
57
+ import { asEncryptablePacket, isInsertableStreamSupported } from '../e2ee/utils';
58
58
  import log, { LoggerNames, getLogger } from '../logger';
59
59
  import type { InternalRoomOptions } from '../options';
60
+ import {
61
+ hasPacketTrailerPublishOptions,
62
+ isPacketTrailerSupported,
63
+ shouldUsePacketTrailerScriptTransform,
64
+ } from '../packetTrailer/utils';
60
65
  import TypedPromise from '../utils/TypedPromise';
61
66
  import { DataPacketBuffer } from '../utils/dataPacketBuffer';
62
67
  import { TTLMap } from '../utils/ttlmap';
@@ -762,7 +767,14 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
762
767
  ): RTCConfiguration {
763
768
  const rtcConfig = { ...this.rtcConfig };
764
769
 
765
- if (this.signalOpts?.e2eeEnabled) {
770
+ // E2EE and packet trailer extraction both rely on encoded frame transforms.
771
+ // Only opt into the createEncodedStreams flavor when that path will be
772
+ // used; RTCRtpScriptTransform does not need the PeerConnection flag.
773
+ const needsInsertableStreams =
774
+ this.signalOpts?.e2eeEnabled ||
775
+ (this.options.packetTrailer?.worker && !shouldUsePacketTrailerScriptTransform());
776
+
777
+ if (needsInsertableStreams && isInsertableStreamSupported()) {
766
778
  this.log.debug('E2EE - setting up transports with insertable streams');
767
779
  // this makes sure that no data is sent before the transforms are ready
768
780
  // @ts-ignore
@@ -940,8 +952,8 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
940
952
  return;
941
953
  }
942
954
  const decryptedData = await this.e2eeManager?.handleEncryptedData(
943
- dp.value.value.encryptedValue,
944
- dp.value.value.iv,
955
+ dp.value.value.encryptedValue as NonSharedUint8Array,
956
+ dp.value.value.iv as NonSharedUint8Array,
945
957
  dp.participantIdentity,
946
958
  dp.value.value.keyIndex,
947
959
  );
@@ -1004,16 +1016,17 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1004
1016
  opts: TrackPublishOptions,
1005
1017
  encodings?: RTCRtpEncodingParameters[],
1006
1018
  ) {
1019
+ let sender: RTCRtpSender;
1007
1020
  if (supportsTransceiver()) {
1008
- const sender = await this.createTransceiverRTCRtpSender(track, opts, encodings);
1009
- return sender;
1010
- }
1011
- if (supportsAddTrack()) {
1021
+ sender = await this.createTransceiverRTCRtpSender(track, opts, encodings);
1022
+ } else if (supportsAddTrack()) {
1012
1023
  this.log.warn('using add-track fallback');
1013
- const sender = await this.createRTCRtpSender(track.mediaStreamTrack);
1014
- return sender;
1024
+ sender = await this.createRTCRtpSender(track.mediaStreamTrack);
1025
+ } else {
1026
+ throw new UnexpectedConnectionState('Required webRTC APIs not supported on this device');
1015
1027
  }
1016
- throw new UnexpectedConnectionState('Required webRTC APIs not supported on this device');
1028
+ this.setupPacketTrailerSender(sender, opts);
1029
+ return sender;
1017
1030
  }
1018
1031
 
1019
1032
  async createSimulcastSender(
@@ -1022,16 +1035,67 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1022
1035
  opts: TrackPublishOptions,
1023
1036
  encodings?: RTCRtpEncodingParameters[],
1024
1037
  ) {
1025
- // store RTCRtpSender
1038
+ let sender: RTCRtpSender | undefined;
1026
1039
  if (supportsTransceiver()) {
1027
- return this.createSimulcastTransceiverSender(track, simulcastTrack, opts, encodings);
1028
- }
1029
- if (supportsAddTrack()) {
1040
+ sender = await this.createSimulcastTransceiverSender(track, simulcastTrack, opts, encodings);
1041
+ } else if (supportsAddTrack()) {
1030
1042
  this.log.debug('using add-track fallback');
1031
- return this.createRTCRtpSender(track.mediaStreamTrack);
1043
+ sender = await this.createRTCRtpSender(track.mediaStreamTrack);
1044
+ } else {
1045
+ throw new UnexpectedConnectionState('Cannot stream on this device');
1032
1046
  }
1047
+ if (sender) {
1048
+ this.setupPacketTrailerSender(sender, opts);
1049
+ }
1050
+ return sender;
1051
+ }
1033
1052
 
1034
- throw new UnexpectedConnectionState('Cannot stream on this device');
1053
+ private setupPacketTrailerSender(sender: RTCRtpSender, opts: TrackPublishOptions = {}) {
1054
+ if (!this.options.packetTrailer?.worker || this.signalOpts?.e2eeEnabled) {
1055
+ return;
1056
+ }
1057
+
1058
+ const packetTrailer = opts.packetTrailer;
1059
+ const hasPacketTrailer = hasPacketTrailerPublishOptions(packetTrailer);
1060
+
1061
+ if (shouldUsePacketTrailerScriptTransform()) {
1062
+ if (hasPacketTrailer) {
1063
+ // @ts-ignore
1064
+ sender.transform = new RTCRtpScriptTransform(this.options.packetTrailer.worker, {
1065
+ kind: 'encode',
1066
+ packetTrailer,
1067
+ });
1068
+ }
1069
+ return;
1070
+ }
1071
+
1072
+ if (
1073
+ !isPacketTrailerSupported(this.options.packetTrailer) ||
1074
+ !('createEncodedStreams' in sender)
1075
+ ) {
1076
+ if (hasPacketTrailer) {
1077
+ this.log.warn('packet trailer transform not supported; skipping write', this.logContext);
1078
+ }
1079
+ return;
1080
+ }
1081
+
1082
+ // @ts-ignore
1083
+ const { readable, writable } = sender.createEncodedStreams();
1084
+ if (hasPacketTrailer) {
1085
+ this.options.packetTrailer.worker.postMessage(
1086
+ {
1087
+ kind: 'encode',
1088
+ data: {
1089
+ readableStream: readable,
1090
+ writableStream: writable,
1091
+ packetTrailer,
1092
+ },
1093
+ },
1094
+ [readable, writable],
1095
+ );
1096
+ } else {
1097
+ readable.pipeTo(writable);
1098
+ }
1035
1099
  }
1036
1100
 
1037
1101
  private async createTransceiverRTCRtpSender(
@@ -1447,7 +1511,9 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1447
1511
  if (this.e2eeManager && this.e2eeManager.isDataChannelEncryptionEnabled) {
1448
1512
  const encryptablePacket = asEncryptablePacket(packet);
1449
1513
  if (encryptablePacket) {
1450
- const encryptedData = await this.e2eeManager.encryptData(encryptablePacket.toBinary());
1514
+ const encryptedData = await this.e2eeManager.encryptData(
1515
+ encryptablePacket.toBinary() as NonSharedUint8Array,
1516
+ );
1451
1517
  packet.value = {
1452
1518
  case: 'encryptedPacket',
1453
1519
  value: new EncryptedPacket({
@@ -1464,7 +1530,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1464
1530
  this.reliableDataSequence += 1;
1465
1531
  }
1466
1532
 
1467
- const msg = packet.toBinary();
1533
+ const msg = packet.toBinary() as Uint8Array<ArrayBuffer>;
1468
1534
 
1469
1535
  switch (kind) {
1470
1536
  case DataChannelKind.LOSSY:
@@ -1491,7 +1557,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
1491
1557
 
1492
1558
  /* @internal */
1493
1559
  async sendLossyBytes(
1494
- bytes: Uint8Array,
1560
+ bytes: NonSharedUint8Array,
1495
1561
  kind: Exclude<DataChannelKind, DataChannelKind.RELIABLE>,
1496
1562
  bufferStatusLowBehavior: 'drop' | 'wait' = 'drop',
1497
1563
  ) {
@@ -1,5 +1,7 @@
1
- import { describe, expect, it } from 'vitest';
1
+ import { ClientInfo_Capability, JoinResponse } from '@livekit/protocol';
2
+ import { describe, expect, it, vi } from 'vitest';
2
3
  import Room from './Room';
4
+ import { roomConnectOptionDefaults, roomOptionDefaults } from './defaults';
3
5
  import { RoomEvent } from './events';
4
6
 
5
7
  describe('Active device switch', () => {
@@ -28,3 +30,62 @@ describe('Active device switch', () => {
28
30
  expect(kind).toBe('audioinput');
29
31
  });
30
32
  });
33
+
34
+ describe('Room signaling options', () => {
35
+ it('advertises packet trailer capability when E2EE can handle trailers', async () => {
36
+ const room = new Room();
37
+ const join = vi.fn().mockResolvedValue({
38
+ joinResponse: new JoinResponse({
39
+ room: { name: 'test-room', sid: 'room-sid' },
40
+ participant: { sid: 'participant-sid', identity: 'test-user' },
41
+ }),
42
+ serverInfo: { version: '1.0.0' },
43
+ });
44
+ const engine = { join };
45
+
46
+ (
47
+ room as unknown as {
48
+ e2eeManager: unknown;
49
+ connectSignal: (
50
+ url: string,
51
+ token: string,
52
+ engine: unknown,
53
+ connectOptions: typeof roomConnectOptionDefaults,
54
+ roomOptions: typeof roomOptionDefaults,
55
+ abortController: AbortController,
56
+ ) => Promise<JoinResponse>;
57
+ }
58
+ ).e2eeManager = {};
59
+
60
+ await (
61
+ room as unknown as {
62
+ connectSignal: (
63
+ url: string,
64
+ token: string,
65
+ engine: unknown,
66
+ connectOptions: typeof roomConnectOptionDefaults,
67
+ roomOptions: typeof roomOptionDefaults,
68
+ abortController: AbortController,
69
+ ) => Promise<JoinResponse>;
70
+ }
71
+ ).connectSignal(
72
+ 'wss://test.livekit.io',
73
+ 'test-token',
74
+ engine,
75
+ roomConnectOptionDefaults,
76
+ roomOptionDefaults,
77
+ new AbortController(),
78
+ );
79
+
80
+ expect(join).toHaveBeenCalledWith(
81
+ 'wss://test.livekit.io',
82
+ 'test-token',
83
+ expect.objectContaining({
84
+ clientInfoCapabilities: [ClientInfo_Capability.CAP_PACKET_TRAILER],
85
+ e2eeEnabled: true,
86
+ }),
87
+ expect.any(AbortSignal),
88
+ false,
89
+ );
90
+ });
91
+ });