livekit-client 2.22.3 → 2.23.0

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 (272) hide show
  1. package/README.md +35 -0
  2. package/dist/livekit-client.e2ee.worker.js +1 -1
  3. package/dist/livekit-client.e2ee.worker.js.map +1 -1
  4. package/dist/livekit-client.e2ee.worker.mjs +71 -8
  5. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  6. package/dist/livekit-client.esm.mjs +1746 -547
  7. package/dist/livekit-client.esm.mjs.map +1 -1
  8. package/dist/livekit-client.fm.worker.js +1 -1
  9. package/dist/livekit-client.fm.worker.js.map +1 -1
  10. package/dist/livekit-client.fm.worker.mjs +2 -1
  11. package/dist/livekit-client.fm.worker.mjs.map +1 -1
  12. package/dist/livekit-client.umd.js +1 -1
  13. package/dist/livekit-client.umd.js.map +1 -1
  14. package/dist/src/api/SignalClient.d.ts +4 -2
  15. package/dist/src/api/SignalClient.d.ts.map +1 -1
  16. package/dist/src/api/WebSocketStream.d.ts +1 -0
  17. package/dist/src/api/WebSocketStream.d.ts.map +1 -1
  18. package/dist/src/api/utils.d.ts +1 -1
  19. package/dist/src/api/utils.d.ts.map +1 -1
  20. package/dist/src/e2ee/E2eeManager.d.ts.map +1 -1
  21. package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -1
  22. package/dist/src/e2ee/worker/naluUtils.d.ts +12 -1
  23. package/dist/src/e2ee/worker/naluUtils.d.ts.map +1 -1
  24. package/dist/src/index.d.ts +2 -2
  25. package/dist/src/index.d.ts.map +1 -1
  26. package/dist/src/options.d.ts +14 -0
  27. package/dist/src/options.d.ts.map +1 -1
  28. package/dist/src/room/DefaultReconnectPolicy.d.ts.map +1 -1
  29. package/dist/src/room/PCTransport.d.ts +100 -7
  30. package/dist/src/room/PCTransport.d.ts.map +1 -1
  31. package/dist/src/room/PCTransportManager.d.ts.map +1 -1
  32. package/dist/src/room/RTCEngine.d.ts +16 -0
  33. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  34. package/dist/src/room/Room.d.ts +54 -1
  35. package/dist/src/room/Room.d.ts.map +1 -1
  36. package/dist/src/room/data-channel/DataChannelManager.d.ts +9 -8
  37. package/dist/src/room/data-channel/DataChannelManager.d.ts.map +1 -1
  38. package/dist/src/room/data-channel/FlowControlledDataChannel.d.ts +25 -12
  39. package/dist/src/room/data-channel/FlowControlledDataChannel.d.ts.map +1 -1
  40. package/dist/src/room/data-channel/LossyDataChannel.d.ts.map +1 -1
  41. package/dist/src/room/data-channel/ReliableDataChannel.d.ts.map +1 -1
  42. package/dist/src/room/data-stream/incoming/IncomingDataStreamManager.d.ts.map +1 -1
  43. package/dist/src/room/defaults.d.ts.map +1 -1
  44. package/dist/src/room/errors.d.ts +18 -0
  45. package/dist/src/room/errors.d.ts.map +1 -1
  46. package/dist/src/room/events.d.ts +3 -2
  47. package/dist/src/room/events.d.ts.map +1 -1
  48. package/dist/src/room/participant/LocalParticipant.d.ts +4 -0
  49. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  50. package/dist/src/room/participant/Participant.d.ts +1 -1
  51. package/dist/src/room/participant/Participant.d.ts.map +1 -1
  52. package/dist/src/room/participant/publishUtils.d.ts.map +1 -1
  53. package/dist/src/room/rpc/client/RpcClientManager.d.ts.map +1 -1
  54. package/dist/src/room/rpc/utils.d.ts.map +1 -1
  55. package/dist/src/room/track/LocalAudioTrack.d.ts +24 -1
  56. package/dist/src/room/track/LocalAudioTrack.d.ts.map +1 -1
  57. package/dist/src/room/track/LocalTrack.d.ts +18 -0
  58. package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
  59. package/dist/src/room/track/LocalVideoTrack.d.ts +9 -0
  60. package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
  61. package/dist/src/room/track/RemoteAudioTrack.d.ts +6 -0
  62. package/dist/src/room/track/RemoteAudioTrack.d.ts.map +1 -1
  63. package/dist/src/room/track/processor/types.d.ts +11 -0
  64. package/dist/src/room/track/processor/types.d.ts.map +1 -1
  65. package/dist/src/room/track/utils.d.ts +13 -0
  66. package/dist/src/room/track/utils.d.ts.map +1 -1
  67. package/dist/src/room/utils.d.ts +60 -1
  68. package/dist/src/room/utils.d.ts.map +1 -1
  69. package/dist/src/test/compressionStreamStubs.d.ts +12 -0
  70. package/dist/src/test/compressionStreamStubs.d.ts.map +1 -0
  71. package/dist/ts4.2/api/SignalClient.d.ts +187 -185
  72. package/dist/ts4.2/api/SignalClientStateMachine.d.ts +85 -85
  73. package/dist/ts4.2/api/WebSocketStream.d.ts +34 -33
  74. package/dist/ts4.2/api/utils.d.ts +9 -9
  75. package/dist/ts4.2/connectionHelper/ConnectionCheck.d.ts +30 -30
  76. package/dist/ts4.2/connectionHelper/checks/Checker.d.ts +62 -62
  77. package/dist/ts4.2/connectionHelper/checks/cloudRegion.d.ts +18 -18
  78. package/dist/ts4.2/connectionHelper/checks/connectionProtocol.d.ts +20 -20
  79. package/dist/ts4.2/connectionHelper/checks/publishAudio.d.ts +6 -6
  80. package/dist/ts4.2/connectionHelper/checks/publishVideo.d.ts +7 -7
  81. package/dist/ts4.2/connectionHelper/checks/reconnect.d.ts +6 -6
  82. package/dist/ts4.2/connectionHelper/checks/turn.d.ts +6 -6
  83. package/dist/ts4.2/connectionHelper/checks/webrtc.d.ts +6 -6
  84. package/dist/ts4.2/connectionHelper/checks/websocket.d.ts +6 -6
  85. package/dist/ts4.2/e2ee/E2eeManager.d.ts +111 -111
  86. package/dist/ts4.2/e2ee/KeyProvider.d.ts +54 -54
  87. package/dist/ts4.2/e2ee/constants.d.ts +21 -21
  88. package/dist/ts4.2/e2ee/errors.d.ts +12 -12
  89. package/dist/ts4.2/e2ee/events.d.ts +39 -39
  90. package/dist/ts4.2/e2ee/index.d.ts +6 -6
  91. package/dist/ts4.2/e2ee/types.d.ts +231 -231
  92. package/dist/ts4.2/e2ee/utils.d.ts +32 -32
  93. package/dist/ts4.2/e2ee/worker/DataCryptor.d.ts +16 -16
  94. package/dist/ts4.2/e2ee/worker/ErrorRateLimiter.d.ts +21 -21
  95. package/dist/ts4.2/e2ee/worker/FrameCryptor.d.ts +209 -209
  96. package/dist/ts4.2/e2ee/worker/ParticipantKeyHandler.d.ts +81 -84
  97. package/dist/ts4.2/e2ee/worker/e2ee.worker.d.ts +2 -2
  98. package/dist/ts4.2/e2ee/worker/naluUtils.d.ts +39 -28
  99. package/dist/ts4.2/e2ee/worker/sifPayload.d.ts +23 -23
  100. package/dist/ts4.2/frameMetadata/FrameMetadataManager.d.ts +53 -53
  101. package/dist/ts4.2/frameMetadata/frameMetadata.d.ts +33 -33
  102. package/dist/ts4.2/frameMetadata/types.d.ts +62 -62
  103. package/dist/ts4.2/frameMetadata/utils.d.ts +9 -9
  104. package/dist/ts4.2/frameMetadata/worker/frameMetadata.worker.d.ts +2 -2
  105. package/dist/ts4.2/index.d.ts +76 -76
  106. package/dist/ts4.2/logger.d.ts +66 -66
  107. package/dist/ts4.2/options.d.ts +163 -149
  108. package/dist/ts4.2/room/BackOffStrategy.d.ts +27 -27
  109. package/dist/ts4.2/room/DefaultReconnectPolicy.d.ts +8 -8
  110. package/dist/ts4.2/room/DeviceManager.d.ts +12 -12
  111. package/dist/ts4.2/room/PCTransport.d.ts +268 -175
  112. package/dist/ts4.2/room/PCTransportManager.d.ts +77 -77
  113. package/dist/ts4.2/room/RTCEngine.d.ts +277 -262
  114. package/dist/ts4.2/room/ReconnectPolicy.d.ts +24 -24
  115. package/dist/ts4.2/room/RegionUrlProvider.d.ts +36 -36
  116. package/dist/ts4.2/room/Room.d.ts +407 -365
  117. package/dist/ts4.2/room/attribute-typings.d.ts +35 -35
  118. package/dist/ts4.2/room/data-channel/DataChannelManager.d.ts +63 -62
  119. package/dist/ts4.2/room/data-channel/FlowControlledDataChannel.d.ts +107 -95
  120. package/dist/ts4.2/room/data-channel/LossyDataChannel.d.ts +41 -41
  121. package/dist/ts4.2/room/data-channel/ReliableDataChannel.d.ts +57 -57
  122. package/dist/ts4.2/room/data-channel/types.d.ts +12 -12
  123. package/dist/ts4.2/room/data-stream/compression.d.ts +49 -49
  124. package/dist/ts4.2/room/data-stream/constants.d.ts +18 -18
  125. package/dist/ts4.2/room/data-stream/incoming/IncomingDataStreamManager.d.ts +29 -29
  126. package/dist/ts4.2/room/data-stream/incoming/StreamReader.d.ts +80 -80
  127. package/dist/ts4.2/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts +73 -73
  128. package/dist/ts4.2/room/data-stream/outgoing/StreamWriter.d.ts +16 -16
  129. package/dist/ts4.2/room/data-stream/outgoing/header-utils.d.ts +18 -18
  130. package/dist/ts4.2/room/data-track/LocalDataTrack.d.ts +79 -80
  131. package/dist/ts4.2/room/data-track/RemoteDataTrack.d.ts +50 -50
  132. package/dist/ts4.2/room/data-track/depacketizer.d.ts +59 -59
  133. package/dist/ts4.2/room/data-track/frame.d.ts +22 -22
  134. package/dist/ts4.2/room/data-track/handle.d.ts +27 -27
  135. package/dist/ts4.2/room/data-track/incoming/IncomingDataTrackManager.d.ts +124 -124
  136. package/dist/ts4.2/room/data-track/incoming/errors.d.ts +24 -24
  137. package/dist/ts4.2/room/data-track/incoming/pipeline.d.ts +41 -41
  138. package/dist/ts4.2/room/data-track/incoming/types.d.ts +20 -20
  139. package/dist/ts4.2/room/data-track/outgoing/OutgoingDataTrackManager.d.ts +126 -126
  140. package/dist/ts4.2/room/data-track/outgoing/errors.d.ts +74 -74
  141. package/dist/ts4.2/room/data-track/outgoing/pipeline.d.ts +23 -23
  142. package/dist/ts4.2/room/data-track/outgoing/types.d.ts +48 -48
  143. package/dist/ts4.2/room/data-track/packet/constants.d.ts +20 -20
  144. package/dist/ts4.2/room/data-track/packet/errors.d.ts +40 -40
  145. package/dist/ts4.2/room/data-track/packet/extensions.d.ts +69 -69
  146. package/dist/ts4.2/room/data-track/packet/index.d.ts +98 -98
  147. package/dist/ts4.2/room/data-track/packet/serializable.d.ts +13 -13
  148. package/dist/ts4.2/room/data-track/packetizer.d.ts +43 -43
  149. package/dist/ts4.2/room/data-track/track-interfaces.d.ts +23 -23
  150. package/dist/ts4.2/room/data-track/types.d.ts +22 -22
  151. package/dist/ts4.2/room/data-track/utils.d.ts +60 -60
  152. package/dist/ts4.2/room/debounce.d.ts +11 -11
  153. package/dist/ts4.2/room/defaults.d.ts +9 -9
  154. package/dist/ts4.2/room/errors.d.ts +180 -162
  155. package/dist/ts4.2/room/events.d.ts +638 -637
  156. package/dist/ts4.2/room/participant/LocalParticipant.d.ts +291 -287
  157. package/dist/ts4.2/room/participant/Participant.d.ts +142 -144
  158. package/dist/ts4.2/room/participant/ParticipantTrackPermission.d.ts +26 -26
  159. package/dist/ts4.2/room/participant/RemoteParticipant.d.ts +85 -86
  160. package/dist/ts4.2/room/participant/publishUtils.d.ts +53 -53
  161. package/dist/ts4.2/room/rpc/client/RpcClientManager.d.ts +43 -43
  162. package/dist/ts4.2/room/rpc/client/events.d.ts +8 -8
  163. package/dist/ts4.2/room/rpc/index.d.ts +7 -7
  164. package/dist/ts4.2/room/rpc/server/RpcServerManager.d.ts +44 -44
  165. package/dist/ts4.2/room/rpc/server/events.d.ts +8 -8
  166. package/dist/ts4.2/room/rpc/utils.d.ts +131 -131
  167. package/dist/ts4.2/room/stats.d.ts +71 -71
  168. package/dist/ts4.2/room/statsSummary.d.ts +13 -13
  169. package/dist/ts4.2/room/timers.d.ts +12 -12
  170. package/dist/ts4.2/room/token-source/TokenSource.d.ts +82 -82
  171. package/dist/ts4.2/room/token-source/types.d.ts +70 -70
  172. package/dist/ts4.2/room/token-source/utils.d.ts +8 -8
  173. package/dist/ts4.2/room/track/FrameMetadataExtractor.d.ts +19 -19
  174. package/dist/ts4.2/room/track/LocalAudioTrack.d.ts +62 -42
  175. package/dist/ts4.2/room/track/LocalTrack.d.ts +143 -125
  176. package/dist/ts4.2/room/track/LocalTrackPublication.d.ts +42 -42
  177. package/dist/ts4.2/room/track/LocalVideoTrack.d.ts +109 -100
  178. package/dist/ts4.2/room/track/RemoteAudioTrack.d.ts +61 -55
  179. package/dist/ts4.2/room/track/RemoteTrack.d.ts +38 -38
  180. package/dist/ts4.2/room/track/RemoteTrackPublication.d.ts +72 -75
  181. package/dist/ts4.2/room/track/RemoteVideoTrack.d.ts +73 -76
  182. package/dist/ts4.2/room/track/Track.d.ts +156 -166
  183. package/dist/ts4.2/room/track/TrackPublication.d.ts +75 -81
  184. package/dist/ts4.2/room/track/create.d.ts +25 -25
  185. package/dist/ts4.2/room/track/facingMode.d.ts +41 -41
  186. package/dist/ts4.2/room/track/options.d.ts +403 -403
  187. package/dist/ts4.2/room/track/processor/types.d.ts +42 -31
  188. package/dist/ts4.2/room/track/record.d.ts +13 -13
  189. package/dist/ts4.2/room/track/types.d.ts +28 -28
  190. package/dist/ts4.2/room/track/utils.d.ts +60 -47
  191. package/dist/ts4.2/room/types.d.ts +127 -127
  192. package/dist/ts4.2/room/utils.d.ts +273 -214
  193. package/dist/ts4.2/test/MockMediaStreamTrack.d.ts +26 -26
  194. package/dist/ts4.2/test/compressionStreamStubs.d.ts +12 -0
  195. package/dist/ts4.2/test/mocks.d.ts +12 -12
  196. package/dist/ts4.2/test/promiseState.d.ts +12 -12
  197. package/dist/ts4.2/test/signalToken.d.ts +26 -26
  198. package/dist/ts4.2/type-polyfills/non-shared-typed-arrays.d.ts +2 -2
  199. package/dist/ts4.2/utils/AsyncQueue.d.ts +23 -23
  200. package/dist/ts4.2/utils/TypedPromise.d.ts +17 -17
  201. package/dist/ts4.2/utils/abort-signal-polyfill.d.ts +13 -13
  202. package/dist/ts4.2/utils/browserParser.d.ts +13 -13
  203. package/dist/ts4.2/utils/camelToSnakeCase.d.ts +8 -8
  204. package/dist/ts4.2/utils/cloneDeep.d.ts +2 -2
  205. package/dist/ts4.2/utils/dataPacketBuffer.d.ts +34 -34
  206. package/dist/ts4.2/utils/deferrable-map.d.ts +32 -32
  207. package/dist/ts4.2/utils/machineInspector.d.ts +54 -54
  208. package/dist/ts4.2/utils/serializer.d.ts +48 -48
  209. package/dist/ts4.2/utils/subscribeToEvents.d.ts +15 -15
  210. package/dist/ts4.2/utils/ttlmap.d.ts +20 -20
  211. package/dist/ts4.2/version.d.ts +15 -15
  212. package/package.json +5 -4
  213. package/src/api/SignalClient.e2e.test.ts +1 -3
  214. package/src/api/SignalClient.test.ts +394 -10
  215. package/src/api/SignalClient.ts +132 -76
  216. package/src/api/WebSocketStream.test.ts +11 -0
  217. package/src/api/WebSocketStream.ts +22 -1
  218. package/src/api/utils.ts +1 -1
  219. package/src/e2ee/E2eeManager.test.ts +43 -0
  220. package/src/e2ee/E2eeManager.ts +6 -2
  221. package/src/e2ee/worker/FrameCryptor.test.ts +212 -90
  222. package/src/e2ee/worker/FrameCryptor.ts +21 -4
  223. package/src/e2ee/worker/naluUtils.test.ts +123 -0
  224. package/src/e2ee/worker/naluUtils.ts +57 -2
  225. package/src/index.ts +2 -0
  226. package/src/options.ts +16 -0
  227. package/src/room/DefaultReconnectPolicy.test.ts +58 -0
  228. package/src/room/DefaultReconnectPolicy.ts +14 -2
  229. package/src/room/PCTransport.test.ts +327 -9
  230. package/src/room/PCTransport.ts +339 -36
  231. package/src/room/PCTransportManager.ts +3 -1
  232. package/src/room/RTCEngine.test.ts +318 -2
  233. package/src/room/RTCEngine.ts +126 -12
  234. package/src/room/Room.test.ts +148 -6
  235. package/src/room/Room.ts +312 -80
  236. package/src/room/data-channel/DataChannelManager.test.ts +24 -7
  237. package/src/room/data-channel/DataChannelManager.ts +43 -38
  238. package/src/room/data-channel/FlowControlledDataChannel.test.ts +60 -18
  239. package/src/room/data-channel/FlowControlledDataChannel.ts +40 -22
  240. package/src/room/data-channel/LossyDataChannel.test.ts +38 -0
  241. package/src/room/data-channel/LossyDataChannel.ts +10 -1
  242. package/src/room/data-channel/ReliableDataChannel.test.ts +72 -0
  243. package/src/room/data-channel/ReliableDataChannel.ts +16 -5
  244. package/src/room/data-stream/incoming/IncomingDataStreamManager.test.ts +47 -20
  245. package/src/room/data-stream/incoming/IncomingDataStreamManager.ts +16 -5
  246. package/src/room/data-stream/outgoing/OutgoingDataStreamManager.test.ts +60 -39
  247. package/src/room/data-stream/outgoing/OutgoingDataStreamManager.ts +4 -4
  248. package/src/room/defaults.ts +1 -0
  249. package/src/room/errors.test.ts +36 -0
  250. package/src/room/errors.ts +27 -0
  251. package/src/room/events.ts +2 -1
  252. package/src/room/participant/LocalParticipant.test.ts +136 -1
  253. package/src/room/participant/LocalParticipant.ts +66 -17
  254. package/src/room/participant/Participant.ts +6 -3
  255. package/src/room/participant/publishUtils.ts +0 -19
  256. package/src/room/participant/restartOnDefaultAudioDevice.test.ts +177 -0
  257. package/src/room/rpc/client/RpcClientManager.test.ts +51 -0
  258. package/src/room/rpc/client/RpcClientManager.ts +32 -15
  259. package/src/room/rpc/utils.test.ts +32 -0
  260. package/src/room/rpc/utils.ts +7 -0
  261. package/src/room/track/LocalAudioTrack.ts +101 -10
  262. package/src/room/track/LocalTrack.ts +87 -19
  263. package/src/room/track/LocalVideoTrack.ts +17 -3
  264. package/src/room/track/RemoteAudioTrack.ts +59 -14
  265. package/src/room/track/RemoteVideoTrack.test.ts +210 -0
  266. package/src/room/track/RemoteVideoTrack.ts +29 -3
  267. package/src/room/track/processor/types.ts +11 -0
  268. package/src/room/track/utils.test.ts +206 -2
  269. package/src/room/track/utils.ts +117 -5
  270. package/src/room/utils.test.ts +250 -0
  271. package/src/room/utils.ts +152 -19
  272. package/src/test/compressionStreamStubs.ts +53 -0
