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
@@ -32,6 +32,7 @@ import {
32
32
  import type { InternalRoomOptions } from '../../options';
33
33
  import type { NonSharedUint8Array } from '../../type-polyfills/non-shared-typed-arrays';
34
34
  import TypedPromise from '../../utils/TypedPromise';
35
+ import DeviceManager from '../DeviceManager';
35
36
  import { PCTransportState } from '../PCTransportManager';
36
37
  import type RTCEngine from '../RTCEngine';
37
38
  import { DataChannelKind } from '../RTCEngine';
@@ -96,11 +97,14 @@ import {
96
97
  import {
97
98
  Future,
98
99
  isAudioTrack,
100
+ isChromiumBased,
101
+ isDeviceAcquisitionFailure,
99
102
  isE2EESimulcastSupported,
100
103
  isFireFox,
101
104
  isLocalAudioTrack,
102
105
  isLocalTrack,
103
106
  isLocalVideoTrack,
107
+ isPermissionDeniedError,
104
108
  isSVCCodec,
105
109
  isSVCSimulcast,
106
110
  isSVCSimulcastSupportedByServer,
@@ -691,19 +695,21 @@ export default class LocalParticipant extends Participant {
691
695
  loggerName: this.roomOptions.loggerName,
692
696
  loggerContextCb: () => this.logContext,
693
697
  });
694
- const localTracks = tracks.map((track) => {
695
- if (isAudioTrack(track)) {
696
- this.microphoneError = undefined;
697
- track.setAudioContext(this.audioContext);
698
- track.source = Track.Source.Microphone;
699
- this.emit(ParticipantEvent.AudioStreamAcquired);
700
- }
701
- if (isVideoTrack(track)) {
702
- this.cameraError = undefined;
703
- track.source = Track.Source.Camera;
704
- }
705
- return track;
706
- });
698
+ const localTracks = await Promise.all(
699
+ tracks.map(async (track) => {
700
+ if (isAudioTrack(track)) {
701
+ this.microphoneError = undefined;
702
+ await track.setAudioContext(this.audioContext);
703
+ track.source = Track.Source.Microphone;
704
+ this.emit(ParticipantEvent.AudioStreamAcquired);
705
+ }
706
+ if (isVideoTrack(track)) {
707
+ this.cameraError = undefined;
708
+ track.source = Track.Source.Camera;
709
+ }
710
+ return track;
711
+ }),
712
+ );
707
713
  return localTracks;
708
714
  } catch (err) {
709
715
  if (err instanceof Error) {
@@ -819,7 +825,7 @@ export default class LocalParticipant extends Participant {
819
825
  hasRetriedAfterNegotiationError = false,
820
826
  ): Promise<LocalTrackPublication> {
821
827
  if (isLocalAudioTrack(track)) {
822
- track.setAudioContext(this.audioContext);
828
+ await track.setAudioContext(this.audioContext);
823
829
  }
824
830
 
825
831
  await this.reconnectFuture?.promise;
@@ -1532,21 +1538,37 @@ export default class LocalParticipant extends Participant {
1532
1538
  }
1533
1539
  const packetTrailerFeatures = this.normalizeRequestedFrameMetadataOptions(track, opts);
1534
1540
 
1541
+ // the backup sender encodes the processed track, not the raw capture track.dimensions reports
1542
+ const { width, height } = track.mediaStreamTrack.getSettings();
1543
+ const dims = width && height ? { width, height } : undefined;
1544
+ if (!dims) {
1545
+ this.log.warn(
1546
+ 'could not determine track dimensions, publishing backup codec without layers',
1547
+ getLogContextFromTrack(track),
1548
+ );
1549
+ }
1550
+ // the server reads the layers off the codec entry for a request that carries a sid, and
1551
+ // falls back to the top level ones when it predates per codec layers
1552
+ const layers = dims ? videoLayersFromEncodings(dims.width, dims.height, encodings) : [];
1553
+
1535
1554
  const req = new AddTrackRequest({
1536
1555
  cid: simulcastTrack.mediaStreamTrack.id,
1537
1556
  type: Track.kindToProto(track.kind),
1538
1557
  muted: track.isMuted,
1539
1558
  source: Track.sourceToProto(track.source),
1540
1559
  sid: track.sid,
1560
+ width: dims?.width,
1561
+ height: dims?.height,
1541
1562
  packetTrailerFeatures,
1563
+ layers,
1542
1564
  simulcastCodecs: [
1543
1565
  {
1544
1566
  codec: opts.videoCodec,
1545
1567
  cid: simulcastTrack.mediaStreamTrack.id,
1568
+ layers,
1546
1569
  },
1547
1570
  ],
1548
1571
  });
1549
- req.layers = videoLayersFromEncodings(req.width, req.height, encodings);
1550
1572
 
1551
1573
  if (!this.engine || this.engine.isClosed) {
1552
1574
  throw new UnexpectedConnectionState('cannot publish track when not connected');
@@ -2140,8 +2162,7 @@ export default class LocalParticipant extends Participant {
2140
2162
  getLogContextFromTrack(track),
2141
2163
  );
2142
2164
  if (isLocalAudioTrack(track)) {
2143
- // fall back to default device if available
2144
- await track.restartTrack({ deviceId: 'default' });
2165
+ await this.restartOnDefaultAudioDevice(track);
2145
2166
  } else {
2146
2167
  await track.restartTrack();
2147
2168
  }
@@ -2153,6 +2174,34 @@ export default class LocalParticipant extends Participant {
2153
2174
  }
2154
2175
  };
2155
2176
 
2177
+ /**
2178
+ * @internal
2179
+ */
2180
+ private async restartOnDefaultAudioDevice(track: LocalAudioTrack) {
2181
+ try {
2182
+ if (isChromiumBased()) {
2183
+ // only chrome has the notion of a literal `default` device
2184
+ await track.setDeviceId({ exact: 'default' });
2185
+ } else {
2186
+ // just try to restart for other browsers and try to change the device id as a fallback in the catch clause
2187
+ await track.restartTrack();
2188
+ }
2189
+ } catch (e) {
2190
+ // restart() clears the manually-stopped flag on entry, so retrying a track the user stopped
2191
+ // while the first attempt was in flight would re-open the capture device behind their back
2192
+ if (!isDeviceAcquisitionFailure(e) || isPermissionDeniedError(e) || track.isStopped) {
2193
+ throw e;
2194
+ }
2195
+ this.log.debug(
2196
+ 'could not reacquire the audio device, retrying with an enumerated device',
2197
+ getLogContextFromTrack(track),
2198
+ );
2199
+ // permission is already granted at this point, so skip the probe that would re-acquire media
2200
+ const allDevices = await DeviceManager.getInstance().getDevices('audioinput', false);
2201
+ await track.setDeviceId(allDevices?.[0]?.deviceId ?? 'default');
2202
+ }
2203
+ }
2204
+
2156
2205
  private getPublicationForTrack(
2157
2206
  track: LocalTrack | MediaStreamTrack,
2158
2207
  ): LocalTrackPublication | undefined {
@@ -363,10 +363,13 @@ export default class Participant extends (EventEmitter as new () => TypedEmitter
363
363
  /**
364
364
  * @internal
365
365
  */
366
- setAudioContext(ctx: AudioContext | undefined) {
366
+ async setAudioContext(ctx: AudioContext | undefined) {
367
367
  this.audioContext = ctx;
368
- this.audioTrackPublications.forEach(
369
- (track) => isAudioTrack(track.track) && track.track.setAudioContext(ctx),
368
+ await Promise.all(
369
+ Array.from(this.audioTrackPublications.values())
370
+ .map((publication) => publication.track)
371
+ .filter(isAudioTrack)
372
+ .map((track) => track.setAudioContext(ctx)),
370
373
  );
371
374
  }
372
375
 
@@ -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 */
@@ -0,0 +1,177 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import DeviceManager from '../DeviceManager';
3
+ import type LocalAudioTrack from '../track/LocalAudioTrack';
4
+ import { Track } from '../track/Track';
5
+ import { isChromiumBased, markDeviceAcquisitionFailure } from '../utils';
6
+ import LocalParticipant from './LocalParticipant';
7
+
8
+ vi.mock('../utils', async () => {
9
+ const actual = await vi.importActual('../utils');
10
+ return {
11
+ ...actual,
12
+ isChromiumBased: vi.fn(() => true),
13
+ };
14
+ });
15
+
16
+ type DefaultDeviceTestParticipant = {
17
+ restartOnDefaultAudioDevice: (track: LocalAudioTrack) => Promise<void>;
18
+ log: { debug: ReturnType<typeof vi.fn> };
19
+ };
20
+
21
+ function makeDefaultDeviceParticipant() {
22
+ const participant = Object.create(
23
+ LocalParticipant.prototype,
24
+ ) as unknown as DefaultDeviceTestParticipant;
25
+ participant.log = { debug: vi.fn() };
26
+ return participant;
27
+ }
28
+
29
+ function errorNamed(name: string) {
30
+ const e = new Error(name);
31
+ e.name = name;
32
+ markDeviceAcquisitionFailure(e);
33
+ return e;
34
+ }
35
+
36
+ function makeAudioTrack(
37
+ setDeviceId: ReturnType<typeof vi.fn>,
38
+ restartTrack: ReturnType<typeof vi.fn> = vi.fn(),
39
+ isStopped = false,
40
+ ) {
41
+ return {
42
+ sid: 'track-sid',
43
+ source: Track.Source.Microphone,
44
+ setDeviceId,
45
+ restartTrack,
46
+ isStopped,
47
+ } as unknown as LocalAudioTrack;
48
+ }
49
+
50
+ function stubAudioDevices(...deviceIds: string[]) {
51
+ const getDevices = vi.fn().mockResolvedValue(deviceIds.map((deviceId) => ({ deviceId })));
52
+ vi.spyOn(DeviceManager, 'getInstance').mockReturnValue({
53
+ getDevices,
54
+ } as unknown as DeviceManager);
55
+ return getDevices;
56
+ }
57
+
58
+ describe('LocalParticipant restartOnDefaultAudioDevice', () => {
59
+ beforeEach(() => {
60
+ vi.mocked(isChromiumBased).mockReturnValue(true);
61
+ stubAudioDevices('mic-1', 'mic-2');
62
+ });
63
+
64
+ it('falls back to an available device when the exact default is rejected', async () => {
65
+ const getDevices = stubAudioDevices('mic-1', 'mic-2');
66
+ const setDeviceId = vi.fn().mockRejectedValueOnce(errorNamed('OverconstrainedError'));
67
+ const participant = makeDefaultDeviceParticipant();
68
+
69
+ await participant.restartOnDefaultAudioDevice(makeAudioTrack(setDeviceId));
70
+
71
+ expect(getDevices).toHaveBeenCalledWith('audioinput', false);
72
+ expect(setDeviceId).toHaveBeenCalledTimes(2);
73
+ expect(setDeviceId).toHaveBeenNthCalledWith(1, { exact: 'default' });
74
+ expect(setDeviceId).toHaveBeenNthCalledWith(2, 'mic-1');
75
+ });
76
+
77
+ it('falls back to the default id when no device is enumerated', async () => {
78
+ stubAudioDevices();
79
+ const setDeviceId = vi.fn().mockRejectedValueOnce(errorNamed('OverconstrainedError'));
80
+ const participant = makeDefaultDeviceParticipant();
81
+
82
+ await participant.restartOnDefaultAudioDevice(makeAudioTrack(setDeviceId));
83
+
84
+ expect(setDeviceId).toHaveBeenNthCalledWith(2, 'default');
85
+ });
86
+
87
+ it('falls back for a rejection name the spec does not prescribe', async () => {
88
+ const setDeviceId = vi.fn().mockRejectedValueOnce(errorNamed('ConstraintNotSatisfiedError'));
89
+ const participant = makeDefaultDeviceParticipant();
90
+
91
+ await participant.restartOnDefaultAudioDevice(makeAudioTrack(setDeviceId));
92
+
93
+ expect(setDeviceId).toHaveBeenCalledTimes(2);
94
+ });
95
+
96
+ it('does not retry a denied permission, so one prompt never becomes two', async () => {
97
+ const setDeviceId = vi.fn().mockRejectedValue(errorNamed('NotAllowedError'));
98
+ const participant = makeDefaultDeviceParticipant();
99
+
100
+ await expect(
101
+ participant.restartOnDefaultAudioDevice(makeAudioTrack(setDeviceId)),
102
+ ).rejects.toThrow();
103
+
104
+ expect(setDeviceId).toHaveBeenCalledOnce();
105
+ });
106
+
107
+ it('asks for the exact default only, when that succeeds', async () => {
108
+ const setDeviceId = vi.fn().mockResolvedValue(undefined);
109
+ const participant = makeDefaultDeviceParticipant();
110
+
111
+ await participant.restartOnDefaultAudioDevice(makeAudioTrack(setDeviceId));
112
+
113
+ expect(setDeviceId).toHaveBeenCalledOnce();
114
+ expect(setDeviceId).toHaveBeenCalledWith({ exact: 'default' });
115
+ });
116
+
117
+ it('propagates the second rejection, not the first', async () => {
118
+ const setDeviceId = vi
119
+ .fn()
120
+ .mockRejectedValueOnce(errorNamed('OverconstrainedError'))
121
+ .mockRejectedValueOnce(errorNamed('NotReadableError'));
122
+ const participant = makeDefaultDeviceParticipant();
123
+
124
+ await expect(
125
+ participant.restartOnDefaultAudioDevice(makeAudioTrack(setDeviceId)),
126
+ ).rejects.toThrow('NotReadableError');
127
+
128
+ expect(setDeviceId).toHaveBeenCalledTimes(2);
129
+ });
130
+
131
+ it('does not retry a failure that happened after the device was acquired', async () => {
132
+ const postAcquisition = new Error('processor restart failed');
133
+ const setDeviceId = vi.fn().mockRejectedValue(postAcquisition);
134
+ const participant = makeDefaultDeviceParticipant();
135
+
136
+ await expect(
137
+ participant.restartOnDefaultAudioDevice(makeAudioTrack(setDeviceId)),
138
+ ).rejects.toThrow('processor restart failed');
139
+
140
+ expect(setDeviceId).toHaveBeenCalledOnce();
141
+ });
142
+
143
+ it('does not retry a track the user stopped while the first attempt was in flight', async () => {
144
+ const setDeviceId = vi.fn().mockRejectedValue(errorNamed('OverconstrainedError'));
145
+ const participant = makeDefaultDeviceParticipant();
146
+
147
+ await expect(
148
+ participant.restartOnDefaultAudioDevice(makeAudioTrack(setDeviceId, vi.fn(), true)),
149
+ ).rejects.toThrow();
150
+
151
+ expect(setDeviceId).toHaveBeenCalledOnce();
152
+ });
153
+
154
+ it('keeps the existing device id on a browser without a literal default', async () => {
155
+ vi.mocked(isChromiumBased).mockReturnValue(false);
156
+ const setDeviceId = vi.fn();
157
+ const restartTrack = vi.fn().mockResolvedValue(undefined);
158
+ const participant = makeDefaultDeviceParticipant();
159
+
160
+ await participant.restartOnDefaultAudioDevice(makeAudioTrack(setDeviceId, restartTrack));
161
+
162
+ expect(restartTrack).toHaveBeenCalledOnce();
163
+ expect(setDeviceId).not.toHaveBeenCalled();
164
+ });
165
+
166
+ it('switches device when a browser without a literal default cannot reacquire', async () => {
167
+ vi.mocked(isChromiumBased).mockReturnValue(false);
168
+ const setDeviceId = vi.fn();
169
+ const restartTrack = vi.fn().mockRejectedValue(errorNamed('OverconstrainedError'));
170
+ const participant = makeDefaultDeviceParticipant();
171
+
172
+ await participant.restartOnDefaultAudioDevice(makeAudioTrack(setDeviceId, restartTrack));
173
+
174
+ expect(restartTrack).toHaveBeenCalledOnce();
175
+ expect(setDeviceId).toHaveBeenCalledExactlyOnceWith('mic-1');
176
+ });
177
+ });
@@ -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
  }
@@ -0,0 +1,32 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { RpcError, byteLength, truncateBytes } from './utils';
3
+
4
+ describe('truncateBytes', () => {
5
+ it('returns the string unchanged when it fits', () => {
6
+ expect(truncateBytes('hello', 5)).toBe('hello');
7
+ });
8
+
9
+ it('truncates ASCII on the byte limit', () => {
10
+ expect(truncateBytes('hello world', 5)).toBe('hello');
11
+ });
12
+
13
+ it('does not split a multibyte character', () => {
14
+ expect(truncateBytes('aéé', 4)).toBe('aé');
15
+ });
16
+
17
+ it('does not leave half of a surrogate pair when truncating emoji', () => {
18
+ // 'a' (1 byte) + 63 emoji (4 bytes each) = 253 bytes, so the limit falls inside the next emoji.
19
+ const str = `a${'😀'.repeat(64)}`;
20
+ const truncated = truncateBytes(str, 256);
21
+
22
+ expect(truncated).toBe(`a${'😀'.repeat(63)}`);
23
+ expect(new TextDecoder().decode(new TextEncoder().encode(truncated))).toBe(truncated);
24
+ });
25
+
26
+ it('keeps an RpcError message valid when it is cut inside an emoji', () => {
27
+ const error = new RpcError(1500, `a${'😀'.repeat(100)}`);
28
+
29
+ expect(byteLength(error.message)).toBeLessThanOrEqual(RpcError.MAX_MESSAGE_BYTES);
30
+ expect(error.message).not.toMatch(/[\ud800-\udbff](?![\udc00-\udfff])/);
31
+ });
32
+ });
@@ -214,5 +214,12 @@ export function truncateBytes(str: string, maxBytes: number): string {
214
214
  }
215
215
  }
216
216
 
217
+ // Never end on the first half of a surrogate pair: it would be encoded as a replacement
218
+ // character, and the truncated string would no longer round trip.
219
+ const lastCode = low > 0 ? str.charCodeAt(low - 1) : 0;
220
+ if (lastCode >= 0xd800 && lastCode <= 0xdbff) {
221
+ low -= 1;
222
+ }
223
+
217
224
  return str.slice(0, low);
218
225
  }
@@ -18,6 +18,12 @@ export default class LocalAudioTrack extends LocalTrack<Track.Kind.Audio> {
18
18
 
19
19
  private isKrispNoiseFilterEnabled = false;
20
20
 
21
+ /**
22
+ * the processed track we currently have krisp feature listeners registered on. Tracked
23
+ * separately, as `processor.processedTrack` can be swapped out while we hold the listeners.
24
+ */
25
+ private observedProcessedTrack?: MediaStreamTrack;
26
+
21
27
  protected processor?: TrackProcessor<Track.Kind.Audio, AudioProcessorOptions> | undefined;
22
28
 
23
29
  /**
@@ -184,6 +190,51 @@ export default class LocalAudioTrack extends LocalTrack<Track.Kind.Audio> {
184
190
  );
185
191
  };
186
192
 
193
+ /**
194
+ * registers the krisp feature listeners on the given processed track, moving them over from
195
+ * a previously observed one. Passing `undefined` just removes the existing listeners.
196
+ */
197
+ private observeProcessedTrack(processedTrack: MediaStreamTrack | undefined) {
198
+ if (this.observedProcessedTrack === processedTrack) {
199
+ return;
200
+ }
201
+ if (this.observedProcessedTrack) {
202
+ this.observedProcessedTrack.removeEventListener(
203
+ 'enable-lk-krisp-noise-filter',
204
+ this.handleKrispNoiseFilterEnable,
205
+ );
206
+ this.observedProcessedTrack.removeEventListener(
207
+ 'disable-lk-krisp-noise-filter',
208
+ this.handleKrispNoiseFilterDisable,
209
+ );
210
+ this.observedProcessedTrack = undefined;
211
+ if (this.isKrispNoiseFilterEnabled) {
212
+ // we're no longer observing the track that had the filter enabled, so the feature
213
+ // can't be active anymore. Make sure the state doesn't get stuck on `true`.
214
+ this.handleKrispNoiseFilterDisable();
215
+ }
216
+ }
217
+ if (!processedTrack) {
218
+ return;
219
+ }
220
+ processedTrack.addEventListener(
221
+ 'enable-lk-krisp-noise-filter',
222
+ this.handleKrispNoiseFilterEnable,
223
+ );
224
+ processedTrack.addEventListener(
225
+ 'disable-lk-krisp-noise-filter',
226
+ this.handleKrispNoiseFilterDisable,
227
+ );
228
+ this.observedProcessedTrack = processedTrack;
229
+ }
230
+
231
+ /**
232
+ * Sets a processor on this track.
233
+ *
234
+ * The processor is destroyed when the track is stopped, and also when the audio context goes
235
+ * away, so a disconnect ends it on both counts and it has to be set again after reconnecting.
236
+ * See {@link TrackProcessor} for the full lifecycle.
237
+ */
187
238
  async setProcessor(processor: TrackProcessor<Track.Kind.Audio, AudioProcessorOptions>) {
188
239
  const unlock = await this.trackChangeLock.lock();
189
240
  try {
@@ -209,14 +260,7 @@ export default class LocalAudioTrack extends LocalTrack<Track.Kind.Audio> {
209
260
  this.processor = processor;
210
261
  if (this.processor.processedTrack) {
211
262
  await this.sender?.replaceTrack(this.processor.processedTrack);
212
- this.processor.processedTrack.addEventListener(
213
- 'enable-lk-krisp-noise-filter',
214
- this.handleKrispNoiseFilterEnable,
215
- );
216
- this.processor.processedTrack.addEventListener(
217
- 'disable-lk-krisp-noise-filter',
218
- this.handleKrispNoiseFilterDisable,
219
- );
263
+ this.observeProcessedTrack(this.processor.processedTrack);
220
264
  }
221
265
  this.emit(TrackEvent.TrackProcessorUpdate, this.processor);
222
266
  } finally {
@@ -224,12 +268,59 @@ export default class LocalAudioTrack extends LocalTrack<Track.Kind.Audio> {
224
268
  }
225
269
  }
226
270
 
271
+ protected override async onMediaStreamTrackChanged(): Promise<void> {
272
+ // the processor rebuilds its graph around the new track, so the krisp listeners have to
273
+ // follow it onto whatever it hands back
274
+ this.observeProcessedTrack(this.processor?.processedTrack);
275
+ }
276
+
277
+ protected async internalStopProcessor(keepElement = true) {
278
+ this.observeProcessedTrack(undefined);
279
+ await super.internalStopProcessor(keepElement);
280
+ }
281
+
282
+ stop() {
283
+ this.observeProcessedTrack(undefined);
284
+ super.stop();
285
+ }
286
+
227
287
  /**
228
288
  * @internal
229
289
  * @experimental
290
+ *
291
+ * A processor's nodes (`AudioWorkletNode`, ...) can't move between contexts, so the processor
292
+ * is rebuilt whenever the context changes and torn down when there is none left to run in.
230
293
  */
231
- setAudioContext(audioContext: AudioContext | undefined) {
232
- this.audioContext = audioContext;
294
+ async setAudioContext(audioContext: AudioContext | undefined) {
295
+ if (this.audioContext === audioContext) {
296
+ return;
297
+ }
298
+ const unlock = await this.trackChangeLock.lock();
299
+ try {
300
+ this.audioContext = audioContext;
301
+ if (!this.processor || isReactNative()) {
302
+ return;
303
+ }
304
+ if (!audioContext) {
305
+ this.log.debug('stopping audio processor, audio context has been removed', this.logContext);
306
+ await this.internalStopProcessor();
307
+ return;
308
+ }
309
+ this.log.debug('restarting audio processor on new audio context', this.logContext);
310
+ await this.processor.restart({
311
+ kind: this.kind,
312
+ track: this._mediaStreamTrack,
313
+ audioContext,
314
+ localTrack: this,
315
+ });
316
+ if (this.processor.processedTrack) {
317
+ await this.sender?.replaceTrack(this.processor.processedTrack);
318
+ this.observeProcessedTrack(this.processor.processedTrack);
319
+ }
320
+ this.emit(TrackEvent.TrackProcessorUpdate, this.processor);
321
+ } finally {
322
+ unlock();
323
+ }
233
324
  }
234
325
 
235
326
  async getSenderStats(): Promise<AudioSenderStats | undefined> {