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
@@ -5,12 +5,20 @@ 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 {
9
+ compareVersions,
10
+ isAppleMobile,
11
+ isMobile,
12
+ markDeviceAcquisitionFailure,
13
+ sleep,
14
+ unwrapConstraint,
15
+ } from '../utils';
9
16
  import { Track, attachToElement, detachTrack } from './Track';
10
17
  import type { VideoCodec } from './options';
11
18
  import type { TrackProcessor } from './processor/types';
12
19
  import { LocalTrackRecorder, isRecordingSupported } from './record';
13
20
  import type { ReplaceTrackOptions } from './types';
21
+ import { waitForFirstVideoFrame } from './utils';
14
22
 
15
23
  const DEFAULT_DIMENSIONS_TIMEOUT = 1000;
16
24
  const PRE_CONNECT_BUFFER_TIMEOUT = 10_000;
@@ -131,6 +139,10 @@ export default abstract class LocalTrack<
131
139
  return this.providedByUser;
132
140
  }
133
141
 
142
+ get isStopped() {
143
+ return this.manuallyStopped;
144
+ }
145
+
134
146
  get mediaStreamTrack() {
135
147
  return this.processor?.processedTrack ?? this._mediaStreamTrack;
136
148
  }
@@ -194,6 +206,9 @@ export default abstract class LocalTrack<
194
206
  track: newTrack,
195
207
  kind: this.kind,
196
208
  element: this.processorElement,
209
+ // audio processors need the context they were initialised with in order to rebuild
210
+ // their graph, omitting it here would leave them without one
211
+ audioContext: this.audioContext,
197
212
  localTrack: this,
198
213
  });
199
214
  processedTrack = this.processor.processedTrack;
@@ -217,6 +232,17 @@ export default abstract class LocalTrack<
217
232
  attachToElement(processedTrack ?? newTrack, el);
218
233
  });
219
234
  }
235
+ await this.onMediaStreamTrackChanged();
236
+ }
237
+
238
+ /**
239
+ * Hook invoked after the underlying MediaStreamTrack has been swapped and the processor has
240
+ * been restarted around it, so `processor.processedTrack` already holds the new track.
241
+ * Unlike `onSenderTrackSwapped` this also fires on the restart and unmute paths.
242
+ */
243
+ protected async onMediaStreamTrackChanged(): Promise<void> {
244
+ // base implementation is a no-op; LocalAudioTrack overrides this to move the krisp
245
+ // feature listeners onto the new processed track.
220
246
  }
221
247
 
222
248
  async waitForDimensions(timeout = DEFAULT_DIMENSIONS_TIMEOUT): Promise<Track.Dimensions> {
@@ -224,18 +250,28 @@ export default abstract class LocalTrack<
224
250
  throw new Error('cannot get dimensions for audio tracks');
225
251
  }
226
252
 
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);
253
+ const started = Date.now();
254
+
255
+ if (isAppleMobile()) {
256
+ // iOS and iPadOS keep reporting the camera's sensor frame from getSettings() until the
257
+ // camera has delivered its first picture, so a portrait capture initially reads as
258
+ // landscape. Wait for that first frame instead of guessing at a delay.
259
+ // Gated to the Apple capture pipeline rather than run everywhere: elsewhere getSettings() is
260
+ // already right and the first frame can be hundreds of milliseconds out (measured ~500ms on
261
+ // macOS Safari), which would be pure added publish latency.
262
+ // https://github.com/livekit/client-sdk-js/issues/2099
263
+ // https://github.com/livekit/client-sdk-js/issues/2107
264
+ await waitForFirstVideoFrame(this._mediaStreamTrack, timeout, this.attachedElements);
231
265
  }
232
266
 
