livekit-client 2.22.2 → 2.22.4

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 (245) 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 +488 -438
  5. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  6. package/dist/livekit-client.esm.mjs +954 -301
  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 +10 -2
  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.map +1 -1
  17. package/dist/src/api/utils.d.ts +1 -0
  18. package/dist/src/api/utils.d.ts.map +1 -1
  19. package/dist/src/e2ee/E2eeManager.d.ts +26 -0
  20. package/dist/src/e2ee/E2eeManager.d.ts.map +1 -1
  21. package/dist/src/e2ee/types.d.ts +15 -1
  22. package/dist/src/e2ee/types.d.ts.map +1 -1
  23. package/dist/src/e2ee/worker/DataCryptor.d.ts.map +1 -1
  24. package/dist/src/e2ee/worker/ErrorRateLimiter.d.ts +21 -0
  25. package/dist/src/e2ee/worker/ErrorRateLimiter.d.ts.map +1 -0
  26. package/dist/src/e2ee/worker/FrameCryptor.d.ts +1 -18
  27. package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -1
  28. package/dist/src/logger.d.ts +4 -0
  29. package/dist/src/logger.d.ts.map +1 -1
  30. package/dist/src/options.d.ts +14 -0
  31. package/dist/src/options.d.ts.map +1 -1
  32. package/dist/src/room/PCTransport.d.ts +78 -7
  33. package/dist/src/room/PCTransport.d.ts.map +1 -1
  34. package/dist/src/room/PCTransportManager.d.ts.map +1 -1
  35. package/dist/src/room/RTCEngine.d.ts +9 -0
  36. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  37. package/dist/src/room/Room.d.ts +1 -1
  38. package/dist/src/room/Room.d.ts.map +1 -1
  39. package/dist/src/room/defaults.d.ts.map +1 -1
  40. package/dist/src/room/errors.d.ts +18 -0
  41. package/dist/src/room/errors.d.ts.map +1 -1
  42. package/dist/src/room/events.d.ts +3 -2
  43. package/dist/src/room/events.d.ts.map +1 -1
  44. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  45. package/dist/src/room/participant/publishUtils.d.ts +16 -0
  46. package/dist/src/room/participant/publishUtils.d.ts.map +1 -1
  47. package/dist/src/room/rpc/client/RpcClientManager.d.ts.map +1 -1
  48. package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
  49. package/dist/src/room/track/LocalVideoTrack.d.ts +9 -0
  50. package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
  51. package/dist/src/room/track/options.d.ts +1 -1
  52. package/dist/src/room/track/utils.d.ts +13 -0
  53. package/dist/src/room/track/utils.d.ts.map +1 -1
  54. package/dist/src/room/utils.d.ts +66 -0
  55. package/dist/src/room/utils.d.ts.map +1 -1
  56. package/dist/src/test/compressionStreamStubs.d.ts +12 -0
  57. package/dist/src/test/compressionStreamStubs.d.ts.map +1 -0
  58. package/dist/ts4.2/api/SignalClient.d.ts +187 -185
  59. package/dist/ts4.2/api/SignalClientStateMachine.d.ts +85 -85
  60. package/dist/ts4.2/api/WebSocketStream.d.ts +33 -33
  61. package/dist/ts4.2/api/utils.d.ts +9 -8
  62. package/dist/ts4.2/connectionHelper/ConnectionCheck.d.ts +30 -30
  63. package/dist/ts4.2/connectionHelper/checks/Checker.d.ts +62 -62
  64. package/dist/ts4.2/connectionHelper/checks/cloudRegion.d.ts +18 -18
  65. package/dist/ts4.2/connectionHelper/checks/connectionProtocol.d.ts +20 -20
  66. package/dist/ts4.2/connectionHelper/checks/publishAudio.d.ts +6 -6
  67. package/dist/ts4.2/connectionHelper/checks/publishVideo.d.ts +7 -7
  68. package/dist/ts4.2/connectionHelper/checks/reconnect.d.ts +6 -6
  69. package/dist/ts4.2/connectionHelper/checks/turn.d.ts +6 -6
  70. package/dist/ts4.2/connectionHelper/checks/webrtc.d.ts +6 -6
  71. package/dist/ts4.2/connectionHelper/checks/websocket.d.ts +6 -6
  72. package/dist/ts4.2/e2ee/E2eeManager.d.ts +111 -85
  73. package/dist/ts4.2/e2ee/KeyProvider.d.ts +54 -54
  74. package/dist/ts4.2/e2ee/constants.d.ts +21 -21
  75. package/dist/ts4.2/e2ee/errors.d.ts +12 -12
  76. package/dist/ts4.2/e2ee/events.d.ts +39 -39
  77. package/dist/ts4.2/e2ee/index.d.ts +6 -6
  78. package/dist/ts4.2/e2ee/types.d.ts +231 -217
  79. package/dist/ts4.2/e2ee/utils.d.ts +32 -32
  80. package/dist/ts4.2/e2ee/worker/DataCryptor.d.ts +16 -16
  81. package/dist/ts4.2/e2ee/worker/ErrorRateLimiter.d.ts +21 -0
  82. package/dist/ts4.2/e2ee/worker/FrameCryptor.d.ts +209 -226
  83. package/dist/ts4.2/e2ee/worker/ParticipantKeyHandler.d.ts +81 -84
  84. package/dist/ts4.2/e2ee/worker/e2ee.worker.d.ts +2 -2
  85. package/dist/ts4.2/e2ee/worker/naluUtils.d.ts +28 -28
  86. package/dist/ts4.2/e2ee/worker/sifPayload.d.ts +23 -23
  87. package/dist/ts4.2/frameMetadata/FrameMetadataManager.d.ts +53 -53
  88. package/dist/ts4.2/frameMetadata/frameMetadata.d.ts +33 -33
  89. package/dist/ts4.2/frameMetadata/types.d.ts +62 -62
  90. package/dist/ts4.2/frameMetadata/utils.d.ts +9 -9
  91. package/dist/ts4.2/frameMetadata/worker/frameMetadata.worker.d.ts +2 -2
  92. package/dist/ts4.2/index.d.ts +76 -76
  93. package/dist/ts4.2/logger.d.ts +66 -62
  94. package/dist/ts4.2/options.d.ts +163 -149
  95. package/dist/ts4.2/room/BackOffStrategy.d.ts +27 -27
  96. package/dist/ts4.2/room/DefaultReconnectPolicy.d.ts +8 -8
  97. package/dist/ts4.2/room/DeviceManager.d.ts +12 -12
  98. package/dist/ts4.2/room/PCTransport.d.ts +246 -175
  99. package/dist/ts4.2/room/PCTransportManager.d.ts +77 -77
  100. package/dist/ts4.2/room/RTCEngine.d.ts +269 -261
  101. package/dist/ts4.2/room/ReconnectPolicy.d.ts +24 -24
  102. package/dist/ts4.2/room/RegionUrlProvider.d.ts +36 -36
  103. package/dist/ts4.2/room/Room.d.ts +354 -365
  104. package/dist/ts4.2/room/attribute-typings.d.ts +35 -35
  105. package/dist/ts4.2/room/data-channel/DataChannelManager.d.ts +62 -62
  106. package/dist/ts4.2/room/data-channel/FlowControlledDataChannel.d.ts +94 -95
  107. package/dist/ts4.2/room/data-channel/LossyDataChannel.d.ts +41 -41
  108. package/dist/ts4.2/room/data-channel/ReliableDataChannel.d.ts +57 -57
  109. package/dist/ts4.2/room/data-channel/types.d.ts +12 -12
  110. package/dist/ts4.2/room/data-stream/compression.d.ts +49 -49
  111. package/dist/ts4.2/room/data-stream/constants.d.ts +18 -18
  112. package/dist/ts4.2/room/data-stream/incoming/IncomingDataStreamManager.d.ts +29 -29
  113. package/dist/ts4.2/room/data-stream/incoming/StreamReader.d.ts +80 -80
  114. package/dist/ts4.2/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts +73 -73
  115. package/dist/ts4.2/room/data-stream/outgoing/StreamWriter.d.ts +16 -16
  116. package/dist/ts4.2/room/data-stream/outgoing/header-utils.d.ts +18 -18
  117. package/dist/ts4.2/room/data-track/LocalDataTrack.d.ts +79 -80
  118. package/dist/ts4.2/room/data-track/RemoteDataTrack.d.ts +50 -50
  119. package/dist/ts4.2/room/data-track/depacketizer.d.ts +59 -59
  120. package/dist/ts4.2/room/data-track/frame.d.ts +22 -22
  121. package/dist/ts4.2/room/data-track/handle.d.ts +27 -27
  122. package/dist/ts4.2/room/data-track/incoming/IncomingDataTrackManager.d.ts +124 -124
  123. package/dist/ts4.2/room/data-track/incoming/errors.d.ts +24 -24
  124. package/dist/ts4.2/room/data-track/incoming/pipeline.d.ts +41 -41
  125. package/dist/ts4.2/room/data-track/incoming/types.d.ts +20 -20
  126. package/dist/ts4.2/room/data-track/outgoing/OutgoingDataTrackManager.d.ts +126 -126
  127. package/dist/ts4.2/room/data-track/outgoing/errors.d.ts +74 -74
  128. package/dist/ts4.2/room/data-track/outgoing/pipeline.d.ts +23 -23
  129. package/dist/ts4.2/room/data-track/outgoing/types.d.ts +48 -48
  130. package/dist/ts4.2/room/data-track/packet/constants.d.ts +20 -20
  131. package/dist/ts4.2/room/data-track/packet/errors.d.ts +40 -40
  132. package/dist/ts4.2/room/data-track/packet/extensions.d.ts +69 -69
  133. package/dist/ts4.2/room/data-track/packet/index.d.ts +98 -98
  134. package/dist/ts4.2/room/data-track/packet/serializable.d.ts +13 -13
  135. package/dist/ts4.2/room/data-track/packetizer.d.ts +43 -43
  136. package/dist/ts4.2/room/data-track/track-interfaces.d.ts +23 -23
  137. package/dist/ts4.2/room/data-track/types.d.ts +22 -22
  138. package/dist/ts4.2/room/data-track/utils.d.ts +60 -60
  139. package/dist/ts4.2/room/debounce.d.ts +11 -11
  140. package/dist/ts4.2/room/defaults.d.ts +9 -9
  141. package/dist/ts4.2/room/errors.d.ts +180 -162
  142. package/dist/ts4.2/room/events.d.ts +638 -637
  143. package/dist/ts4.2/room/participant/LocalParticipant.d.ts +287 -287
  144. package/dist/ts4.2/room/participant/Participant.d.ts +142 -144
  145. package/dist/ts4.2/room/participant/ParticipantTrackPermission.d.ts +26 -26
  146. package/dist/ts4.2/room/participant/RemoteParticipant.d.ts +85 -86
  147. package/dist/ts4.2/room/participant/publishUtils.d.ts +53 -37
  148. package/dist/ts4.2/room/rpc/client/RpcClientManager.d.ts +43 -43
  149. package/dist/ts4.2/room/rpc/client/events.d.ts +8 -8
  150. package/dist/ts4.2/room/rpc/index.d.ts +7 -7
  151. package/dist/ts4.2/room/rpc/server/RpcServerManager.d.ts +44 -44
  152. package/dist/ts4.2/room/rpc/server/events.d.ts +8 -8
  153. package/dist/ts4.2/room/rpc/utils.d.ts +131 -131
  154. package/dist/ts4.2/room/stats.d.ts +71 -71
  155. package/dist/ts4.2/room/statsSummary.d.ts +13 -13
  156. package/dist/ts4.2/room/timers.d.ts +12 -12
  157. package/dist/ts4.2/room/token-source/TokenSource.d.ts +82 -82
  158. package/dist/ts4.2/room/token-source/types.d.ts +70 -70
  159. package/dist/ts4.2/room/token-source/utils.d.ts +8 -8
  160. package/dist/ts4.2/room/track/FrameMetadataExtractor.d.ts +19 -19
  161. package/dist/ts4.2/room/track/LocalAudioTrack.d.ts +39 -42
  162. package/dist/ts4.2/room/track/LocalTrack.d.ts +125 -125
  163. package/dist/ts4.2/room/track/LocalTrackPublication.d.ts +42 -42
  164. package/dist/ts4.2/room/track/LocalVideoTrack.d.ts +102 -93
  165. package/dist/ts4.2/room/track/RemoteAudioTrack.d.ts +55 -55
  166. package/dist/ts4.2/room/track/RemoteTrack.d.ts +38 -38
  167. package/dist/ts4.2/room/track/RemoteTrackPublication.d.ts +72 -75
  168. package/dist/ts4.2/room/track/RemoteVideoTrack.d.ts +73 -76
  169. package/dist/ts4.2/room/track/Track.d.ts +156 -166
  170. package/dist/ts4.2/room/track/TrackPublication.d.ts +75 -81
  171. package/dist/ts4.2/room/track/create.d.ts +25 -25
  172. package/dist/ts4.2/room/track/facingMode.d.ts +41 -41
  173. package/dist/ts4.2/room/track/options.d.ts +403 -403
  174. package/dist/ts4.2/room/track/processor/types.d.ts +31 -31
  175. package/dist/ts4.2/room/track/record.d.ts +13 -13
  176. package/dist/ts4.2/room/track/types.d.ts +28 -28
  177. package/dist/ts4.2/room/track/utils.d.ts +60 -47
  178. package/dist/ts4.2/room/types.d.ts +127 -127
  179. package/dist/ts4.2/room/utils.d.ts +246 -180
  180. package/dist/ts4.2/test/MockMediaStreamTrack.d.ts +26 -26
  181. package/dist/ts4.2/test/compressionStreamStubs.d.ts +12 -0
  182. package/dist/ts4.2/test/mocks.d.ts +12 -12
  183. package/dist/ts4.2/test/promiseState.d.ts +12 -12
  184. package/dist/ts4.2/test/signalToken.d.ts +26 -26
  185. package/dist/ts4.2/type-polyfills/non-shared-typed-arrays.d.ts +2 -2
  186. package/dist/ts4.2/utils/AsyncQueue.d.ts +23 -23
  187. package/dist/ts4.2/utils/TypedPromise.d.ts +17 -17
  188. package/dist/ts4.2/utils/abort-signal-polyfill.d.ts +13 -13
  189. package/dist/ts4.2/utils/browserParser.d.ts +13 -13
  190. package/dist/ts4.2/utils/camelToSnakeCase.d.ts +8 -8
  191. package/dist/ts4.2/utils/cloneDeep.d.ts +2 -2
  192. package/dist/ts4.2/utils/dataPacketBuffer.d.ts +34 -34
  193. package/dist/ts4.2/utils/deferrable-map.d.ts +32 -32
  194. package/dist/ts4.2/utils/machineInspector.d.ts +54 -54
  195. package/dist/ts4.2/utils/serializer.d.ts +48 -48
  196. package/dist/ts4.2/utils/subscribeToEvents.d.ts +15 -15
  197. package/dist/ts4.2/utils/ttlmap.d.ts +20 -20
  198. package/dist/ts4.2/version.d.ts +15 -15
  199. package/package.json +5 -4
  200. package/src/api/SignalClient.e2e.test.ts +1 -3
  201. package/src/api/SignalClient.test.ts +160 -10
  202. package/src/api/SignalClient.ts +75 -54
  203. package/src/api/WebSocketStream.ts +3 -8
  204. package/src/api/utils.ts +10 -0
  205. package/src/e2ee/E2eeManager.test.ts +196 -0
  206. package/src/e2ee/E2eeManager.ts +114 -19
  207. package/src/e2ee/types.ts +19 -1
  208. package/src/e2ee/worker/DataCryptor.ts +2 -1
  209. package/src/e2ee/worker/ErrorRateLimiter.test.ts +53 -0
  210. package/src/e2ee/worker/ErrorRateLimiter.ts +52 -0
  211. package/src/e2ee/worker/FrameCryptor.ts +20 -70
  212. package/src/e2ee/worker/e2ee.worker.ts +34 -11
  213. package/src/logger.ts +22 -0
  214. package/src/options.ts +16 -0
  215. package/src/room/PCTransport.test.ts +183 -7
  216. package/src/room/PCTransport.ts +197 -36
  217. package/src/room/PCTransportManager.ts +3 -1
  218. package/src/room/RTCEngine.test.ts +84 -0
  219. package/src/room/RTCEngine.ts +52 -12
  220. package/src/room/Room.test.ts +32 -6
  221. package/src/room/Room.ts +27 -7
  222. package/src/room/data-stream/incoming/IncomingDataStreamManager.test.ts +16 -20
  223. package/src/room/data-stream/incoming/IncomingDataStreamManager.ts +5 -5
  224. package/src/room/data-stream/outgoing/OutgoingDataStreamManager.test.ts +60 -39
  225. package/src/room/data-stream/outgoing/OutgoingDataStreamManager.ts +4 -4
  226. package/src/room/defaults.ts +1 -0
  227. package/src/room/errors.test.ts +36 -0
  228. package/src/room/errors.ts +27 -0
  229. package/src/room/events.ts +2 -1
  230. package/src/room/participant/LocalParticipant.ts +30 -14
  231. package/src/room/participant/publishUtils.test.ts +133 -0
  232. package/src/room/participant/publishUtils.ts +54 -38
  233. package/src/room/rpc/client/RpcClientManager.test.ts +51 -0
  234. package/src/room/rpc/client/RpcClientManager.ts +32 -15
  235. package/src/room/track/LocalTrack.ts +18 -7
  236. package/src/room/track/LocalVideoTrack.ts +25 -8
  237. package/src/room/track/RemoteAudioTrack.ts +3 -3
  238. package/src/room/track/RemoteVideoTrack.test.ts +210 -0
  239. package/src/room/track/RemoteVideoTrack.ts +29 -3
  240. package/src/room/track/options.ts +1 -1
  241. package/src/room/track/utils.test.ts +201 -2
  242. package/src/room/track/utils.ts +88 -0
  243. package/src/room/utils.test.ts +211 -0
  244. package/src/room/utils.ts +129 -0
  245. package/src/test/compressionStreamStubs.ts +53 -0