@@ -1,6 +1,8 @@
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 { workerLogger } from '../../logger';
5
+ import type { VideoCodec } from '../../room/track/options';
4
6
  import type { NonSharedUint8Array } from '../../type-polyfills/non-shared-typed-arrays';
5
7
  import { IV_LENGTH, KEY_PROVIDER_DEFAULTS } from '../constants';
6
8
  import { CryptorEvent } from '../events';
@@ -71,12 +73,14 @@ function prepareParticipantTestEncoder(
71
73
  participantIdentity: string,
72
74
  partialKeyProviderOptions: Partial<KeyProviderOptions>,
73
75
  packetTrailer?: FrameMetadataPublishOptions,
76
+ codec?: VideoCodec,
74
77
  ) {
75
78
  return prepareParticipantTest(
76
79
  'encode',
77
80
  participantIdentity,
78
81
  partialKeyProviderOptions,
79
82
  packetTrailer,
83
+ codec,
80
84
  );
81
85
  }
82
86
 
@@ -85,6 +89,7 @@ function prepareParticipantTest(
85
89
  participantIdentity: string,
86
90
  partialKeyProviderOptions: Partial<KeyProviderOptions>,
87
91
  packetTrailer?: FrameMetadataPublishOptions,
92
+ codec?: VideoCodec,
88
93
  ): {
89
94
  keys: ParticipantKeyHandler;
90
95
  cryptor: FrameCryptor;
@@ -110,7 +115,7 @@ function prepareParticipantTest(
110
115
  new ReadableStream(input),
111
116
  new WritableStream(output),
112
117
  'testTrack',
113
- undefined,
118
+ codec,
114
119
  packetTrailer,
115
120
  );
116
121
 
@@ -233,81 +238,224 @@ describe('FrameCryptor', () => {
233
238
  }
234
239
  });
