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
@@ -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
 
@@ -79,7 +79,7 @@ export interface TrackPublishDefaults {
79
79
 
80
80
  /**
81
81
  * scalability mode for svc codecs, defaults to 'L3T3_KEY'.
82
- * for svc codecs, simulcast is disabled.
82
+ * for svc codecs, simulcast is disabled if more than one spatial layer is used ('L2Tx' or 'L3Tx').
83
83
  */
84
84
  scalabilityMode?: ScalabilityMode;
85
85
 
@@ -1,7 +1,12 @@
1
- import { describe, expect, it } from 'vitest';
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
2
  import { audioDefaults, videoDefaults } from '../defaults';
3
3
  import { type AudioCaptureOptions, VideoPresets } from './options';
4
- import { constraintsForOptions, diffAttributes, mergeDefaultOptions } from './utils';
4
+ import {
5
+ constraintsForOptions,
6
+ diffAttributes,
7
+ mergeDefaultOptions,
8
+ waitForFirstVideoFrame,
9
+ } from './utils';
5
10
 
6
11
  describe('mergeDefaultOptions', () => {
7
12
  it('does not enable undefined options', () => {
@@ -135,3 +140,197 @@ describe('diffAttributes', () => {
135
140
  expect(diff.a).toBe('value');
136
141
  });
137
142
  });
143
+
144
+ class FakeVideoTrack extends EventTarget {
145
+ kind = 'video';
146
+
147
+ id = 'fake-video-track';
148
+
149
+ label = 'fake';
150
+
151
+ enabled = true;
152
+
153
+ muted = false;
154
+
155
+ readyState: MediaStreamTrackState = 'live';
156
+
157
+ settings: MediaTrackSettings;
158
+
159
+ constructor(settings: MediaTrackSettings = { width: 1280, height: 720 }) {
160
+ super();
161
+ this.settings = settings;
162
+ }
163
+
164
+ getSettings() {
165
+ return this.settings;
166
+ }
167
+
168
+ getConstraints() {
169
+ return {};
170
+ }
171
+ }
172
+
173
+ const asMediaStreamTrack = (track: FakeVideoTrack) => track as unknown as MediaStreamTrack;
174
+
175
+ describe('waitForFirstVideoFrame', () => {
176
+ let frameCallbacks: Map<number, VideoFrameRequestCallback>;
177
+ let cancelledHandles: number[];
178
+ let createdVideoElements: HTMLVideoElement[];
179
+
180
+ /** happy-dom has no requestVideoFrameCallback, install a controllable one */
181
+ function enableFrameCallbacks() {
182
+ let nextHandle = 1;
183
+ HTMLVideoElement.prototype.requestVideoFrameCallback = function requestVideoFrameCallback(
184
+ callback: VideoFrameRequestCallback,
185
+ ) {
186
+ const handle = nextHandle;
187
+ nextHandle += 1;
188
+ frameCallbacks.set(handle, callback);
189
+ return handle;
190
+ };
191
+ HTMLVideoElement.prototype.cancelVideoFrameCallback = function cancelVideoFrameCallback(
192
+ handle: number,
193
+ ) {
194
+ cancelledHandles.push(handle);
195
+ frameCallbacks.delete(handle);
196
+ };
197
+ }
198
+
199
+ function deliverFrame() {
200
+ Array.from(frameCallbacks.values()).forEach((callback) =>
201
+ callback(0, {} as VideoFrameCallbackMetadata),
202
+ );
203
+ }
204
+
205
+ beforeEach(() => {
206
+ frameCallbacks = new Map();
207
+ cancelledHandles = [];
208
+ createdVideoElements = [];
209
+ const createElement = document.createElement.bind(document);
210
+ vi.spyOn(document, 'createElement').mockImplementation(((
211
+ tagName: string,
212
+ options?: ElementCreationOptions,
213
+ ) => {
214
+ const element = createElement(tagName, options);
215
+ if (tagName === 'video') {
216
+ createdVideoElements.push(element as HTMLVideoElement);
217
+ }
218
+ return element;
219
+ }) as typeof document.createElement);
220
+ });
221
+
222
+ afterEach(() => {
223
+ vi.restoreAllMocks();
224
+ // @ts-expect-error removing the stand-in we installed above
225
+ delete HTMLVideoElement.prototype.requestVideoFrameCallback;
226
+ // @ts-expect-error removing the stand-in we installed above
227
+ delete HTMLVideoElement.prototype.cancelVideoFrameCallback;
228
+ });
229
+
230
+ it('resolves once a frame has been delivered', async () => {
231
+ enableFrameCallbacks();
232
+ const pending = waitForFirstVideoFrame(asMediaStreamTrack(new FakeVideoTrack()), 1000);
233
+ expect(createdVideoElements).toHaveLength(1);
234
+
235
+ deliverFrame();
236
+
237
+ await expect(pending).resolves.toBe(true);
238
+ });
239
+
240
+ it('gives up once the timeout elapses', async () => {
241
+ enableFrameCallbacks();
242
+
243
+ await expect(
244
+ waitForFirstVideoFrame(asMediaStreamTrack(new FakeVideoTrack()), 20),
245
+ ).resolves.toBe(false);
246
+ });
247
+
248
+ it.each([
249
+ ['ended', { readyState: 'ended' as MediaStreamTrackState }],
250
+ ['muted', { muted: true }],
251
+ ['disabled', { enabled: false }],
252
+ ])('does not wait on a track that is %s', async (_label, overrides) => {
253
+ enableFrameCallbacks();
254
+ const track = Object.assign(new FakeVideoTrack(), overrides);
255
+
256
+ await expect(waitForFirstVideoFrame(asMediaStreamTrack(track), 10_000)).resolves.toBe(false);
257
+ expect(createdVideoElements).toHaveLength(0);
258
+ });
259
+
260
+ it('falls back to loadeddata when requestVideoFrameCallback is unavailable', async () => {
261
+ const pending = waitForFirstVideoFrame(asMediaStreamTrack(new FakeVideoTrack()), 1000);
262
+
263
+ createdVideoElements[0].dispatchEvent(new Event('loadeddata'));
264
+
265
+ await expect(pending).resolves.toBe(true);
266
+ });
267
+
268
+ it('keeps waiting when play() is rejected', async () => {
269
+ enableFrameCallbacks();
270
+ vi.spyOn(HTMLMediaElement.prototype, 'play').mockRejectedValue(new Error('NotAllowedError'));
271
+ const pending = waitForFirstVideoFrame(asMediaStreamTrack(new FakeVideoTrack()), 1000);
272
+
273
+ deliverFrame();
274
+
275
+ await expect(pending).resolves.toBe(true);
276
+ });
277
+
278
+ it('reuses an element that already renders the track', async () => {
279
+ enableFrameCallbacks();
280
+ const track = new FakeVideoTrack();
281
+ const attached = document.createElement('video');
282
+ attached.srcObject = new MediaStream([asMediaStreamTrack(track)]);
283
+ createdVideoElements.length = 0;
284
+
285
+ const pending = waitForFirstVideoFrame(asMediaStreamTrack(track), 1000, [attached]);
286
+ expect(createdVideoElements).toHaveLength(0);
287
+ deliverFrame();
288
+
289
+ await expect(pending).resolves.toBe(true);
290
+ // the caller owns that element, it must be left rendering
291
+ expect(attached.srcObject).not.toBeNull();
292
+ });
293
+
294
+ it('reuses an element that has not decoded a frame yet without requestVideoFrameCallback', async () => {
295
+ const track = new FakeVideoTrack();
296
+ const attached = document.createElement('video');
297
+ attached.srcObject = new MediaStream([asMediaStreamTrack(track)]);
298
+ Object.defineProperty(attached, 'readyState', { value: 1, configurable: true });
299
+ createdVideoElements.length = 0;
300
+
301
+ const pending = waitForFirstVideoFrame(asMediaStreamTrack(track), 1000, [attached]);
302
+ expect(createdVideoElements).toHaveLength(0);
303
+ // `loadeddata` has not fired on that element yet, so listening for it still works
304
+ attached.dispatchEvent(new Event('loadeddata'));
305
+
306
+ await expect(pending).resolves.toBe(true);
307
+ });
308
+
309
+ it('declines an already loaded element without requestVideoFrameCallback', async () => {
310
+ const track = new FakeVideoTrack();
311
+ const attached = document.createElement('video');
312
+ attached.srcObject = new MediaStream([asMediaStreamTrack(track)]);
313
+ Object.defineProperty(attached, 'readyState', { value: 2, configurable: true });
314
+ createdVideoElements.length = 0;
315
+
316
+ const pending = waitForFirstVideoFrame(asMediaStreamTrack(track), 1000, [attached]);
317
+ // `loadeddata` already fired on that element and is never replayed, so we need our own
318
+ expect(createdVideoElements).toHaveLength(1);
319
+ createdVideoElements[0].dispatchEvent(new Event('loadeddata'));
320
+
321
+ await expect(pending).resolves.toBe(true);
322
+ });
323
+
324
+ it('tears down the element it created', async () => {
325
+ enableFrameCallbacks();
326
+ const pause = vi.spyOn(HTMLMediaElement.prototype, 'pause');
327
+ const pending = waitForFirstVideoFrame(asMediaStreamTrack(new FakeVideoTrack()), 1000);
328
+
329
+ deliverFrame();
330
+ await pending;
331
+
332
+ expect(createdVideoElements[0].srcObject).toBeNull();
333
+ expect(pause).toHaveBeenCalled();
334
+ expect(cancelledHandles).toHaveLength(1);
335
+ });
336
+ });
@@ -1,5 +1,6 @@
1
1
  import { TrackInfo, TrackPublishedResponse, TrackSource, VideoQuality } from '@livekit/protocol';
2
2
  import type { AudioProcessorOptions, TrackProcessor, VideoProcessorOptions } from '../..';
3
+ import log from '../../logger';
3
4
  import { cloneDeep } from '../../utils/cloneDeep';
4
5
  import { isSafari, sleep } from '../utils';
5
6
  import { Track } from './Track';
@@ -351,3 +352,90 @@ export function layerDimensionsFor(
351
352
  ): Track.Dimensions | undefined {
352
353
  return trackInfo.layers?.find((l) => l.quality === quality);
353
354
  }
355
+
356
+ /**
357
+ * Waits until `track` has actually produced a video frame.
358
+ *
359
+ * iOS keeps reporting the camera's sensor frame from `getSettings()` until the camera has
360
+ * delivered its first picture, so a portrait capture initially reads as landscape. The arrival
361
+ * of that first frame is the only reliable signal that the reported settings describe reality.
362
+ *
363
+ * Best effort, never throws. Resolves `true` when a frame was observed, `false` when no frame
364
+ * can be observed (the track is already ended, muted or disabled) or `timeoutMs` elapsed first.
365
+ *
366
+ * @internal
367
+ */
368
+ export async function waitForFirstVideoFrame(
369
+ track: MediaStreamTrack,
370
+ timeoutMs: number,
371
+ candidateElements: HTMLMediaElement[] = [],
372
+ ): Promise<boolean> {
373
+ if (typeof document === 'undefined') {
374
+ return false;
375
+ }
376
+ if (track.readyState === 'ended' || track.muted || !track.enabled) {
377
+ // no frame is ever going to arrive, don't hold the caller up for the full timeout
378
+ return false;
379
+ }
380
+
381
+ // requestVideoFrameCallback (Safari 15.4 and up) is a platform capability, so the answer is the
382
+ // same for every element
383
+ const supportsFrameCallback =
384
+ typeof HTMLVideoElement.prototype.requestVideoFrameCallback === 'function';
385
+
386
+ // prefer an element that is already rendering this track (a local preview, typically) over
387
+ // spinning up a second decode just to observe a frame
388
+ const existingElement = candidateElements.find(
389
+ (el): el is HTMLVideoElement =>
390
+ el instanceof HTMLVideoElement &&
391
+ el.srcObject instanceof MediaStream &&
392
+ el.srcObject.getVideoTracks().includes(track) &&
393
+ // without requestVideoFrameCallback we depend on `loadeddata`, which is one-shot: an element
394
+ // that already decoded a frame (readyState >= HAVE_CURRENT_DATA) never fires it again.
395
+ // Leave those alone and set up our own element, which starts out empty.
396
+ (supportsFrameCallback || el.readyState < 2),
397
+ );
398
+
399
+ const element = existingElement ?? document.createElement('video');
400
+ if (!existingElement) {
401
+ element.playsInline = true;
402
+ element.muted = true;
403
+ element.srcObject = new MediaStream([track]);
404
+ // the element is deliberately never added to the DOM, it only exists to pull frames.
405
+ // WebKit delivers frames to a paused element too, so a rejected play() (Safari's low power
406
+ // mode, for instance) is not a reason to stop waiting
407
+ element.play().catch((error) => {
408
+ log.debug('could not play element while waiting for the first video frame', { error });
409
+ });
410
+ }
411
+
412
+ let timer: ReturnType<typeof setTimeout> | undefined;
413
+ let frameHandle: number | undefined;
414
+ let onLoadedData: (() => void) | undefined;
415
+
416
+ try {
417
+ return await new Promise<boolean>((resolve) => {
418
+ timer = setTimeout(() => resolve(false), timeoutMs);
419
+ if (supportsFrameCallback) {
420
+ frameHandle = element.requestVideoFrameCallback(() => resolve(true));
421
+ } else {
422
+ // Safari below 15.4 has no requestVideoFrameCallback. `loadeddata` fires once the first
423
+ // frame has been decoded, which is the same signal a few milliseconds earlier.
424
+ onLoadedData = () => resolve(true);
425
+ element.addEventListener('loadeddata', onLoadedData);
426
+ }
427
+ });
428
+ } finally {
429
+ clearTimeout(timer);
430
+ if (frameHandle !== undefined) {
431
+ element.cancelVideoFrameCallback?.(frameHandle);
432
+ }
433
+ if (onLoadedData) {
434
+ element.removeEventListener('loadeddata', onLoadedData);
435
+ }
436
+ if (!existingElement) {
437
+ element.pause();
438
+ element.srcObject = null;
439
+ }
440
+ }
441
+ }