233
- const started = Date.now();
234
- while (Date.now() - started < timeout) {
267
+ while (true) {
235
268
  const dims = this.dimensions;
236
269
  if (dims) {
237
270
  return dims;
238
271
  }
272
+ if (Date.now() - started >= timeout) {
273
+ break;
274
+ }
239
275
  await sleep(50);
240
276
  }
241
277
  throw new TrackInvalidError('unable to get track dimensions after timeout');
@@ -244,7 +280,8 @@ export default abstract class LocalTrack<
244
280
  async setDeviceId(deviceId: ConstrainDOMString): Promise<boolean> {
245
281
  if (
246
282
  this._constraints.deviceId === deviceId &&
247
- this._mediaStreamTrack.getSettings().deviceId === unwrapConstraint(deviceId)
283
+ this._mediaStreamTrack.getSettings().deviceId === unwrapConstraint(deviceId) &&
284
+ this._mediaStreamTrack.readyState === 'live'
248
285
  ) {
249
286
  return true;
250
287
  }
@@ -375,19 +412,37 @@ export default abstract class LocalTrack<
375
412
  this._mediaStreamTrack.stop();
376
413
 
377
414
  // create new track and attach
378
- const mediaStream = await navigator.mediaDevices.getUserMedia(streamConstraints);
415
+ let mediaStream: MediaStream;
416
+ try {
417
+ mediaStream = await navigator.mediaDevices.getUserMedia(streamConstraints);
418
+ } catch (e) {
419
+ markDeviceAcquisitionFailure(e);
420
+ throw e;
421
+ }
379
422
  const newTrack = mediaStream.getTracks()[0];
380
- if (this.kind === Track.Kind.Video) {
381
- // we already captured the audio track with the constraints, so we only need to apply the video constraints
382
- await newTrack.applyConstraints(otherConstraints);
423
+ try {
424
+ if (this.kind === Track.Kind.Video) {
425
+ // we already captured the audio track with the constraints, so we only need to apply the video constraints
426
+ await newTrack.applyConstraints(otherConstraints);
427
+ }
428
+ newTrack.addEventListener('ended', this.handleEnded);
429
+ this.log.debug('re-acquired MediaStreamTrack', this.logContext);
430
+
431
+ await this.setMediaStreamTrack(newTrack, false, isUnmuting);
432
+ } catch (e) {
433
+ // setMediaStreamTrack can throw after acquisition but before it adopts the new track,
434
+ // which would leave the capture device live with nothing referencing it
435
+ if (this._mediaStreamTrack !== newTrack) {
436
+ newTrack.stop();
437
+ }
438
+ throw e;
383
439
  }
384
- newTrack.addEventListener('ended', this.handleEnded);
385
- this.log.debug('re-acquired MediaStreamTrack', this.logContext);
386
-
387
- await this.setMediaStreamTrack(newTrack, false, isUnmuting);
388
440
  this._constraints = constraints;
389
441
  this.pendingDeviceChange = false;
390
442
  this.emit(TrackEvent.Restarted, this);
443
+ return this;
444
+ } finally {
445
+ // in the finally because a throw can also leave the new track adopted, and so still live
391
446
  if (this.manuallyStopped) {
392
447
  this.log.warn(
393
448
  'track was stopped during a restart, stopping restarted track',
@@ -395,8 +450,6 @@ export default abstract class LocalTrack<
395
450
  );
396
451
  this.stop();
397
452
  }
398
- return this;
399
- } finally {
400
453
  unlock();
401
454
  }
402
455
  }
@@ -464,7 +517,11 @@ export default abstract class LocalTrack<
464
517
  this._mediaStreamTrack.removeEventListener('ended', this.handleEnded);
465
518
  this._mediaStreamTrack.removeEventListener('mute', this.handleTrackMuteEvent);
466
519
  this._mediaStreamTrack.removeEventListener('unmute', this.handleTrackUnmuteEvent);
467
- this.processor?.destroy();
520
+ // `stop` is synchronous, so we can't await the teardown of the processor here.
521
+ // make sure a failing teardown doesn't surface as an unhandled rejection.
522
+ this.processor?.destroy().catch((error) => {
523
+ this.log.error('failed to destroy processor', { ...this.logContext, error });
524
+ });
468
525
  this.processor = undefined;
469
526
  }