235
240
 
236
- it('appends packet trailer after encryption', async () => {
237
- vitest.useFakeTimers();
238
- const now = new Date('2025-04-10T12:00:00.123Z');
239
- vitest.setSystemTime(now);
240
- try {
241
- const { keys, input, output } = prepareParticipantTestEncoder(
242
- participantIdentity,
243
- {},
244
- { timestamp: true, frameId: true },
245
- );
246
-
247
- await keys.setKey(await createKeyMaterialFromString('key1'), 1);
241
+ describe('H.264 start codes (#2140)', () => {
242
+ const sps = [0x67, 0x64, 0x00, 0x1f, 0xac, 0xd9];
243
+ const pps = [0x68, 0xee, 0x3c, 0xb0];
244
+ const idr = [0x65, 0x88, 0x84, 0x21, 0x43, 0x55, 0x66, 0x77];
245
+
246
+ async function encryptAndDecrypt(plainTextData: NonSharedUint8Array) {
247
+ const encoder = prepareParticipantTestEncoder(participantIdentity, {});
248
+ await encoder.keys.setKey(await createKeyMaterialFromString('key1'), 1);
249
+ const encrypted = mockRTCEncodedVideoFrame(plainTextData);
250
+ encoder.input.write(encrypted);
251
+ await vitest.waitFor(() => expect(encoder.output.chunks).toHaveLength(1));
252
+ const encryptedData = new Uint8Array(encrypted.data.slice(0));
253
+
254
+ const decoder = prepareParticipantTestDecoder(participantIdentity, {});
255
+ await decoder.keys.setKey(await createKeyMaterialFromString('key1'), 1);
256
+ const decrypted = mockRTCEncodedVideoFrame(new Uint8Array(encryptedData));
257
+ decoder.input.write(decrypted);
258
+ await vitest.waitFor(() => expect(decoder.output.chunks).toHaveLength(1));
259
+
260
+ return { encryptedData, decryptedData: new Uint8Array(decrypted.data) };
261
+ }
248
262
 
249
- const frame = mockRTCEncodedVideoFrame(
250
- new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]),
263
+ it('rewrites 3-byte start codes in the unencrypted header to 4-byte start codes', async () => {
264
+ const { encryptedData, decryptedData } = await encryptAndDecrypt(
265
+ new Uint8Array([0, 0, 1, ...sps, 0, 0, 1, ...pps, 0, 0, 1, ...idr]),
251
266
  );
252
267
 
253
- input.write(frame);
254
- await vitest.waitFor(() => expect(output.chunks).toHaveLength(1));
255
-
256
- const extracted = extractPacketTrailer(frame.data);
257
- expect(extracted.metadata?.userTimestamp).toBeGreaterThanOrEqual(
258
- BigInt(now.getTime()) * BigInt(1000),
268
+ // The unencrypted header runs through the first 2 bytes of the slice NALU.
269
+ const expectedHeader = [0, 0, 0, 1, ...sps, 0, 0, 0, 1, ...pps, 0, 0, 0, 1, 0x65, 0x88];
270
+ expect(Array.from(encryptedData.subarray(0, expectedHeader.length))).toEqual(
271
+ expectedHeader,
259
272
  );
260
- expect(extracted.metadata?.frameId).toBe(1);
273
+ expect(Array.from(decryptedData)).toEqual([
274
+ 0,
275
+ 0,
276
+ 0,
277
+ 1,
278
+ ...sps,
279
+ 0,
280
+ 0,
281
+ 0,
282
+ 1,
283
+ ...pps,
284
+ 0,
285
+ 0,
286
+ 0,
287
+ 1,
288
+ ...idr,
289
+ ]);
290
+ });
261
291
 
262
- const frameTrailer = new Uint8Array(
263
- extracted.data.buffer.slice(extracted.data.byteLength - 2),
292
+ it('rewrites a 3-byte start code on a delta frame', async () => {
293
+ const pSlice = [0x41, 0x9a, 0x21, 0x43, 0x55, 0x66];
294
+ const { encryptedData, decryptedData } = await encryptAndDecrypt(
295
+ new Uint8Array([0, 0, 1, ...pSlice]),
264
296
  );
265
- expect(frameTrailer[0]).toEqual(IV_LENGTH);
266
- expect(frameTrailer[1]).toEqual(1);
267
- } finally {
268
- vitest.useRealTimers();
269
- }
270
- });
271
297
 
272
- it('does not append packet trailer after encryption when no trailer features are enabled', async () => {
273
- vitest.useFakeTimers();
274
- try {
275
- const { keys, input, output } = prepareParticipantTestEncoder(
276
- participantIdentity,
277
- {},
278
- { timestamp: false, frameId: false },
279
- );
298
+ expect(Array.from(encryptedData.subarray(0, 6))).toEqual([0, 0, 0, 1, 0x41, 0x9a]);
299
+ expect(Array.from(decryptedData)).toEqual([0, 0, 0, 1, ...pSlice]);
300
+ });
280
301
 
281
- await keys.setKey(await createKeyMaterialFromString('key1'), 1);
302
+ it('leaves 4-byte start codes in the unencrypted header unchanged', async () => {
303
+ const plainTextData = [0, 0, 0, 1, ...sps, 0, 0, 0, 1, ...pps, 0, 0, 0, 1, ...idr];
304
+ const { encryptedData, decryptedData } = await encryptAndDecrypt(
305
+ new Uint8Array(plainTextData),
306
+ );
282
307
 
283
- const frame = mockRTCEncodedVideoFrame(
284
- new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]),
308
+ const expectedHeader = [0, 0, 0, 1, ...sps, 0, 0, 0, 1, ...pps, 0, 0, 0, 1, 0x65, 0x88];
309
+ expect(Array.from(encryptedData.subarray(0, expectedHeader.length))).toEqual(
310
+ expectedHeader,
285
311
  );
312
+ expect(Array.from(decryptedData)).toEqual(plainTextData);
313
+ });
314
+ it('logs the frame bytes once when NALU processing fails', async () => {
315
+ vitest.useFakeTimers();
316
+ const warn = vitest.spyOn(workerLogger, 'warn');
317
+ try {
318
+ const { keys, input, output } = prepareParticipantTestEncoder(
319
+ participantIdentity,
320
+ {},
321
+ undefined,
322
+ 'h264',
323
+ );
324
+
325
+ await keys.setKey(await createKeyMaterialFromString('key1'), 1);
326
+
327
+ // No start code at offset 0, so findNALUIndices throws 'byte stream contains
328
+ // leading data' and the cryptor falls back to VP8 handling.
329
+ input.write(
330
+ mockRTCEncodedVideoFrame(new Uint8Array([9, 9, 9, 9, 1, 2, 3, 4, 5, 6, 7, 8])),
331
+ );
332
+ await vitest.waitFor(() => expect(output.chunks).toHaveLength(1));
333
+
334
+ expect(warn).toHaveBeenCalledWith(
335
+ 'NALU processing failed, falling back to VP8 handling',
336
+ expect.objectContaining({
337
+ detectedCodec: 'h264',
338
+ frameType: 'key',
339
+ byteLength: 12,
340
+ firstBytes: expect.stringMatching(/^09 09 09 09 /),
341
+ }),
342
+ );
343
+
344
+ // same payload type, so the once-per-tuple guard must suppress the second log
345
+ input.write(
346
+ mockRTCEncodedVideoFrame(new Uint8Array([8, 8, 8, 8, 1, 2, 3, 4, 5, 6, 7, 8])),
347
+ );
348
+ await vitest.waitFor(() => expect(output.chunks).toHaveLength(2));
349
+
350
+ expect(warn).toHaveBeenCalledTimes(1);
351
+ } finally {
352
+ warn.mockRestore();
353
+ vitest.useRealTimers();
354
+ }
355
+ });
286
356
 
287
- input.write(frame);
288
- await vitest.waitFor(() => expect(output.chunks).toHaveLength(1));
357
+ it('appends packet trailer after encryption', async () => {
358
+ vitest.useFakeTimers();
359
+ const now = new Date('2025-04-10T12:00:00.123Z');
360
+ vitest.setSystemTime(now);
361
+ try {
362
+ const { keys, input, output } = prepareParticipantTestEncoder(
363
+ participantIdentity,
364
+ {},
365
+ { timestamp: true, frameId: true },
366
+ );
367
+
368
+ await keys.setKey(await createKeyMaterialFromString('key1'), 1);
369
+
370
+ const frame = mockRTCEncodedVideoFrame(
371
+ new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]),
372
+ );
373
+
374
+ input.write(frame);
375
+ await vitest.waitFor(() => expect(output.chunks).toHaveLength(1));
376
+
377
+ const extracted = extractPacketTrailer(frame.data);
378
+ expect(extracted.metadata?.userTimestamp).toBeGreaterThanOrEqual(
379
+ BigInt(now.getTime()) * BigInt(1000),
380
+ );
381
+ expect(extracted.metadata?.frameId).toBe(1);
382
+
383
+ const frameTrailer = new Uint8Array(
384
+ extracted.data.buffer.slice(extracted.data.byteLength - 2),
385
+ );
386
+ expect(frameTrailer[0]).toEqual(IV_LENGTH);
387
+ expect(frameTrailer[1]).toEqual(1);
388
+ } finally {
389
+ vitest.useRealTimers();
390
+ }
391
+ });
289
392
 
290
- const extracted = extractPacketTrailer(frame.data);
291
- expect(extracted.metadata).toBeUndefined();
292
- expect(extracted.data.byteLength).toBe(frame.data.byteLength);
393
+ it('does not append packet trailer after encryption when no trailer features are enabled', async () => {
394
+ vitest.useFakeTimers();
395
+ try {
396
+ const { keys, input, output } = prepareParticipantTestEncoder(
397
+ participantIdentity,
398
+ {},
399
+ { timestamp: false, frameId: false },
400
+ );
401
+
402
+ await keys.setKey(await createKeyMaterialFromString('key1'), 1);
403
+
404
+ const frame = mockRTCEncodedVideoFrame(
405
+ new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]),
406
+ );
407
+
408
+ input.write(frame);
409
+ await vitest.waitFor(() => expect(output.chunks).toHaveLength(1));
410
+
411
+ const extracted = extractPacketTrailer(frame.data);
412
+ expect(extracted.metadata).toBeUndefined();
413
+ expect(extracted.data.byteLength).toBe(frame.data.byteLength);
414
+
415
+ const frameTrailer = new Uint8Array(frame.data.slice(frame.data.byteLength - 2));
416
+ expect(frameTrailer[0]).toEqual(IV_LENGTH);
417
+ expect(frameTrailer[1]).toEqual(1);
418
+ } finally {
419
+ vitest.useRealTimers();
420
+ }
421
+ });
293
422
 
294
- const frameTrailer = new Uint8Array(frame.data.slice(frame.data.byteLength - 2));
295
- expect(frameTrailer[0]).toEqual(IV_LENGTH);
296
- expect(frameTrailer[1]).toEqual(1);
297
- } finally {
298
- vitest.useRealTimers();
299
- }
300
- });
423
+ it('appends only timestamp packet trailer metadata after encryption', async () => {
424
+ vitest.useFakeTimers();
425
+ const now = new Date('2025-04-10T12:00:00.123Z');
426
+ vitest.setSystemTime(now);
427
+ try {
428
+ const { keys, input, output } = prepareParticipantTestEncoder(
429
+ participantIdentity,
430
+ {},
431
+ { timestamp: true },
432
+ );
433
+
434
+ await keys.setKey(await createKeyMaterialFromString('key1'), 1);
435
+
436
+ const frame = mockRTCEncodedVideoFrame(
437
+ new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]),
438
+ );
439
+
440
+ input.write(frame);
441
+ await vitest.waitFor(() => expect(output.chunks).toHaveLength(1));
442
+
443
+ const extracted = extractPacketTrailer(frame.data);
444
+ expect(frame.data.byteLength - extracted.data.byteLength).toBe(15);
445
+ expect(extracted.metadata?.userTimestamp).toBeGreaterThanOrEqual(
446
+ BigInt(now.getTime()) * BigInt(1000),
447
+ );
448
+ expect(extracted.metadata?.frameId).toBe(0);
449
+ } finally {
450
+ vitest.useRealTimers();
451
+ }
452
+ });
301
453
 