@@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest';
2
2
  import { ScreenSharePresets, VideoPreset, VideoPresets, VideoPresets43 } from '../track/options';
3
3
  import {
4
4
  computeDefaultScreenShareSimulcastPresets,
5
+ computeStartTargetBitrate,
5
6
  computeVideoEncodings,
6
7
  determineAppropriateEncoding,
7
8
  presets43,
@@ -116,6 +117,78 @@ describe('computeVideoEncodings', () => {
116
117
  expect(encodings![0].scaleResolutionDownBy).toBe(1);
117
118
  });
118
119
 
120
+ // svc carries the scalabilityMode on the first encoding only (whether it emits a
121
+ // single encoding or the legacy multi-encoding shape), simulcast carries it on all
122
+ const countScalabilityModes = (encodings?: RTCRtpEncodingParameters[]) =>
123
+ /* @ts-ignore */
124
+ encodings!.filter((encoding) => encoding.scalabilityMode !== undefined).length;
125
+
126
+ it('keeps svc for an svc codec without simulcast', () => {
127
+ const encodings = computeVideoEncodings(false, 960, 540, {
128
+ simulcast: false,
129
+ videoCodec: 'vp9',
130
+ scalabilityMode: 'L3T3_KEY',
131
+ });
132
+ /* @ts-ignore */
133
+ expect(encodings![0].scalabilityMode).toBe('L3T3_KEY');
134
+ expect(countScalabilityModes(encodings)).toBe(1);
135
+ });
136
+
137
+ it('keeps svc for an svc codec with a multi spatial layer mode even if simulcast is set', () => {
138
+ const encodings = computeVideoEncodings(false, 960, 540, {
139
+ simulcast: true,
140
+ videoCodec: 'vp9',
141
+ scalabilityMode: 'L3T3_KEY',
142
+ });
143
+ /* @ts-ignore */
144
+ expect(encodings![0].scalabilityMode).toBe('L3T3_KEY');
145
+ expect(countScalabilityModes(encodings)).toBe(1);
146
+ });
147
+
148
+ it('returns a simulcast ladder for an svc codec with simulcast and an L1Tx mode', () => {
149
+ for (const videoCodec of ['vp9', 'av1'] as const) {
150
+ const encodings = computeVideoEncodings(false, 960, 540, {
151
+ simulcast: true,
152
+ videoCodec,
153
+ scalabilityMode: 'L1T2',
154
+ });
155
+ expect(encodings).toHaveLength(3);
156
+ expect(encodings!.map((e) => e.rid)).toEqual(['q', 'h', 'f']);
157
+ // every encoding needs both scalabilityMode and scaleResolutionDownBy for chrome
158
+ // M113+ to treat them as real simulcast rather than legacy svc
159
+ encodings!.forEach((encoding) => {
160
+ /* @ts-ignore */
161
+ expect(encoding.scalabilityMode).toBe('L1T2');
162
+ expect(encoding.scaleResolutionDownBy).toBeGreaterThanOrEqual(1);
163
+ });
164
+ }
165
+ });
166
+
167
+ it('sets the scalability mode on a single encoding svc simulcast ladder', () => {
168
+ const encodings = computeVideoEncodings(false, 100, 120, {
169
+ simulcast: true,
170
+ videoCodec: 'vp9',
171
+ scalabilityMode: 'L1T3',
172
+ });
173
+ expect(encodings).toHaveLength(1);
174
+ expect(encodings![0].rid).toBe('q');
175
+ /* @ts-ignore */
176
+ expect(encodings![0].scalabilityMode).toBe('L1T3');
177
+ });
178
+
179
+ it('does not set a scalability mode for non-svc simulcast', () => {
180
+ const encodings = computeVideoEncodings(false, 960, 540, {
181
+ simulcast: true,
182
+ videoCodec: 'vp8',
183
+ scalabilityMode: 'L1T2',
184
+ });
185
+ expect(encodings).toHaveLength(3);
186
+ encodings!.forEach((encoding) => {
187
+ /* @ts-ignore */
188
+ expect(encoding.scalabilityMode).toBeUndefined();
189
+ });
190
+ });
191
+
119
192
  // it('respects default backup codec encoding', () => {
120
193
  // const vp8Encodings = computeTrackBackupEncodings(false, 100, 120, { simulcast: true });
121
194
  // const h264Encodings = computeVideoEncodings(false, 100, 120, {
@@ -193,3 +266,63 @@ describe('screenShareSimulcastDefaults', () => {
193
266
  expect(defaultSimulcastLayers[0].encoding.maxBitrate).toBe(375000);
194
267
  });
195
268
  });