470
527
 
@@ -540,6 +597,11 @@ export default abstract class LocalTrack<
540
597
  * Sets a processor on this track.
541
598
  * See https://github.com/livekit/track-processors-js for example usage
542
599
  *
600
+ * The processor lives on the track, so stopping the track destroys it. Unpublishing and
601
+ * disconnecting both stop the track by default, which means a processor has to be set again on
602
+ * the track published after a reconnect. See {@link TrackProcessor} for the full lifecycle,
603
+ * including what `stopLocalTrackOnUnpublish: false` changes.
604
+ *
543
605
  * @param processor
544
606
  * @param showProcessedStreamLocally
545
607
  * @returns
@@ -606,6 +668,10 @@ export default abstract class LocalTrack<
606
668
  await this.onSenderTrackSwapped();
607
669
  }
608
670
 
671
+ /**
672
+ * Returns the processor currently set on this track, or `undefined` if there is none. A track
673
+ * that was stopped by an unpublish or a disconnect has none. See {@link TrackProcessor}.
674
+ */
609
675
  getProcessor() {
610
676
  return this.processor;
611
677
  }
@@ -614,6 +680,8 @@ export default abstract class LocalTrack<
614
680
  * Stops the track processor
615
681
  * See https://github.com/livekit/track-processors-js for example usage
616
682
  *
683
+ * Only needed to remove a processor from a track that stays published. Unpublishing or
684
+ * disconnecting already destroys it. See {@link TrackProcessor}.
617
685
  */