302
- it('appends only timestamp packet trailer metadata after encryption', async () => {
303
- vitest.useFakeTimers();
304
- const now = new Date('2025-04-10T12:00:00.123Z');
305
- vitest.setSystemTime(now);
306
- try {
454
+ it('appends only frame id packet trailer metadata after encryption', async () => {
307
455
  const { keys, input, output } = prepareParticipantTestEncoder(
308
456
  participantIdentity,
309
457
  {},
310
- { timestamp: true },
458
+ { frameId: true },
311
459
  );
312
460
 
313
461
  await keys.setKey(await createKeyMaterialFromString('key1'), 1);
@@ -320,37 +468,11 @@ describe('FrameCryptor', () => {
320
468
  await vitest.waitFor(() => expect(output.chunks).toHaveLength(1));
321
469
 
322
470
  const extracted = extractPacketTrailer(frame.data);
323
- expect(frame.data.byteLength - extracted.data.byteLength).toBe(15);
324
- expect(extracted.metadata?.userTimestamp).toBeGreaterThanOrEqual(
325
- BigInt(now.getTime()) * BigInt(1000),
326
- );
327
- expect(extracted.metadata?.frameId).toBe(0);
328
- } finally {
329
- vitest.useRealTimers();
330
- }
331
- });
332
-
333
- it('appends only frame id packet trailer metadata after encryption', async () => {
334
- const { keys, input, output } = prepareParticipantTestEncoder(
335
- participantIdentity,
336
- {},
337
- { frameId: true },
338
- );
339
-
340
- await keys.setKey(await createKeyMaterialFromString('key1'), 1);
341
-
342
- const frame = mockRTCEncodedVideoFrame(
343
- new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]),
344
- );
345
-
346
- input.write(frame);
347
- await vitest.waitFor(() => expect(output.chunks).toHaveLength(1));
348
-
349
- const extracted = extractPacketTrailer(frame.data);
350
- expect(frame.data.byteLength - extracted.data.byteLength).toBe(11);
351
- expect(extracted.metadata).toEqual({
352
- userTimestamp: BigInt(0),
353
- frameId: 1,
471
+ expect(frame.data.byteLength - extracted.data.byteLength).toBe(11);
472
+ expect(extracted.metadata).toEqual({
473
+ userTimestamp: BigInt(0),
474
+ frameId: 1,
475
+ });
354
476
  });
355
477
  });
356
478
  });