269
+
270
+ describe('computeStartTargetBitrate', () => {
271
+ // ordered q..f, as encodingsFromPresets builds them
272
+ const simulcastLadder: RTCRtpEncodingParameters[] = [
273
+ { rid: 'q', maxBitrate: 160_000 },
274
+ { rid: 'h', maxBitrate: 450_000 },
275
+ { rid: 'f', maxBitrate: 680_000 },
276
+ ];
277
+
278
+ it('sums the ladder for plain simulcast', () => {
279
+ expect(computeStartTargetBitrate('vp8', { simulcast: true }, simulcastLadder)).toBe(1_290_000);
280
+ });
281
+
282
+ it('sums the ladder for vp9/av1 published as rid simulcast', () => {
283
+ // encodings[0] is the *smallest* layer here, so taking it would under-hint BWE
284
+ for (const codec of ['vp9', 'av1'] as const) {
285
+ expect(
286
+ computeStartTargetBitrate(
287
+ codec,
288
+ { simulcast: true, videoCodec: codec, scalabilityMode: 'L1T2' },
289
+ simulcastLadder,
290
+ ),
291
+ ).toBe(1_290_000);
292
+ }
293
+ });
294
+
295
+ it('uses the single encoding for vp9/av1 svc', () => {
296
+ for (const codec of ['vp9', 'av1'] as const) {
297
+ expect(
298
+ computeStartTargetBitrate(
299
+ codec,
300
+ { simulcast: false, videoCodec: codec, scalabilityMode: 'L3T3_KEY' },
301
+ [{ maxBitrate: 680_000 }],
302
+ ),
303
+ ).toBe(680_000);
304
+ }
305
+ });
306
+
307
+ it('uses the first encoding for the legacy svc shape, which is ordered f..q', () => {
308
+ // legacy SVC pushes videoRids[2 - i], so encodings[0] carries the full bitrate
309
+ const legacySvc: RTCRtpEncodingParameters[] = [
310
+ { rid: 'f', maxBitrate: 680_000 },
311
+ { rid: 'h', maxBitrate: 226_667 },
312
+ { rid: 'q', maxBitrate: 75_556 },
313
+ ];
314
+ expect(
315
+ computeStartTargetBitrate(
316
+ 'vp9',
317
+ { simulcast: false, videoCodec: 'vp9', scalabilityMode: 'L3T3_KEY' },
318
+ legacySvc,
319
+ ),
320
+ ).toBe(680_000);
321
+ });
322
+
323
+ it('handles missing bitrates and empty encodings', () => {
324
+ expect(computeStartTargetBitrate('vp8', { simulcast: true }, [])).toBe(0);
325
+ expect(computeStartTargetBitrate('vp9', undefined, [])).toBe(0);
326
+ expect(computeStartTargetBitrate('vp8', { simulcast: true }, [{ rid: 'q' }])).toBe(0);
327
+ });
328
+ });
@@ -13,12 +13,12 @@ import type {
13
13
  import { ScreenSharePresets, VideoPreset, VideoPresets, VideoPresets43 } from '../track/options';
14
14
  import type { LoggerOptions } from '../types';
15
15
  import {
16
- compareVersions,
17
16
  getReactNativeOs,
18
17
  isReactNative,
19
18
  isSVCCodec,
20
- isSafariBased,
19
+ isSVCSimulcast,
21
20
  isSafariSvcApi,
21
+ usesLegacySVCEncodings,
22
22
  } from '../utils';
23
23
 
24
24
  /** @internal */
@@ -74,25 +74,6 @@ export const computeDefaultScreenShareSimulcastPresets = (fromPreset: VideoPrese
74
74
  );
75
75
  };