618
686
  async stopProcessor(keepElement = true) {
619
687
  const unlock = await this.trackChangeLock.lock();
@@ -401,6 +401,13 @@ export default class LocalVideoTrack extends LocalTrack<Track.Kind.Video> {
401
401
  await sender.setParameters(params);
402
402
  }
403
403
 
404
+ /**
405
+ * Sets a processor on this track, and wires its processed track into the simulcast codecs.
406
+ *
407
+ * The processor is destroyed when the track is stopped, which unpublishing and disconnecting
408
+ * both do by default, so it has to be set again after reconnecting. See {@link TrackProcessor}
409
+ * for the full lifecycle.
410
+ */
404
411
  async setProcessor(
405
412
  processor: TrackProcessor<Track.Kind.Video>,
406
413
  showProcessedStreamLocally = true,
@@ -482,12 +489,19 @@ export default class LocalVideoTrack extends LocalTrack<Track.Kind.Video> {
482
489
  // browser will reenable disabled codec/layers after new codec has been published,
483
490
  // so refresh subscribedCodecs after publish a new codec
484
491
  setTimeout(() => {
485
- if (this.subscribedCodecs) {
486
- this.setPublishingCodecs(this.subscribedCodecs);
487
- }
492
+ this.refreshSubscribedCodecs();
488
493
  }, refreshSubscribedCodecAfterNewCodec);
489
494
  }
490
495
 
496
+ /** @interal */
497
+ async refreshSubscribedCodecs() {
498
+ if (this.subscribedCodecs) {
499
+ return this.setPublishingCodecs(this.subscribedCodecs).catch((e) =>
500
+ this.log.warn(`failed to refresh codec for track`, { ...this.logContext, error: e }),
501
+ );
502
+ }
503
+ }
504
+
491
505
  /**
492
506
  * @internal
493
507
  * Sets codecs that should be publishing, returns new codecs that have not yet
@@ -20,6 +20,13 @@ export default class RemoteAudioTrack extends RemoteTrack<Track.Kind.Audio> {
20
20
 
21
21
  private webAudioPluginNodes: AudioNode[];
22
22
 
23
+ /**
24
+ * the nodes of the graph currently wired up, in order, so teardown can undo exactly the edges
25
+ * `connectWebAudio` created. Reading `webAudioPluginNodes` instead would miss the old plugins
26
+ * once `setWebAudioPlugins` has replaced them.
27
+ */
28
+ private connectedNodes: AudioNode[] = [];
29
+
23
30
  private sinkId?: string;
24
31
 
25
32
  constructor(
@@ -42,10 +49,11 @@ export default class RemoteAudioTrack extends RemoteTrack<Track.Kind.Audio> {
42
49
  * sets the volume for all attached audio elements
43
50
  */
44
51
  setVolume(volume: number) {
45
- for (const el of this.attachedElements) {
46
- if (this.audioContext) {
47
- this.gainNode?.gain.setTargetAtTime(volume, 0, 0.1);
48
- } else {
52
+ if (this.audioContext) {
53
+ // playback goes through the web audio graph, the attached elements themselves stay silent
54
+ this.gainNode?.gain.setTargetAtTime(volume, 0, 0.1);
55
+ } else {
56
+ for (const el of this.attachedElements) {
49
57
  el.volume = volume;
50
58
  }
51
59
  }
@@ -60,7 +68,7 @@ export default class RemoteAudioTrack extends RemoteTrack<Track.Kind.Audio> {
60
68
  * gets the volume of attached audio elements (loudest)
61
69
  */
62
70
  getVolume(): number {
63
- if (this.elementVolume) {
71
+ if (this.elementVolume !== undefined) {
64
72
  return this.elementVolume;
65
73
  }
66
74
  if (isReactNative()) {
@@ -96,7 +104,9 @@ export default class RemoteAudioTrack extends RemoteTrack<Track.Kind.Audio> {
96
104
  attach(): HTMLMediaElement;
97
105
  attach(element: HTMLMediaElement): HTMLMediaElement;
98
106
  attach(element?: HTMLMediaElement): HTMLMediaElement {
99
- const needsNewWebAudioConnection = this.attachedElements.length === 0;
107
+ // the graph is fed by the track rather than by a specific element, so one connection covers
108
+ // all attached elements
109
+ const needsNewWebAudioConnection = !this.sourceNode;
100
110
  if (!element) {
101
111
  element = super.attach();
102
112
  } else {
@@ -108,14 +118,18 @@ export default class RemoteAudioTrack extends RemoteTrack<Track.Kind.Audio> {
108
118
  this.log.error('Failed to set sink id on remote audio track', e, this.logContext);
109
119
  });
110
120
  }
111
- if (this.audioContext && needsNewWebAudioConnection) {
112
- this.log.debug('using audio context mapping', this.logContext);
113
- this.connectWebAudio(this.audioContext, element);
121
+ if (this.audioContext) {
122
+ // audio is routed through the web audio graph, so every attached element has to stay
123
+ // silent, no matter whether it is the one the graph was built from
114
124
  element.volume = 0;
115
125
  element.muted = true;
126
+ if (needsNewWebAudioConnection) {
127
+ this.log.debug('using audio context mapping', this.logContext);
128
+ this.connectWebAudio(this.audioContext, element);
129
+ }
116
130
  }
117
131
 
118
- if (this.elementVolume) {
132
+ if (this.elementVolume !== undefined) {
119
133
  // make sure volume setting is being applied to the newly attached element
120
134
  this.setVolume(this.elementVolume);
121
135
  }
@@ -158,11 +172,26 @@ export default class RemoteAudioTrack extends RemoteTrack<Track.Kind.Audio> {
158
172
  * @experimental
159
173
  */
160
174
  setAudioContext(audioContext: AudioContext | undefined) {
175
+ if (this.audioContext === audioContext) {
176
+ return;
177
+ }
161
178
  this.audioContext = audioContext;
162
- if (audioContext && this.attachedElements.length > 0) {
163
- this.connectWebAudio(audioContext, this.attachedElements[0]);
164
- } else if (!audioContext) {
179
+ if (audioContext) {
180
+ // playback moves into the web audio graph, mute the elements to avoid double playback
181
+ for (const element of this.attachedElements) {
182
+ element.volume = 0;
183
+ element.muted = true;
184
+ }
185
+ if (this.attachedElements.length > 0) {
186
+ this.connectWebAudio(audioContext, this.attachedElements[0]);
187
+ }
188
+ } else {
165
189
  this.disconnectWebAudio();
190
+ // playback moves back into the elements, which `attach` had muted in favour of the graph
191
+ for (const element of this.attachedElements) {
192
+ element.muted = false;
193
+ element.volume = this.elementVolume ?? 1;
194
+ }
166
195
  }
167
196
  }
168
197
 
@@ -183,15 +212,18 @@ export default class RemoteAudioTrack extends RemoteTrack<Track.Kind.Audio> {
183
212
  // @ts-ignore attached elements always have a srcObject set
184
213
  this.sourceNode = context.createMediaStreamSource(element.srcObject);
185
214
  let lastNode: AudioNode = this.sourceNode;
215
+ this.connectedNodes = [this.sourceNode];
186
216
  this.webAudioPluginNodes.forEach((node) => {
187
217
  lastNode.connect(node);
188
218
  lastNode = node;
219
+ this.connectedNodes.push(node);
189
220
  });
190
221
  this.gainNode = context.createGain();
191
222
  lastNode.connect(this.gainNode);
223
+ this.connectedNodes.push(this.gainNode);
192
224
  this.gainNode.connect(context.destination);
193
225
 
194
- if (this.elementVolume) {
226
+ if (this.elementVolume !== undefined) {
195
227
  this.gainNode.gain.setTargetAtTime(this.elementVolume, 0, 0.1);
196
228
  }
197
229
 
@@ -214,6 +246,19 @@ export default class RemoteAudioTrack extends RemoteTrack<Track.Kind.Audio> {
214
246
  }
215
247
 
216
248
  private disconnectWebAudio() {
249
+ // the plugin nodes belong to whoever passed them in and may feed graphs of their own, so only
250
+ // the edges this track created come down. A bare `node.disconnect()` would drop the
251
+ // application's own routing with them
252
+ for (let i = 0; i < this.connectedNodes.length - 1; i += 1) {
253
+ try {
254
+ this.connectedNodes[i].disconnect(this.connectedNodes[i + 1]);
255
+ } catch {
256
+ // a plugin node the application already disconnected itself, the rest of the chain still
257
+ // has to come apart
258
+ }
259
+ }
260
+ this.connectedNodes = [];
261
+ // the source and gain nodes are ours alone, so every remaining edge of theirs is ours to drop
217
262
  this.gainNode?.disconnect();
218
263
  this.sourceNode?.disconnect();
219
264
  this.gainNode = undefined;
@@ -98,6 +98,216 @@ describe('RemoteVideoTrack', () => {
98
98
  });
99
99
  });
100
100
 
101
+ describe('Document Picture-in-Picture detection', () => {
102
+ /**
103
+ * happy-dom's MediaStream implements everything except getTracks(), which Track.attach()
104
+ * reads. Subclassing keeps it assignable to the element's srcObject, which type checks the
105
+ * value against happy-dom's own MediaStream.
106
+ */
107
+ class TestMediaStream extends MediaStream {
108
+ getTracks() {
109
+ return [...this.getAudioTracks(), ...this.getVideoTracks()];
110
+ }
111
+ }
112
+
113
+ /** Stands in for window.documentPictureInPicture, which happy-dom does not implement. */
114
+ class FakeDocumentPictureInPicture extends EventTarget {
115
+ window?: Window;
116
+ }
117
+
118
+ let documentPiP: FakeDocumentPictureInPicture;
119
+
120
+ /** Gives an element the layout box that isElementInViewport reads. */
121
+ function placeElement(
122
+ el: HTMLElement,
123
+ { top, left, width, height }: { top: number; left: number; width: number; height: number },
124
+ ) {
125
+ Object.defineProperties(el, {
126
+ offsetTop: { value: top, configurable: true },
127
+ offsetLeft: { value: left, configurable: true },
128
+ offsetWidth: { value: width, configurable: true },
129
+ offsetHeight: { value: height, configurable: true },
130
+ offsetParent: { value: null, configurable: true },
131
+ });
132
+ }
133
+
134
+ function createVideo(
135
+ ownerDocument: Document,
136
+ box = { top: 40, left: 40, width: 320, height: 180 },
137
+ ) {
138
+ const video = ownerDocument.createElement('video');
139
+ ownerDocument.body.appendChild(video);
140
+ placeElement(video, box);
141
+ return video;
142
+ }
143
+
144
+ /**
145
+ * Creates a frame in the given document and returns its document. happy-dom implements
146
+ * neither frameElement nor a correct top on a frame's window, so both are wired up here.
147
+ */
148
+ function createFrame(parentDocument: Document, topWindow: Window) {
149
+ const frame = parentDocument.createElement('iframe');
150
+ parentDocument.body.appendChild(frame);
151
+ const frameDocument = frame.contentDocument!;
152
+ Object.defineProperties(frameDocument.defaultView!, {
153
+ frameElement: { value: frame, configurable: true },
154
+ top: { value: topWindow, configurable: true },
155
+ });
156
+ return frameDocument;
157
+ }
158
+
159
+ /**
160
+ * Makes a frame document look like one reached through a cross-origin frame: the containing
161
+ * frame belongs to another origin, so frameElement reads null while top stays readable.
162
+ */
163
+ function hideContainingFrame(frameDocument: Document) {
164
+ Object.defineProperty(frameDocument.defaultView!, 'frameElement', {
165
+ value: null,
166
+ configurable: true,
167
+ });
168
+ return frameDocument;
169
+ }
170
+
171
+ /** Opens a Document PiP window and fires the 'enter' event the SDK listens for. */
172
+ function openPiPWindow({ width, height } = { width: 1280, height: 820 }) {
173
+ const pipDocument = document.implementation.createHTMLDocument('pip');
174
+ documentPiP.window = Object.assign(new EventTarget(), {
175
+ document: pipDocument,
176
+ innerWidth: width,
177
+ innerHeight: height,
178
+ pageXOffset: 0,
179
+ pageYOffset: 0,
180
+ }) as unknown as Window;
181
+ documentPiP.dispatchEvent(new Event('enter'));
182
+ return pipDocument;
183
+ }
184
+
185
+ /** Attaches a fresh track to the element and records every VisibilityChanged emission. */
186
+ function attachTrack(video: HTMLElement) {
187
+ const track = new RemoteVideoTrack(new MockMediaStreamTrack(), 'sid', undefined!, {});
188
+ const events: boolean[] = [];
189
+ track.on(TrackEvent.VisibilityChanged, (visible) => events.push(visible));
190
+ track.attach(video as HTMLMediaElement);
191
+ return events;
192
+ }
193
+
194
+ /** Flushes the microtask + animation frame that onEnterPiP defers its re-check behind. */
195
+ async function flushDeferredPiPCheck() {
196
+ await vi.advanceTimersByTimeAsync(50);
197
+ }
198
+
199
+ function setOpenerHidden() {
200
+ Object.defineProperty(document, 'visibilityState', { value: 'hidden', configurable: true });
201
+ }
202
+
203
+ beforeEach(() => {
204
+ vi.stubGlobal('MediaStream', TestMediaStream);
205
+ documentPiP = new FakeDocumentPictureInPicture();
206
+ (window as any).documentPictureInPicture = documentPiP;
207
+ // The opener tab is backgrounded in every case, so pauseVideoInBackground (on by default)
208
+ // reports the element hidden unless it is genuinely in PiP.
209
+ setOpenerHidden();
210
+ });
211
+
212
+ afterEach(() => {
213
+ vi.unstubAllGlobals();
214
+ delete (window as any).documentPictureInPicture;
215
+ Object.defineProperty(document, 'visibilityState', { value: 'visible', configurable: true });
216
+ document.body.innerHTML = '';
217
+ });
218
+
219
+ it('does not report an opener-document element as being in PiP', () => {
220
+ openPiPWindow();
221
+ const video = createVideo(document);
222
+
223
+ // The top-window check must not open a hole here: the opener's top is the opener itself.
224
+ expect(window.top).toBe(window);
225
+ expect(window.top).not.toBe(documentPiP.window);
226
+ // The element sits at (40, 40) of the OPENER page, which falls inside the PiP window's
227
+ // 1280x820 box. It is not in PiP, so the background pause applies.
228
+ expect(attachTrack(video)).toEqual([false]);
229
+ });
230
+
231
+ it('does not report an element in an opener frame as being in PiP', () => {
232
+ openPiPWindow();
233
+ const openerFrame = createFrame(document, window);
234
+ const video = createVideo(openerFrame);
235
+
236
+ // An opener frame tops out at the opener, so neither the top check nor the walk matches.
237
+ expect(openerFrame.defaultView!.top).toBe(window);
238
+ expect(attachTrack(video)).toEqual([false]);
239
+ });
240
+
241
+ it('reports an element moved into the PiP document after the enter event', async () => {
242
+ const video = createVideo(document);
243
+ const events = attachTrack(video);
244
+ expect(events).toEqual([false]);
245
+
246
+ const pipDocument = openPiPWindow();
247
+ // The SDK defers its re-check behind a microtask and an animation frame precisely so the
248
+ // embedder can move its subtree across after the enter event fires.
249
+ pipDocument.body.appendChild(video);
250
+ expect(video.ownerDocument).toBe(pipDocument);
251
+ await flushDeferredPiPCheck();
252
+
253
+ expect(events).toEqual([false, true]);
254
+ });
255
+
256
+ it('leaves an unrelated opener element hidden when another element enters PiP', async () => {
257
+ const openerVideo = createVideo(document);
258
+ const movedVideo = createVideo(document);
259
+ const openerEvents = attachTrack(openerVideo);
260
+ const movedEvents = attachTrack(movedVideo);
261
+
262
+ const pipDocument = openPiPWindow();
263
+ pipDocument.body.appendChild(movedVideo);
264
+ await flushDeferredPiPCheck();
265
+
266
+ expect(movedEvents).toEqual([false, true]);
267
+ expect(openerEvents).toEqual([false]);
268
+ });
269
+
270
+ it('reports an element inside a frame nested in the PiP document', () => {
271
+ const pipDocument = openPiPWindow();
272
+ const video = createVideo(createFrame(pipDocument, documentPiP.window!), {
273
+ top: 10,
274
+ left: 10,
275
+ width: 320,
276
+ height: 180,
277
+ });
278
+
279
+ expect(attachTrack(video)).toEqual([true]);
280
+ });
281
+
282
+ it('reports an element behind a cross-origin frame nested in the PiP window', () => {
283
+ const pipDocument = openPiPWindow();
284
+ const pipWindow = documentPiP.window!;
285
+ // PiP document (origin A) -> frame (origin B) -> frame (origin A). The opener can reach
286
+ // the innermost document, but its containing frame belongs to B, so frameElement is null
287
+ // there and the same-origin walk cannot get back to the PiP document.
288
+ const crossOriginFrame = createFrame(pipDocument, pipWindow);
289
+ const innerFrame = hideContainingFrame(createFrame(crossOriginFrame, pipWindow));
290
+ const video = createVideo(innerFrame, { top: 10, left: 10, width: 320, height: 180 });
291
+
292
+ expect(innerFrame.defaultView!.frameElement).toBeNull();
293
+ expect(innerFrame.defaultView!.top).toBe(pipWindow);
294
+ expect(attachTrack(video)).toEqual([true]);
295
+ });
296
+
297
+ it('does not report an element scrolled out of the PiP window viewport', () => {
298
+ const pipDocument = openPiPWindow();
299
+ const video = createVideo(pipDocument, { top: 2000, left: 40, width: 320, height: 180 });
300
+
301
+ expect(attachTrack(video)).toEqual([false]);
302
+ });
303
+
304
+ it('does not report an element as being in PiP when no PiP window is open', () => {
305
+ const video = createVideo(document);
306
+
307
+ expect(attachTrack(video)).toEqual([false]);
308
+ });
309
+ });
310
+
101
311
  class MockElementInfo implements ElementInfo {
102
312
  element: object = {};
103
313
 
@@ -435,9 +435,35 @@ class HTMLElementInfo implements ElementInfo {
435
435
  function isElementInPiP(el: HTMLElement) {
436
436
  // Simple video PiP
437
437
  if (document.pictureInPictureElement === el) return true;
438
- // Document PiP
439
- if (window.documentPictureInPicture?.window)
440
- return isElementInViewport(el, window.documentPictureInPicture?.window);
438
+ // Document PiP: the element has to actually live in the PiP window, otherwise an
439
+ // opener-document element whose coordinates happen to fall inside the PiP window's
440
+ // bounds is reported as being in PiP.
441
+ const pipWindow = window.documentPictureInPicture?.window;
442
+ if (pipWindow) {
443
+ return isDocumentInPiPWindow(el.ownerDocument, pipWindow) && isElementInViewport(el, pipWindow);
444
+ }
445
+ return false;
446
+ }
447
+
448
+ // Whether the document is the Document PiP window's own document, or the document of a frame
449
+ // nested inside it. The spec allows frames in a PiP window, so an element in one is in PiP:
450
+ // https://wicg.github.io/document-picture-in-picture/#iframes
451
+ function isDocumentInPiPWindow(doc: Document, pipWindow: Window) {
452
+ // A cross-origin frame anywhere between the element and the PiP window hides frameElement
453
+ // from the walk below, but top stays readable across origins and identifies the top-level
454
+ // window the element belongs to. For an opener element that is the opener, not the PiP window.
455
+ if (doc.defaultView?.top === pipWindow) return true;
456
+ let current: Document | null | undefined = doc;
457
+ while (current) {
458
+ if (current === pipWindow.document) return true;
459
+ try {
460
+ // Reading frameElement across a cross-origin boundary throws, which means we cannot
461
+ // establish the ancestry and must not claim the element is in PiP.
462
+ current = current.defaultView?.frameElement?.ownerDocument;
463
+ } catch {
464
+ return false;
465
+ }
466
+ }
441
467
  return false;
442
468
  }
443
469
 
@@ -22,6 +22,17 @@ export interface AudioProcessorOptions extends ProcessorOptions<Track.Kind.Audio
22
22
  */
23
23
  export interface VideoProcessorOptions extends ProcessorOptions<Track.Kind.Video> {}
24
24
 
25
+ /**
26
+ * A processor is bound to the track it was set on, not to the room, and it does not survive the
27
+ * track being stopped: unpublishing a track or disconnecting from a room destroys its processor.
28
+ * Both paths stop the track by default, so after reconnecting or republishing, call
29
+ * `setProcessor` again on the new track to put the processor back.
30
+ *
31
+ * The exception is `stopLocalTrackOnUnpublish: false`, which keeps the track alive across an
32
+ * unpublish or disconnect. A video processor then survives with it. An audio processor only
33
+ * survives if the audio context came from the application, because a context the SDK created is
34
+ * closed on disconnect and the processor goes with it.
35
+ */
25
36
  export interface TrackProcessor<
26
37
  T extends Track.Kind,
27
38
  U extends ProcessorOptions<T> = ProcessorOptions<T>,