@@ -25,7 +25,7 @@ import type {
25
25
  import { deriveKeys, isVideoFrame, needsRbspUnescaping, parseRbsp, writeRbsp } from '../utils';
26
26
  import { ErrorRateLimiter } from './ErrorRateLimiter';
27
27
  import type { ParticipantKeyHandler } from './ParticipantKeyHandler';
28
- import { processNALUsForEncryption } from './naluUtils';
28
+ import { normalizeAnnexBStartCodes, processNALUsForEncryption } from './naluUtils';
29
29
  import { identifySifPayload } from './sifPayload';
30
30
 
31
31
  export const encryptionEnabledMap: Map<string, boolean> = new Map();
@@ -490,7 +490,16 @@ export class FrameCryptor extends BaseFrameCryptor {
490
490
  let frameInfo = this.getUnencryptedBytes(encodedFrame);
491
491
 
492
492
  // Thіs is not encrypted and contains the VP8 payload descriptor or the Opus TOC byte.
493
- const frameHeader = new Uint8Array(encodedFrame.data, 0, frameInfo.unencryptedBytes);
493
+ let frameHeader: NonSharedUint8Array = new Uint8Array(
494
+ encodedFrame.data,
495
+ 0,
496
+ frameInfo.unencryptedBytes,
497
+ );
498
+ if (frameInfo.requiresNALUProcessing) {
499
+ // The receiver's depacketizer rebuilds NALUs with 4-byte start codes, so authenticate (and
500
+ // send) the header in that form or decryption fails for encoders emitting 3-byte ones.
501
+ frameHeader = normalizeAnnexBStartCodes(frameHeader);
502
+ }
494
503
 
495
504
  // Frame trailer contains the R|IV_LENGTH and key index
496
505
  const frameTrailer = new Uint8Array(2);
@@ -510,7 +519,7 @@ export class FrameCryptor extends BaseFrameCryptor {
510
519
  {
511
520
  name: ENCRYPTION_ALGORITHM,
512
521
  iv,
513
- additionalData: new Uint8Array(encodedFrame.data, 0, frameHeader.byteLength),
522
+ additionalData: frameHeader,
514
523
  },
515
524
  encryptionKey,
516
525
  new Uint8Array(encodedFrame.data, frameInfo.unencryptedBytes),
@@ -906,7 +915,7 @@ export class FrameCryptor extends BaseFrameCryptor {
906
915
  };
907
916
  }
908
917
  } catch (e) {
909
- this.logNALUFallbackOnce(fallbackKey, payloadType, e);
918
+ this.logNALUFallbackOnce(fallbackKey, frame, payloadType, detectedCodec, e);
910
919
  }
911
920
 
912
921
  // Fallback to VP8 handling
@@ -919,7 +928,9 @@ export class FrameCryptor extends BaseFrameCryptor {
919
928
  */
920
929
  private logNALUFallbackOnce(
921
930
  fallbackKey: string,
931
+ frame: RTCEncodedVideoFrame,
922
932
  payloadType: number | undefined,
933
+ detectedCodec: VideoCodec | undefined,
923
934
  error: unknown,
924
935
  ) {
925
936
  if (this.loggedNALUFallbacks.has(fallbackKey)) {
@@ -929,6 +940,12 @@ export class FrameCryptor extends BaseFrameCryptor {
929
940
  workerLogger.warn('NALU processing failed, falling back to VP8 handling', {
930
941
  error,
931
942
  payloadType,
943
+ detectedCodec,
944
+ frameType: frame.type,
945
+ byteLength: frame.data.byteLength,
946
+ firstBytes: Array.from(new Uint8Array(frame.data, 0, Math.min(16, frame.data.byteLength)))
947
+ .map((b) => b.toString(16).padStart(2, '0'))
948
+ .join(' '),
932
949
  ...this.logContext,
933
950
  });
934
951
  }
@@ -0,0 +1,123 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { normalizeAnnexBStartCodes, processNALUsForEncryption } from './naluUtils';
3
+
4
+ /** Annex-B frame: a 4-byte start code before every NALU. */
5
+ function annexB(...nalus: number[][]) {
6
+ return Uint8Array.from(nalus.flatMap((nalu) => [0x00, 0x00, 0x00, 0x01, ...nalu]));
7
+ }
8
+
9
+ // h264: 0x06 SEI, 0x65 IDR slice, 0x67 SPS. h265: 0x26 IDR_W_RADL slice.
10
+ // An h264 SEI byte (0x06) reads as h265 TSA_R, which is why the SEI leads here.
11
+ // findNALUIndices stops at stream.length - 3. A trailing NALU with fewer than 4 payload
12
+ // bytes is never indexed, so its fixture tests nothing. Interior NALUs are fine at any
13
+ // length, because the next start code ends them.
14
+ const h264KeyFrame = annexB([0x06, 0x11, 0x22], [0x65, 0x33, 0x44, 0x55, 0x66]);
15
+ const h265KeyFrame = annexB([0x26, 0x11, 0x22], [0x02, 0x33, 0x44, 0x55, 0x66]);
16
+ const noSliceFrame = annexB([0x67, 0x11, 0x22, 0x33, 0x44, 0x55]);
17
+ // 0x25 is a slice under both: h264 &0x1f = 5 IDR, h265 (b >> 1) & 0x3f = 18 BLA_W_RADL.
18
+ const ambiguousFrame = annexB([0x25, 0x11, 0x22, 0x33, 0x44, 0x55]);
19
+
20
+ describe('normalizeAnnexBStartCodes', () => {
21
+ it('rewrites 3-byte start codes to 4-byte start codes', () => {
22
+ // prettier-ignore
23
+ const input = new Uint8Array([
24
+ 0x00, 0x00, 0x01, 0x67, 0x64, // SPS
25
+ 0x00, 0x00, 0x01, 0x68, 0xee, // PPS
26
+ 0x00, 0x00, 0x01, 0x65, 0x88, // IDR slice
27
+ ]);
28
+ // prettier-ignore
29
+ const expected = new Uint8Array([
30
+ 0x00, 0x00, 0x00, 0x01, 0x67, 0x64,
31
+ 0x00, 0x00, 0x00, 0x01, 0x68, 0xee,
32
+ 0x00, 0x00, 0x00, 0x01, 0x65, 0x88,
33
+ ]);
34
+
35
+ expect(normalizeAnnexBStartCodes(input)).toEqual(expected);
36
+ });
37
+
38
+ it('rewrites only the 3-byte start codes when mixed with 4-byte ones', () => {
39
+ // prettier-ignore
40
+ const input = new Uint8Array([
41
+ 0x00, 0x00, 0x00, 0x01, 0x67, 0x64,
42
+ 0x00, 0x00, 0x01, 0x68, 0xee,
43
+ 0x00, 0x00, 0x00, 0x01, 0x06, 0x05,
44
+ 0x00, 0x00, 0x01, 0x65, 0x88,
45
+ ]);
46
+ // prettier-ignore
47
+ const expected = new Uint8Array([
48
+ 0x00, 0x00, 0x00, 0x01, 0x67, 0x64,
49
+ 0x00, 0x00, 0x00, 0x01, 0x68, 0xee,
50
+ 0x00, 0x00, 0x00, 0x01, 0x06, 0x05,
51
+ 0x00, 0x00, 0x00, 0x01, 0x65, 0x88,
52
+ ]);
53
+
54
+ expect(normalizeAnnexBStartCodes(input)).toEqual(expected);
55
+ });
56
+
57
+ it('returns the same instance when all start codes are already 4-byte', () => {
58
+ // prettier-ignore
59
+ const input = new Uint8Array([
60
+ 0x00, 0x00, 0x00, 0x01, 0x67, 0x64,
61
+ 0x00, 0x00, 0x00, 0x01, 0x65, 0x88,
62
+ ]);
63
+
64
+ expect(normalizeAnnexBStartCodes(input)).toBe(input);
65
+ });
66
+
67
+ it('does not add a zero when a start code already has extra leading zeros', () => {
68
+ const input = new Uint8Array([0x00, 0x00, 0x00, 0x00, 0x01, 0x65, 0x88]);
69
+
70
+ expect(normalizeAnnexBStartCodes(input)).toBe(input);
71
+ });
72
+ });
73
+
74
+ describe('processNALUsForEncryption', () => {
75
+ it('slices at the h264 slice NALU even when seeded with h265', () => {
76
+ // The whole point: the h265 seed must not win, or the offset lands at 6 (inside the
77
+ // SEI) and the slice header gets encrypted with no error raised.
78
+ expect(processNALUsForEncryption(h264KeyFrame, 'h265')).toEqual({
79
+ unencryptedBytes: 13,
80
+ detectedCodec: 'h264',
81
+ requiresNALUProcessing: true,
82
+ });
83
+ });
84
+
85
+ it('reads an h264 frame whose SEI precedes the slice as h264', () => {
86
+ expect(processNALUsForEncryption(h264KeyFrame)).toEqual({
87
+ unencryptedBytes: 13,
88
+ detectedCodec: 'h264',
89
+ requiresNALUProcessing: true,
90
+ });
91
+ });
92
+
93
+ it('still reads an h265 frame as h265', () => {
94
+ expect(processNALUsForEncryption(h265KeyFrame)).toEqual({
95
+ unencryptedBytes: 6,
96
+ detectedCodec: 'h265',
97
+ requiresNALUProcessing: true,
98
+ });
99
+ expect(processNALUsForEncryption(h265KeyFrame, 'h264')).toEqual({
100
+ unencryptedBytes: 6,
101
+ detectedCodec: 'h265',
102
+ requiresNALUProcessing: true,
103
+ });
104
+ });
105
+
106
+ it('resolves a byte that is a slice under both codecs as h264', () => {
107
+ // Only the h264-first ordering decides this one. Swap the two loops back and it reads h265.
108
+ expect(processNALUsForEncryption(ambiguousFrame)).toEqual({
109
+ unencryptedBytes: 6,
110
+ detectedCodec: 'h264',
111
+ requiresNALUProcessing: true,
112
+ });
113
+ });
114
+
115
+ it('throws with a seed, and returns quietly without one, when no slice NALU is present', () => {
116
+ expect(() => processNALUsForEncryption(noSliceFrame, 'h264')).toThrow('Could not find NALU');
117
+ expect(processNALUsForEncryption(noSliceFrame)).toEqual({
118
+ unencryptedBytes: 0,
119
+ detectedCodec: 'unknown',
120
+ requiresNALUProcessing: false,
121
+ });
122
+ });
123
+ });
@@ -202,8 +202,14 @@ export interface NALUProcessingResult {
202
202
  * @returns Detected codec type
203
203
  */
204
204
  function detectCodecFromNALUs(data: NonSharedUint8Array, naluIndices: number[]): DetectedCodec {
205
+ // h264 runs to completion first. Several non-slice h264 NALUs read as h265 slice types
206
+ // (AUD 0x09 -> STSA_N, SEI 0x06 -> TSA_R), so interleaving the two checks lets one of
207
+ // those outrank the real h264 slice that follows it. The reverse cannot happen: h265
208
+ // header bytes are even at nuh_layer_id 0, so they never mask to an h264 slice type.
205
209
  for (const naluIndex of naluIndices) {
206
210
  if (isH264SliceNALU(parseH264NALUType(data[naluIndex]))) return 'h264';
211
+ }
212
+ for (const naluIndex of naluIndices) {
207
213
  if (isH265SliceNALU(parseH265NALUType(data[naluIndex]))) return 'h265';
208
214
  }
209
215
  return 'unknown';
@@ -306,7 +312,7 @@ function findNALUIndices(stream: NonSharedUint8Array): number[] {
306
312
  /**
307
313
  * Process NALU data for frame encryption, detecting codec and finding unencrypted bytes
308
314
  * @param data Frame data
309
- * @param knownCodec Known codec from other sources (optional)
315
+ * @param knownCodec Codec from other sources, used only when the bytes are inconclusive (optional)
310
316
  * @returns NALU processing result
311
317
  */
312
318
  export function processNALUsForEncryption(
@@ -314,7 +320,16 @@ export function processNALUsForEncryption(
314
320
  knownCodec?: 'h264' | 'h265',
315
321
  ): NALUProcessingResult {
316
322
  const naluIndices = findNALUIndices(data);
317
- const detectedCodec = knownCodec ?? detectCodecFromNALUs(data, naluIndices);
323
+ // The bytes outrank knownCodec, which can be the codec the client asked for rather than the
324
+ // one that was negotiated. Parsing the wrong one mis-slices without raising anything: the
325
+ // offset lands before the real slice header, so the header itself gets encrypted.
326
+ // That holds in one direction only. An h264 access unit carrying no slice NALU but carrying
327
+ // an AUD or SEI that reads as an h265 slice now slices there instead of throwing. No real
328
+ // WebRTC access unit can reach it, because every one of them carries a type 1 or 5 slice.
329
+ let detectedCodec = detectCodecFromNALUs(data, naluIndices);
330
+ if (detectedCodec === 'unknown') {
331
+ detectedCodec = knownCodec ?? 'unknown';
332
+ }
318
333
 
319
334
  if (detectedCodec === 'unknown') {
320
335
  return { unencryptedBytes: 0, detectedCodec, requiresNALUProcessing: false };
@@ -327,3 +342,43 @@ export function processNALUsForEncryption(
327
342
 
328
343
  return { unencryptedBytes, detectedCodec, requiresNALUProcessing: true };
329
344
  }
345
+
346
+ /**
347
+ * Rewrite every 3-byte Annex B start code (00 00 01) in `data` to the 4-byte form (00 00 00 01).
348
+ *
349
+ * WebRTC's H.264/H.265 depacketizer always reconstructs NALUs with 4-byte start codes, regardless
350
+ * of what the encoder emitted. Some encoders (e.g. Intel Quick Sync) emit 3-byte start codes, so
351
+ * the unencrypted header a receiver sees would differ from what the sender authenticated as
352
+ * AES-GCM additionalData. Normalizing on the sender makes both sides agree.
353
+ *
354
+ * Returns `data` unchanged (same instance) when it contains no 3-byte start codes.
355
+ */
356
+ export function normalizeAnnexBStartCodes(data: NonSharedUint8Array): NonSharedUint8Array {
357
+ const shortStartCodes: number[] = [];
358
+ for (let i = 0; i + 2 < data.length;) {
359
+ if (data[i + 2] === 0) {
360
+ i += 1;
361
+ continue;
362
+ }
363
+ if (data[i + 2] === 1 && data[i] === 0 && data[i + 1] === 0 && (i === 0 || data[i - 1] !== 0)) {
364
+ shortStartCodes.push(i);
365
+ }
366
+ // data[i + 2] is non-zero, so no start code can begin at i + 1 or i + 2 either.
367
+ i += 3;
368
+ }
369
+ if (shortStartCodes.length === 0) {
370
+ return data;
371
+ }
372
+
373
+ // The new array is zero-filled, so the one-byte gap left before each short start code becomes
374
+ // the inserted leading 00.
375
+ const result = new Uint8Array(data.length + shortStartCodes.length);
376
+ let from = 0;
377
+ for (let inserted = 0; inserted < shortStartCodes.length; inserted++) {
378
+ const to = shortStartCodes[inserted];
379
+ result.set(data.subarray(from, to), from + inserted);
380
+ from = to;
381
+ }
382
+ result.set(data.subarray(from), from + shortStartCodes.length);
383
+ return result;
384
+ }
package/src/index.ts CHANGED
@@ -56,6 +56,7 @@ import {
56
56
  isSVCCodec,
57
57
  isVideoCodec,
58
58
  isVideoTrack,
59
+ releaseEmptyAudioStreamTrack,
59
60
  supportsAV1,
60
61
  supportsAdaptiveStream,
61
62
  supportsAudioOutputSelection,
@@ -145,6 +146,7 @@ export {
145
146
  getEmptyVideoStreamTrack,
146
147
  getLogger,
147
148
  isBrowserSupported,
149
+ releaseEmptyAudioStreamTrack,
148
150
  setLogExtension,
149
151
  setLogLevel,
150
152
  supportsAV1,
package/src/options.ts CHANGED
@@ -166,6 +166,22 @@ export interface InternalRoomConnectOptions {
166
166
  */
167
167
  rtcConfig?: RTCConfiguration;
168
168
 
169
+ /**
170
+ * when the server runs ICE lite, asks it to use a full ICE agent for this participant.
171
+ * this is a server-side setting that the server may ignore, defaults to false
172
+ */
173
+ disableIceLite: boolean;
174
+
175
+ /**
176
+ * whether the SFU may pause subscribed video tracks when this participant's downstream
177
+ * connection is congested, so that other tracks can continue to stream smoothly.
178
+ * paused tracks resume automatically when conditions allow, and `RoomEvent.TrackStreamStateChanged`
179
+ * fires on each transition.
180
+ * when left undefined, the server's default applies (the project setting on LiveKit Cloud,
181
+ * or `rtc.congestion_control.allow_pause` when self-hosting)
182
+ */
183
+ subscriberAllowPause?: boolean;
184
+
169
185
  /** specifies how often an initial join connection is allowed to retry (only applicable if server is not reachable) */
170
186
  maxRetries: number;
171
187