76
76
 
77
- // /**
78
- // *
79
- // * @internal
80
- // * @experimental
81
- // */
82
- // const computeDefaultMultiCodecSimulcastEncodings = (width: number, height: number) => {
83
- // // use vp8 as a default
84
- // const vp8 = determineAppropriateEncoding(false, width, height);
85
- // const vp9 = { ...vp8, maxBitrate: vp8.maxBitrate * 0.9 };
86
- // const h264 = { ...vp8, maxBitrate: vp8.maxBitrate * 1.1 };
87
- // const av1 = { ...vp8, maxBitrate: vp8.maxBitrate * 0.7 };
88
- // return {
89
- // vp8,
90
- // vp9,
91
- // h264,
92
- // av1,
93
- // };
94
- // };
95
-
96
77
  const videoRids = ['q', 'h', 'f'];
97
78
 
98
79
  /* @internal */
@@ -111,6 +92,8 @@ export function computeVideoEncodings(
111
92
  const useSimulcast = options?.simulcast;
112
93
  const scalabilityMode = options?.scalabilityMode;
113
94
  const videoCodec = options?.videoCodec;
95
+ // VP9/AV1 published as rid based simulcast rather than SVC, see isSVCSimulcast
96
+ const useSVCSimulcast = isSVCSimulcast(videoCodec, options);
114
97
 
115
98
  if ((!videoEncoding && !useSimulcast && !scalabilityMode) || !width || !height) {
116
99
  // when we aren't simulcasting or svc, will need to return a single encoding without
@@ -134,7 +117,7 @@ export function computeVideoEncodings(
134
117
  videoEncoding.priority,
135
118
  );
136
119
 
137
- if (scalabilityMode && isSVCCodec(videoCodec)) {
120
+ if (scalabilityMode && isSVCCodec(videoCodec) && !useSVCSimulcast) {
138
121
  const sm = new ScalabilityMode(scalabilityMode);
139
122
 
140
123
  const encodings: RTCRtpEncodingParameters[] = [];
@@ -142,20 +125,10 @@ export function computeVideoEncodings(
142
125
  if (sm.spatial > 3) {
143
126
  throw new Error(`unsupported scalabilityMode: ${scalabilityMode}`);
144
127
  }
145
- // Before M113 in Chrome, defining multiple encodings with an SVC codec indicated
146
- // that SVC mode should be used. Safari still works this way.
147
- // This is a bit confusing but is due to how libwebrtc interpreted the encodings field
148
- // before M113.
149
- // Announced here: https://groups.google.com/g/discuss-webrtc/c/-QQ3pxrl-fw?pli=1
128
+ // Browsers that read multiple encodings on an SVC codec as SVC rather than as
129
+ // simulcast, see usesLegacySVCEncodings
150
130
  const browser = getBrowser();
151
- if (
152
- isSafariBased() ||
153
- // Even tho RN runs M114, it does not produce SVC layers when a single encoding
154
- // is provided. So we'll use the legacy SVC specification for now.
155
- // TODO: when we upstream libwebrtc, this will need additional verification
156
- isReactNative() ||
157
- (browser?.name === 'Chrome' && compareVersions(browser?.version, '113') < 0)
158
- ) {
131
+ if (usesLegacySVCEncodings()) {
159
132
  const bitratesRatio = sm.suffix == 'h' ? 2 : 3;
160
133
  // safari 18.4 uses a different svc API that requires scaleResolutionDownBy to be set.
161
134
  const requireScale = isSafariSvcApi(browser);
@@ -193,6 +166,19 @@ export function computeVideoEncodings(
193
166
  return [videoEncoding];
194
167
  }
195
168
 
169
+ // Chrome M113+ only treats multiple encodings on an SVC capable codec as real
170
+ // simulcast when every encoding carries an explicit scalabilityMode next to its
171
+ // scaleResolutionDownBy. Without it the encodings are interpreted as legacy SVC.
172
+ const applySVCSimulcastMode = (encodings: RTCRtpEncodingParameters[]) => {
173
+ if (useSVCSimulcast) {
174
+ encodings.forEach((encoding) => {
175
+ /* @ts-ignore */
176
+ encoding.scalabilityMode = scalabilityMode;
177
+ });
178
+ }
179
+ return encodings;
180
+ };
181
+
196
182
  let presets: Array<VideoPreset>;
197
183
  if (isScreenShare) {
198
184
  presets =
@@ -220,13 +206,43 @@ export function computeVideoEncodings(
220
206
  // based on other conditions.
221
207
  const size = Math.max(width, height);
222
208
  if (size >= 960 && midPreset) {
223
- return encodingsFromPresets(width, height, [lowPreset, midPreset, original], sourceFramerate);
209
+ return applySVCSimulcastMode(
210
+ encodingsFromPresets(width, height, [lowPreset, midPreset, original], sourceFramerate),
211
+ );
224
212
  }
225
213
  if (size >= 480) {
226
- return encodingsFromPresets(width, height, [lowPreset, original], sourceFramerate);
214
+ return applySVCSimulcastMode(
215
+ encodingsFromPresets(width, height, [lowPreset, original], sourceFramerate),
216
+ );
227
217
  }
228
218
  }
229
- return encodingsFromPresets(width, height, [original]);
219
+ return applySVCSimulcastMode(encodingsFromPresets(width, height, [original]));
220
+ }
221
+
222
+ /**
223
+ * Bitrate to hint to the bandwidth estimator through `x-google-start-bitrate`, so that
224
+ * a publish does not spend its first seconds ramping up from a very low rate.
225
+ *
226
+ * It has to be the total the encoder will put on the wire, which means picking the
227
+ * encoding that carries the inclusive bitrate:
228
+ * - SVC publishes a single stream with the layers built in. `encodings[0]` holds the
229
+ * full bitrate — the legacy SVC shape orders its encodings `f`..`q`, so that holds
230
+ * for both SVC shapes.
231
+ * - Simulcast publishes independent streams ordered `q`..`f`, so the total is the sum.
232
+ * This includes VP9/AV1 published as rid based simulcast, where `encodings[0]` is
233
+ * the *smallest* layer even though the codec is SVC capable.
234
+ *
235
+ * @internal
236
+ */
237
+ export function computeStartTargetBitrate(
238
+ codec: string,
239
+ options: TrackPublishOptions | undefined,
240
+ encodings: RTCRtpEncodingParameters[],
241
+ ): number {
242
+ if (isSVCCodec(codec) && !isSVCSimulcast(codec, options)) {
243
+ return encodings[0]?.maxBitrate ?? 0;
244
+ }
245
+ return encodings.reduce((sum, enc) => sum + (enc.maxBitrate ?? 0), 0);
230
246
  }
231
247
 
232
248
  export function computeTrackBackupEncodings(
@@ -255,6 +255,57 @@ describe('RpcClientManager', () => {
255
255
  }
256
256
  });
257
257
 
258
+ it('cleans up a failed v2 publish without a later ack timeout', async () => {
259
+ vi.useFakeTimers();
260
+
261
+ try {
262
+ sendTextMock.mockRejectedValue(new Error('engine closed'));
263
+ await expect(
264
+ rpcClientManager.performRpc({
265
+ destinationIdentity: 'remote-identity',
266
+ method: 'test-method',
267
+ payload: 'test-payload',
268
+ }),
269
+ ).rejects.toThrow('engine closed');
270
+
271
+ expect(vi.getTimerCount()).toBe(0);
272
+ expect((rpcClientManager as any).pendingAcks.size).toBe(0);
273
+ expect((rpcClientManager as any).pendingResponses.size).toBe(0);
274
+ await vi.advanceTimersByTimeAsync(7001);
275
+ // An unhandled rejection from completionFuture would also fail Vitest.
276
+ } finally {
277
+ vi.useRealTimers();
278
+ }
279
+ });
280
+
281
+ it('handles an ack timeout while a v2 publish is still pending', async () => {
282
+ vi.useFakeTimers();
283
+
284
+ try {
285
+ let resolveSend!: () => void;
286
+ sendTextMock.mockReturnValue(
287
+ new Promise<void>((resolve) => {
288
+ resolveSend = resolve;
289
+ }),
290
+ );
291
+ const performRpcPromise = rpcClientManager.performRpc({
292
+ destinationIdentity: 'remote-identity',
293
+ method: 'test-method',
294
+ payload: 'test-payload',
295
+ });
296
+
297
+ await vi.advanceTimersByTimeAsync(7001);
298
+ resolveSend();
299
+ const [, completionPromise] = await performRpcPromise;
300
+ await expect(completionPromise).rejects.toThrow(/Connection timeout/i);
301
+ expect(vi.getTimerCount()).toBe(0);
302
+ expect((rpcClientManager as any).pendingAcks.size).toBe(0);
303
+ expect((rpcClientManager as any).pendingResponses.size).toBe(0);
304
+ } finally {
305
+ vi.useRealTimers();
306
+ }
307
+ });
308
+
258
309
  it('should handle a v2 RPC error response', async () => {
259
310
  const errorCode = 101;
260
311
  const errorMessage = 'Test error message';
@@ -108,22 +108,13 @@ export default class RpcClientManager extends (EventEmitter as new () => TypedEm
108
108
  participantIdentity: destinationIdentity,
109
109
  });
110
110
 
111
- await this.publishRpcRequest(
112
- destinationIdentity,
113
- id,
114
- method,
115
- payload,
116
- effectiveTimeoutMs,
117
- remoteClientProtocol,
118
- );
119
-
120
- responseTimeoutId = setTimeout(() => {
121
- this.pendingResponses.delete(id);
122
- completionFuture.reject?.(RpcError.builtIn('RESPONSE_TIMEOUT'));
123
- }, responseTimeoutMs);
124
-
111
+ // Observe the future before publishing: a slow send can outlast the ack timer.
112
+ // Keep the returned promise rejected for callers, while marking its rejection
113
+ // handled until performRpc has finished publishing and can return it.
125
114
  const completionPromise = completionFuture.promise.finally(() => {
126
- clearTimeout(responseTimeoutId);
115
+ if (responseTimeoutId !== null) {
116
+ clearTimeout(responseTimeoutId);
117
+ }
127
118
 
128
119
  if (this.pendingAcks.has(id)) {
129
120
  this.log.warn('RPC response received before ack', id);
@@ -131,6 +122,32 @@ export default class RpcClientManager extends (EventEmitter as new () => TypedEm
131
122
  clearTimeout(ackTimeoutId);
132
123
  }
133
124
  });
125
+ completionPromise.catch(() => {});
126
+
127
+ try {
128
+ await this.publishRpcRequest(
129
+ destinationIdentity,
130
+ id,
131
+ method,
132
+ payload,
133
+ effectiveTimeoutMs,
134
+ remoteClientProtocol,
135
+ );
136
+ } catch (e) {
137
+ // The caller receives the publish error. Do not leave an ack timer that
138
+ // later rejects a future the caller never received.
139
+ clearTimeout(ackTimeoutId);
140
+ this.pendingAcks.delete(id);
141
+ this.pendingResponses.delete(id);
142
+ throw e;
143
+ }
144
+
145
+ if (this.pendingResponses.has(id)) {
146
+ responseTimeoutId = setTimeout(() => {
147
+ this.pendingResponses.delete(id);
148
+ completionFuture.reject?.(RpcError.builtIn('RESPONSE_TIMEOUT'));
149
+ }, responseTimeoutMs);
150
+ }
134
151
 
135
152
  return [id, completionPromise];
136
153
  }
@@ -5,12 +5,13 @@ import { debounce } from '../debounce';
5
5
  import { DeviceUnsupportedError, TrackInvalidError } from '../errors';
6
6
  import { TrackEvent } from '../events';
7
7
  import type { LoggerOptions } from '../types';
8
- import { compareVersions, isMobile, sleep, unwrapConstraint } from '../utils';
8
+ import { compareVersions, isAppleMobile, isMobile, sleep, unwrapConstraint } from '../utils';
9
9
  import { Track, attachToElement, detachTrack } from './Track';
10
10
  import type { VideoCodec } from './options';
11
11
  import type { TrackProcessor } from './processor/types';
12
12
  import { LocalTrackRecorder, isRecordingSupported } from './record';
13
13
  import type { ReplaceTrackOptions } from './types';
14
+ import { waitForFirstVideoFrame } from './utils';
14
15
 
15
16
  const DEFAULT_DIMENSIONS_TIMEOUT = 1000;
16
17
  const PRE_CONNECT_BUFFER_TIMEOUT = 10_000;
@@ -224,18 +225,28 @@ export default abstract class LocalTrack<
224
225
  throw new Error('cannot get dimensions for audio tracks');
225
226
  }
226
227
 
227
- if (getBrowser()?.os === 'iOS') {
228
- // browsers report wrong initial resolution on iOS.
229
- // when slightly delaying the call to .getSettings(), the correct resolution is being reported
230
- await sleep(10);
228
+ const started = Date.now();
229
+
230
+ if (isAppleMobile()) {
231
+ // iOS and iPadOS keep reporting the camera's sensor frame from getSettings() until the
232
+ // camera has delivered its first picture, so a portrait capture initially reads as
233
+ // landscape. Wait for that first frame instead of guessing at a delay.
234
+ // Gated to the Apple capture pipeline rather than run everywhere: elsewhere getSettings() is
235
+ // already right and the first frame can be hundreds of milliseconds out (measured ~500ms on
236
+ // macOS Safari), which would be pure added publish latency.
237
+ // https://github.com/livekit/client-sdk-js/issues/2099
238
+ // https://github.com/livekit/client-sdk-js/issues/2107
239
+ await waitForFirstVideoFrame(this._mediaStreamTrack, timeout, this.attachedElements);
231
240
  }
232
241
 
233
- const started = Date.now();
234
- while (Date.now() - started < timeout) {
242
+ while (true) {
235
243
  const dims = this.dimensions;
236
244
  if (dims) {
237
245
  return dims;
238
246
  }
247
+ if (Date.now() - started >= timeout) {
248
+ break;
249
+ }
239
250
  await sleep(50);
240
251
  }
241
252
  throw new TrackInvalidError('unable to get track dimensions after timeout');
@@ -16,7 +16,7 @@ import {
16
16
  import type { VideoSenderStats } from '../stats';
17
17
  import { computeBitrate, monitorFrequency } from '../stats';
18
18
  import type { LoggerOptions } from '../types';
19
- import { isFireFox, isMobile, isSVCCodec, isWeb } from '../utils';
19
+ import { isFireFox, isMobile, isSVCCodec, isSVCSimulcast, isWeb } from '../utils';
20
20
  import LocalTrack from './LocalTrack';
21
21
  import { Track, VideoQuality } from './Track';
22
22
  import type { TrackPublishOptions, VideoCaptureOptions, VideoCodec } from './options';
@@ -243,6 +243,16 @@ export default class LocalVideoTrack extends LocalTrack<Track.Kind.Video> {
243
243
  return items;
244
244
  }
245
245
 
246
+ /**
247
+ * Whether `codec` is being published as SVC (a single stream carrying all spatial
248
+ * layers) as opposed to rid based simulcast. VP9/AV1 are SVC unless the publisher
249
+ * opted into simulcast, in which case each rid is an independent stream and the
250
+ * layers can be enabled/disabled individually.
251
+ */
252
+ private isSvcPublish(codec?: string): boolean {
253
+ return isSVCCodec(codec) && !isSVCSimulcast(codec, this.publishOptions);
254
+ }
255
+
246
256
  setPublishingQuality(maxQuality: VideoQuality) {
247
257
  const qualities: SubscribedQuality[] = [];
248
258
  for (let q = VideoQuality.LOW; q <= VideoQuality.HIGH; q += 1) {
@@ -254,7 +264,7 @@ export default class LocalVideoTrack extends LocalTrack<Track.Kind.Video> {
254
264
  );
255
265
  }
256
266
  this.log.debug(`setting publishing quality. max quality ${maxQuality}`, this.logContext);
257
- this.setPublishingLayers(isSVCCodec(this.codec), qualities);
267
+ this.setPublishingLayers(this.isSvcPublish(this.codec), qualities);
258
268
  }
259
269
 
260
270
  async restartTrack(options?: VideoCaptureOptions) {
@@ -472,12 +482,19 @@ export default class LocalVideoTrack extends LocalTrack<Track.Kind.Video> {
472
482
  // browser will reenable disabled codec/layers after new codec has been published,
473
483
  // so refresh subscribedCodecs after publish a new codec
474
484
  setTimeout(() => {
475
- if (this.subscribedCodecs) {
476
- this.setPublishingCodecs(this.subscribedCodecs);
477
- }
485
+ this.refreshSubscribedCodecs();
478
486
  }, refreshSubscribedCodecAfterNewCodec);
479
487
  }
480
488
 
489
+ /** @interal */
490
+ async refreshSubscribedCodecs() {
491
+ if (this.subscribedCodecs) {
492
+ return this.setPublishingCodecs(this.subscribedCodecs).catch((e) =>
493
+ this.log.warn(`failed to refresh codec for track`, { ...this.logContext, error: e }),
494
+ );
495
+ }
496
+ }
497
+
481
498
  /**
482
499
  * @internal
483
500
  * Sets codecs that should be publishing, returns new codecs that have not yet
@@ -491,7 +508,7 @@ export default class LocalVideoTrack extends LocalTrack<Track.Kind.Video> {
491
508
  });
492
509
  // only enable simulcast codec for preference codec setted
493
510
  if (!this.codec && codecs.length > 0) {
494
- await this.setPublishingLayers(isSVCCodec(codecs[0].codec), codecs[0].qualities);
511
+ await this.setPublishingLayers(this.isSvcPublish(codecs[0].codec), codecs[0].qualities);
495
512
 
496
513
  return [];
497
514
  }
@@ -501,7 +518,7 @@ export default class LocalVideoTrack extends LocalTrack<Track.Kind.Video> {
501
518
  const newCodecs: VideoCodec[] = [];
502
519
  for await (const codec of codecs) {
503
520
  if (!this.codec || this.codec === codec.codec) {
504
- await this.setPublishingLayers(isSVCCodec(codec.codec), codec.qualities);
521
+ await this.setPublishingLayers(this.isSvcPublish(codec.codec), codec.qualities);
505
522
  } else {
506
523
  const simulcastCodecInfo = this.simulcastCodecs.get(codec.codec as VideoCodec);
507
524
  this.log.debug(`try setPublishingCodec for ${codec.codec}`, {
@@ -522,7 +539,7 @@ export default class LocalVideoTrack extends LocalTrack<Track.Kind.Video> {
522
539
  simulcastCodecInfo.encodings!,
523
540
  codec.qualities,
524
541
  this.senderLock,
525
- isSVCCodec(codec.codec),
542
+ this.isSvcPublish(codec.codec),
526
543
  this.log,
527
544
  this.logContext,
528
545
  );
@@ -60,7 +60,7 @@ export default class RemoteAudioTrack extends RemoteTrack<Track.Kind.Audio> {
60
60
  * gets the volume of attached audio elements (loudest)
61
61
  */
62
62
  getVolume(): number {
63
- if (this.elementVolume) {
63
+ if (this.elementVolume !== undefined) {
64
64
  return this.elementVolume;
65
65
  }
66
66
  if (isReactNative()) {
@@ -115,7 +115,7 @@ export default class RemoteAudioTrack extends RemoteTrack<Track.Kind.Audio> {
115
115
  element.muted = true;
116
116
  }
117
117
 
118
- if (this.elementVolume) {
118
+ if (this.elementVolume !== undefined) {
119
119
  // make sure volume setting is being applied to the newly attached element
120
120
  this.setVolume(this.elementVolume);
121
121
  }
@@ -191,7 +191,7 @@ export default class RemoteAudioTrack extends RemoteTrack<Track.Kind.Audio> {
191
191
  lastNode.connect(this.gainNode);
192
192
  this.gainNode.connect(context.destination);
193
193
 
194
- if (this.elementVolume) {
194
+ if (this.elementVolume !== undefined) {
195
195
  this.gainNode.gain.setTargetAtTime(this.elementVolume, 0, 0.1);
196
196
  }
197
197