@hanifhan1f/vidstack 1.12.25 → 1.12.27

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 (92) hide show
  1. package/cdn/chunks/vidstack-8JHLDxl5.js +1 -0
  2. package/cdn/chunks/vidstack-Bpnl-N6k.js +1 -0
  3. package/cdn/chunks/vidstack-CnWKPIKT.js +16 -0
  4. package/cdn/chunks/vidstack-CqzAnF2W.js +16 -0
  5. package/cdn/vidstack.js +1 -1
  6. package/cdn/with-layouts/chunks/vidstack-BD5YoTt5.js +937 -0
  7. package/cdn/with-layouts/chunks/vidstack-DCaNJN4T.js +1 -0
  8. package/cdn/with-layouts/chunks/vidstack-Dd3L-eQj.js +1 -0
  9. package/cdn/with-layouts/chunks/vidstack-T2rZVigk.js +912 -0
  10. package/cdn/with-layouts/vidstack.js +1 -1
  11. package/dev/chunks/vidstack--aukHYxl.js +1520 -0
  12. package/dev/chunks/vidstack-B__DfQsT.js +1621 -0
  13. package/dev/chunks/vidstack-BoLIUOyq.js +204 -0
  14. package/dev/chunks/vidstack-CSryZFvY.js +1521 -0
  15. package/dev/chunks/vidstack-Cky9ors4.js +297 -0
  16. package/dev/chunks/vidstack-Crz0ROkT.js +3009 -0
  17. package/dev/chunks/vidstack-D-sqb6YI.js +308 -0
  18. package/dev/chunks/vidstack-DLXCqdYV.js +3010 -0
  19. package/dev/chunks/vidstack-DS7nRfge.js +204 -0
  20. package/dev/chunks/vidstack-Dco6kA4h.js +104 -0
  21. package/dev/chunks/vidstack-DpS0Kt4b.js +297 -0
  22. package/dev/chunks/vidstack-zJT-7ncH.js +5182 -0
  23. package/dev/define/plyr-layout.js +3 -4
  24. package/dev/define/templates/vidstack-audio-layout.js +4 -4
  25. package/dev/define/templates/vidstack-video-layout.js +4 -4
  26. package/dev/define/vidstack-player-default-layout.js +4 -4
  27. package/dev/define/vidstack-player-layouts.js +4 -4
  28. package/dev/define/vidstack-player-ui.js +5 -6
  29. package/dev/define/vidstack-player.js +3 -4
  30. package/dev/global/plyr.js +5 -6
  31. package/dev/global/vidstack-player.js +3 -4
  32. package/dev/providers/vidstack-dash.js +1 -2
  33. package/dev/providers/vidstack-hls.js +1 -2
  34. package/dev/providers/vidstack-video.js +1 -2
  35. package/dev/providers/vidstack-vimeo.js +1 -2
  36. package/dev/vidstack-elements.js +8 -9
  37. package/dev/vidstack.js +6 -7
  38. package/package.json +2 -1
  39. package/prod/chunks/vidstack-BVSJtdRd.js +297 -0
  40. package/prod/chunks/vidstack-BnEo_Sla.js +1621 -0
  41. package/prod/chunks/vidstack-CFXAYpuh.js +1521 -0
  42. package/prod/chunks/vidstack-CIvL96_j.js +297 -0
  43. package/prod/chunks/vidstack-CLTPjjXX.js +4772 -0
  44. package/prod/chunks/vidstack-CSHHV2zO.js +201 -0
  45. package/prod/chunks/vidstack-CYVCrFjx.js +201 -0
  46. package/prod/chunks/vidstack-D_atbNqH.js +3000 -0
  47. package/prod/chunks/vidstack-Eo46ZHu7.js +2999 -0
  48. package/prod/chunks/vidstack-sP7TQMB1.js +300 -0
  49. package/prod/chunks/vidstack-uVm3xX8H.js +104 -0
  50. package/prod/chunks/vidstack-zknLxihl.js +1520 -0
  51. package/prod/define/plyr-layout.js +3 -4
  52. package/prod/define/templates/vidstack-audio-layout.js +4 -4
  53. package/prod/define/templates/vidstack-video-layout.js +4 -4
  54. package/prod/define/vidstack-player-default-layout.js +4 -4
  55. package/prod/define/vidstack-player-layouts.js +4 -4
  56. package/prod/define/vidstack-player-ui.js +5 -6
  57. package/prod/define/vidstack-player.js +3 -4
  58. package/prod/global/plyr.js +5 -6
  59. package/prod/global/vidstack-player.js +3 -4
  60. package/prod/providers/vidstack-dash.js +1 -2
  61. package/prod/providers/vidstack-hls.js +1 -2
  62. package/prod/providers/vidstack-video.js +1 -2
  63. package/prod/providers/vidstack-vimeo.js +1 -2
  64. package/prod/vidstack-elements.js +8 -9
  65. package/prod/vidstack.js +6 -7
  66. package/server/chunks/vidstack-B3eA67nX.js +205 -0
  67. package/server/chunks/vidstack-B8P1aUCK.js +1503 -0
  68. package/server/chunks/vidstack-B8_v1VQn.js +3059 -0
  69. package/server/chunks/vidstack-BK4xGWUK.js +207 -0
  70. package/server/chunks/vidstack-BO8FLks6.js +295 -0
  71. package/server/chunks/vidstack-BaXvZgx2.js +141 -0
  72. package/server/chunks/vidstack-BlvJg_5A.js +4636 -0
  73. package/server/chunks/vidstack-CBhikwSz.js +67 -0
  74. package/server/chunks/vidstack-COczNXom.js +3059 -0
  75. package/server/chunks/vidstack-CyZPtpwO.js +1503 -0
  76. package/server/chunks/vidstack-Db22EuE_.js +207 -0
  77. package/server/chunks/vidstack-Dh1ZDEI-.js +29 -0
  78. package/server/chunks/vidstack-Dm-ETAZh.js +295 -0
  79. package/server/chunks/vidstack-NpAD9hfP.js +620 -0
  80. package/server/chunks/vidstack-O4BgIcQI.js +104 -0
  81. package/server/chunks/vidstack-n4zAyLEV.js +2139 -0
  82. package/server/chunks/vidstack-za5Yh5DQ.js +566 -0
  83. package/server/chunks/vidstack-zoXyfYxa.js +107 -0
  84. package/server/define/plyr-layout.js +7 -7
  85. package/server/define/vidstack-player-default-layout.js +3 -3
  86. package/server/define/vidstack-player-layouts.js +5 -5
  87. package/server/define/vidstack-player-ui.js +6 -6
  88. package/server/define/vidstack-player.js +4 -4
  89. package/server/global/plyr.js +9 -9
  90. package/server/global/vidstack-player.js +4 -4
  91. package/server/vidstack-elements.js +13 -13
  92. package/server/vidstack.js +8 -8
@@ -0,0 +1,4772 @@
1
+ import { EventsTarget, DOMEvent, fscreen, ViewController, EventsController, onDispose, signal, listenEvent, peek, isString, State, tick, Component, functionThrottle, effect, untrack, functionDebounce, isArray, isKeyboardClick, isKeyboardEvent, waitIdlePeriod, deferredPromise, isUndefined, isNumber, prop, method, provideContext, setAttribute, animationFrameThrottle, uppercaseFirstChar, camelToKebabCase, setStyle, computed, scoped, noop } from './vidstack-BNpgCJJ1.js';
2
+ import { mediaContext, useMediaContext } from './vidstack-tt3O1zL6.js';
3
+ import { canOrientScreen, IS_IPHONE, isAudioSrc, canPlayAudioType, isVideoSrc, canPlayVideoType, isHLSSupported, isHLSSrc, isDASHSupported, isDASHSrc, IS_CHROME, IS_IOS, canGoogleCastSrc, canChangeVolume } from './vidstack-wTTCvdqe.js';
4
+ import { TimeRange, getTimeRangesEnd, getTimeRangesStart, updateTimeIntervals } from './vidstack-Fem0yF3c.js';
5
+ import { isTrackCaptionKind, TextTrackSymbol, TextTrack } from './vidstack-sP7TQMB1.js';
6
+ import { ListSymbol } from './vidstack-D5EzK014.js';
7
+ import { QualitySymbol } from './vidstack-B01xzxC4.js';
8
+ import { isHTMLElement, isTouchPinchEvent, prefersReducedMotion, setAttributeIfEmpty } from './vidstack-DB9WDRL5.js';
9
+ import { coerceToError } from './vidstack-C9vIqaYT.js';
10
+ import { preconnect, getRequestCredentials } from './vidstack-KShKSmYu.js';
11
+ import { clampNumber } from './vidstack-Dihypf8P.js';
12
+ import { FocusVisibleController } from './vidstack-COLU-zPZ.js';
13
+
14
+ class List extends EventsTarget {
15
+ items = [];
16
+ /** @internal */
17
+ [ListSymbol.readonly] = false;
18
+ get length() {
19
+ return this.items.length;
20
+ }
21
+ get readonly() {
22
+ return this[ListSymbol.readonly];
23
+ }
24
+ /**
25
+ * Returns the index of the first occurrence of the given item, or -1 if it is not present.
26
+ */
27
+ indexOf(item) {
28
+ return this.items.indexOf(item);
29
+ }
30
+ /**
31
+ * Returns an item matching the given `id`, or `null` if not present.
32
+ */
33
+ getById(id) {
34
+ if (id === "") return null;
35
+ return this.items.find((item) => item.id === id) ?? null;
36
+ }
37
+ /**
38
+ * Transform list to an array.
39
+ */
40
+ toArray() {
41
+ return [...this.items];
42
+ }
43
+ [Symbol.iterator]() {
44
+ return this.items.values();
45
+ }
46
+ /** @internal */
47
+ [ListSymbol.add](item, trigger) {
48
+ const index = this.items.length;
49
+ if (!("" + index in this)) {
50
+ Object.defineProperty(this, index, {
51
+ get() {
52
+ return this.items[index];
53
+ }
54
+ });
55
+ }
56
+ if (this.items.includes(item)) return;
57
+ this.items.push(item);
58
+ this.dispatchEvent(new DOMEvent("add", { detail: item, trigger }));
59
+ }
60
+ /** @internal */
61
+ [ListSymbol.remove](item, trigger) {
62
+ const index = this.items.indexOf(item);
63
+ if (index >= 0) {
64
+ this[ListSymbol.onRemove]?.(item, trigger);
65
+ this.items.splice(index, 1);
66
+ this.dispatchEvent(new DOMEvent("remove", { detail: item, trigger }));
67
+ }
68
+ }
69
+ /** @internal */
70
+ [ListSymbol.reset](trigger) {
71
+ for (const item of [...this.items]) this[ListSymbol.remove](item, trigger);
72
+ this.items = [];
73
+ this[ListSymbol.setReadonly](false, trigger);
74
+ this[ListSymbol.onReset]?.();
75
+ }
76
+ /** @internal */
77
+ [ListSymbol.setReadonly](readonly, trigger) {
78
+ if (this[ListSymbol.readonly] === readonly) return;
79
+ this[ListSymbol.readonly] = readonly;
80
+ this.dispatchEvent(new DOMEvent("readonly-change", { detail: readonly, trigger }));
81
+ }
82
+ }
83
+
84
+ const CAN_FULLSCREEN = fscreen.fullscreenEnabled;
85
+ class FullscreenController extends ViewController {
86
+ /**
87
+ * Tracks whether we're the active fullscreen event listener. Fullscreen events can only be
88
+ * listened to globally on the document so we need to know if they relate to the current host
89
+ * element or not.
90
+ */
91
+ #listening = false;
92
+ #active = false;
93
+ get active() {
94
+ return this.#active;
95
+ }
96
+ get supported() {
97
+ return CAN_FULLSCREEN;
98
+ }
99
+ onConnect() {
100
+ new EventsController(fscreen).add("fullscreenchange", this.#onChange.bind(this)).add("fullscreenerror", this.#onError.bind(this));
101
+ onDispose(this.#onDisconnect.bind(this));
102
+ }
103
+ async #onDisconnect() {
104
+ if (CAN_FULLSCREEN) await this.exit();
105
+ }
106
+ #onChange(event) {
107
+ const active = isFullscreen(this.el);
108
+ if (active === this.#active) return;
109
+ if (!active) this.#listening = false;
110
+ this.#active = active;
111
+ this.dispatch("fullscreen-change", { detail: active, trigger: event });
112
+ }
113
+ #onError(event) {
114
+ if (!this.#listening) return;
115
+ this.dispatch("fullscreen-error", { detail: null, trigger: event });
116
+ this.#listening = false;
117
+ }
118
+ async enter() {
119
+ try {
120
+ this.#listening = true;
121
+ if (!this.el || isFullscreen(this.el)) return;
122
+ assertFullscreenAPI();
123
+ return fscreen.requestFullscreen(this.el);
124
+ } catch (error) {
125
+ this.#listening = false;
126
+ throw error;
127
+ }
128
+ }
129
+ async exit() {
130
+ if (!this.el || !isFullscreen(this.el)) return;
131
+ assertFullscreenAPI();
132
+ return fscreen.exitFullscreen();
133
+ }
134
+ }
135
+ function canFullscreen() {
136
+ return CAN_FULLSCREEN;
137
+ }
138
+ function isFullscreen(host) {
139
+ if (fscreen.fullscreenElement === host) return true;
140
+ try {
141
+ return host.matches(
142
+ // @ts-expect-error - `fullscreenPseudoClass` is missing from `@types/fscreen`.
143
+ fscreen.fullscreenPseudoClass
144
+ );
145
+ } catch (error) {
146
+ return false;
147
+ }
148
+ }
149
+ function assertFullscreenAPI() {
150
+ if (CAN_FULLSCREEN) return;
151
+ throw Error(
152
+ "[vidstack] no fullscreen API"
153
+ );
154
+ }
155
+
156
+ class ScreenOrientationController extends ViewController {
157
+ #type = signal(this.#getScreenOrientation());
158
+ #locked = signal(false);
159
+ #currentLock;
160
+ /**
161
+ * The current screen orientation type.
162
+ *
163
+ * @signal
164
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/ScreenOrientation}
165
+ * @see https://w3c.github.io/screen-orientation/#screen-orientation-types-and-locks
166
+ */
167
+ get type() {
168
+ return this.#type();
169
+ }
170
+ /**
171
+ * Whether the screen orientation is currently locked.
172
+ *
173
+ * @signal
174
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/ScreenOrientation}
175
+ * @see https://w3c.github.io/screen-orientation/#screen-orientation-types-and-locks
176
+ */
177
+ get locked() {
178
+ return this.#locked();
179
+ }
180
+ /**
181
+ * Whether the viewport is in a portrait orientation.
182
+ *
183
+ * @signal
184
+ */
185
+ get portrait() {
186
+ return this.#type().startsWith("portrait");
187
+ }
188
+ /**
189
+ * Whether the viewport is in a landscape orientation.
190
+ *
191
+ * @signal
192
+ */
193
+ get landscape() {
194
+ return this.#type().startsWith("landscape");
195
+ }
196
+ /**
197
+ * Whether the native Screen Orientation API is available.
198
+ */
199
+ static supported = canOrientScreen();
200
+ /**
201
+ * Whether the native Screen Orientation API is available.
202
+ */
203
+ get supported() {
204
+ return ScreenOrientationController.supported;
205
+ }
206
+ onConnect() {
207
+ if (this.supported) {
208
+ listenEvent(screen.orientation, "change", this.#onOrientationChange.bind(this));
209
+ } else {
210
+ const query = window.matchMedia("(orientation: landscape)");
211
+ query.onchange = this.#onOrientationChange.bind(this);
212
+ onDispose(() => query.onchange = null);
213
+ }
214
+ onDispose(this.#onDisconnect.bind(this));
215
+ }
216
+ async #onDisconnect() {
217
+ if (this.supported && this.#locked()) await this.unlock();
218
+ }
219
+ #onOrientationChange(event) {
220
+ this.#type.set(this.#getScreenOrientation());
221
+ this.dispatch("orientation-change", {
222
+ detail: {
223
+ orientation: peek(this.#type),
224
+ lock: this.#currentLock
225
+ },
226
+ trigger: event
227
+ });
228
+ }
229
+ /**
230
+ * Locks the orientation of the screen to the desired orientation type using the
231
+ * Screen Orientation API.
232
+ *
233
+ * @param lockType - The screen lock orientation type.
234
+ * @throws Error - If screen orientation API is unavailable.
235
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Screen/orientation}
236
+ * @see {@link https://w3c.github.io/screen-orientation}
237
+ */
238
+ async lock(lockType) {
239
+ if (peek(this.#locked) || this.#currentLock === lockType) return;
240
+ this.#assertScreenOrientationAPI();
241
+ await screen.orientation.lock(lockType);
242
+ this.#locked.set(true);
243
+ this.#currentLock = lockType;
244
+ }
245
+ /**
246
+ * Unlocks the orientation of the screen to it's default state using the Screen Orientation
247
+ * API. This method will throw an error if the API is unavailable.
248
+ *
249
+ * @throws Error - If screen orientation API is unavailable.
250
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Screen/orientation}
251
+ * @see {@link https://w3c.github.io/screen-orientation}
252
+ */
253
+ async unlock() {
254
+ if (!peek(this.#locked)) return;
255
+ this.#assertScreenOrientationAPI();
256
+ this.#currentLock = void 0;
257
+ await screen.orientation.unlock();
258
+ this.#locked.set(false);
259
+ }
260
+ #assertScreenOrientationAPI() {
261
+ if (this.supported) return;
262
+ throw Error(
263
+ "[vidstack] no orientation API"
264
+ );
265
+ }
266
+ #getScreenOrientation() {
267
+ if (this.supported) return window.screen.orientation.type;
268
+ return window.innerWidth >= window.innerHeight ? "landscape-primary" : "portrait-primary";
269
+ }
270
+ }
271
+
272
+ function isVideoQualitySrc(src) {
273
+ return !isString(src) && ("height" in src || "label" in src);
274
+ }
275
+
276
+ const mediaState = new State({
277
+ artist: "",
278
+ artwork: null,
279
+ audioTrack: null,
280
+ audioTracks: [],
281
+ autoPlay: false,
282
+ autoPlayError: null,
283
+ audioGain: null,
284
+ buffered: new TimeRange(),
285
+ canLoad: false,
286
+ canLoadPoster: false,
287
+ canFullscreen: false,
288
+ canOrientScreen: canOrientScreen(),
289
+ canPictureInPicture: false,
290
+ canPlay: false,
291
+ clipStartTime: 0,
292
+ clipEndTime: 0,
293
+ controls: false,
294
+ get iOSControls() {
295
+ return IS_IPHONE && this.mediaType === "video" && (!this.playsInline || !fscreen.fullscreenEnabled && this.fullscreen);
296
+ },
297
+ get nativeControls() {
298
+ return this.controls || this.iOSControls;
299
+ },
300
+ controlsVisible: false,
301
+ get controlsHidden() {
302
+ return !this.controlsVisible;
303
+ },
304
+ crossOrigin: null,
305
+ ended: false,
306
+ error: null,
307
+ fullscreen: false,
308
+ get loop() {
309
+ return this.providedLoop || this.userPrefersLoop;
310
+ },
311
+ logLevel: "silent",
312
+ mediaType: "unknown",
313
+ muted: false,
314
+ paused: true,
315
+ played: new TimeRange(),
316
+ playing: false,
317
+ playsInline: false,
318
+ pictureInPicture: false,
319
+ preload: "metadata",
320
+ playbackRate: 1,
321
+ qualities: [],
322
+ quality: null,
323
+ autoQuality: false,
324
+ canSetQuality: true,
325
+ canSetPlaybackRate: true,
326
+ canSetVolume: false,
327
+ canSetAudioGain: false,
328
+ seekable: new TimeRange(),
329
+ seeking: false,
330
+ source: { src: "", type: "" },
331
+ sources: [],
332
+ started: false,
333
+ textTracks: [],
334
+ textTrack: null,
335
+ get hasCaptions() {
336
+ return this.textTracks.filter(isTrackCaptionKind).length > 0;
337
+ },
338
+ volume: 1,
339
+ waiting: false,
340
+ realCurrentTime: 0,
341
+ get currentTime() {
342
+ return this.ended ? this.duration : this.clipStartTime > 0 ? Math.max(0, Math.min(this.realCurrentTime - this.clipStartTime, this.duration)) : this.realCurrentTime;
343
+ },
344
+ providedDuration: -1,
345
+ intrinsicDuration: 0,
346
+ get duration() {
347
+ return this.seekableWindow;
348
+ },
349
+ get title() {
350
+ return this.providedTitle || this.inferredTitle;
351
+ },
352
+ get poster() {
353
+ return this.providedPoster || this.inferredPoster;
354
+ },
355
+ get viewType() {
356
+ return this.providedViewType !== "unknown" ? this.providedViewType : this.inferredViewType;
357
+ },
358
+ get streamType() {
359
+ return this.providedStreamType !== "unknown" ? this.providedStreamType : this.inferredStreamType;
360
+ },
361
+ get currentSrc() {
362
+ return this.source;
363
+ },
364
+ get bufferedStart() {
365
+ const start = getTimeRangesStart(this.buffered) ?? 0;
366
+ return Math.max(start, this.clipStartTime);
367
+ },
368
+ get bufferedEnd() {
369
+ const end = getTimeRangesEnd(this.buffered) ?? 0;
370
+ return Math.min(this.seekableEnd, Math.max(0, end - this.clipStartTime));
371
+ },
372
+ get bufferedWindow() {
373
+ return Math.max(0, this.bufferedEnd - this.bufferedStart);
374
+ },
375
+ get seekableStart() {
376
+ if (this.isLiveDVR && this.liveDVRWindow > 0) {
377
+ return Math.max(0, this.seekableEnd - this.liveDVRWindow);
378
+ }
379
+ const start = getTimeRangesStart(this.seekable) ?? 0;
380
+ return Math.max(start, this.clipStartTime);
381
+ },
382
+ get seekableEnd() {
383
+ if (this.providedDuration > 0) return this.providedDuration;
384
+ const end = this.liveSyncPosition > 0 ? this.liveSyncPosition : this.canPlay ? getTimeRangesEnd(this.seekable) ?? Infinity : 0;
385
+ return this.clipEndTime > 0 ? Math.min(this.clipEndTime, end) : end;
386
+ },
387
+ get seekableWindow() {
388
+ const window = this.seekableEnd - this.seekableStart;
389
+ return !isNaN(window) ? Math.max(0, window) : Infinity;
390
+ },
391
+ // ~~ remote playback ~~
392
+ canAirPlay: false,
393
+ canGoogleCast: false,
394
+ remotePlaybackState: "disconnected",
395
+ remotePlaybackType: "none",
396
+ remotePlaybackLoader: null,
397
+ remotePlaybackInfo: null,
398
+ get isAirPlayConnected() {
399
+ return this.remotePlaybackType === "airplay" && this.remotePlaybackState === "connected";
400
+ },
401
+ get isGoogleCastConnected() {
402
+ return this.remotePlaybackType === "google-cast" && this.remotePlaybackState === "connected";
403
+ },
404
+ // ~~ responsive design ~~
405
+ pointer: "fine",
406
+ orientation: "landscape",
407
+ width: 0,
408
+ height: 0,
409
+ mediaWidth: 0,
410
+ mediaHeight: 0,
411
+ lastKeyboardAction: null,
412
+ // ~~ user props ~~
413
+ userBehindLiveEdge: false,
414
+ // ~~ live props ~~
415
+ liveEdgeTolerance: 10,
416
+ minLiveDVRWindow: 60,
417
+ get canSeek() {
418
+ return /unknown|on-demand|:dvr/.test(this.streamType) && Number.isFinite(this.duration) && (!this.isLiveDVR || this.duration >= this.liveDVRWindow);
419
+ },
420
+ get live() {
421
+ return this.streamType.includes("live") || !Number.isFinite(this.duration);
422
+ },
423
+ get liveEdgeStart() {
424
+ return this.live && Number.isFinite(this.seekableEnd) ? Math.max(0, this.seekableEnd - this.liveEdgeTolerance) : 0;
425
+ },
426
+ get liveEdge() {
427
+ return this.live && (!this.canSeek || !this.userBehindLiveEdge && this.currentTime >= this.liveEdgeStart);
428
+ },
429
+ get liveEdgeWindow() {
430
+ return this.live && Number.isFinite(this.seekableEnd) ? this.seekableEnd - this.liveEdgeStart : 0;
431
+ },
432
+ get isLiveDVR() {
433
+ return /:dvr/.test(this.streamType);
434
+ },
435
+ get liveDVRWindow() {
436
+ return Math.max(this.inferredLiveDVRWindow, this.minLiveDVRWindow);
437
+ },
438
+ // ~~ internal props ~~
439
+ autoPlaying: false,
440
+ providedTitle: "",
441
+ inferredTitle: "",
442
+ providedLoop: false,
443
+ userPrefersLoop: false,
444
+ providedPoster: "",
445
+ inferredPoster: "",
446
+ inferredViewType: "unknown",
447
+ providedViewType: "unknown",
448
+ providedStreamType: "unknown",
449
+ inferredStreamType: "unknown",
450
+ liveSyncPosition: null,
451
+ inferredLiveDVRWindow: 0,
452
+ savedState: null
453
+ });
454
+ const RESET_ON_SRC_QUALITY_CHANGE = /* @__PURE__ */ new Set([
455
+ "autoPlayError",
456
+ "autoPlaying",
457
+ "buffered",
458
+ "canPlay",
459
+ "error",
460
+ "paused",
461
+ "played",
462
+ "playing",
463
+ "seekable",
464
+ "seeking",
465
+ "waiting"
466
+ ]);
467
+ const RESET_ON_SRC_CHANGE = /* @__PURE__ */ new Set([
468
+ ...RESET_ON_SRC_QUALITY_CHANGE,
469
+ "ended",
470
+ "inferredPoster",
471
+ "inferredStreamType",
472
+ "inferredTitle",
473
+ "intrinsicDuration",
474
+ "inferredLiveDVRWindow",
475
+ "liveSyncPosition",
476
+ "realCurrentTime",
477
+ "savedState",
478
+ "started",
479
+ "userBehindLiveEdge"
480
+ ]);
481
+ function softResetMediaState($media, isSourceQualityChange = false) {
482
+ const filter = isSourceQualityChange ? RESET_ON_SRC_QUALITY_CHANGE : RESET_ON_SRC_CHANGE;
483
+ mediaState.reset($media, (prop) => filter.has(prop));
484
+ tick();
485
+ }
486
+ function boundTime(time, store) {
487
+ const clippedTime = time + store.clipStartTime(), isStart = Math.floor(time) === Math.floor(store.seekableStart()), isEnd = Math.floor(clippedTime) === Math.floor(store.seekableEnd());
488
+ if (isStart) {
489
+ return store.seekableStart();
490
+ }
491
+ if (isEnd) {
492
+ return store.seekableEnd();
493
+ }
494
+ if (store.isLiveDVR() && store.liveDVRWindow() > 0 && clippedTime < store.seekableEnd() - store.liveDVRWindow()) {
495
+ return store.bufferedStart();
496
+ }
497
+ return Math.min(Math.max(store.seekableStart() + 0.1, clippedTime), store.seekableEnd() - 0.1);
498
+ }
499
+
500
+ class MediaRemoteControl {
501
+ #target = null;
502
+ #player = null;
503
+ #prevTrackIndex = -1;
504
+ #logger;
505
+ constructor(logger = void 0) {
506
+ this.#logger = logger;
507
+ }
508
+ /**
509
+ * Set the target from which to dispatch media requests events from. The events should bubble
510
+ * up from this target to the player element.
511
+ *
512
+ * @example
513
+ * ```ts
514
+ * const button = document.querySelector('button');
515
+ * remote.setTarget(button);
516
+ * ```
517
+ */
518
+ setTarget(target) {
519
+ this.#target = target;
520
+ }
521
+ /**
522
+ * Returns the current player element. This method will attempt to find the player by
523
+ * searching up from either the given `target` or default target set via `remote.setTarget`.
524
+ *
525
+ * @example
526
+ * ```ts
527
+ * const player = remote.getPlayer();
528
+ * ```
529
+ */
530
+ getPlayer(target) {
531
+ if (this.#player) return this.#player;
532
+ (target ?? this.#target)?.dispatchEvent(
533
+ new DOMEvent("find-media-player", {
534
+ detail: (player) => void (this.#player = player),
535
+ bubbles: true,
536
+ composed: true
537
+ })
538
+ );
539
+ return this.#player;
540
+ }
541
+ /**
542
+ * Set the current player element so the remote can support toggle methods such as
543
+ * `togglePaused` as they rely on the current media state.
544
+ */
545
+ setPlayer(player) {
546
+ this.#player = player;
547
+ }
548
+ /**
549
+ * Dispatch a request to start the media loading process. This will only work if the media
550
+ * player has been initialized with a custom loading strategy `load="custom">`.
551
+ *
552
+ * @docs {@link https://www.vidstack.io/docs/player/core-concepts/loading#load-strategies}
553
+ */
554
+ startLoading(trigger) {
555
+ this.#dispatchRequest("media-start-loading", trigger);
556
+ }
557
+ /**
558
+ * Dispatch a request to start the poster loading process. This will only work if the media
559
+ * player has been initialized with a custom poster loading strategy `posterLoad="custom">`.
560
+ *
561
+ * @docs {@link https://www.vidstack.io/docs/player/core-concepts/loading#load-strategies}
562
+ */
563
+ startLoadingPoster(trigger) {
564
+ this.#dispatchRequest("media-poster-start-loading", trigger);
565
+ }
566
+ /**
567
+ * Dispatch a request to connect to AirPlay.
568
+ *
569
+ * @see {@link https://www.apple.com/au/airplay}
570
+ */
571
+ requestAirPlay(trigger) {
572
+ this.#dispatchRequest("media-airplay-request", trigger);
573
+ }
574
+ /**
575
+ * Dispatch a request to connect to Google Cast.
576
+ *
577
+ * @see {@link https://developers.google.com/cast/docs/overview}
578
+ */
579
+ requestGoogleCast(trigger) {
580
+ this.#dispatchRequest("media-google-cast-request", trigger);
581
+ }
582
+ /**
583
+ * Dispatch a request to begin/resume media playback.
584
+ */
585
+ play(trigger) {
586
+ this.#dispatchRequest("media-play-request", trigger);
587
+ }
588
+ /**
589
+ * Dispatch a request to pause media playback.
590
+ */
591
+ pause(trigger) {
592
+ this.#dispatchRequest("media-pause-request", trigger);
593
+ }
594
+ /**
595
+ * Dispatch a request to set the media volume to mute (0).
596
+ */
597
+ mute(trigger) {
598
+ this.#dispatchRequest("media-mute-request", trigger);
599
+ }
600
+ /**
601
+ * Dispatch a request to unmute the media volume and set it back to it's previous state.
602
+ */
603
+ unmute(trigger) {
604
+ this.#dispatchRequest("media-unmute-request", trigger);
605
+ }
606
+ /**
607
+ * Dispatch a request to enter fullscreen.
608
+ *
609
+ * @docs {@link https://www.vidstack.io/docs/player/api/fullscreen#remote-control}
610
+ */
611
+ enterFullscreen(target, trigger) {
612
+ this.#dispatchRequest("media-enter-fullscreen-request", trigger, target);
613
+ }
614
+ /**
615
+ * Dispatch a request to exit fullscreen.
616
+ *
617
+ * @docs {@link https://www.vidstack.io/docs/player/api/fullscreen#remote-control}
618
+ */
619
+ exitFullscreen(target, trigger) {
620
+ this.#dispatchRequest("media-exit-fullscreen-request", trigger, target);
621
+ }
622
+ /**
623
+ * Dispatch a request to lock the screen orientation.
624
+ *
625
+ * @docs {@link https://www.vidstack.io/docs/player/screen-orientation#remote-control}
626
+ */
627
+ lockScreenOrientation(lockType, trigger) {
628
+ this.#dispatchRequest("media-orientation-lock-request", trigger, lockType);
629
+ }
630
+ /**
631
+ * Dispatch a request to unlock the screen orientation.
632
+ *
633
+ * @docs {@link https://www.vidstack.io/docs/player/api/screen-orientation#remote-control}
634
+ */
635
+ unlockScreenOrientation(trigger) {
636
+ this.#dispatchRequest("media-orientation-unlock-request", trigger);
637
+ }
638
+ /**
639
+ * Dispatch a request to enter picture-in-picture mode.
640
+ *
641
+ * @docs {@link https://www.vidstack.io/docs/player/api/picture-in-picture#remote-control}
642
+ */
643
+ enterPictureInPicture(trigger) {
644
+ this.#dispatchRequest("media-enter-pip-request", trigger);
645
+ }
646
+ /**
647
+ * Dispatch a request to exit picture-in-picture mode.
648
+ *
649
+ * @docs {@link https://www.vidstack.io/docs/player/api/picture-in-picture#remote-control}
650
+ */
651
+ exitPictureInPicture(trigger) {
652
+ this.#dispatchRequest("media-exit-pip-request", trigger);
653
+ }
654
+ /**
655
+ * Notify the media player that a seeking process is happening and to seek to the given `time`.
656
+ */
657
+ seeking(time, trigger) {
658
+ this.#dispatchRequest("media-seeking-request", trigger, time);
659
+ }
660
+ /**
661
+ * Notify the media player that a seeking operation has completed and to seek to the given `time`.
662
+ * This is generally called after a series of `remote.seeking()` calls.
663
+ */
664
+ seek(time, trigger) {
665
+ this.#dispatchRequest("media-seek-request", trigger, time);
666
+ }
667
+ seekToLiveEdge(trigger) {
668
+ this.#dispatchRequest("media-live-edge-request", trigger);
669
+ }
670
+ /**
671
+ * Dispatch a request to update the length of the media in seconds.
672
+ *
673
+ * @example
674
+ * ```ts
675
+ * remote.changeDuration(100); // 100 seconds
676
+ * ```
677
+ */
678
+ changeDuration(duration, trigger) {
679
+ this.#dispatchRequest("media-duration-change-request", trigger, duration);
680
+ }
681
+ /**
682
+ * Dispatch a request to update the clip start time. This is the time at which media playback
683
+ * should start at.
684
+ *
685
+ * @example
686
+ * ```ts
687
+ * remote.changeClipStart(100); // start at 100 seconds
688
+ * ```
689
+ */
690
+ changeClipStart(startTime, trigger) {
691
+ this.#dispatchRequest("media-clip-start-change-request", trigger, startTime);
692
+ }
693
+ /**
694
+ * Dispatch a request to update the clip end time. This is the time at which media playback
695
+ * should end at.
696
+ *
697
+ * @example
698
+ * ```ts
699
+ * remote.changeClipEnd(100); // end at 100 seconds
700
+ * ```
701
+ */
702
+ changeClipEnd(endTime, trigger) {
703
+ this.#dispatchRequest("media-clip-end-change-request", trigger, endTime);
704
+ }
705
+ /**
706
+ * Dispatch a request to update the media volume to the given `volume` level which is a value
707
+ * between 0 and 1.
708
+ *
709
+ * @docs {@link https://www.vidstack.io/docs/player/api/audio-gain#remote-control}
710
+ * @example
711
+ * ```ts
712
+ * remote.changeVolume(0); // 0%
713
+ * remote.changeVolume(0.05); // 5%
714
+ * remote.changeVolume(0.5); // 50%
715
+ * remote.changeVolume(0.75); // 70%
716
+ * remote.changeVolume(1); // 100%
717
+ * ```
718
+ */
719
+ changeVolume(volume, trigger) {
720
+ this.#dispatchRequest("media-volume-change-request", trigger, Math.max(0, Math.min(1, volume)));
721
+ }
722
+ /**
723
+ * Dispatch a request to change the current audio track.
724
+ *
725
+ * @example
726
+ * ```ts
727
+ * remote.changeAudioTrack(1); // track at index 1
728
+ * ```
729
+ */
730
+ changeAudioTrack(index, trigger) {
731
+ this.#dispatchRequest("media-audio-track-change-request", trigger, index);
732
+ }
733
+ /**
734
+ * Dispatch a request to change the video quality. The special value `-1` represents auto quality
735
+ * selection.
736
+ *
737
+ * @example
738
+ * ```ts
739
+ * remote.changeQuality(-1); // auto
740
+ * remote.changeQuality(1); // quality at index 1
741
+ * ```
742
+ */
743
+ changeQuality(index, trigger) {
744
+ this.#dispatchRequest("media-quality-change-request", trigger, index);
745
+ }
746
+ /**
747
+ * Request auto quality selection.
748
+ */
749
+ requestAutoQuality(trigger) {
750
+ this.changeQuality(-1, trigger);
751
+ }
752
+ /**
753
+ * Dispatch a request to change the mode of the text track at the given index.
754
+ *
755
+ * @example
756
+ * ```ts
757
+ * remote.changeTextTrackMode(1, 'showing'); // track at index 1
758
+ * ```
759
+ */
760
+ changeTextTrackMode(index, mode, trigger) {
761
+ this.#dispatchRequest("media-text-track-change-request", trigger, {
762
+ index,
763
+ mode
764
+ });
765
+ }
766
+ /**
767
+ * Dispatch a request to change the media playback rate.
768
+ *
769
+ * @example
770
+ * ```ts
771
+ * remote.changePlaybackRate(0.5); // Half the normal speed
772
+ * remote.changePlaybackRate(1); // Normal speed
773
+ * remote.changePlaybackRate(1.5); // 50% faster than normal
774
+ * remote.changePlaybackRate(2); // Double the normal speed
775
+ * ```
776
+ */
777
+ changePlaybackRate(rate, trigger) {
778
+ this.#dispatchRequest("media-rate-change-request", trigger, rate);
779
+ }
780
+ /**
781
+ * Dispatch a request to change the media audio gain.
782
+ *
783
+ * @example
784
+ * ```ts
785
+ * remote.changeAudioGain(1); // Disable audio gain
786
+ * remote.changeAudioGain(1.5); // 50% louder
787
+ * remote.changeAudioGain(2); // 100% louder
788
+ * ```
789
+ */
790
+ changeAudioGain(gain, trigger) {
791
+ this.#dispatchRequest("media-audio-gain-change-request", trigger, gain);
792
+ }
793
+ /**
794
+ * Dispatch a request to resume idle tracking on controls.
795
+ */
796
+ resumeControls(trigger) {
797
+ this.#dispatchRequest("media-resume-controls-request", trigger);
798
+ }
799
+ /**
800
+ * Dispatch a request to pause controls idle tracking. Pausing tracking will result in the
801
+ * controls being visible until `remote.resumeControls()` is called. This method
802
+ * is generally used when building custom controls and you'd like to prevent the UI from
803
+ * disappearing.
804
+ *
805
+ * @example
806
+ * ```ts
807
+ * // Prevent controls hiding while menu is being interacted with.
808
+ * function onSettingsOpen() {
809
+ * remote.pauseControls();
810
+ * }
811
+ *
812
+ * function onSettingsClose() {
813
+ * remote.resumeControls();
814
+ * }
815
+ * ```
816
+ */
817
+ pauseControls(trigger) {
818
+ this.#dispatchRequest("media-pause-controls-request", trigger);
819
+ }
820
+ /**
821
+ * Dispatch a request to toggle the media playback state.
822
+ */
823
+ togglePaused(trigger) {
824
+ const player = this.getPlayer(trigger?.target);
825
+ if (!player) {
826
+ return;
827
+ }
828
+ if (player.state.paused) this.play(trigger);
829
+ else this.pause(trigger);
830
+ }
831
+ /**
832
+ * Dispatch a request to toggle the controls visibility.
833
+ */
834
+ toggleControls(trigger) {
835
+ const player = this.getPlayer(trigger?.target);
836
+ if (!player) {
837
+ return;
838
+ }
839
+ if (!player.controls.showing) {
840
+ player.controls.show(0, trigger);
841
+ } else {
842
+ player.controls.hide(0, trigger);
843
+ }
844
+ }
845
+ /**
846
+ * Dispatch a request to toggle the media muted state.
847
+ */
848
+ toggleMuted(trigger) {
849
+ const player = this.getPlayer(trigger?.target);
850
+ if (!player) {
851
+ return;
852
+ }
853
+ if (player.state.muted) this.unmute(trigger);
854
+ else this.mute(trigger);
855
+ }
856
+ /**
857
+ * Dispatch a request to toggle the media fullscreen state.
858
+ *
859
+ * @docs {@link https://www.vidstack.io/docs/player/api/fullscreen#remote-control}
860
+ */
861
+ toggleFullscreen(target, trigger) {
862
+ const player = this.getPlayer(trigger?.target);
863
+ if (!player) {
864
+ return;
865
+ }
866
+ if (player.state.fullscreen) this.exitFullscreen(target, trigger);
867
+ else this.enterFullscreen(target, trigger);
868
+ }
869
+ /**
870
+ * Dispatch a request to toggle the media picture-in-picture mode.
871
+ *
872
+ * @docs {@link https://www.vidstack.io/docs/player/api/picture-in-picture#remote-control}
873
+ */
874
+ togglePictureInPicture(trigger) {
875
+ const player = this.getPlayer(trigger?.target);
876
+ if (!player) {
877
+ return;
878
+ }
879
+ if (player.state.pictureInPicture) this.exitPictureInPicture(trigger);
880
+ else this.enterPictureInPicture(trigger);
881
+ }
882
+ /**
883
+ * Show captions.
884
+ */
885
+ showCaptions(trigger) {
886
+ const player = this.getPlayer(trigger?.target);
887
+ if (!player) {
888
+ return;
889
+ }
890
+ let tracks = player.state.textTracks, index = this.#prevTrackIndex;
891
+ if (!tracks[index] || !isTrackCaptionKind(tracks[index])) {
892
+ index = -1;
893
+ }
894
+ if (index === -1) {
895
+ index = tracks.findIndex((track) => isTrackCaptionKind(track) && track.default);
896
+ }
897
+ if (index === -1) {
898
+ index = tracks.findIndex((track) => isTrackCaptionKind(track));
899
+ }
900
+ if (index >= 0) this.changeTextTrackMode(index, "showing", trigger);
901
+ this.#prevTrackIndex = -1;
902
+ }
903
+ /**
904
+ * Turn captions off.
905
+ */
906
+ disableCaptions(trigger) {
907
+ const player = this.getPlayer(trigger?.target);
908
+ if (!player) {
909
+ return;
910
+ }
911
+ const tracks = player.state.textTracks, track = player.state.textTrack;
912
+ if (track) {
913
+ const index = tracks.indexOf(track);
914
+ this.changeTextTrackMode(index, "disabled", trigger);
915
+ this.#prevTrackIndex = index;
916
+ }
917
+ }
918
+ /**
919
+ * Dispatch a request to toggle the current captions mode.
920
+ */
921
+ toggleCaptions(trigger) {
922
+ const player = this.getPlayer(trigger?.target);
923
+ if (!player) {
924
+ return;
925
+ }
926
+ if (player.state.textTrack) {
927
+ this.disableCaptions();
928
+ } else {
929
+ this.showCaptions();
930
+ }
931
+ }
932
+ userPrefersLoopChange(prefersLoop, trigger) {
933
+ this.#dispatchRequest("media-user-loop-change-request", trigger, prefersLoop);
934
+ }
935
+ #dispatchRequest(type, trigger, detail) {
936
+ const request = new DOMEvent(type, {
937
+ bubbles: true,
938
+ composed: true,
939
+ cancelable: true,
940
+ detail,
941
+ trigger
942
+ });
943
+ let target = trigger?.target || null;
944
+ if (target && target instanceof Component) target = target.el;
945
+ const shouldUsePlayer = !target || target === document || target === window || target === document.body || this.#player?.el && target instanceof Node && !this.#player.el.contains(target);
946
+ target = shouldUsePlayer ? this.#target ?? this.getPlayer()?.el : target ?? this.#target;
947
+ if (this.#player) {
948
+ if (type === "media-play-request" && !this.#player.state.canLoad) {
949
+ target?.dispatchEvent(request);
950
+ } else {
951
+ this.#player.canPlayQueue.enqueue(type, () => target?.dispatchEvent(request));
952
+ }
953
+ } else {
954
+ target?.dispatchEvent(request);
955
+ }
956
+ }
957
+ #noPlayerWarning(method) {
958
+ }
959
+ }
960
+
961
+ class LocalMediaStorage {
962
+ playerId = "vds-player";
963
+ mediaId = null;
964
+ #data = {
965
+ volume: null,
966
+ muted: null,
967
+ audioGain: null,
968
+ time: null,
969
+ lang: null,
970
+ captions: null,
971
+ rate: null,
972
+ quality: null
973
+ };
974
+ async getVolume() {
975
+ return this.#data.volume;
976
+ }
977
+ async setVolume(volume) {
978
+ this.#data.volume = volume;
979
+ this.save();
980
+ }
981
+ async getMuted() {
982
+ return this.#data.muted;
983
+ }
984
+ async setMuted(muted) {
985
+ this.#data.muted = muted;
986
+ this.save();
987
+ }
988
+ async getTime() {
989
+ return this.#data.time;
990
+ }
991
+ async setTime(time, ended) {
992
+ const shouldClear = time < 0;
993
+ this.#data.time = !shouldClear ? time : null;
994
+ if (shouldClear || ended) this.saveTime();
995
+ else this.saveTimeThrottled();
996
+ }
997
+ async getLang() {
998
+ return this.#data.lang;
999
+ }
1000
+ async setLang(lang) {
1001
+ this.#data.lang = lang;
1002
+ this.save();
1003
+ }
1004
+ async getCaptions() {
1005
+ return this.#data.captions;
1006
+ }
1007
+ async setCaptions(enabled) {
1008
+ this.#data.captions = enabled;
1009
+ this.save();
1010
+ }
1011
+ async getPlaybackRate() {
1012
+ return this.#data.rate;
1013
+ }
1014
+ async setPlaybackRate(rate) {
1015
+ this.#data.rate = rate;
1016
+ this.save();
1017
+ }
1018
+ async getAudioGain() {
1019
+ return this.#data.audioGain;
1020
+ }
1021
+ async setAudioGain(gain) {
1022
+ this.#data.audioGain = gain;
1023
+ this.save();
1024
+ }
1025
+ async getVideoQuality() {
1026
+ return this.#data.quality;
1027
+ }
1028
+ async setVideoQuality(quality) {
1029
+ this.#data.quality = quality;
1030
+ this.save();
1031
+ }
1032
+ onChange(src, mediaId, playerId = "vds-player") {
1033
+ const savedData = playerId ? localStorage.getItem(playerId) : null, savedTime = mediaId ? localStorage.getItem(mediaId) : null;
1034
+ this.playerId = playerId;
1035
+ this.mediaId = mediaId;
1036
+ this.#data = {
1037
+ volume: null,
1038
+ muted: null,
1039
+ audioGain: null,
1040
+ lang: null,
1041
+ captions: null,
1042
+ rate: null,
1043
+ quality: null,
1044
+ ...savedData ? JSON.parse(savedData) : {},
1045
+ time: savedTime ? +savedTime : null
1046
+ };
1047
+ }
1048
+ save() {
1049
+ if (!this.playerId) return;
1050
+ const data = JSON.stringify({ ...this.#data, time: void 0 });
1051
+ localStorage.setItem(this.playerId, data);
1052
+ }
1053
+ saveTimeThrottled = functionThrottle(this.saveTime.bind(this), 1e3);
1054
+ saveTime() {
1055
+ if (!this.mediaId) return;
1056
+ const data = (this.#data.time ?? 0).toString();
1057
+ localStorage.setItem(this.mediaId, data);
1058
+ }
1059
+ }
1060
+
1061
+ const SELECTED = Symbol(0);
1062
+ class SelectList extends List {
1063
+ get selected() {
1064
+ return this.items.find((item) => item.selected) ?? null;
1065
+ }
1066
+ get selectedIndex() {
1067
+ return this.items.findIndex((item) => item.selected);
1068
+ }
1069
+ /** @internal */
1070
+ [ListSymbol.onRemove](item, trigger) {
1071
+ this[ListSymbol.select](item, false, trigger);
1072
+ }
1073
+ /** @internal */
1074
+ [ListSymbol.add](item, trigger) {
1075
+ item[SELECTED] = false;
1076
+ Object.defineProperty(item, "selected", {
1077
+ get() {
1078
+ return this[SELECTED];
1079
+ },
1080
+ set: (selected) => {
1081
+ if (this.readonly) return;
1082
+ this[ListSymbol.onUserSelect]?.();
1083
+ this[ListSymbol.select](item, selected);
1084
+ }
1085
+ });
1086
+ super[ListSymbol.add](item, trigger);
1087
+ }
1088
+ /** @internal */
1089
+ [ListSymbol.select](item, selected, trigger) {
1090
+ if (selected === item?.[SELECTED]) return;
1091
+ const prev = this.selected;
1092
+ if (item) item[SELECTED] = selected;
1093
+ const changed = !selected ? prev === item : prev !== item;
1094
+ if (changed) {
1095
+ if (prev) prev[SELECTED] = false;
1096
+ this.dispatchEvent(
1097
+ new DOMEvent("change", {
1098
+ detail: {
1099
+ prev,
1100
+ current: this.selected
1101
+ },
1102
+ trigger
1103
+ })
1104
+ );
1105
+ }
1106
+ }
1107
+ }
1108
+
1109
+ class AudioTrackList extends SelectList {
1110
+ }
1111
+
1112
+ class NativeTextRenderer {
1113
+ priority = 0;
1114
+ #display = true;
1115
+ #video = null;
1116
+ #track = null;
1117
+ #tracks = /* @__PURE__ */ new Set();
1118
+ canRender(_, video) {
1119
+ return !!video;
1120
+ }
1121
+ attach(video) {
1122
+ this.#video = video;
1123
+ if (video) video.textTracks.onchange = this.#onChange.bind(this);
1124
+ }
1125
+ addTrack(track) {
1126
+ this.#tracks.add(track);
1127
+ this.#attachTrack(track);
1128
+ }
1129
+ removeTrack(track) {
1130
+ track[TextTrackSymbol.native]?.remove?.();
1131
+ track[TextTrackSymbol.native] = null;
1132
+ this.#tracks.delete(track);
1133
+ }
1134
+ changeTrack(track) {
1135
+ const current = track?.[TextTrackSymbol.native];
1136
+ if (current && current.track.mode !== "showing") {
1137
+ current.track.mode = "showing";
1138
+ }
1139
+ this.#track = track;
1140
+ }
1141
+ setDisplay(display) {
1142
+ this.#display = display;
1143
+ this.#onChange();
1144
+ }
1145
+ detach() {
1146
+ if (this.#video) this.#video.textTracks.onchange = null;
1147
+ for (const track of this.#tracks) this.removeTrack(track);
1148
+ this.#tracks.clear();
1149
+ this.#video = null;
1150
+ this.#track = null;
1151
+ }
1152
+ #attachTrack(track) {
1153
+ if (!this.#video) return;
1154
+ const el = track[TextTrackSymbol.native] ??= this.#createTrackElement(track);
1155
+ if (isHTMLElement(el)) {
1156
+ this.#video.append(el);
1157
+ el.track.mode = el.default ? "showing" : "disabled";
1158
+ }
1159
+ }
1160
+ #createTrackElement(track) {
1161
+ const el = document.createElement("track"), isDefault = track.default || track.mode === "showing", isSupported = track.src && track.type === "vtt";
1162
+ el.id = track.id;
1163
+ el.src = isSupported ? track.src : "";
1164
+ el.label = track.label;
1165
+ el.kind = track.kind;
1166
+ el.default = isDefault;
1167
+ track.language && (el.srclang = track.language);
1168
+ if (isDefault && !isSupported) {
1169
+ this.#copyCues(track, el.track);
1170
+ }
1171
+ return el;
1172
+ }
1173
+ #copyCues(track, native) {
1174
+ if (track.src && track.type === "vtt" || native.cues?.length) return;
1175
+ for (const cue of track.cues) native.addCue(cue);
1176
+ }
1177
+ #onChange(event) {
1178
+ for (const track of this.#tracks) {
1179
+ const native = track[TextTrackSymbol.native];
1180
+ if (!native) continue;
1181
+ if (!this.#display) {
1182
+ native.track.mode = native.managed ? "hidden" : "disabled";
1183
+ continue;
1184
+ }
1185
+ const isShowing = native.track.mode === "showing";
1186
+ if (isShowing) this.#copyCues(track, native.track);
1187
+ track.setMode(isShowing ? "showing" : "disabled", event);
1188
+ }
1189
+ }
1190
+ }
1191
+
1192
+ class TextRenderers {
1193
+ #video = null;
1194
+ #textTracks;
1195
+ #renderers = [];
1196
+ #media;
1197
+ #nativeDisplay = false;
1198
+ #nativeRenderer = null;
1199
+ #customRenderer = null;
1200
+ constructor(media) {
1201
+ this.#media = media;
1202
+ const textTracks = media.textTracks;
1203
+ this.#textTracks = textTracks;
1204
+ effect(this.#watchControls.bind(this));
1205
+ onDispose(this.#detach.bind(this));
1206
+ new EventsController(textTracks).add("add", this.#onAddTrack.bind(this)).add("remove", this.#onRemoveTrack.bind(this)).add("mode-change", this.#update.bind(this));
1207
+ }
1208
+ #watchControls() {
1209
+ const { nativeControls } = this.#media.$state;
1210
+ this.#nativeDisplay = nativeControls();
1211
+ this.#update();
1212
+ }
1213
+ add(renderer) {
1214
+ this.#renderers.push(renderer);
1215
+ untrack(this.#update.bind(this));
1216
+ }
1217
+ remove(renderer) {
1218
+ renderer.detach();
1219
+ this.#renderers.splice(this.#renderers.indexOf(renderer), 1);
1220
+ untrack(this.#update.bind(this));
1221
+ }
1222
+ /** @internal */
1223
+ attachVideo(video) {
1224
+ requestAnimationFrame(() => {
1225
+ this.#video = video;
1226
+ if (video) {
1227
+ this.#nativeRenderer = new NativeTextRenderer();
1228
+ this.#nativeRenderer.attach(video);
1229
+ for (const track of this.#textTracks) this.#addNativeTrack(track);
1230
+ }
1231
+ this.#update();
1232
+ });
1233
+ }
1234
+ #addNativeTrack(track) {
1235
+ if (!isTrackCaptionKind(track)) return;
1236
+ this.#nativeRenderer?.addTrack(track);
1237
+ }
1238
+ #removeNativeTrack(track) {
1239
+ if (!isTrackCaptionKind(track)) return;
1240
+ this.#nativeRenderer?.removeTrack(track);
1241
+ }
1242
+ #onAddTrack(event) {
1243
+ this.#addNativeTrack(event.detail);
1244
+ }
1245
+ #onRemoveTrack(event) {
1246
+ this.#removeNativeTrack(event.detail);
1247
+ }
1248
+ #update() {
1249
+ const currentTrack = this.#textTracks.selected;
1250
+ if (this.#video && (this.#nativeDisplay || currentTrack?.[TextTrackSymbol.nativeHLS])) {
1251
+ this.#customRenderer?.changeTrack(null);
1252
+ this.#nativeRenderer?.setDisplay(true);
1253
+ this.#nativeRenderer?.changeTrack(currentTrack);
1254
+ return;
1255
+ }
1256
+ this.#nativeRenderer?.setDisplay(false);
1257
+ this.#nativeRenderer?.changeTrack(null);
1258
+ if (!currentTrack) {
1259
+ this.#customRenderer?.changeTrack(null);
1260
+ return;
1261
+ }
1262
+ const customRenderer = this.#renderers.sort((a, b) => a.priority - b.priority).find((renderer) => renderer.canRender(currentTrack, this.#video));
1263
+ if (this.#customRenderer !== customRenderer) {
1264
+ this.#customRenderer?.detach();
1265
+ customRenderer?.attach(this.#video);
1266
+ this.#customRenderer = customRenderer ?? null;
1267
+ }
1268
+ customRenderer?.changeTrack(currentTrack);
1269
+ }
1270
+ #detach() {
1271
+ this.#nativeRenderer?.detach();
1272
+ this.#nativeRenderer = null;
1273
+ this.#customRenderer?.detach();
1274
+ this.#customRenderer = null;
1275
+ }
1276
+ }
1277
+
1278
+ class TextTrackList extends List {
1279
+ #canLoad = false;
1280
+ #defaults = {};
1281
+ #storage = null;
1282
+ #preferredLang = null;
1283
+ /** @internal */
1284
+ [TextTrackSymbol.crossOrigin];
1285
+ constructor() {
1286
+ super();
1287
+ }
1288
+ get selected() {
1289
+ const track = this.items.find((t) => t.mode === "showing" && isTrackCaptionKind(t));
1290
+ return track ?? null;
1291
+ }
1292
+ get selectedIndex() {
1293
+ const selected = this.selected;
1294
+ return selected ? this.indexOf(selected) : -1;
1295
+ }
1296
+ get preferredLang() {
1297
+ return this.#preferredLang;
1298
+ }
1299
+ set preferredLang(lang) {
1300
+ this.#preferredLang = lang;
1301
+ this.#saveLang(lang);
1302
+ }
1303
+ add(init, trigger) {
1304
+ const isTrack = init instanceof TextTrack, track = isTrack ? init : new TextTrack(init), kind = init.kind === "captions" || init.kind === "subtitles" ? "captions" : init.kind;
1305
+ if (this.#defaults[kind] && init.default) delete init.default;
1306
+ track.addEventListener("mode-change", this.#onTrackModeChangeBind);
1307
+ this[ListSymbol.add](track, trigger);
1308
+ track[TextTrackSymbol.crossOrigin] = this[TextTrackSymbol.crossOrigin];
1309
+ if (this.#canLoad) track[TextTrackSymbol.canLoad]();
1310
+ if (init.default) this.#defaults[kind] = track;
1311
+ this.#selectTracks();
1312
+ return this;
1313
+ }
1314
+ remove(track, trigger) {
1315
+ this.#pendingRemoval = track;
1316
+ if (!this.items.includes(track)) return;
1317
+ if (track === this.#defaults[track.kind]) delete this.#defaults[track.kind];
1318
+ track.mode = "disabled";
1319
+ track[TextTrackSymbol.onModeChange] = null;
1320
+ track.removeEventListener("mode-change", this.#onTrackModeChangeBind);
1321
+ this[ListSymbol.remove](track, trigger);
1322
+ this.#pendingRemoval = null;
1323
+ return this;
1324
+ }
1325
+ clear(trigger) {
1326
+ for (const track of [...this.items]) {
1327
+ this.remove(track, trigger);
1328
+ }
1329
+ return this;
1330
+ }
1331
+ getByKind(kind) {
1332
+ const kinds = Array.isArray(kind) ? kind : [kind];
1333
+ return this.items.filter((track) => kinds.includes(track.kind));
1334
+ }
1335
+ /** @internal */
1336
+ [TextTrackSymbol.canLoad]() {
1337
+ if (this.#canLoad) return;
1338
+ for (const track of this.items) track[TextTrackSymbol.canLoad]();
1339
+ this.#canLoad = true;
1340
+ this.#selectTracks();
1341
+ }
1342
+ #selectTracks = functionDebounce(async () => {
1343
+ if (!this.#canLoad) return;
1344
+ if (!this.#preferredLang && this.#storage) {
1345
+ this.#preferredLang = await this.#storage.getLang();
1346
+ }
1347
+ const showCaptions = await this.#storage?.getCaptions(), kinds = [
1348
+ ["captions", "subtitles"],
1349
+ "chapters",
1350
+ "descriptions",
1351
+ "metadata"
1352
+ ];
1353
+ for (const kind of kinds) {
1354
+ const tracks = this.getByKind(kind);
1355
+ if (tracks.find((t) => t.mode === "showing")) continue;
1356
+ const preferredTrack = this.#preferredLang ? tracks.find((track2) => track2.language === this.#preferredLang) : null;
1357
+ const defaultTrack = isArray(kind) ? this.#defaults[kind.find((kind2) => this.#defaults[kind2]) || ""] : this.#defaults[kind];
1358
+ const track = preferredTrack ?? defaultTrack, isCaptionsKind = track && isTrackCaptionKind(track);
1359
+ if (track && (!isCaptionsKind || showCaptions !== false)) {
1360
+ track.mode = "showing";
1361
+ if (isCaptionsKind) this.#saveCaptionsTrack(track);
1362
+ }
1363
+ }
1364
+ }, 300);
1365
+ #pendingRemoval = null;
1366
+ #onTrackModeChangeBind = this.#onTrackModeChange.bind(this);
1367
+ #onTrackModeChange(event) {
1368
+ const track = event.detail;
1369
+ if (this.#storage && isTrackCaptionKind(track) && track !== this.#pendingRemoval) {
1370
+ this.#saveCaptionsTrack(track);
1371
+ }
1372
+ if (track.mode === "showing") {
1373
+ const kinds = isTrackCaptionKind(track) ? ["captions", "subtitles"] : [track.kind];
1374
+ for (const t of this.items) {
1375
+ if (t.mode === "showing" && t != track && kinds.includes(t.kind)) {
1376
+ t.mode = "disabled";
1377
+ }
1378
+ }
1379
+ }
1380
+ this.dispatchEvent(
1381
+ new DOMEvent("mode-change", {
1382
+ detail: event.detail,
1383
+ trigger: event
1384
+ })
1385
+ );
1386
+ }
1387
+ #saveCaptionsTrack(track) {
1388
+ if (track.mode !== "disabled") {
1389
+ this.#saveLang(track.language);
1390
+ }
1391
+ this.#storage?.setCaptions?.(track.mode === "showing");
1392
+ }
1393
+ #saveLang(lang) {
1394
+ this.#storage?.setLang?.(this.#preferredLang = lang);
1395
+ }
1396
+ setStorage(storage) {
1397
+ this.#storage = storage;
1398
+ }
1399
+ }
1400
+
1401
+ class VideoQualityList extends SelectList {
1402
+ #auto = false;
1403
+ /**
1404
+ * Configures quality switching:
1405
+ *
1406
+ * - `current`: Trigger an immediate quality level switch. This will abort the current fragment
1407
+ * request if any, flush the whole buffer, and fetch fragment matching with current position
1408
+ * and requested quality level.
1409
+ *
1410
+ * - `next`: Trigger a quality level switch for next fragment. This could eventually flush
1411
+ * already buffered next fragment.
1412
+ *
1413
+ * - `load`: Set quality level for next loaded fragment.
1414
+ *
1415
+ * @see {@link https://www.vidstack.io/docs/player/api/video-quality#switch}
1416
+ * @see {@link https://github.com/video-dev/hls.js/blob/master/docs/API.md#quality-switch-control-api}
1417
+ */
1418
+ switch = "current";
1419
+ /**
1420
+ * Whether automatic quality selection is enabled.
1421
+ */
1422
+ get auto() {
1423
+ return this.#auto || this.readonly;
1424
+ }
1425
+ /** @internal */
1426
+ [QualitySymbol.enableAuto];
1427
+ /** @internal */
1428
+ [ListSymbol.onUserSelect]() {
1429
+ this[QualitySymbol.setAuto](false);
1430
+ }
1431
+ /** @internal */
1432
+ [ListSymbol.onReset](trigger) {
1433
+ this[QualitySymbol.enableAuto] = void 0;
1434
+ this[QualitySymbol.setAuto](false, trigger);
1435
+ }
1436
+ /**
1437
+ * Request automatic quality selection (if supported). This will be a no-op if the list is
1438
+ * `readonly` as that already implies auto-selection.
1439
+ */
1440
+ autoSelect(trigger) {
1441
+ if (this.readonly || this.#auto || !this[QualitySymbol.enableAuto]) return;
1442
+ this[QualitySymbol.enableAuto]?.(trigger);
1443
+ this[QualitySymbol.setAuto](true, trigger);
1444
+ }
1445
+ getBySrc(src) {
1446
+ return this.items.find((quality) => quality.src === src);
1447
+ }
1448
+ /** @internal */
1449
+ [QualitySymbol.setAuto](auto, trigger) {
1450
+ if (this.#auto === auto) return;
1451
+ this.#auto = auto;
1452
+ this.dispatchEvent(
1453
+ new DOMEvent("auto-change", {
1454
+ detail: auto,
1455
+ trigger
1456
+ })
1457
+ );
1458
+ }
1459
+ }
1460
+
1461
+ function isAudioProvider(provider) {
1462
+ return provider?.$$PROVIDER_TYPE === "AUDIO";
1463
+ }
1464
+ function isVideoProvider(provider) {
1465
+ return provider?.$$PROVIDER_TYPE === "VIDEO";
1466
+ }
1467
+ function isHLSProvider(provider) {
1468
+ return provider?.$$PROVIDER_TYPE === "HLS";
1469
+ }
1470
+ function isDASHProvider(provider) {
1471
+ return provider?.$$PROVIDER_TYPE === "DASH";
1472
+ }
1473
+ function isYouTubeProvider(provider) {
1474
+ return provider?.$$PROVIDER_TYPE === "YOUTUBE";
1475
+ }
1476
+ function isVimeoProvider(provider) {
1477
+ return provider?.$$PROVIDER_TYPE === "VIMEO";
1478
+ }
1479
+ function isGoogleCastProvider(provider) {
1480
+ return provider?.$$PROVIDER_TYPE === "GOOGLE_CAST";
1481
+ }
1482
+ function isHTMLAudioElement(element) {
1483
+ return element instanceof HTMLAudioElement;
1484
+ }
1485
+ function isHTMLVideoElement(element) {
1486
+ return element instanceof HTMLVideoElement;
1487
+ }
1488
+ function isHTMLMediaElement(element) {
1489
+ return isHTMLAudioElement(element) || isHTMLVideoElement(element);
1490
+ }
1491
+ function isHTMLIFrameElement(element) {
1492
+ return element instanceof HTMLIFrameElement;
1493
+ }
1494
+
1495
+ class MediaPlayerController extends ViewController {
1496
+ }
1497
+
1498
+ const MEDIA_KEY_SHORTCUTS = {
1499
+ togglePaused: "k Space",
1500
+ toggleMuted: "m",
1501
+ toggleFullscreen: "f",
1502
+ togglePictureInPicture: "i",
1503
+ toggleCaptions: "c",
1504
+ seekBackward: "j J ArrowLeft",
1505
+ seekForward: "l L ArrowRight",
1506
+ volumeUp: "ArrowUp",
1507
+ volumeDown: "ArrowDown",
1508
+ speedUp: ">",
1509
+ slowDown: "<"
1510
+ };
1511
+ const MODIFIER_KEYS = /* @__PURE__ */ new Set(["Shift", "Alt", "Meta", "Ctrl"]), BUTTON_SELECTORS = 'button, [role="button"]', IGNORE_SELECTORS = 'input, textarea, select, [contenteditable], [role^="menuitem"], [role="timer"]';
1512
+ class MediaKeyboardController extends MediaPlayerController {
1513
+ #media;
1514
+ constructor(media) {
1515
+ super();
1516
+ this.#media = media;
1517
+ }
1518
+ onConnect() {
1519
+ effect(this.#onTargetChange.bind(this));
1520
+ }
1521
+ #onTargetChange() {
1522
+ const { keyDisabled, keyTarget } = this.$props;
1523
+ if (keyDisabled()) return;
1524
+ const target = keyTarget() === "player" ? this.el : document, $active = signal(false);
1525
+ if (target === this.el) {
1526
+ new EventsController(this.el).add("focusin", () => $active.set(true)).add("focusout", (event) => {
1527
+ if (!this.el.contains(event.target)) $active.set(false);
1528
+ });
1529
+ } else {
1530
+ if (!peek($active)) $active.set(document.querySelector("[data-media-player]") === this.el);
1531
+ listenEvent(document, "focusin", (event) => {
1532
+ const activePlayer = event.composedPath().find((el) => el instanceof Element && el.localName === "media-player");
1533
+ if (activePlayer !== void 0) $active.set(this.el === activePlayer);
1534
+ });
1535
+ }
1536
+ effect(() => {
1537
+ if (!$active()) return;
1538
+ new EventsController(target).add("keyup", this.#onKeyUp.bind(this)).add("keydown", this.#onKeyDown.bind(this)).add("keydown", this.#onPreventVideoKeys.bind(this), { capture: true });
1539
+ });
1540
+ }
1541
+ #onKeyUp(event) {
1542
+ const focusedEl = document.activeElement;
1543
+ if (!event.key || !this.$state.canSeek() || focusedEl?.matches(IGNORE_SELECTORS)) {
1544
+ return;
1545
+ }
1546
+ let { method, value } = this.#getMatchingMethod(event);
1547
+ if (!isString(value) && !isArray(value)) {
1548
+ value?.onKeyUp?.({
1549
+ event,
1550
+ player: this.#media.player,
1551
+ remote: this.#media.remote
1552
+ });
1553
+ value?.callback?.(event, this.#media.remote);
1554
+ return;
1555
+ }
1556
+ if (method?.startsWith("seek")) {
1557
+ event.preventDefault();
1558
+ event.stopPropagation();
1559
+ if (this.#timeSlider) {
1560
+ this.#forwardTimeKeyboardEvent(event, method === "seekForward");
1561
+ this.#timeSlider = null;
1562
+ } else {
1563
+ this.#media.remote.seek(this.#seekTotal, event);
1564
+ this.#seekTotal = void 0;
1565
+ }
1566
+ }
1567
+ if (method?.startsWith("volume")) {
1568
+ const volumeSlider = this.el.querySelector("[data-media-volume-slider]");
1569
+ volumeSlider?.dispatchEvent(
1570
+ new KeyboardEvent("keyup", {
1571
+ key: method === "volumeUp" ? "Up" : "Down",
1572
+ shiftKey: event.shiftKey,
1573
+ trigger: event
1574
+ })
1575
+ );
1576
+ }
1577
+ }
1578
+ #onKeyDown(event) {
1579
+ if (!event.key || MODIFIER_KEYS.has(event.key)) return;
1580
+ const focusedEl = document.activeElement;
1581
+ if (focusedEl?.matches(IGNORE_SELECTORS) || isKeyboardClick(event) && focusedEl?.matches(BUTTON_SELECTORS)) {
1582
+ return;
1583
+ }
1584
+ let { method, value } = this.#getMatchingMethod(event), isNumberPress = !event.metaKey && /^[0-9]$/.test(event.key);
1585
+ if (!isString(value) && !isArray(value) && !isNumberPress) {
1586
+ value?.onKeyDown?.({
1587
+ event,
1588
+ player: this.#media.player,
1589
+ remote: this.#media.remote
1590
+ });
1591
+ value?.callback?.(event, this.#media.remote);
1592
+ return;
1593
+ }
1594
+ if (!method && isNumberPress && !modifierKeyPressed(event)) {
1595
+ event.preventDefault();
1596
+ event.stopPropagation();
1597
+ this.#media.remote.seek(this.$state.duration() / 10 * Number(event.key), event);
1598
+ return;
1599
+ }
1600
+ if (!method) return;
1601
+ event.preventDefault();
1602
+ event.stopPropagation();
1603
+ switch (method) {
1604
+ case "seekForward":
1605
+ case "seekBackward":
1606
+ this.#seeking(event, method, method === "seekForward");
1607
+ break;
1608
+ case "volumeUp":
1609
+ case "volumeDown":
1610
+ const volumeSlider = this.el.querySelector("[data-media-volume-slider]");
1611
+ if (volumeSlider) {
1612
+ volumeSlider.dispatchEvent(
1613
+ new KeyboardEvent("keydown", {
1614
+ key: method === "volumeUp" ? "Up" : "Down",
1615
+ shiftKey: event.shiftKey,
1616
+ trigger: event
1617
+ })
1618
+ );
1619
+ } else {
1620
+ const value2 = event.shiftKey ? 0.1 : 0.05;
1621
+ this.#media.remote.changeVolume(
1622
+ this.$state.volume() + (method === "volumeUp" ? +value2 : -value2),
1623
+ event
1624
+ );
1625
+ }
1626
+ break;
1627
+ case "toggleFullscreen":
1628
+ this.#media.remote.toggleFullscreen("prefer-media", event);
1629
+ break;
1630
+ case "speedUp":
1631
+ case "slowDown":
1632
+ const playbackRate = this.$state.playbackRate();
1633
+ this.#media.remote.changePlaybackRate(
1634
+ Math.max(0.25, Math.min(2, playbackRate + (method === "speedUp" ? 0.25 : -0.25))),
1635
+ event
1636
+ );
1637
+ break;
1638
+ default:
1639
+ this.#media.remote[method]?.(event);
1640
+ }
1641
+ this.$state.lastKeyboardAction.set({
1642
+ action: method,
1643
+ event
1644
+ });
1645
+ }
1646
+ #onPreventVideoKeys(event) {
1647
+ if (isHTMLMediaElement(event.target) && this.#getMatchingMethod(event).method) {
1648
+ event.preventDefault();
1649
+ }
1650
+ }
1651
+ #getMatchingMethod(event) {
1652
+ const keyShortcuts = {
1653
+ ...this.$props.keyShortcuts(),
1654
+ ...this.#media.ariaKeys
1655
+ };
1656
+ const method = Object.keys(keyShortcuts).find((method2) => {
1657
+ const value = keyShortcuts[method2], keys = isArray(value) ? value.join(" ") : isString(value) ? value : value?.keys;
1658
+ const combinations = (isArray(keys) ? keys : keys?.split(" "))?.map(
1659
+ (key) => replaceSymbolKeys(key).replace(/Control/g, "Ctrl").split("+")
1660
+ );
1661
+ return combinations?.some((combo) => {
1662
+ const modifierKeys = new Set(combo.filter((key) => MODIFIER_KEYS.has(key)));
1663
+ if ("<>".includes(event.key)) {
1664
+ modifierKeys.add("Shift");
1665
+ }
1666
+ for (const modKey of MODIFIER_KEYS) {
1667
+ const modKeyProp = modKey.toLowerCase() + "Key";
1668
+ if (!modifierKeys.has(modKey) && event[modKeyProp]) {
1669
+ return false;
1670
+ }
1671
+ }
1672
+ return combo.every((key) => {
1673
+ return MODIFIER_KEYS.has(key) ? event[key.toLowerCase() + "Key"] : event.key === key.replace("Space", " ");
1674
+ });
1675
+ });
1676
+ });
1677
+ return {
1678
+ method,
1679
+ value: method ? keyShortcuts[method] : null
1680
+ };
1681
+ }
1682
+ #seekTotal;
1683
+ #calcSeekAmount(event, type) {
1684
+ const seekBy = event.shiftKey ? 10 : 5;
1685
+ return this.#seekTotal = Math.max(
1686
+ 0,
1687
+ Math.min(
1688
+ (this.#seekTotal ?? this.$state.currentTime()) + (type === "seekForward" ? +seekBy : -seekBy),
1689
+ this.$state.duration()
1690
+ )
1691
+ );
1692
+ }
1693
+ #timeSlider = null;
1694
+ #forwardTimeKeyboardEvent(event, forward) {
1695
+ this.#timeSlider?.dispatchEvent(
1696
+ new KeyboardEvent(event.type, {
1697
+ key: !forward ? "Left" : "Right",
1698
+ shiftKey: event.shiftKey,
1699
+ trigger: event
1700
+ })
1701
+ );
1702
+ }
1703
+ #seeking(event, type, forward) {
1704
+ if (!this.$state.canSeek()) return;
1705
+ if (!this.#timeSlider) {
1706
+ this.#timeSlider = this.el.querySelector("[data-media-time-slider]");
1707
+ }
1708
+ if (this.#timeSlider) {
1709
+ this.#forwardTimeKeyboardEvent(event, forward);
1710
+ } else {
1711
+ this.#media.remote.seeking(this.#calcSeekAmount(event, type), event);
1712
+ }
1713
+ }
1714
+ }
1715
+ const SYMBOL_KEY_MAP = ["!", "@", "#", "$", "%", "^", "&", "*", "(", ")"];
1716
+ function replaceSymbolKeys(key) {
1717
+ return key.replace(/Shift\+(\d)/g, (_, num) => SYMBOL_KEY_MAP[num - 1]);
1718
+ }
1719
+ function modifierKeyPressed(event) {
1720
+ for (const key of MODIFIER_KEYS) {
1721
+ if (event[key.toLowerCase() + "Key"]) {
1722
+ return true;
1723
+ }
1724
+ }
1725
+ return false;
1726
+ }
1727
+
1728
+ class MediaControls extends MediaPlayerController {
1729
+ #idleTimer = -2;
1730
+ #pausedTracking = false;
1731
+ #hideOnMouseLeave = signal(false);
1732
+ #isMouseOutside = signal(false);
1733
+ #focusedItem = null;
1734
+ #canIdle = signal(true);
1735
+ /**
1736
+ * The default amount of delay in milliseconds while media playback is progressing without user
1737
+ * activity to indicate an idle state (i.e., hide controls).
1738
+ *
1739
+ * @defaultValue 2000
1740
+ */
1741
+ defaultDelay = 2e3;
1742
+ /**
1743
+ * Whether controls can hide after a delay in user interaction. If this is false, controls will
1744
+ * not hide and be user controlled.
1745
+ */
1746
+ get canIdle() {
1747
+ return this.#canIdle();
1748
+ }
1749
+ set canIdle(canIdle) {
1750
+ this.#canIdle.set(canIdle);
1751
+ }
1752
+ /**
1753
+ * Whether controls visibility should be toggled when the mouse enters and leaves the player
1754
+ * container.
1755
+ *
1756
+ * @defaultValue false
1757
+ */
1758
+ get hideOnMouseLeave() {
1759
+ const { hideControlsOnMouseLeave } = this.$props;
1760
+ return this.#hideOnMouseLeave() || hideControlsOnMouseLeave();
1761
+ }
1762
+ set hideOnMouseLeave(hide) {
1763
+ this.#hideOnMouseLeave.set(hide);
1764
+ }
1765
+ /**
1766
+ * Whether media controls are currently visible.
1767
+ */
1768
+ get showing() {
1769
+ return this.$state.controlsVisible();
1770
+ }
1771
+ /**
1772
+ * Show controls.
1773
+ */
1774
+ show(delay = 0, trigger) {
1775
+ this.#clearIdleTimer();
1776
+ if (!this.#pausedTracking) {
1777
+ this.#changeVisibility(true, delay, trigger);
1778
+ }
1779
+ }
1780
+ /**
1781
+ * Hide controls.
1782
+ */
1783
+ hide(delay = this.defaultDelay, trigger) {
1784
+ this.#clearIdleTimer();
1785
+ if (!this.#pausedTracking) {
1786
+ this.#changeVisibility(false, delay, trigger);
1787
+ }
1788
+ }
1789
+ /**
1790
+ * Whether all idle tracking on controls should be paused until resumed again.
1791
+ */
1792
+ pause(trigger) {
1793
+ this.#pausedTracking = true;
1794
+ this.#clearIdleTimer();
1795
+ this.#changeVisibility(true, 0, trigger);
1796
+ }
1797
+ resume(trigger) {
1798
+ this.#pausedTracking = false;
1799
+ if (this.$state.paused()) return;
1800
+ this.#changeVisibility(false, this.defaultDelay, trigger);
1801
+ }
1802
+ onConnect() {
1803
+ effect(this.#init.bind(this));
1804
+ }
1805
+ #init() {
1806
+ const { viewType } = this.$state;
1807
+ if (!this.el || !this.#canIdle()) return;
1808
+ if (viewType() === "audio") {
1809
+ this.show();
1810
+ return;
1811
+ }
1812
+ effect(this.#watchMouse.bind(this));
1813
+ effect(this.#watchPaused.bind(this));
1814
+ const onPlay = this.#onPlay.bind(this), onPause = this.#onPause.bind(this), onEnd = this.#onEnd.bind(this);
1815
+ new EventsController(this.el).add("can-play", (event) => this.show(0, event)).add("play", onPlay).add("pause", onPause).add("end", onEnd).add("auto-play-fail", onPause);
1816
+ }
1817
+ #watchMouse() {
1818
+ if (!this.el) return;
1819
+ const { started, pointer, paused } = this.$state;
1820
+ if (!started() || pointer() !== "fine") return;
1821
+ const events = new EventsController(this.el), shouldHideOnMouseLeave = this.hideOnMouseLeave;
1822
+ if (!shouldHideOnMouseLeave || !this.#isMouseOutside()) {
1823
+ effect(() => {
1824
+ if (!paused()) events.add("pointermove", this.#onStopIdle.bind(this));
1825
+ });
1826
+ }
1827
+ if (shouldHideOnMouseLeave) {
1828
+ events.add("mouseenter", this.#onMouseEnter.bind(this)).add("mouseleave", this.#onMouseLeave.bind(this));
1829
+ }
1830
+ }
1831
+ #watchPaused() {
1832
+ const { paused, started, autoPlayError } = this.$state;
1833
+ if (paused() || autoPlayError() && !started()) return;
1834
+ const onStopIdle = this.#onStopIdle.bind(this);
1835
+ effect(() => {
1836
+ if (!this.el) return;
1837
+ const pointer = this.$state.pointer(), isTouch = pointer === "coarse", events = new EventsController(this.el), eventTypes = [isTouch ? "touchend" : "pointerup", "keydown"];
1838
+ for (const eventType of eventTypes) {
1839
+ events.add(eventType, onStopIdle, { passive: false });
1840
+ }
1841
+ });
1842
+ }
1843
+ #onPlay(event) {
1844
+ if (event.triggers.hasType("ended")) return;
1845
+ this.show(0, event);
1846
+ this.hide(void 0, event);
1847
+ }
1848
+ #onPause(event) {
1849
+ this.show(0, event);
1850
+ }
1851
+ #onEnd(event) {
1852
+ const { loop } = this.$state;
1853
+ if (loop()) this.hide(0, event);
1854
+ }
1855
+ #onMouseEnter(event) {
1856
+ this.#isMouseOutside.set(false);
1857
+ this.show(0, event);
1858
+ this.hide(void 0, event);
1859
+ }
1860
+ #onMouseLeave(event) {
1861
+ this.#isMouseOutside.set(true);
1862
+ this.hide(0, event);
1863
+ }
1864
+ #clearIdleTimer() {
1865
+ window.clearTimeout(this.#idleTimer);
1866
+ this.#idleTimer = -1;
1867
+ }
1868
+ #onStopIdle(event) {
1869
+ if (
1870
+ // @ts-expect-error
1871
+ event.MEDIA_GESTURE || this.#pausedTracking || isTouchPinchEvent(event)
1872
+ ) {
1873
+ return;
1874
+ }
1875
+ if (isKeyboardEvent(event)) {
1876
+ if (event.key === "Escape") {
1877
+ this.el?.focus();
1878
+ this.#focusedItem = null;
1879
+ } else if (this.#focusedItem) {
1880
+ event.preventDefault();
1881
+ requestAnimationFrame(() => {
1882
+ this.#focusedItem?.focus();
1883
+ this.#focusedItem = null;
1884
+ });
1885
+ }
1886
+ }
1887
+ this.show(0, event);
1888
+ this.hide(this.defaultDelay, event);
1889
+ }
1890
+ #changeVisibility(visible, delay, trigger) {
1891
+ if (delay === 0) {
1892
+ this.#onChange(visible, trigger);
1893
+ return;
1894
+ }
1895
+ this.#idleTimer = window.setTimeout(() => {
1896
+ if (!this.scope) return;
1897
+ this.#onChange(visible && !this.#pausedTracking, trigger);
1898
+ }, delay);
1899
+ }
1900
+ #onChange(visible, trigger) {
1901
+ if (this.$state.controlsVisible() === visible) return;
1902
+ this.$state.controlsVisible.set(visible);
1903
+ if (!visible && document.activeElement && this.el?.contains(document.activeElement)) {
1904
+ this.#focusedItem = document.activeElement;
1905
+ requestAnimationFrame(() => {
1906
+ this.el?.focus({ preventScroll: true });
1907
+ });
1908
+ }
1909
+ this.dispatch("controls-change", {
1910
+ detail: visible,
1911
+ trigger
1912
+ });
1913
+ }
1914
+ }
1915
+
1916
+ class AudioProviderLoader {
1917
+ name = "audio";
1918
+ target;
1919
+ canPlay(src) {
1920
+ if (!isAudioSrc(src)) return false;
1921
+ return !isString(src.src) || src.type === "?" || canPlayAudioType(this.target, src.type);
1922
+ }
1923
+ mediaType() {
1924
+ return "audio";
1925
+ }
1926
+ async load(ctx) {
1927
+ return new (await import('../providers/vidstack-audio.js')).AudioProvider(this.target, ctx);
1928
+ }
1929
+ }
1930
+
1931
+ class VideoProviderLoader {
1932
+ name = "video";
1933
+ target;
1934
+ canPlay(src) {
1935
+ if (!isVideoSrc(src)) return false;
1936
+ return !isString(src.src) || src.type === "?" || canPlayVideoType(this.target, src.type);
1937
+ }
1938
+ mediaType() {
1939
+ return "video";
1940
+ }
1941
+ async load(ctx) {
1942
+ return new (await import('../providers/vidstack-video.js')).VideoProvider(this.target, ctx);
1943
+ }
1944
+ }
1945
+
1946
+ class HLSProviderLoader extends VideoProviderLoader {
1947
+ static supported = isHLSSupported();
1948
+ name = "hls";
1949
+ canPlay(src) {
1950
+ return HLSProviderLoader.supported && isHLSSrc(src);
1951
+ }
1952
+ async load(context) {
1953
+ return new (await import('../providers/vidstack-hls.js')).HLSProvider(this.target, context);
1954
+ }
1955
+ }
1956
+
1957
+ class DASHProviderLoader extends VideoProviderLoader {
1958
+ static supported = isDASHSupported();
1959
+ name = "dash";
1960
+ canPlay(src) {
1961
+ return DASHProviderLoader.supported && isDASHSrc(src);
1962
+ }
1963
+ async load(context) {
1964
+ return new (await import('../providers/vidstack-dash.js')).DASHProvider(this.target, context);
1965
+ }
1966
+ }
1967
+
1968
+ class VimeoProviderLoader {
1969
+ name = "vimeo";
1970
+ target;
1971
+ preconnect() {
1972
+ const connections = [
1973
+ "https://i.vimeocdn.com",
1974
+ "https://f.vimeocdn.com",
1975
+ "https://fresnel.vimeocdn.com"
1976
+ ];
1977
+ for (const url of connections) {
1978
+ preconnect(url);
1979
+ }
1980
+ }
1981
+ canPlay(src) {
1982
+ return isString(src.src) && src.type === "video/vimeo";
1983
+ }
1984
+ mediaType() {
1985
+ return "video";
1986
+ }
1987
+ async load(ctx) {
1988
+ return new (await import('../providers/vidstack-vimeo.js')).VimeoProvider(this.target, ctx);
1989
+ }
1990
+ async loadPoster(src, ctx, abort) {
1991
+ const { resolveVimeoVideoId, getVimeoVideoInfo } = await import('./vidstack-krOAtKMi.js');
1992
+ if (!isString(src.src)) return null;
1993
+ const { videoId, hash } = resolveVimeoVideoId(src.src);
1994
+ if (videoId) {
1995
+ return getVimeoVideoInfo(videoId, abort, hash).then((info) => info ? info.poster : null);
1996
+ }
1997
+ return null;
1998
+ }
1999
+ }
2000
+
2001
+ class YouTubeProviderLoader {
2002
+ name = "youtube";
2003
+ target;
2004
+ preconnect() {
2005
+ const connections = [
2006
+ // Botguard script.
2007
+ "https://www.google.com",
2008
+ // Posters.
2009
+ "https://i.ytimg.com",
2010
+ // Ads.
2011
+ "https://googleads.g.doubleclick.net",
2012
+ "https://static.doubleclick.net"
2013
+ ];
2014
+ for (const url of connections) {
2015
+ preconnect(url);
2016
+ }
2017
+ }
2018
+ canPlay(src) {
2019
+ return isString(src.src) && src.type === "video/youtube";
2020
+ }
2021
+ mediaType() {
2022
+ return "video";
2023
+ }
2024
+ async load(ctx) {
2025
+ return new (await import('../providers/vidstack-youtube.js')).YouTubeProvider(this.target, ctx);
2026
+ }
2027
+ async loadPoster(src, ctx, abort) {
2028
+ const { findYouTubePoster, resolveYouTubeVideoId } = await import('./vidstack-Dm1xEU9Q.js');
2029
+ const videoId = isString(src.src) && resolveYouTubeVideoId(src.src);
2030
+ if (videoId) return findYouTubePoster(videoId, abort);
2031
+ return null;
2032
+ }
2033
+ }
2034
+
2035
+ const MEDIA_ATTRIBUTES = Symbol(0);
2036
+ const mediaAttributes = [
2037
+ "autoPlay",
2038
+ "canAirPlay",
2039
+ "canFullscreen",
2040
+ "canGoogleCast",
2041
+ "canLoad",
2042
+ "canLoadPoster",
2043
+ "canPictureInPicture",
2044
+ "canPlay",
2045
+ "canSeek",
2046
+ "ended",
2047
+ "fullscreen",
2048
+ "isAirPlayConnected",
2049
+ "isGoogleCastConnected",
2050
+ "live",
2051
+ "liveEdge",
2052
+ "loop",
2053
+ "mediaType",
2054
+ "muted",
2055
+ "paused",
2056
+ "pictureInPicture",
2057
+ "playing",
2058
+ "playsInline",
2059
+ "remotePlaybackState",
2060
+ "remotePlaybackType",
2061
+ "seeking",
2062
+ "started",
2063
+ "streamType",
2064
+ "viewType",
2065
+ "waiting"
2066
+ ];
2067
+
2068
+ const mediaPlayerProps = {
2069
+ artist: "",
2070
+ artwork: null,
2071
+ autoplay: false,
2072
+ autoPlay: false,
2073
+ clipStartTime: 0,
2074
+ clipEndTime: 0,
2075
+ controls: false,
2076
+ currentTime: 0,
2077
+ crossorigin: null,
2078
+ crossOrigin: null,
2079
+ duration: -1,
2080
+ fullscreenOrientation: "landscape",
2081
+ googleCast: {},
2082
+ load: "visible",
2083
+ posterLoad: "visible",
2084
+ logLevel: "silent",
2085
+ loop: false,
2086
+ muted: false,
2087
+ paused: true,
2088
+ playsinline: false,
2089
+ playsInline: false,
2090
+ playbackRate: 1,
2091
+ poster: "",
2092
+ preload: "metadata",
2093
+ preferNativeHLS: false,
2094
+ src: "",
2095
+ title: "",
2096
+ controlsDelay: 2e3,
2097
+ hideControlsOnMouseLeave: false,
2098
+ viewType: "unknown",
2099
+ streamType: "unknown",
2100
+ volume: 1,
2101
+ liveEdgeTolerance: 10,
2102
+ minLiveDVRWindow: 60,
2103
+ keyDisabled: false,
2104
+ keyTarget: "player",
2105
+ keyShortcuts: MEDIA_KEY_SHORTCUTS,
2106
+ storage: null
2107
+ };
2108
+
2109
+ class MediaLoadController extends MediaPlayerController {
2110
+ #type;
2111
+ #callback;
2112
+ constructor(type, callback) {
2113
+ super();
2114
+ this.#type = type;
2115
+ this.#callback = callback;
2116
+ }
2117
+ async onAttach(el) {
2118
+ const load = this.$props[this.#type]();
2119
+ if (load === "eager") {
2120
+ requestAnimationFrame(this.#callback);
2121
+ } else if (load === "idle") {
2122
+ waitIdlePeriod(this.#callback);
2123
+ } else if (load === "visible") {
2124
+ let dispose, observer = new IntersectionObserver((entries) => {
2125
+ if (!this.scope) return;
2126
+ if (entries[0].isIntersecting) {
2127
+ dispose?.();
2128
+ dispose = void 0;
2129
+ this.#callback();
2130
+ }
2131
+ });
2132
+ observer.observe(el);
2133
+ dispose = onDispose(() => observer.disconnect());
2134
+ }
2135
+ }
2136
+ }
2137
+
2138
+ class MediaPlayerDelegate {
2139
+ #handle;
2140
+ #media;
2141
+ constructor(handle, media) {
2142
+ this.#handle = handle;
2143
+ this.#media = media;
2144
+ }
2145
+ notify(type, ...init) {
2146
+ this.#handle(
2147
+ new DOMEvent(type, {
2148
+ detail: init?.[0],
2149
+ trigger: init?.[1]
2150
+ })
2151
+ );
2152
+ }
2153
+ async ready(info, trigger) {
2154
+ return untrack(async () => {
2155
+ const { logger } = this.#media, {
2156
+ autoPlay,
2157
+ canPlay,
2158
+ started,
2159
+ duration,
2160
+ seekable,
2161
+ buffered,
2162
+ remotePlaybackInfo,
2163
+ playsInline,
2164
+ savedState,
2165
+ source
2166
+ } = this.#media.$state;
2167
+ if (canPlay()) return;
2168
+ const detail = {
2169
+ duration: info?.duration ?? duration(),
2170
+ seekable: info?.seekable ?? seekable(),
2171
+ buffered: info?.buffered ?? buffered(),
2172
+ provider: this.#media.$provider()
2173
+ };
2174
+ this.notify("can-play", detail, trigger);
2175
+ tick();
2176
+ let provider = this.#media.$provider(), { storage, qualities } = this.#media, { muted, volume, clipStartTime, playbackRate } = this.#media.$props;
2177
+ await storage?.onLoad?.(source());
2178
+ const savedPlaybackTime = savedState()?.currentTime, savedPausedState = savedState()?.paused, storageTime = await storage?.getTime(), startTime = savedPlaybackTime ?? storageTime ?? clipStartTime(), shouldAutoPlay = savedPausedState === false || savedPausedState !== true && !started() && autoPlay();
2179
+ if (provider) {
2180
+ provider.setVolume(await storage?.getVolume() ?? volume());
2181
+ provider.setMuted(muted() || !!await storage?.getMuted());
2182
+ const audioGain = await storage?.getAudioGain() ?? 1;
2183
+ if (audioGain > 1) provider.audioGain?.setGain?.(audioGain);
2184
+ provider.setPlaybackRate?.(await storage?.getPlaybackRate() ?? playbackRate());
2185
+ provider.setPlaysInline?.(playsInline());
2186
+ if (startTime > 0) provider.setCurrentTime(startTime);
2187
+ }
2188
+ const prefQuality = await storage?.getVideoQuality();
2189
+ if (prefQuality && qualities.length) {
2190
+ let currentQuality = null, currentScore = Infinity;
2191
+ for (const quality of qualities) {
2192
+ const score = Math.abs(prefQuality.width - quality.width) + Math.abs(prefQuality.height - quality.height) + (prefQuality.bitrate ? Math.abs(prefQuality.bitrate - (quality.bitrate ?? 0)) : 0);
2193
+ if (score < currentScore) {
2194
+ currentQuality = quality;
2195
+ currentScore = score;
2196
+ }
2197
+ }
2198
+ if (currentQuality) currentQuality.selected = true;
2199
+ }
2200
+ if (canPlay() && shouldAutoPlay) {
2201
+ await this.#attemptAutoplay(trigger);
2202
+ } else if (storageTime && storageTime > 0) {
2203
+ this.notify("started", void 0, trigger);
2204
+ }
2205
+ remotePlaybackInfo.set(null);
2206
+ });
2207
+ }
2208
+ async #attemptAutoplay(trigger) {
2209
+ const {
2210
+ player,
2211
+ $state: { autoPlaying, muted }
2212
+ } = this.#media;
2213
+ autoPlaying.set(true);
2214
+ const attemptEvent = new DOMEvent("auto-play-attempt", { trigger });
2215
+ try {
2216
+ await player.play(attemptEvent);
2217
+ } catch (error) {
2218
+ }
2219
+ }
2220
+ }
2221
+
2222
+ class Queue {
2223
+ #queue = /* @__PURE__ */ new Map();
2224
+ /**
2225
+ * Queue the given `item` under the given `key` to be processed at a later time by calling
2226
+ * `serve(key)`.
2227
+ */
2228
+ enqueue(key, item) {
2229
+ this.#queue.set(key, item);
2230
+ }
2231
+ /**
2232
+ * Process item in queue for the given `key`.
2233
+ */
2234
+ serve(key) {
2235
+ const value = this.peek(key);
2236
+ this.#queue.delete(key);
2237
+ return value;
2238
+ }
2239
+ /**
2240
+ * Peek at item in queue for the given `key`.
2241
+ */
2242
+ peek(key) {
2243
+ return this.#queue.get(key);
2244
+ }
2245
+ /**
2246
+ * Removes queued item under the given `key`.
2247
+ */
2248
+ delete(key) {
2249
+ this.#queue.delete(key);
2250
+ }
2251
+ /**
2252
+ * Clear all items in the queue.
2253
+ */
2254
+ clear() {
2255
+ this.#queue.clear();
2256
+ }
2257
+ }
2258
+
2259
+ class RequestQueue {
2260
+ #serving = false;
2261
+ #pending = deferredPromise();
2262
+ #queue = /* @__PURE__ */ new Map();
2263
+ /**
2264
+ * The number of callbacks that are currently in queue.
2265
+ */
2266
+ get size() {
2267
+ return this.#queue.size;
2268
+ }
2269
+ /**
2270
+ * Whether items in the queue are being served immediately, otherwise they're queued to
2271
+ * be processed later.
2272
+ */
2273
+ get isServing() {
2274
+ return this.#serving;
2275
+ }
2276
+ /**
2277
+ * Waits for the queue to be flushed (ie: start serving).
2278
+ */
2279
+ async waitForFlush() {
2280
+ if (this.#serving) return;
2281
+ await this.#pending.promise;
2282
+ }
2283
+ /**
2284
+ * Queue the given `callback` to be invoked at a later time by either calling the `serve()` or
2285
+ * `start()` methods. If the queue has started serving (i.e., `start()` was already called),
2286
+ * then the callback will be invoked immediately.
2287
+ *
2288
+ * @param key - Uniquely identifies this callback so duplicates are ignored.
2289
+ * @param callback - The function to call when this item in the queue is being served.
2290
+ */
2291
+ enqueue(key, callback) {
2292
+ if (this.#serving) {
2293
+ callback();
2294
+ return;
2295
+ }
2296
+ this.#queue.delete(key);
2297
+ this.#queue.set(key, callback);
2298
+ }
2299
+ /**
2300
+ * Invokes the callback with the given `key` in the queue (if it exists).
2301
+ */
2302
+ serve(key) {
2303
+ this.#queue.get(key)?.();
2304
+ this.#queue.delete(key);
2305
+ }
2306
+ /**
2307
+ * Flush all queued items and start serving future requests immediately until `stop()` is called.
2308
+ */
2309
+ start() {
2310
+ this.#flush();
2311
+ this.#serving = true;
2312
+ if (this.#queue.size > 0) this.#flush();
2313
+ }
2314
+ /**
2315
+ * Stop serving requests, they'll be queued until you begin processing again by calling `start()`.
2316
+ */
2317
+ stop() {
2318
+ this.#serving = false;
2319
+ }
2320
+ /**
2321
+ * Stop serving requests, empty the request queue, and release any promises waiting for the
2322
+ * queue to flush.
2323
+ */
2324
+ reset() {
2325
+ this.stop();
2326
+ this.#queue.clear();
2327
+ this.#release();
2328
+ }
2329
+ #flush() {
2330
+ for (const key of this.#queue.keys()) this.serve(key);
2331
+ this.#release();
2332
+ }
2333
+ #release() {
2334
+ this.#pending.resolve();
2335
+ this.#pending = deferredPromise();
2336
+ }
2337
+ }
2338
+
2339
+ class MediaRequestManager extends MediaPlayerController {
2340
+ #stateMgr;
2341
+ #request;
2342
+ #media;
2343
+ controls;
2344
+ #fullscreen;
2345
+ #orientation;
2346
+ #$provider;
2347
+ #providerQueue = new RequestQueue();
2348
+ constructor(stateMgr, request, media) {
2349
+ super();
2350
+ this.#stateMgr = stateMgr;
2351
+ this.#request = request;
2352
+ this.#media = media;
2353
+ this.#$provider = media.$provider;
2354
+ this.controls = new MediaControls();
2355
+ this.#fullscreen = new FullscreenController();
2356
+ this.#orientation = new ScreenOrientationController();
2357
+ }
2358
+ onAttach() {
2359
+ this.listen("fullscreen-change", this.#onFullscreenChange.bind(this));
2360
+ }
2361
+ onConnect(el) {
2362
+ const names = Object.getOwnPropertyNames(Object.getPrototypeOf(this)), events = new EventsController(el), handleRequest = this.#handleRequest.bind(this);
2363
+ for (const name of names) {
2364
+ if (name.startsWith("media-")) {
2365
+ events.add(name, handleRequest);
2366
+ }
2367
+ }
2368
+ this.#attachLoadPlayListener();
2369
+ effect(this.#watchProvider.bind(this));
2370
+ effect(this.#watchControlsDelayChange.bind(this));
2371
+ effect(this.#watchAudioGainSupport.bind(this));
2372
+ effect(this.#watchAirPlaySupport.bind(this));
2373
+ effect(this.#watchGoogleCastSupport.bind(this));
2374
+ effect(this.#watchFullscreenSupport.bind(this));
2375
+ effect(this.#watchPiPSupport.bind(this));
2376
+ }
2377
+ onDestroy() {
2378
+ try {
2379
+ const destroyEvent = this.createEvent("destroy"), { pictureInPicture, fullscreen } = this.$state;
2380
+ if (fullscreen()) this.exitFullscreen("prefer-media", destroyEvent);
2381
+ if (pictureInPicture()) this.exitPictureInPicture(destroyEvent);
2382
+ } catch (e) {
2383
+ }
2384
+ this.#providerQueue.reset();
2385
+ }
2386
+ #attachLoadPlayListener() {
2387
+ const { load } = this.$props, { canLoad } = this.$state;
2388
+ if (load() !== "play" || canLoad()) return;
2389
+ const off = this.listen("media-play-request", (event) => {
2390
+ this.#handleLoadPlayStrategy(event);
2391
+ off();
2392
+ });
2393
+ }
2394
+ #watchProvider() {
2395
+ const provider = this.#$provider(), canPlay = this.$state.canPlay();
2396
+ if (provider && canPlay) {
2397
+ this.#providerQueue.start();
2398
+ }
2399
+ return () => {
2400
+ this.#providerQueue.stop();
2401
+ };
2402
+ }
2403
+ #handleRequest(event) {
2404
+ event.stopPropagation();
2405
+ if (event.defaultPrevented) return;
2406
+ if (!this[event.type]) return;
2407
+ if (peek(this.#$provider)) {
2408
+ this[event.type](event);
2409
+ } else {
2410
+ this.#providerQueue.enqueue(event.type, () => {
2411
+ if (peek(this.#$provider)) this[event.type](event);
2412
+ });
2413
+ }
2414
+ }
2415
+ async play(trigger) {
2416
+ const { canPlay, paused, autoPlaying } = this.$state;
2417
+ if (this.#handleLoadPlayStrategy(trigger)) return;
2418
+ if (!peek(paused)) return;
2419
+ if (trigger) this.#request.queue.enqueue("media-play-request", trigger);
2420
+ const isAutoPlaying = peek(autoPlaying);
2421
+ try {
2422
+ const provider = peek(this.#$provider);
2423
+ throwIfNotReadyForPlayback(provider, peek(canPlay));
2424
+ throwIfAutoplayingWithReducedMotion(isAutoPlaying);
2425
+ return await provider.play();
2426
+ } catch (error) {
2427
+ const errorEvent = this.createEvent("play-fail", {
2428
+ detail: coerceToError(error),
2429
+ trigger
2430
+ });
2431
+ errorEvent.autoPlay = isAutoPlaying;
2432
+ this.#stateMgr.handle(errorEvent);
2433
+ throw error;
2434
+ }
2435
+ }
2436
+ #handleLoadPlayStrategy(trigger) {
2437
+ const { load } = this.$props, { canLoad } = this.$state;
2438
+ if (load() === "play" && !canLoad()) {
2439
+ const event = this.createEvent("media-start-loading", { trigger });
2440
+ this.dispatchEvent(event);
2441
+ this.#providerQueue.enqueue("media-play-request", async () => {
2442
+ try {
2443
+ await this.play(event);
2444
+ } catch (error) {
2445
+ }
2446
+ });
2447
+ return true;
2448
+ }
2449
+ return false;
2450
+ }
2451
+ async pause(trigger) {
2452
+ const { canPlay, paused } = this.$state;
2453
+ if (peek(paused)) return;
2454
+ if (trigger) {
2455
+ this.#request.queue.enqueue("media-pause-request", trigger);
2456
+ }
2457
+ try {
2458
+ const provider = peek(this.#$provider);
2459
+ throwIfNotReadyForPlayback(provider, peek(canPlay));
2460
+ return await provider.pause();
2461
+ } catch (error) {
2462
+ this.#request.queue.delete("media-pause-request");
2463
+ throw error;
2464
+ }
2465
+ }
2466
+ setAudioGain(gain, trigger) {
2467
+ const { audioGain, canSetAudioGain } = this.$state;
2468
+ if (audioGain() === gain) return;
2469
+ const provider = this.#$provider();
2470
+ if (!provider?.audioGain || !canSetAudioGain()) {
2471
+ throw Error("[vidstack] audio gain api not available");
2472
+ }
2473
+ if (trigger) {
2474
+ this.#request.queue.enqueue("media-audio-gain-change-request", trigger);
2475
+ }
2476
+ provider.audioGain.setGain(gain);
2477
+ }
2478
+ seekToLiveEdge(trigger) {
2479
+ const { canPlay, live, liveEdge, canSeek, liveSyncPosition, seekableEnd, userBehindLiveEdge } = this.$state;
2480
+ userBehindLiveEdge.set(false);
2481
+ if (peek(() => !live() || liveEdge() || !canSeek())) return;
2482
+ const provider = peek(this.#$provider);
2483
+ throwIfNotReadyForPlayback(provider, peek(canPlay));
2484
+ if (trigger) this.#request.queue.enqueue("media-seek-request", trigger);
2485
+ const end = seekableEnd() - 2;
2486
+ provider.setCurrentTime(Math.min(end, liveSyncPosition() ?? end));
2487
+ }
2488
+ #wasPIPActive = false;
2489
+ async enterFullscreen(target = "prefer-media", trigger) {
2490
+ const adapter = this.#getFullscreenAdapter(target);
2491
+ throwIfFullscreenNotSupported(target, adapter);
2492
+ if (adapter.active) return;
2493
+ if (peek(this.$state.pictureInPicture)) {
2494
+ this.#wasPIPActive = true;
2495
+ await this.exitPictureInPicture(trigger);
2496
+ }
2497
+ if (trigger) {
2498
+ this.#request.queue.enqueue("media-enter-fullscreen-request", trigger);
2499
+ }
2500
+ return adapter.enter();
2501
+ }
2502
+ async exitFullscreen(target = "prefer-media", trigger) {
2503
+ const adapter = this.#getFullscreenAdapter(target);
2504
+ throwIfFullscreenNotSupported(target, adapter);
2505
+ if (!adapter.active) return;
2506
+ if (trigger) {
2507
+ this.#request.queue.enqueue("media-exit-fullscreen-request", trigger);
2508
+ }
2509
+ try {
2510
+ const result = await adapter.exit();
2511
+ if (this.#wasPIPActive && peek(this.$state.canPictureInPicture)) {
2512
+ await this.enterPictureInPicture();
2513
+ }
2514
+ return result;
2515
+ } finally {
2516
+ this.#wasPIPActive = false;
2517
+ }
2518
+ }
2519
+ #getFullscreenAdapter(target) {
2520
+ const provider = peek(this.#$provider);
2521
+ return target === "prefer-media" && this.#fullscreen.supported || target === "media" ? this.#fullscreen : provider?.fullscreen;
2522
+ }
2523
+ async enterPictureInPicture(trigger) {
2524
+ this.#throwIfPIPNotSupported();
2525
+ if (this.$state.pictureInPicture()) return;
2526
+ if (trigger) {
2527
+ this.#request.queue.enqueue("media-enter-pip-request", trigger);
2528
+ }
2529
+ return await this.#$provider().pictureInPicture.enter();
2530
+ }
2531
+ async exitPictureInPicture(trigger) {
2532
+ this.#throwIfPIPNotSupported();
2533
+ if (!this.$state.pictureInPicture()) return;
2534
+ if (trigger) {
2535
+ this.#request.queue.enqueue("media-exit-pip-request", trigger);
2536
+ }
2537
+ try {
2538
+ if (document?.pictureInPictureElement && document.exitPictureInPicture) {
2539
+ await document.exitPictureInPicture();
2540
+ return;
2541
+ }
2542
+ const provider = this.#$provider();
2543
+ if (provider?.pictureInPicture) {
2544
+ await provider.pictureInPicture.exit();
2545
+ return;
2546
+ }
2547
+ const videoElements = document?.querySelectorAll?.("video") || [];
2548
+ for (const video of videoElements) {
2549
+ if (video === document.pictureInPictureElement) {
2550
+ if (document.exitPictureInPicture) {
2551
+ await document.exitPictureInPicture();
2552
+ return;
2553
+ }
2554
+ }
2555
+ if (video.webkitPresentationMode === "picture-in-picture") {
2556
+ video.webkitSetPresentationMode("inline");
2557
+ return;
2558
+ }
2559
+ }
2560
+ } catch (error) {
2561
+ }
2562
+ }
2563
+ #throwIfPIPNotSupported() {
2564
+ if (this.$state.canPictureInPicture()) return;
2565
+ throw Error(
2566
+ "[vidstack] no pip support"
2567
+ );
2568
+ }
2569
+ #watchControlsDelayChange() {
2570
+ this.controls.defaultDelay = this.$props.controlsDelay();
2571
+ }
2572
+ #watchAudioGainSupport() {
2573
+ const { canSetAudioGain } = this.$state, supported = !!this.#$provider()?.audioGain?.supported;
2574
+ canSetAudioGain.set(supported);
2575
+ }
2576
+ #watchAirPlaySupport() {
2577
+ const { canAirPlay } = this.$state, supported = !!this.#$provider()?.airPlay?.supported;
2578
+ canAirPlay.set(supported);
2579
+ }
2580
+ #watchGoogleCastSupport() {
2581
+ const { canGoogleCast, source } = this.$state, supported = IS_CHROME && !IS_IOS && canGoogleCastSrc(source());
2582
+ canGoogleCast.set(supported);
2583
+ }
2584
+ #watchFullscreenSupport() {
2585
+ const { canFullscreen } = this.$state, supported = this.#fullscreen.supported || !!this.#$provider()?.fullscreen?.supported;
2586
+ canFullscreen.set(supported);
2587
+ }
2588
+ #watchPiPSupport() {
2589
+ const { canPictureInPicture } = this.$state, supported = !!this.#$provider()?.pictureInPicture?.supported;
2590
+ canPictureInPicture.set(supported);
2591
+ }
2592
+ async ["media-airplay-request"](event) {
2593
+ try {
2594
+ await this.requestAirPlay(event);
2595
+ } catch (error) {
2596
+ }
2597
+ }
2598
+ async requestAirPlay(trigger) {
2599
+ try {
2600
+ const adapter = this.#$provider()?.airPlay;
2601
+ if (!adapter?.supported) {
2602
+ throw Error(false ? "AirPlay adapter not available on provider." : "No AirPlay adapter.");
2603
+ }
2604
+ if (trigger) {
2605
+ this.#request.queue.enqueue("media-airplay-request", trigger);
2606
+ }
2607
+ return await adapter.prompt();
2608
+ } catch (error) {
2609
+ this.#request.queue.delete("media-airplay-request");
2610
+ throw error;
2611
+ }
2612
+ }
2613
+ async ["media-google-cast-request"](event) {
2614
+ try {
2615
+ await this.requestGoogleCast(event);
2616
+ } catch (error) {
2617
+ }
2618
+ }
2619
+ #googleCastLoader;
2620
+ async requestGoogleCast(trigger) {
2621
+ try {
2622
+ const { canGoogleCast } = this.$state;
2623
+ if (!peek(canGoogleCast)) {
2624
+ const error = Error(
2625
+ false ? "Google Cast not available on this platform." : "Cast not available."
2626
+ );
2627
+ error.code = "CAST_NOT_AVAILABLE";
2628
+ throw error;
2629
+ }
2630
+ preconnect("https://www.gstatic.com");
2631
+ if (!this.#googleCastLoader) {
2632
+ const $module = await import('./vidstack-Bf1Q6kqO.js');
2633
+ this.#googleCastLoader = new $module.GoogleCastLoader();
2634
+ }
2635
+ await this.#googleCastLoader.prompt(this.#media);
2636
+ if (trigger) {
2637
+ this.#request.queue.enqueue("media-google-cast-request", trigger);
2638
+ }
2639
+ const isConnecting = peek(this.$state.remotePlaybackState) !== "disconnected";
2640
+ if (isConnecting) {
2641
+ this.$state.savedState.set({
2642
+ paused: peek(this.$state.paused),
2643
+ currentTime: peek(this.$state.currentTime)
2644
+ });
2645
+ }
2646
+ this.$state.remotePlaybackLoader.set(isConnecting ? this.#googleCastLoader : null);
2647
+ } catch (error) {
2648
+ this.#request.queue.delete("media-google-cast-request");
2649
+ throw error;
2650
+ }
2651
+ }
2652
+ ["media-clip-start-change-request"](event) {
2653
+ const { clipStartTime } = this.$state;
2654
+ clipStartTime.set(event.detail);
2655
+ }
2656
+ ["media-clip-end-change-request"](event) {
2657
+ const { clipEndTime } = this.$state;
2658
+ clipEndTime.set(event.detail);
2659
+ this.dispatch("duration-change", {
2660
+ detail: event.detail,
2661
+ trigger: event
2662
+ });
2663
+ }
2664
+ ["media-duration-change-request"](event) {
2665
+ const { providedDuration, clipEndTime } = this.$state;
2666
+ providedDuration.set(event.detail);
2667
+ if (clipEndTime() <= 0) {
2668
+ this.dispatch("duration-change", {
2669
+ detail: event.detail,
2670
+ trigger: event
2671
+ });
2672
+ }
2673
+ }
2674
+ ["media-audio-track-change-request"](event) {
2675
+ const { logger, audioTracks } = this.#media;
2676
+ if (audioTracks.readonly) {
2677
+ return;
2678
+ }
2679
+ const index = event.detail, track = audioTracks[index];
2680
+ if (track) {
2681
+ const key = event.type;
2682
+ this.#request.queue.enqueue(key, event);
2683
+ track.selected = true;
2684
+ }
2685
+ }
2686
+ async ["media-enter-fullscreen-request"](event) {
2687
+ try {
2688
+ await this.enterFullscreen(event.detail, event);
2689
+ } catch (error) {
2690
+ this.#onFullscreenError(error, event);
2691
+ }
2692
+ }
2693
+ async ["media-exit-fullscreen-request"](event) {
2694
+ try {
2695
+ await this.exitFullscreen(event.detail, event);
2696
+ } catch (error) {
2697
+ this.#onFullscreenError(error, event);
2698
+ }
2699
+ }
2700
+ async #onFullscreenChange(event) {
2701
+ const lockType = peek(this.$props.fullscreenOrientation), isFullscreen = event.detail;
2702
+ if (isUndefined(lockType) || lockType === "none" || !this.#orientation.supported) return;
2703
+ if (isFullscreen) {
2704
+ if (this.#orientation.locked) return;
2705
+ this.dispatch("media-orientation-lock-request", {
2706
+ detail: lockType,
2707
+ trigger: event
2708
+ });
2709
+ } else if (this.#orientation.locked) {
2710
+ this.dispatch("media-orientation-unlock-request", {
2711
+ trigger: event
2712
+ });
2713
+ }
2714
+ }
2715
+ #onFullscreenError(error, request) {
2716
+ this.#stateMgr.handle(
2717
+ this.createEvent("fullscreen-error", {
2718
+ detail: coerceToError(error)
2719
+ })
2720
+ );
2721
+ }
2722
+ async ["media-orientation-lock-request"](event) {
2723
+ const key = event.type;
2724
+ try {
2725
+ this.#request.queue.enqueue(key, event);
2726
+ await this.#orientation.lock(event.detail);
2727
+ } catch (error) {
2728
+ this.#request.queue.delete(key);
2729
+ }
2730
+ }
2731
+ async ["media-orientation-unlock-request"](event) {
2732
+ const key = event.type;
2733
+ try {
2734
+ this.#request.queue.enqueue(key, event);
2735
+ await this.#orientation.unlock();
2736
+ } catch (error) {
2737
+ this.#request.queue.delete(key);
2738
+ }
2739
+ }
2740
+ async ["media-enter-pip-request"](event) {
2741
+ try {
2742
+ await this.enterPictureInPicture(event);
2743
+ } catch (error) {
2744
+ this.#onPictureInPictureError(error, event);
2745
+ }
2746
+ }
2747
+ async ["media-exit-pip-request"](event) {
2748
+ try {
2749
+ await this.exitPictureInPicture(event);
2750
+ } catch (error) {
2751
+ this.#onPictureInPictureError(error, event);
2752
+ }
2753
+ }
2754
+ #onPictureInPictureError(error, request) {
2755
+ this.#stateMgr.handle(
2756
+ this.createEvent("picture-in-picture-error", {
2757
+ detail: coerceToError(error)
2758
+ })
2759
+ );
2760
+ }
2761
+ ["media-live-edge-request"](event) {
2762
+ const { live, liveEdge, canSeek } = this.$state;
2763
+ if (!live() || liveEdge() || !canSeek()) return;
2764
+ this.#request.queue.enqueue("media-seek-request", event);
2765
+ try {
2766
+ this.seekToLiveEdge();
2767
+ } catch (error) {
2768
+ this.#request.queue.delete("media-seek-request");
2769
+ }
2770
+ }
2771
+ async ["media-loop-request"](event) {
2772
+ try {
2773
+ this.#request.looping = true;
2774
+ this.#request.replaying = true;
2775
+ await this.play(event);
2776
+ } catch (error) {
2777
+ this.#request.looping = false;
2778
+ }
2779
+ }
2780
+ ["media-user-loop-change-request"](event) {
2781
+ this.$state.userPrefersLoop.set(event.detail);
2782
+ }
2783
+ async ["media-pause-request"](event) {
2784
+ if (this.$state.paused()) return;
2785
+ try {
2786
+ await this.pause(event);
2787
+ } catch (error) {
2788
+ }
2789
+ }
2790
+ async ["media-play-request"](event) {
2791
+ if (!this.$state.paused()) return;
2792
+ try {
2793
+ await this.play(event);
2794
+ } catch (e) {
2795
+ }
2796
+ }
2797
+ ["media-rate-change-request"](event) {
2798
+ const { playbackRate, canSetPlaybackRate } = this.$state;
2799
+ if (playbackRate() === event.detail || !canSetPlaybackRate()) return;
2800
+ const provider = this.#$provider();
2801
+ if (!provider?.setPlaybackRate) return;
2802
+ this.#request.queue.enqueue("media-rate-change-request", event);
2803
+ provider.setPlaybackRate(event.detail);
2804
+ }
2805
+ ["media-audio-gain-change-request"](event) {
2806
+ try {
2807
+ this.setAudioGain(event.detail, event);
2808
+ } catch (e) {
2809
+ }
2810
+ }
2811
+ ["media-quality-change-request"](event) {
2812
+ const { qualities, storage, logger } = this.#media;
2813
+ if (qualities.readonly) {
2814
+ return;
2815
+ }
2816
+ this.#request.queue.enqueue("media-quality-change-request", event);
2817
+ const index = event.detail;
2818
+ if (index < 0) {
2819
+ qualities.autoSelect(event);
2820
+ if (event.isOriginTrusted) storage?.setVideoQuality?.(null);
2821
+ } else {
2822
+ const quality = qualities[index];
2823
+ if (quality) {
2824
+ quality.selected = true;
2825
+ if (event.isOriginTrusted) {
2826
+ storage?.setVideoQuality?.({
2827
+ id: quality.id,
2828
+ width: quality.width,
2829
+ height: quality.height,
2830
+ bitrate: quality.bitrate
2831
+ });
2832
+ }
2833
+ }
2834
+ }
2835
+ }
2836
+ ["media-pause-controls-request"](event) {
2837
+ const key = event.type;
2838
+ this.#request.queue.enqueue(key, event);
2839
+ this.controls.pause(event);
2840
+ }
2841
+ ["media-resume-controls-request"](event) {
2842
+ const key = event.type;
2843
+ this.#request.queue.enqueue(key, event);
2844
+ this.controls.resume(event);
2845
+ }
2846
+ ["media-seek-request"](event) {
2847
+ const { canSeek, ended, live, seekableEnd, userBehindLiveEdge } = this.$state, seekTime = event.detail;
2848
+ if (ended()) this.#request.replaying = true;
2849
+ const key = event.type;
2850
+ this.#request.seeking = false;
2851
+ this.#request.queue.delete(key);
2852
+ const boundedTime = boundTime(seekTime, this.$state);
2853
+ if (!Number.isFinite(boundedTime) || !canSeek()) return;
2854
+ this.#request.queue.enqueue(key, event);
2855
+ this.#$provider().setCurrentTime(boundedTime);
2856
+ if (live() && event.isOriginTrusted && Math.abs(seekableEnd() - boundedTime) >= 2) {
2857
+ userBehindLiveEdge.set(true);
2858
+ }
2859
+ }
2860
+ ["media-seeking-request"](event) {
2861
+ const key = event.type;
2862
+ this.#request.queue.enqueue(key, event);
2863
+ this.$state.seeking.set(true);
2864
+ this.#request.seeking = true;
2865
+ }
2866
+ ["media-start-loading"](event) {
2867
+ if (this.$state.canLoad()) return;
2868
+ const key = event.type;
2869
+ this.#request.queue.enqueue(key, event);
2870
+ this.#stateMgr.handle(this.createEvent("can-load"));
2871
+ }
2872
+ ["media-poster-start-loading"](event) {
2873
+ if (this.$state.canLoadPoster()) return;
2874
+ const key = event.type;
2875
+ this.#request.queue.enqueue(key, event);
2876
+ this.#stateMgr.handle(this.createEvent("can-load-poster"));
2877
+ }
2878
+ ["media-text-track-change-request"](event) {
2879
+ const { index, mode } = event.detail, track = this.#media.textTracks[index];
2880
+ if (track) {
2881
+ const key = event.type;
2882
+ this.#request.queue.enqueue(key, event);
2883
+ track.setMode(mode, event);
2884
+ }
2885
+ }
2886
+ ["media-mute-request"](event) {
2887
+ if (this.$state.muted()) return;
2888
+ const key = event.type;
2889
+ this.#request.queue.enqueue(key, event);
2890
+ this.#$provider().setMuted(true);
2891
+ }
2892
+ ["media-unmute-request"](event) {
2893
+ const { muted, volume } = this.$state;
2894
+ if (!muted()) return;
2895
+ const key = event.type;
2896
+ this.#request.queue.enqueue(key, event);
2897
+ this.#media.$provider().setMuted(false);
2898
+ if (volume() === 0) {
2899
+ this.#request.queue.enqueue(key, event);
2900
+ this.#$provider().setVolume(0.25);
2901
+ }
2902
+ }
2903
+ ["media-volume-change-request"](event) {
2904
+ const { muted, volume } = this.$state;
2905
+ const newVolume = event.detail;
2906
+ if (volume() === newVolume) return;
2907
+ const key = event.type;
2908
+ this.#request.queue.enqueue(key, event);
2909
+ this.#$provider().setVolume(newVolume);
2910
+ if (newVolume > 0 && muted()) {
2911
+ this.#request.queue.enqueue(key, event);
2912
+ this.#$provider().setMuted(false);
2913
+ }
2914
+ }
2915
+ #logError(title, error, request) {
2916
+ return;
2917
+ }
2918
+ }
2919
+ function throwIfNotReadyForPlayback(provider, canPlay) {
2920
+ if (provider && canPlay) return;
2921
+ throw Error(
2922
+ "[vidstack] media not ready"
2923
+ );
2924
+ }
2925
+ function throwIfFullscreenNotSupported(target, fullscreen) {
2926
+ if (fullscreen?.supported) return;
2927
+ throw Error(
2928
+ "[vidstack] no fullscreen support"
2929
+ );
2930
+ }
2931
+ function throwIfAutoplayingWithReducedMotion(autoplaying) {
2932
+ if (!prefersReducedMotion() || !autoplaying) return;
2933
+ throw Error(
2934
+ "[vidstack] autoplay blocked"
2935
+ );
2936
+ }
2937
+ class MediaRequestContext {
2938
+ seeking = false;
2939
+ looping = false;
2940
+ replaying = false;
2941
+ queue = new Queue();
2942
+ }
2943
+
2944
+ const TRACKED_EVENT = /* @__PURE__ */ new Set([
2945
+ "auto-play",
2946
+ "auto-play-fail",
2947
+ "can-load",
2948
+ "sources-change",
2949
+ "source-change",
2950
+ "load-start",
2951
+ "abort",
2952
+ "error",
2953
+ "loaded-metadata",
2954
+ "loaded-data",
2955
+ "can-play",
2956
+ "play",
2957
+ "play-fail",
2958
+ "pause",
2959
+ "playing",
2960
+ "seeking",
2961
+ "seeked",
2962
+ "waiting"
2963
+ ]);
2964
+
2965
+ class MediaStateManager extends MediaPlayerController {
2966
+ #request;
2967
+ #media;
2968
+ #trackedEvents = /* @__PURE__ */ new Map();
2969
+ #clipEnded = false;
2970
+ #playedIntervals = [];
2971
+ #playedInterval = [-1, -1];
2972
+ #firingWaiting = false;
2973
+ #waitingTrigger;
2974
+ constructor(request, media) {
2975
+ super();
2976
+ this.#request = request;
2977
+ this.#media = media;
2978
+ }
2979
+ onAttach(el) {
2980
+ el.setAttribute("aria-busy", "true");
2981
+ new EventsController(this).add("fullscreen-change", this["fullscreen-change"].bind(this)).add("fullscreen-error", this["fullscreen-error"].bind(this)).add("orientation-change", this["orientation-change"].bind(this));
2982
+ }
2983
+ onConnect(el) {
2984
+ effect(this.#watchCanSetVolume.bind(this));
2985
+ this.#addTextTrackListeners();
2986
+ this.#addQualityListeners();
2987
+ this.#addAudioTrackListeners();
2988
+ this.#resumePlaybackOnConnect();
2989
+ onDispose(this.#pausePlaybackOnDisconnect.bind(this));
2990
+ }
2991
+ onDestroy() {
2992
+ const { audioTracks, qualities, textTracks } = this.#media;
2993
+ audioTracks[ListSymbol.reset]();
2994
+ qualities[ListSymbol.reset]();
2995
+ textTracks[ListSymbol.reset]();
2996
+ this.#stopWatchingQualityResize();
2997
+ }
2998
+ handle(event) {
2999
+ if (!this.scope) return;
3000
+ const type = event.type;
3001
+ untrack(() => this[event.type]?.(event));
3002
+ {
3003
+ if (TRACKED_EVENT.has(type)) this.#trackedEvents.set(type, event);
3004
+ this.dispatch(event);
3005
+ }
3006
+ }
3007
+ #isPlayingOnDisconnect = false;
3008
+ #resumePlaybackOnConnect() {
3009
+ if (!this.#isPlayingOnDisconnect) return;
3010
+ requestAnimationFrame(() => {
3011
+ if (!this.scope) return;
3012
+ this.#media.remote.play(new DOMEvent("dom-connect"));
3013
+ });
3014
+ this.#isPlayingOnDisconnect = false;
3015
+ }
3016
+ #pausePlaybackOnDisconnect() {
3017
+ if (this.#isPlayingOnDisconnect) return;
3018
+ this.#isPlayingOnDisconnect = !this.$state.paused();
3019
+ this.#media.$provider()?.pause();
3020
+ }
3021
+ #resetTracking() {
3022
+ this.#stopWaiting();
3023
+ this.#clipEnded = false;
3024
+ this.#request.replaying = false;
3025
+ this.#request.looping = false;
3026
+ this.#firingWaiting = false;
3027
+ this.#waitingTrigger = void 0;
3028
+ this.#trackedEvents.clear();
3029
+ }
3030
+ #satisfyRequest(request, event) {
3031
+ const requestEvent = this.#request.queue.serve(request);
3032
+ if (!requestEvent) return;
3033
+ event.request = requestEvent;
3034
+ event.triggers.add(requestEvent);
3035
+ }
3036
+ #addTextTrackListeners() {
3037
+ this.#onTextTracksChange();
3038
+ this.#onTextTrackModeChange();
3039
+ const textTracks = this.#media.textTracks;
3040
+ new EventsController(textTracks).add("add", this.#onTextTracksChange.bind(this)).add("remove", this.#onTextTracksChange.bind(this)).add("mode-change", this.#onTextTrackModeChange.bind(this));
3041
+ }
3042
+ #addQualityListeners() {
3043
+ const qualities = this.#media.qualities;
3044
+ new EventsController(qualities).add("add", this.#onQualitiesChange.bind(this)).add("remove", this.#onQualitiesChange.bind(this)).add("change", this.#onQualityChange.bind(this)).add("auto-change", this.#onAutoQualityChange.bind(this)).add("readonly-change", this.#onCanSetQualityChange.bind(this));
3045
+ }
3046
+ #addAudioTrackListeners() {
3047
+ const audioTracks = this.#media.audioTracks;
3048
+ new EventsController(audioTracks).add("add", this.#onAudioTracksChange.bind(this)).add("remove", this.#onAudioTracksChange.bind(this)).add("change", this.#onAudioTrackChange.bind(this));
3049
+ }
3050
+ #onTextTracksChange(event) {
3051
+ const { textTracks } = this.$state;
3052
+ textTracks.set(this.#media.textTracks.toArray());
3053
+ this.dispatch("text-tracks-change", {
3054
+ detail: textTracks(),
3055
+ trigger: event
3056
+ });
3057
+ }
3058
+ #onTextTrackModeChange(event) {
3059
+ if (event) this.#satisfyRequest("media-text-track-change-request", event);
3060
+ const current = this.#media.textTracks.selected, { textTrack } = this.$state;
3061
+ if (textTrack() !== current) {
3062
+ textTrack.set(current);
3063
+ this.dispatch("text-track-change", {
3064
+ detail: current,
3065
+ trigger: event
3066
+ });
3067
+ }
3068
+ }
3069
+ #onAudioTracksChange(event) {
3070
+ const { audioTracks } = this.$state;
3071
+ audioTracks.set(this.#media.audioTracks.toArray());
3072
+ this.dispatch("audio-tracks-change", {
3073
+ detail: audioTracks(),
3074
+ trigger: event
3075
+ });
3076
+ }
3077
+ #onAudioTrackChange(event) {
3078
+ const { audioTrack } = this.$state;
3079
+ audioTrack.set(this.#media.audioTracks.selected);
3080
+ if (event) this.#satisfyRequest("media-audio-track-change-request", event);
3081
+ this.dispatch("audio-track-change", {
3082
+ detail: audioTrack(),
3083
+ trigger: event
3084
+ });
3085
+ }
3086
+ #onQualitiesChange(event) {
3087
+ const { qualities } = this.$state;
3088
+ qualities.set(this.#media.qualities.toArray());
3089
+ this.dispatch("qualities-change", {
3090
+ detail: qualities(),
3091
+ trigger: event
3092
+ });
3093
+ }
3094
+ #onQualityChange(event) {
3095
+ const { quality } = this.$state;
3096
+ quality.set(this.#media.qualities.selected);
3097
+ if (event) this.#satisfyRequest("media-quality-change-request", event);
3098
+ this.dispatch("quality-change", {
3099
+ detail: quality(),
3100
+ trigger: event
3101
+ });
3102
+ }
3103
+ #onAutoQualityChange() {
3104
+ const { qualities } = this.#media, isAuto = qualities.auto;
3105
+ this.$state.autoQuality.set(isAuto);
3106
+ if (!isAuto) this.#stopWatchingQualityResize();
3107
+ }
3108
+ #stopQualityResizeEffect = null;
3109
+ #watchQualityResize() {
3110
+ this.#stopWatchingQualityResize();
3111
+ this.#stopQualityResizeEffect = effect(() => {
3112
+ const { qualities } = this.#media, { mediaWidth, mediaHeight } = this.$state, w = mediaWidth(), h = mediaHeight();
3113
+ if (w === 0 || h === 0) return;
3114
+ let selectedQuality = null, minScore = Infinity;
3115
+ for (const quality of qualities) {
3116
+ const score = Math.abs(quality.width - w) + Math.abs(quality.height - h);
3117
+ if (score < minScore) {
3118
+ minScore = score;
3119
+ selectedQuality = quality;
3120
+ }
3121
+ }
3122
+ if (selectedQuality) {
3123
+ qualities[ListSymbol.select](
3124
+ selectedQuality,
3125
+ true,
3126
+ new DOMEvent("resize", { detail: { width: w, height: h } })
3127
+ );
3128
+ }
3129
+ });
3130
+ }
3131
+ #stopWatchingQualityResize() {
3132
+ this.#stopQualityResizeEffect?.();
3133
+ this.#stopQualityResizeEffect = null;
3134
+ }
3135
+ #onCanSetQualityChange() {
3136
+ this.$state.canSetQuality.set(!this.#media.qualities.readonly);
3137
+ }
3138
+ #watchCanSetVolume() {
3139
+ const { canSetVolume, isGoogleCastConnected } = this.$state;
3140
+ if (isGoogleCastConnected()) {
3141
+ canSetVolume.set(false);
3142
+ return;
3143
+ }
3144
+ canChangeVolume().then(canSetVolume.set);
3145
+ }
3146
+ ["provider-change"](event) {
3147
+ const prevProvider = this.#media.$provider(), newProvider = event.detail;
3148
+ if (prevProvider?.type === newProvider?.type) return;
3149
+ prevProvider?.destroy?.();
3150
+ prevProvider?.scope?.dispose();
3151
+ this.#media.$provider.set(event.detail);
3152
+ if (prevProvider && event.detail === null) {
3153
+ this.#resetMediaState(event);
3154
+ }
3155
+ }
3156
+ ["provider-loader-change"](event) {
3157
+ }
3158
+ ["auto-play"](event) {
3159
+ this.$state.autoPlayError.set(null);
3160
+ }
3161
+ ["auto-play-fail"](event) {
3162
+ this.$state.autoPlayError.set(event.detail);
3163
+ this.#resetTracking();
3164
+ }
3165
+ ["can-load"](event) {
3166
+ this.$state.canLoad.set(true);
3167
+ this.#trackedEvents.set("can-load", event);
3168
+ this.#media.textTracks[TextTrackSymbol.canLoad]();
3169
+ this.#satisfyRequest("media-start-loading", event);
3170
+ }
3171
+ ["can-load-poster"](event) {
3172
+ this.$state.canLoadPoster.set(true);
3173
+ this.#trackedEvents.set("can-load-poster", event);
3174
+ this.#satisfyRequest("media-poster-start-loading", event);
3175
+ }
3176
+ ["media-type-change"](event) {
3177
+ const sourceChangeEvent = this.#trackedEvents.get("source-change");
3178
+ if (sourceChangeEvent) event.triggers.add(sourceChangeEvent);
3179
+ const viewType = this.$state.viewType();
3180
+ this.$state.mediaType.set(event.detail);
3181
+ const providedViewType = this.$state.providedViewType(), currentViewType = providedViewType === "unknown" ? event.detail : providedViewType;
3182
+ if (viewType !== currentViewType) {
3183
+ {
3184
+ setTimeout(() => {
3185
+ requestAnimationFrame(() => {
3186
+ if (!this.scope) return;
3187
+ this.$state.inferredViewType.set(event.detail);
3188
+ this.dispatch("view-type-change", {
3189
+ detail: currentViewType,
3190
+ trigger: event
3191
+ });
3192
+ });
3193
+ }, 0);
3194
+ }
3195
+ }
3196
+ }
3197
+ ["stream-type-change"](event) {
3198
+ const sourceChangeEvent = this.#trackedEvents.get("source-change");
3199
+ if (sourceChangeEvent) event.triggers.add(sourceChangeEvent);
3200
+ const { streamType, inferredStreamType } = this.$state;
3201
+ inferredStreamType.set(event.detail);
3202
+ event.detail = streamType();
3203
+ }
3204
+ ["rate-change"](event) {
3205
+ const { storage } = this.#media, { canPlay } = this.$state;
3206
+ this.$state.playbackRate.set(event.detail);
3207
+ this.#satisfyRequest("media-rate-change-request", event);
3208
+ if (canPlay()) {
3209
+ storage?.setPlaybackRate?.(event.detail);
3210
+ }
3211
+ }
3212
+ ["remote-playback-change"](event) {
3213
+ const { remotePlaybackState, remotePlaybackType } = this.$state, { type, state } = event.detail, isConnected = state === "connected";
3214
+ remotePlaybackType.set(type);
3215
+ remotePlaybackState.set(state);
3216
+ const key = type === "airplay" ? "media-airplay-request" : "media-google-cast-request";
3217
+ if (isConnected) {
3218
+ this.#satisfyRequest(key, event);
3219
+ } else {
3220
+ const requestEvent = this.#request.queue.peek(key);
3221
+ if (requestEvent) {
3222
+ event.request = requestEvent;
3223
+ event.triggers.add(requestEvent);
3224
+ }
3225
+ }
3226
+ }
3227
+ ["sources-change"](event) {
3228
+ const prevSources = this.$state.sources(), newSources = event.detail;
3229
+ this.$state.sources.set(newSources);
3230
+ this.#onSourceQualitiesChange(prevSources, newSources, event);
3231
+ }
3232
+ #onSourceQualitiesChange(prevSources, newSources, trigger) {
3233
+ let { qualities } = this.#media, added = false, removed = false;
3234
+ for (const prevSrc of prevSources) {
3235
+ if (!isVideoQualitySrc(prevSrc)) continue;
3236
+ const exists = newSources.some((s) => s.src === prevSrc.src);
3237
+ if (!exists) {
3238
+ const quality = qualities.getBySrc(prevSrc.src);
3239
+ if (quality) {
3240
+ qualities[ListSymbol.remove](quality, trigger);
3241
+ removed = true;
3242
+ }
3243
+ }
3244
+ }
3245
+ if (removed && !qualities.length) {
3246
+ this.$state.savedState.set(null);
3247
+ qualities[ListSymbol.reset](trigger);
3248
+ }
3249
+ for (const src of newSources) {
3250
+ if (!isVideoQualitySrc(src) || qualities.getBySrc(src.src)) continue;
3251
+ const quality = {
3252
+ id: src.id ?? src.label ?? (src.height ? src.height + "p" : "0p"),
3253
+ width: src.width ?? 0,
3254
+ bitrate: null,
3255
+ codec: null,
3256
+ ...src,
3257
+ selected: false
3258
+ };
3259
+ qualities[ListSymbol.add](quality, trigger);
3260
+ added = true;
3261
+ }
3262
+ if (added && !qualities[QualitySymbol.enableAuto]) {
3263
+ this.#watchQualityResize();
3264
+ qualities[QualitySymbol.enableAuto] = this.#watchQualityResize.bind(this);
3265
+ qualities[QualitySymbol.setAuto](true, trigger);
3266
+ }
3267
+ }
3268
+ ["source-change"](event) {
3269
+ event.isQualityChange = event.originEvent?.type === "quality-change";
3270
+ const source = event.detail;
3271
+ this.#resetMediaState(event, event.isQualityChange);
3272
+ this.#trackedEvents.set(event.type, event);
3273
+ this.$state.source.set(source);
3274
+ this.el?.setAttribute("aria-busy", "true");
3275
+ }
3276
+ #resetMediaState(event, isSourceQualityChange = false) {
3277
+ const { audioTracks, qualities } = this.#media;
3278
+ if (!isSourceQualityChange) {
3279
+ this.#playedIntervals = [];
3280
+ this.#playedInterval = [-1, -1];
3281
+ audioTracks[ListSymbol.reset](event);
3282
+ qualities[ListSymbol.reset](event);
3283
+ softResetMediaState(this.$state, isSourceQualityChange);
3284
+ this.#resetTracking();
3285
+ return;
3286
+ }
3287
+ softResetMediaState(this.$state, isSourceQualityChange);
3288
+ this.#resetTracking();
3289
+ }
3290
+ ["abort"](event) {
3291
+ const sourceChangeEvent = this.#trackedEvents.get("source-change");
3292
+ if (sourceChangeEvent) event.triggers.add(sourceChangeEvent);
3293
+ const canLoadEvent = this.#trackedEvents.get("can-load");
3294
+ if (canLoadEvent && !event.triggers.hasType("can-load")) {
3295
+ event.triggers.add(canLoadEvent);
3296
+ }
3297
+ }
3298
+ ["load-start"](event) {
3299
+ const sourceChangeEvent = this.#trackedEvents.get("source-change");
3300
+ if (sourceChangeEvent) event.triggers.add(sourceChangeEvent);
3301
+ }
3302
+ ["error"](event) {
3303
+ this.$state.error.set(event.detail);
3304
+ const abortEvent = this.#trackedEvents.get("abort");
3305
+ if (abortEvent) event.triggers.add(abortEvent);
3306
+ }
3307
+ ["loaded-metadata"](event) {
3308
+ const loadStartEvent = this.#trackedEvents.get("load-start");
3309
+ if (loadStartEvent) event.triggers.add(loadStartEvent);
3310
+ }
3311
+ ["loaded-data"](event) {
3312
+ const loadStartEvent = this.#trackedEvents.get("load-start");
3313
+ if (loadStartEvent) event.triggers.add(loadStartEvent);
3314
+ }
3315
+ ["can-play"](event) {
3316
+ const loadedMetadata = this.#trackedEvents.get("loaded-metadata");
3317
+ if (loadedMetadata) event.triggers.add(loadedMetadata);
3318
+ this.#onCanPlayDetail(event.detail);
3319
+ this.el?.setAttribute("aria-busy", "false");
3320
+ }
3321
+ ["can-play-through"](event) {
3322
+ this.#onCanPlayDetail(event.detail);
3323
+ const canPlay = this.#trackedEvents.get("can-play");
3324
+ if (canPlay) event.triggers.add(canPlay);
3325
+ }
3326
+ #onCanPlayDetail(detail) {
3327
+ const { seekable, buffered, intrinsicDuration, canPlay } = this.$state;
3328
+ canPlay.set(true);
3329
+ buffered.set(detail.buffered);
3330
+ seekable.set(detail.seekable);
3331
+ const seekableEnd = getTimeRangesEnd(detail.seekable) ?? Infinity;
3332
+ intrinsicDuration.set(seekableEnd);
3333
+ }
3334
+ ["duration-change"](event) {
3335
+ const { live, intrinsicDuration, providedDuration, clipEndTime, ended } = this.$state, time = event.detail;
3336
+ if (!live()) {
3337
+ const duration = !Number.isNaN(time) ? time : 0;
3338
+ intrinsicDuration.set(duration);
3339
+ if (ended()) this.#onEndPrecisionChange(event);
3340
+ }
3341
+ if (providedDuration() > 0 || clipEndTime() > 0) {
3342
+ event.stopImmediatePropagation();
3343
+ }
3344
+ }
3345
+ ["progress"](event) {
3346
+ const { buffered, seekable } = this.$state, { buffered: newBuffered, seekable: newSeekable } = event.detail, newBufferedEnd = getTimeRangesEnd(newBuffered), hasBufferedLengthChanged = newBuffered.length !== buffered().length, hasBufferedEndChanged = newBufferedEnd !== getTimeRangesEnd(buffered()), newSeekableEnd = getTimeRangesEnd(newSeekable), hasSeekableLengthChanged = newSeekable.length !== seekable().length, hasSeekableEndChanged = newSeekableEnd !== getTimeRangesEnd(seekable());
3347
+ if (hasBufferedLengthChanged || hasBufferedEndChanged) {
3348
+ buffered.set(newBuffered);
3349
+ }
3350
+ if (hasSeekableLengthChanged || hasSeekableEndChanged) {
3351
+ seekable.set(newSeekable);
3352
+ }
3353
+ }
3354
+ ["play"](event) {
3355
+ const {
3356
+ paused,
3357
+ autoPlayError,
3358
+ ended,
3359
+ autoPlaying,
3360
+ playsInline,
3361
+ pointer,
3362
+ muted,
3363
+ viewType,
3364
+ live,
3365
+ userBehindLiveEdge
3366
+ } = this.$state;
3367
+ this.#resetPlaybackIfNeeded();
3368
+ if (!paused()) {
3369
+ event.stopImmediatePropagation();
3370
+ return;
3371
+ }
3372
+ event.autoPlay = autoPlaying();
3373
+ const waitingEvent = this.#trackedEvents.get("waiting");
3374
+ if (waitingEvent) event.triggers.add(waitingEvent);
3375
+ this.#satisfyRequest("media-play-request", event);
3376
+ this.#trackedEvents.set("play", event);
3377
+ paused.set(false);
3378
+ autoPlayError.set(null);
3379
+ if (event.autoPlay) {
3380
+ this.handle(
3381
+ this.createEvent("auto-play", {
3382
+ detail: { muted: muted() },
3383
+ trigger: event
3384
+ })
3385
+ );
3386
+ autoPlaying.set(false);
3387
+ }
3388
+ if (ended() || this.#request.replaying) {
3389
+ this.#request.replaying = false;
3390
+ ended.set(false);
3391
+ this.handle(this.createEvent("replay", { trigger: event }));
3392
+ }
3393
+ if (!playsInline() && viewType() === "video" && pointer() === "coarse") {
3394
+ this.#media.remote.enterFullscreen("prefer-media", event);
3395
+ }
3396
+ if (live() && !userBehindLiveEdge()) {
3397
+ this.#media.remote.seekToLiveEdge(event);
3398
+ }
3399
+ }
3400
+ #resetPlaybackIfNeeded(trigger) {
3401
+ const provider = peek(this.#media.$provider);
3402
+ if (!provider) return;
3403
+ const { ended, seekableStart, clipEndTime, currentTime, realCurrentTime, duration } = this.$state;
3404
+ const shouldReset = ended() || realCurrentTime() < seekableStart() || clipEndTime() > 0 && realCurrentTime() >= clipEndTime() || Math.abs(currentTime() - duration()) < 0.1;
3405
+ if (shouldReset) {
3406
+ this.dispatch("media-seek-request", {
3407
+ detail: seekableStart(),
3408
+ trigger
3409
+ });
3410
+ }
3411
+ return shouldReset;
3412
+ }
3413
+ ["play-fail"](event) {
3414
+ const { muted, autoPlaying } = this.$state;
3415
+ const playEvent = this.#trackedEvents.get("play");
3416
+ if (playEvent) event.triggers.add(playEvent);
3417
+ this.#satisfyRequest("media-play-request", event);
3418
+ const { paused, playing } = this.$state;
3419
+ paused.set(true);
3420
+ playing.set(false);
3421
+ this.#resetTracking();
3422
+ this.#trackedEvents.set("play-fail", event);
3423
+ if (event.autoPlay) {
3424
+ this.handle(
3425
+ this.createEvent("auto-play-fail", {
3426
+ detail: {
3427
+ muted: muted(),
3428
+ error: event.detail
3429
+ },
3430
+ trigger: event
3431
+ })
3432
+ );
3433
+ autoPlaying.set(false);
3434
+ }
3435
+ }
3436
+ ["playing"](event) {
3437
+ const playEvent = this.#trackedEvents.get("play"), seekedEvent = this.#trackedEvents.get("seeked");
3438
+ if (playEvent) event.triggers.add(playEvent);
3439
+ else if (seekedEvent) event.triggers.add(seekedEvent);
3440
+ setTimeout(() => this.#resetTracking(), 0);
3441
+ const {
3442
+ paused,
3443
+ playing,
3444
+ live,
3445
+ liveSyncPosition,
3446
+ seekableEnd,
3447
+ started,
3448
+ currentTime,
3449
+ seeking,
3450
+ ended
3451
+ } = this.$state;
3452
+ paused.set(false);
3453
+ playing.set(true);
3454
+ seeking.set(false);
3455
+ ended.set(false);
3456
+ if (this.#request.looping) {
3457
+ this.#request.looping = false;
3458
+ return;
3459
+ }
3460
+ if (live() && !started() && currentTime() === 0) {
3461
+ const end = liveSyncPosition() ?? seekableEnd() - 2;
3462
+ if (Number.isFinite(end)) this.#media.$provider().setCurrentTime(end);
3463
+ }
3464
+ this["started"](event);
3465
+ }
3466
+ ["started"](event) {
3467
+ const { started } = this.$state;
3468
+ if (!started()) {
3469
+ started.set(true);
3470
+ this.handle(this.createEvent("started", { trigger: event }));
3471
+ }
3472
+ }
3473
+ ["pause"](event) {
3474
+ if (!this.el?.isConnected) {
3475
+ this.#isPlayingOnDisconnect = true;
3476
+ }
3477
+ this.#satisfyRequest("media-pause-request", event);
3478
+ const seekedEvent = this.#trackedEvents.get("seeked");
3479
+ if (seekedEvent) event.triggers.add(seekedEvent);
3480
+ const { paused, playing } = this.$state;
3481
+ paused.set(true);
3482
+ playing.set(false);
3483
+ if (this.#clipEnded) {
3484
+ setTimeout(() => {
3485
+ this.handle(this.createEvent("end", { trigger: event }));
3486
+ this.#clipEnded = false;
3487
+ }, 0);
3488
+ }
3489
+ this.#resetTracking();
3490
+ }
3491
+ ["time-change"](event) {
3492
+ if (this.#request.looping) {
3493
+ event.stopImmediatePropagation();
3494
+ return;
3495
+ }
3496
+ let { waiting, played, clipEndTime, realCurrentTime, currentTime } = this.$state, newTime = event.detail, endTime = clipEndTime();
3497
+ realCurrentTime.set(newTime);
3498
+ this.#updatePlayed();
3499
+ waiting.set(false);
3500
+ for (const track of this.#media.textTracks) {
3501
+ track[TextTrackSymbol.updateActiveCues](newTime, event);
3502
+ }
3503
+ if (endTime > 0 && newTime >= endTime) {
3504
+ this.#clipEnded = true;
3505
+ this.dispatch("media-pause-request", { trigger: event });
3506
+ }
3507
+ this.#saveTime();
3508
+ this.dispatch("time-update", {
3509
+ detail: { currentTime: currentTime(), played: played() },
3510
+ trigger: event
3511
+ });
3512
+ }
3513
+ #updatePlayed() {
3514
+ const { currentTime, played, paused } = this.$state;
3515
+ if (paused()) return;
3516
+ this.#playedInterval = updateTimeIntervals(
3517
+ this.#playedIntervals,
3518
+ this.#playedInterval,
3519
+ currentTime()
3520
+ );
3521
+ played.set(new TimeRange(this.#playedIntervals));
3522
+ }
3523
+ // Called to update time again incase duration precision has changed.
3524
+ #onEndPrecisionChange(trigger) {
3525
+ const { clipStartTime, clipEndTime, duration } = this.$state, isClipped = clipStartTime() > 0 || clipEndTime() > 0;
3526
+ if (isClipped) return;
3527
+ this.handle(
3528
+ this.createEvent("time-change", {
3529
+ detail: duration(),
3530
+ trigger
3531
+ })
3532
+ );
3533
+ }
3534
+ #saveTime() {
3535
+ const { storage } = this.#media, { canPlay, realCurrentTime } = this.$state;
3536
+ if (canPlay()) {
3537
+ storage?.setTime?.(realCurrentTime());
3538
+ }
3539
+ }
3540
+ ["audio-gain-change"](event) {
3541
+ const { storage } = this.#media, { canPlay, audioGain } = this.$state;
3542
+ audioGain.set(event.detail);
3543
+ this.#satisfyRequest("media-audio-gain-change-request", event);
3544
+ if (canPlay()) storage?.setAudioGain?.(audioGain());
3545
+ }
3546
+ ["volume-change"](event) {
3547
+ const { storage } = this.#media, { volume, muted, canPlay } = this.$state, detail = event.detail;
3548
+ volume.set(detail.volume);
3549
+ muted.set(detail.muted || detail.volume === 0);
3550
+ this.#satisfyRequest("media-volume-change-request", event);
3551
+ this.#satisfyRequest(detail.muted ? "media-mute-request" : "media-unmute-request", event);
3552
+ if (canPlay()) {
3553
+ storage?.setVolume?.(volume());
3554
+ storage?.setMuted?.(muted());
3555
+ }
3556
+ }
3557
+ ["seeking"] = functionThrottle(
3558
+ (event) => {
3559
+ const { seeking, realCurrentTime, paused } = this.$state;
3560
+ seeking.set(true);
3561
+ realCurrentTime.set(event.detail);
3562
+ this.#satisfyRequest("media-seeking-request", event);
3563
+ if (paused()) {
3564
+ this.#waitingTrigger = event;
3565
+ this.#fireWaiting();
3566
+ }
3567
+ this.#playedInterval = [-1, -1];
3568
+ },
3569
+ 150,
3570
+ { leading: true }
3571
+ );
3572
+ ["seeked"](event) {
3573
+ const { seeking, currentTime, realCurrentTime, paused, seekableEnd, ended, live } = this.$state;
3574
+ if (this.#request.seeking) {
3575
+ seeking.set(true);
3576
+ event.stopImmediatePropagation();
3577
+ } else if (seeking()) {
3578
+ const waitingEvent = this.#trackedEvents.get("waiting");
3579
+ if (waitingEvent) event.triggers.add(waitingEvent);
3580
+ const seekingEvent = this.#trackedEvents.get("seeking");
3581
+ if (seekingEvent && !event.triggers.has(seekingEvent)) {
3582
+ event.triggers.add(seekingEvent);
3583
+ }
3584
+ if (paused()) this.#stopWaiting();
3585
+ seeking.set(false);
3586
+ realCurrentTime.set(event.detail);
3587
+ this.#satisfyRequest("media-seek-request", event);
3588
+ const origin = event?.originEvent;
3589
+ if (origin?.isTrusted && !(origin instanceof MessageEvent) && !/seek/.test(origin.type)) {
3590
+ this["started"](event);
3591
+ }
3592
+ }
3593
+ if (!live()) {
3594
+ if (Math.floor(currentTime()) !== Math.floor(seekableEnd())) {
3595
+ ended.set(false);
3596
+ } else {
3597
+ this.end(event);
3598
+ }
3599
+ }
3600
+ }
3601
+ ["waiting"](event) {
3602
+ if (this.#firingWaiting || this.#request.seeking) return;
3603
+ event.stopImmediatePropagation();
3604
+ this.#waitingTrigger = event;
3605
+ this.#fireWaiting();
3606
+ }
3607
+ #fireWaiting = functionDebounce(() => {
3608
+ if (!this.#waitingTrigger) return;
3609
+ this.#firingWaiting = true;
3610
+ const { waiting, playing } = this.$state;
3611
+ waiting.set(true);
3612
+ playing.set(false);
3613
+ const event = this.createEvent("waiting", { trigger: this.#waitingTrigger });
3614
+ this.#trackedEvents.set("waiting", event);
3615
+ this.dispatch(event);
3616
+ this.#waitingTrigger = void 0;
3617
+ this.#firingWaiting = false;
3618
+ }, 300);
3619
+ ["end"](event) {
3620
+ const { loop, ended } = this.$state;
3621
+ if (!loop() && ended()) return;
3622
+ if (loop()) {
3623
+ setTimeout(() => {
3624
+ requestAnimationFrame(() => {
3625
+ this.#resetPlaybackIfNeeded(event);
3626
+ this.dispatch("media-loop-request", { trigger: event });
3627
+ });
3628
+ }, 10);
3629
+ return;
3630
+ }
3631
+ setTimeout(() => this.#onEnded(event), 0);
3632
+ }
3633
+ #onEnded(event) {
3634
+ const { storage } = this.#media, { paused, seeking, ended, duration } = this.$state;
3635
+ this.#onEndPrecisionChange(event);
3636
+ if (!paused()) {
3637
+ this.dispatch("pause", { trigger: event });
3638
+ }
3639
+ if (seeking()) {
3640
+ this.dispatch("seeked", {
3641
+ detail: duration(),
3642
+ trigger: event
3643
+ });
3644
+ }
3645
+ ended.set(true);
3646
+ this.#resetTracking();
3647
+ storage?.setTime?.(duration(), true);
3648
+ this.dispatch("ended", {
3649
+ trigger: event
3650
+ });
3651
+ }
3652
+ #stopWaiting() {
3653
+ this.#fireWaiting.cancel();
3654
+ this.$state.waiting.set(false);
3655
+ }
3656
+ ["fullscreen-change"](event) {
3657
+ const isFullscreen = event.detail;
3658
+ this.$state.fullscreen.set(isFullscreen);
3659
+ this.#satisfyRequest(
3660
+ isFullscreen ? "media-enter-fullscreen-request" : "media-exit-fullscreen-request",
3661
+ event
3662
+ );
3663
+ }
3664
+ ["fullscreen-error"](event) {
3665
+ this.#satisfyRequest("media-enter-fullscreen-request", event);
3666
+ this.#satisfyRequest("media-exit-fullscreen-request", event);
3667
+ }
3668
+ ["orientation-change"](event) {
3669
+ const isLocked = event.detail.lock;
3670
+ this.#satisfyRequest(
3671
+ isLocked ? "media-orientation-lock-request" : "media-orientation-unlock-request",
3672
+ event
3673
+ );
3674
+ }
3675
+ ["picture-in-picture-change"](event) {
3676
+ const isPiP = event.detail;
3677
+ this.$state.pictureInPicture.set(isPiP);
3678
+ this.#satisfyRequest(isPiP ? "media-enter-pip-request" : "media-exit-pip-request", event);
3679
+ }
3680
+ ["picture-in-picture-error"](event) {
3681
+ this.#satisfyRequest("media-enter-pip-request", event);
3682
+ this.#satisfyRequest("media-exit-pip-request", event);
3683
+ }
3684
+ ["title-change"](event) {
3685
+ if (!event.trigger) return;
3686
+ event.stopImmediatePropagation();
3687
+ this.$state.inferredTitle.set(event.detail);
3688
+ }
3689
+ ["poster-change"](event) {
3690
+ if (!event.trigger) return;
3691
+ event.stopImmediatePropagation();
3692
+ this.$state.inferredPoster.set(event.detail);
3693
+ }
3694
+ }
3695
+
3696
+ class MediaStateSync extends MediaPlayerController {
3697
+ onSetup() {
3698
+ this.#init();
3699
+ const effects = [
3700
+ this.#watchMetadata,
3701
+ this.#watchAutoplay,
3702
+ this.#watchClipStartTime,
3703
+ this.#watchClipEndTime,
3704
+ this.#watchControls,
3705
+ this.#watchCrossOrigin,
3706
+ this.#watchDuration,
3707
+ this.#watchLive,
3708
+ this.#watchLiveEdge,
3709
+ this.#watchLiveTolerance,
3710
+ this.#watchLoop,
3711
+ this.#watchPlaysInline,
3712
+ this.#watchPoster,
3713
+ this.#watchProvidedTypes,
3714
+ this.#watchTitle
3715
+ ];
3716
+ for (const callback of effects) {
3717
+ effect(callback.bind(this));
3718
+ }
3719
+ }
3720
+ #init() {
3721
+ const providedProps = {
3722
+ duration: "providedDuration",
3723
+ loop: "providedLoop",
3724
+ poster: "providedPoster",
3725
+ streamType: "providedStreamType",
3726
+ title: "providedTitle",
3727
+ viewType: "providedViewType"
3728
+ };
3729
+ const skip = /* @__PURE__ */ new Set([
3730
+ "currentTime",
3731
+ "paused",
3732
+ "playbackRate",
3733
+ "volume"
3734
+ ]);
3735
+ for (const prop of Object.keys(this.$props)) {
3736
+ if (skip.has(prop)) continue;
3737
+ this.$state[providedProps[prop] ?? prop]?.set(this.$props[prop]());
3738
+ }
3739
+ this.$state.muted.set(this.$props.muted() || this.$props.volume() === 0);
3740
+ }
3741
+ // Sync "provided" props with internal state. Provided props are used to differentiate from
3742
+ // provider inferred values.
3743
+ #watchProvidedTypes() {
3744
+ const { viewType, streamType, title, poster, loop } = this.$props, $state = this.$state;
3745
+ $state.providedPoster.set(poster());
3746
+ $state.providedStreamType.set(streamType());
3747
+ $state.providedViewType.set(viewType());
3748
+ $state.providedTitle.set(title());
3749
+ $state.providedLoop.set(loop());
3750
+ }
3751
+ #watchLogLevel() {
3752
+ return;
3753
+ }
3754
+ #watchMetadata() {
3755
+ const { artist, artwork } = this.$props;
3756
+ this.$state.artist.set(artist());
3757
+ this.$state.artwork.set(artwork());
3758
+ }
3759
+ #watchTitle() {
3760
+ const { title } = this.$state;
3761
+ this.dispatch("title-change", { detail: title() });
3762
+ }
3763
+ #watchAutoplay() {
3764
+ const autoPlay = this.$props.autoPlay() || this.$props.autoplay();
3765
+ this.$state.autoPlay.set(autoPlay);
3766
+ this.dispatch("auto-play-change", { detail: autoPlay });
3767
+ }
3768
+ #watchLoop() {
3769
+ const loop = this.$state.loop();
3770
+ this.dispatch("loop-change", { detail: loop });
3771
+ }
3772
+ #watchControls() {
3773
+ const controls = this.$props.controls();
3774
+ this.$state.controls.set(controls);
3775
+ }
3776
+ #watchPoster() {
3777
+ const { poster } = this.$state;
3778
+ this.dispatch("poster-change", { detail: poster() });
3779
+ }
3780
+ #watchCrossOrigin() {
3781
+ const crossOrigin = this.$props.crossOrigin() ?? this.$props.crossorigin(), value = crossOrigin === true ? "" : crossOrigin;
3782
+ this.$state.crossOrigin.set(value);
3783
+ }
3784
+ #watchDuration() {
3785
+ const { duration } = this.$props;
3786
+ this.dispatch("media-duration-change-request", {
3787
+ detail: duration()
3788
+ });
3789
+ }
3790
+ #watchPlaysInline() {
3791
+ const inline = this.$props.playsInline() || this.$props.playsinline();
3792
+ this.$state.playsInline.set(inline);
3793
+ this.dispatch("plays-inline-change", { detail: inline });
3794
+ }
3795
+ #watchClipStartTime() {
3796
+ const { clipStartTime } = this.$props;
3797
+ this.dispatch("media-clip-start-change-request", {
3798
+ detail: clipStartTime()
3799
+ });
3800
+ }
3801
+ #watchClipEndTime() {
3802
+ const { clipEndTime } = this.$props;
3803
+ this.dispatch("media-clip-end-change-request", {
3804
+ detail: clipEndTime()
3805
+ });
3806
+ }
3807
+ #watchLive() {
3808
+ this.dispatch("live-change", { detail: this.$state.live() });
3809
+ }
3810
+ #watchLiveTolerance() {
3811
+ this.$state.liveEdgeTolerance.set(this.$props.liveEdgeTolerance());
3812
+ this.$state.minLiveDVRWindow.set(this.$props.minLiveDVRWindow());
3813
+ }
3814
+ #watchLiveEdge() {
3815
+ this.dispatch("live-edge-change", { detail: this.$state.liveEdge() });
3816
+ }
3817
+ }
3818
+
3819
+ const actions = ["play", "pause", "seekforward", "seekbackward", "seekto"];
3820
+ class NavigatorMediaSession extends MediaPlayerController {
3821
+ onConnect() {
3822
+ effect(this.#onMetadataChange.bind(this));
3823
+ effect(this.#onPlaybackStateChange.bind(this));
3824
+ const handleAction = this.#handleAction.bind(this);
3825
+ for (const action of actions) {
3826
+ navigator.mediaSession.setActionHandler(action, handleAction);
3827
+ }
3828
+ onDispose(this.#onDisconnect.bind(this));
3829
+ }
3830
+ #onDisconnect() {
3831
+ for (const action of actions) {
3832
+ navigator.mediaSession.setActionHandler(action, null);
3833
+ }
3834
+ }
3835
+ #onMetadataChange() {
3836
+ const { title, artist, artwork, poster } = this.$state;
3837
+ navigator.mediaSession.metadata = new MediaMetadata({
3838
+ title: title(),
3839
+ artist: artist(),
3840
+ artwork: artwork() ?? [{ src: poster() }]
3841
+ });
3842
+ }
3843
+ #onPlaybackStateChange() {
3844
+ const { canPlay, paused } = this.$state;
3845
+ navigator.mediaSession.playbackState = !canPlay() ? "none" : paused() ? "paused" : "playing";
3846
+ }
3847
+ #handleAction(details) {
3848
+ const trigger = new DOMEvent(`media-session-action`, { detail: details });
3849
+ switch (details.action) {
3850
+ case "play":
3851
+ this.dispatch("media-play-request", { trigger });
3852
+ break;
3853
+ case "pause":
3854
+ this.dispatch("media-pause-request", { trigger });
3855
+ break;
3856
+ case "seekto":
3857
+ case "seekforward":
3858
+ case "seekbackward":
3859
+ this.dispatch("media-seek-request", {
3860
+ detail: isNumber(details.seekTime) ? details.seekTime : this.$state.currentTime() + (details.seekOffset ?? (details.action === "seekforward" ? 10 : -10)),
3861
+ trigger
3862
+ });
3863
+ break;
3864
+ }
3865
+ }
3866
+ }
3867
+
3868
+ class MediaPlayer extends Component {
3869
+ static props = mediaPlayerProps;
3870
+ static state = mediaState;
3871
+ #media;
3872
+ #stateMgr;
3873
+ #requestMgr;
3874
+ canPlayQueue = new RequestQueue();
3875
+ remoteControl;
3876
+ get #provider() {
3877
+ return this.#media.$provider();
3878
+ }
3879
+ get #props() {
3880
+ return this.$props;
3881
+ }
3882
+ constructor() {
3883
+ super();
3884
+ new MediaStateSync();
3885
+ const context = {
3886
+ player: this,
3887
+ qualities: new VideoQualityList(),
3888
+ audioTracks: new AudioTrackList(),
3889
+ storage: null,
3890
+ $provider: signal(null),
3891
+ $providerSetup: signal(false),
3892
+ $props: this.$props,
3893
+ $state: this.$state
3894
+ };
3895
+ context.remote = this.remoteControl = new MediaRemoteControl(
3896
+ void 0
3897
+ );
3898
+ context.remote.setPlayer(this);
3899
+ context.textTracks = new TextTrackList();
3900
+ context.textTracks[TextTrackSymbol.crossOrigin] = this.$state.crossOrigin;
3901
+ context.textRenderers = new TextRenderers(context);
3902
+ context.ariaKeys = {};
3903
+ this.#media = context;
3904
+ provideContext(mediaContext, context);
3905
+ this.orientation = new ScreenOrientationController();
3906
+ new FocusVisibleController();
3907
+ new MediaKeyboardController(context);
3908
+ const request = new MediaRequestContext();
3909
+ this.#stateMgr = new MediaStateManager(request, context);
3910
+ this.#requestMgr = new MediaRequestManager(this.#stateMgr, request, context);
3911
+ context.delegate = new MediaPlayerDelegate(this.#stateMgr.handle.bind(this.#stateMgr), context);
3912
+ context.notify = context.delegate.notify.bind(context.delegate);
3913
+ if (typeof navigator !== "undefined" && "mediaSession" in navigator) {
3914
+ new NavigatorMediaSession();
3915
+ }
3916
+ new MediaLoadController("load", this.startLoading.bind(this));
3917
+ new MediaLoadController("posterLoad", this.startLoadingPoster.bind(this));
3918
+ }
3919
+ onSetup() {
3920
+ this.#setupMediaAttributes();
3921
+ effect(this.#watchCanPlay.bind(this));
3922
+ effect(this.#watchMuted.bind(this));
3923
+ effect(this.#watchPaused.bind(this));
3924
+ effect(this.#watchVolume.bind(this));
3925
+ effect(this.#watchCurrentTime.bind(this));
3926
+ effect(this.#watchPlaysInline.bind(this));
3927
+ effect(this.#watchPlaybackRate.bind(this));
3928
+ }
3929
+ onAttach(el) {
3930
+ el.setAttribute("data-media-player", "");
3931
+ setAttributeIfEmpty(el, "tabindex", "0");
3932
+ setAttributeIfEmpty(el, "role", "region");
3933
+ effect(this.#watchStorage.bind(this));
3934
+ effect(this.#watchTitle.bind(this));
3935
+ effect(this.#watchOrientation.bind(this));
3936
+ listenEvent(el, "find-media-player", this.#onFindPlayer.bind(this));
3937
+ }
3938
+ onConnect(el) {
3939
+ if (IS_IPHONE) setAttribute(el, "data-iphone", "");
3940
+ const pointerQuery = window.matchMedia("(pointer: coarse)");
3941
+ this.#onPointerChange(pointerQuery);
3942
+ pointerQuery.onchange = this.#onPointerChange.bind(this);
3943
+ const resize = new ResizeObserver(animationFrameThrottle(this.#onResize.bind(this)));
3944
+ resize.observe(el);
3945
+ effect(this.#onResize.bind(this));
3946
+ this.dispatch("media-player-connect", {
3947
+ detail: this,
3948
+ bubbles: true,
3949
+ composed: true
3950
+ });
3951
+ onDispose(() => {
3952
+ resize.disconnect();
3953
+ pointerQuery.onchange = null;
3954
+ });
3955
+ }
3956
+ onDestroy() {
3957
+ this.#media.player = null;
3958
+ this.canPlayQueue.reset();
3959
+ }
3960
+ #skipTitleUpdate = false;
3961
+ #watchTitle() {
3962
+ const el = this.$el, { title, live, viewType, providedTitle } = this.$state, isLive = live(), type = uppercaseFirstChar(viewType()), typeText = type !== "Unknown" ? `${isLive ? "Live " : ""}${type}` : isLive ? "Live" : "Media", currentTitle = title();
3963
+ setAttribute(
3964
+ this.el,
3965
+ "aria-label",
3966
+ `${typeText} Player` + (currentTitle ? ` - ${currentTitle}` : "")
3967
+ );
3968
+ if (el?.hasAttribute("title")) {
3969
+ this.#skipTitleUpdate = true;
3970
+ el?.removeAttribute("title");
3971
+ }
3972
+ }
3973
+ #watchOrientation() {
3974
+ const orientation = this.orientation.landscape ? "landscape" : "portrait";
3975
+ this.$state.orientation.set(orientation);
3976
+ setAttribute(this.el, "data-orientation", orientation);
3977
+ this.#onResize();
3978
+ }
3979
+ #watchCanPlay() {
3980
+ if (this.$state.canPlay() && this.#provider) this.canPlayQueue.start();
3981
+ else this.canPlayQueue.stop();
3982
+ }
3983
+ #setupMediaAttributes() {
3984
+ if (MediaPlayer[MEDIA_ATTRIBUTES]) {
3985
+ this.setAttributes(MediaPlayer[MEDIA_ATTRIBUTES]);
3986
+ return;
3987
+ }
3988
+ const $attrs = {
3989
+ "data-load": function() {
3990
+ return this.$props.load();
3991
+ },
3992
+ "data-captions": function() {
3993
+ const track = this.$state.textTrack();
3994
+ return !!track && isTrackCaptionKind(track);
3995
+ },
3996
+ "data-ios-controls": function() {
3997
+ return this.$state.iOSControls();
3998
+ },
3999
+ "data-controls": function() {
4000
+ return this.controls.showing;
4001
+ },
4002
+ "data-buffering": function() {
4003
+ const { canLoad, canPlay, waiting } = this.$state;
4004
+ return canLoad() && (!canPlay() || waiting());
4005
+ },
4006
+ "data-error": function() {
4007
+ const { error } = this.$state;
4008
+ return !!error();
4009
+ },
4010
+ "data-autoplay-error": function() {
4011
+ const { autoPlayError } = this.$state;
4012
+ return !!autoPlayError();
4013
+ }
4014
+ };
4015
+ const alias = {
4016
+ autoPlay: "autoplay",
4017
+ canAirPlay: "can-airplay",
4018
+ canPictureInPicture: "can-pip",
4019
+ pictureInPicture: "pip",
4020
+ playsInline: "playsinline",
4021
+ remotePlaybackState: "remote-state",
4022
+ remotePlaybackType: "remote-type",
4023
+ isAirPlayConnected: "airplay",
4024
+ isGoogleCastConnected: "google-cast"
4025
+ };
4026
+ for (const prop2 of mediaAttributes) {
4027
+ const attrName = "data-" + (alias[prop2] ?? camelToKebabCase(prop2));
4028
+ $attrs[attrName] = function() {
4029
+ return this.$state[prop2]();
4030
+ };
4031
+ }
4032
+ delete $attrs.title;
4033
+ MediaPlayer[MEDIA_ATTRIBUTES] = $attrs;
4034
+ this.setAttributes($attrs);
4035
+ }
4036
+ #onFindPlayer(event) {
4037
+ event.detail(this);
4038
+ }
4039
+ #onResize() {
4040
+ if (!this.el) return;
4041
+ const width = this.el.clientWidth, height = this.el.clientHeight;
4042
+ this.$state.width.set(width);
4043
+ this.$state.height.set(height);
4044
+ setStyle(this.el, "--player-width", width + "px");
4045
+ setStyle(this.el, "--player-height", height + "px");
4046
+ }
4047
+ #onPointerChange(queryList) {
4048
+ const pointer = queryList.matches ? "coarse" : "fine";
4049
+ setAttribute(this.el, "data-pointer", pointer);
4050
+ this.$state.pointer.set(pointer);
4051
+ this.#onResize();
4052
+ }
4053
+ /**
4054
+ * The current media provider.
4055
+ */
4056
+ get provider() {
4057
+ return this.#provider;
4058
+ }
4059
+ /**
4060
+ * Media controls settings.
4061
+ */
4062
+ get controls() {
4063
+ return this.#requestMgr.controls;
4064
+ }
4065
+ set controls(controls) {
4066
+ this.#props.controls.set(controls);
4067
+ }
4068
+ /**
4069
+ * Controls the screen orientation of the current browser window and dispatches orientation
4070
+ * change events on the player.
4071
+ */
4072
+ orientation;
4073
+ /**
4074
+ * The title of the current media.
4075
+ */
4076
+ get title() {
4077
+ return peek(this.$state.title);
4078
+ }
4079
+ set title(newTitle) {
4080
+ if (this.#skipTitleUpdate) {
4081
+ this.#skipTitleUpdate = false;
4082
+ return;
4083
+ }
4084
+ this.#props.title.set(newTitle);
4085
+ }
4086
+ /**
4087
+ * A list of all `VideoQuality` objects representing the set of available video renditions.
4088
+ *
4089
+ * @see {@link https://vidstack.io/docs/player/api/video-quality}
4090
+ */
4091
+ get qualities() {
4092
+ return this.#media.qualities;
4093
+ }
4094
+ /**
4095
+ * A list of all `AudioTrack` objects representing the set of available audio tracks.
4096
+ *
4097
+ * @see {@link https://vidstack.io/docs/player/api/audio-tracks}
4098
+ */
4099
+ get audioTracks() {
4100
+ return this.#media.audioTracks;
4101
+ }
4102
+ /**
4103
+ * A list of all `TextTrack` objects representing the set of available text tracks.
4104
+ *
4105
+ * @see {@link https://vidstack.io/docs/player/api/text-tracks}
4106
+ */
4107
+ get textTracks() {
4108
+ return this.#media.textTracks;
4109
+ }
4110
+ /**
4111
+ * Contains text renderers which are responsible for loading, parsing, and rendering text
4112
+ * tracks.
4113
+ */
4114
+ get textRenderers() {
4115
+ return this.#media.textRenderers;
4116
+ }
4117
+ get duration() {
4118
+ return this.$state.duration();
4119
+ }
4120
+ set duration(duration) {
4121
+ this.#props.duration.set(duration);
4122
+ }
4123
+ get paused() {
4124
+ return peek(this.$state.paused);
4125
+ }
4126
+ set paused(paused) {
4127
+ this.#queuePausedUpdate(paused);
4128
+ }
4129
+ #watchPaused() {
4130
+ this.#queuePausedUpdate(this.$props.paused());
4131
+ }
4132
+ #queuePausedUpdate(paused) {
4133
+ if (paused) {
4134
+ this.canPlayQueue.enqueue("paused", () => this.#requestMgr.pause());
4135
+ } else this.canPlayQueue.enqueue("paused", () => this.#requestMgr.play());
4136
+ }
4137
+ get muted() {
4138
+ return peek(this.$state.muted);
4139
+ }
4140
+ set muted(muted) {
4141
+ this.#queueMutedUpdate(muted);
4142
+ }
4143
+ #watchMuted() {
4144
+ this.#queueMutedUpdate(this.$props.muted());
4145
+ }
4146
+ #queueMutedUpdate(muted) {
4147
+ this.canPlayQueue.enqueue("muted", () => {
4148
+ if (this.#provider) this.#provider.setMuted(muted);
4149
+ });
4150
+ }
4151
+ get currentTime() {
4152
+ return peek(this.$state.currentTime);
4153
+ }
4154
+ set currentTime(time) {
4155
+ this.#queueCurrentTimeUpdate(time);
4156
+ }
4157
+ #watchCurrentTime() {
4158
+ this.#queueCurrentTimeUpdate(this.$props.currentTime());
4159
+ }
4160
+ #queueCurrentTimeUpdate(time) {
4161
+ this.canPlayQueue.enqueue("currentTime", () => {
4162
+ const { currentTime } = this.$state;
4163
+ if (time === peek(currentTime)) return;
4164
+ peek(() => {
4165
+ if (!this.#provider) return;
4166
+ const boundedTime = boundTime(time, this.$state);
4167
+ if (Number.isFinite(boundedTime)) {
4168
+ this.#provider.setCurrentTime(boundedTime);
4169
+ }
4170
+ });
4171
+ });
4172
+ }
4173
+ get volume() {
4174
+ return peek(this.$state.volume);
4175
+ }
4176
+ set volume(volume) {
4177
+ this.#queueVolumeUpdate(volume);
4178
+ }
4179
+ #watchVolume() {
4180
+ this.#queueVolumeUpdate(this.$props.volume());
4181
+ }
4182
+ #queueVolumeUpdate(volume) {
4183
+ const clampedVolume = clampNumber(0, volume, 1);
4184
+ this.canPlayQueue.enqueue("volume", () => {
4185
+ if (this.#provider) this.#provider.setVolume(clampedVolume);
4186
+ });
4187
+ }
4188
+ get playbackRate() {
4189
+ return peek(this.$state.playbackRate);
4190
+ }
4191
+ set playbackRate(rate) {
4192
+ this.#queuePlaybackRateUpdate(rate);
4193
+ }
4194
+ #watchPlaybackRate() {
4195
+ this.#queuePlaybackRateUpdate(this.$props.playbackRate());
4196
+ }
4197
+ #queuePlaybackRateUpdate(rate) {
4198
+ this.canPlayQueue.enqueue("rate", () => {
4199
+ if (this.#provider) this.#provider.setPlaybackRate?.(rate);
4200
+ });
4201
+ }
4202
+ #watchPlaysInline() {
4203
+ this.#queuePlaysInlineUpdate(this.$props.playsInline());
4204
+ }
4205
+ #queuePlaysInlineUpdate(inline) {
4206
+ this.canPlayQueue.enqueue("playsinline", () => {
4207
+ if (this.#provider) this.#provider.setPlaysInline?.(inline);
4208
+ });
4209
+ }
4210
+ #watchStorage() {
4211
+ let storageValue = this.$props.storage(), storage = isString(storageValue) ? new LocalMediaStorage() : storageValue;
4212
+ if (storage?.onChange) {
4213
+ const { source } = this.$state, playerId = isString(storageValue) ? storageValue : this.el?.id, mediaId = computed(this.#computeMediaId.bind(this));
4214
+ effect(() => storage.onChange(source(), mediaId(), playerId || void 0));
4215
+ }
4216
+ this.#media.storage = storage;
4217
+ this.#media.textTracks.setStorage(storage);
4218
+ onDispose(() => {
4219
+ storage?.onDestroy?.();
4220
+ this.#media.storage = null;
4221
+ this.#media.textTracks.setStorage(null);
4222
+ });
4223
+ }
4224
+ #computeMediaId() {
4225
+ const { clipStartTime, clipEndTime } = this.$props, { source } = this.$state, src = source();
4226
+ return src.src ? `${src.src}:${clipStartTime()}:${clipEndTime()}` : null;
4227
+ }
4228
+ /**
4229
+ * Begins/resumes playback of the media. If this method is called programmatically before the
4230
+ * user has interacted with the player, the promise may be rejected subject to the browser's
4231
+ * autoplay policies. This method will throw if called before media is ready for playback.
4232
+ *
4233
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/play}
4234
+ */
4235
+ async play(trigger) {
4236
+ return this.#requestMgr.play(trigger);
4237
+ }
4238
+ /**
4239
+ * Pauses playback of the media. This method will throw if called before media is ready for
4240
+ * playback.
4241
+ *
4242
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/pause}
4243
+ */
4244
+ async pause(trigger) {
4245
+ return this.#requestMgr.pause(trigger);
4246
+ }
4247
+ /**
4248
+ * Attempts to display the player in fullscreen. The promise will resolve if successful, and
4249
+ * reject if not. This method will throw if any fullscreen API is _not_ currently available.
4250
+ *
4251
+ * @see {@link https://vidstack.io/docs/player/api/fullscreen}
4252
+ */
4253
+ async enterFullscreen(target, trigger) {
4254
+ return this.#requestMgr.enterFullscreen(target, trigger);
4255
+ }
4256
+ /**
4257
+ * Attempts to display the player inline by exiting fullscreen. This method will throw if any
4258
+ * fullscreen API is _not_ currently available.
4259
+ *
4260
+ * @see {@link https://vidstack.io/docs/player/api/fullscreen}
4261
+ */
4262
+ async exitFullscreen(target, trigger) {
4263
+ return this.#requestMgr.exitFullscreen(target, trigger);
4264
+ }
4265
+ /**
4266
+ * Attempts to display the player in picture-in-picture mode. This method will throw if PIP is
4267
+ * not supported. This method will also return a `PictureInPictureWindow` if the current
4268
+ * provider supports it.
4269
+ *
4270
+ * @see {@link https://vidstack.io/docs/player/api/picture-in-picture}
4271
+ */
4272
+ enterPictureInPicture(trigger) {
4273
+ return this.#requestMgr.enterPictureInPicture(trigger);
4274
+ }
4275
+ /**
4276
+ * Attempts to display the player in inline by exiting picture-in-picture mode. This method
4277
+ * will throw if not supported.
4278
+ *
4279
+ * @see {@link https://vidstack.io/docs/player/api/picture-in-picture}
4280
+ */
4281
+ exitPictureInPicture(trigger) {
4282
+ return this.#requestMgr.exitPictureInPicture(trigger);
4283
+ }
4284
+ /**
4285
+ * Sets the current time to the live edge (i.e., `duration`). This is a no-op for non-live
4286
+ * streams and will throw if called before media is ready for playback.
4287
+ *
4288
+ * @see {@link https://vidstack.io/docs/player/api/live}
4289
+ */
4290
+ seekToLiveEdge(trigger) {
4291
+ this.#requestMgr.seekToLiveEdge(trigger);
4292
+ }
4293
+ /**
4294
+ * Called when media can begin loading. Calling this method will trigger the initial provider
4295
+ * loading process. Calling it more than once has no effect.
4296
+ *
4297
+ * @see {@link https://vidstack.io/docs/player/core-concepts/loading#load-strategies}
4298
+ */
4299
+ startLoading(trigger) {
4300
+ this.#media.notify("can-load", void 0, trigger);
4301
+ }
4302
+ /**
4303
+ * Called when the poster image can begin loading. Calling it more than once has no effect.
4304
+ *
4305
+ * @see {@link https://vidstack.io/docs/player/core-concepts/loading#load-strategies}
4306
+ */
4307
+ startLoadingPoster(trigger) {
4308
+ this.#media.notify("can-load-poster", void 0, trigger);
4309
+ }
4310
+ /**
4311
+ * Request Apple AirPlay picker to open.
4312
+ */
4313
+ requestAirPlay(trigger) {
4314
+ return this.#requestMgr.requestAirPlay(trigger);
4315
+ }
4316
+ /**
4317
+ * Request Google Cast device picker to open. The Google Cast framework will be loaded if it
4318
+ * hasn't yet.
4319
+ */
4320
+ requestGoogleCast(trigger) {
4321
+ return this.#requestMgr.requestGoogleCast(trigger);
4322
+ }
4323
+ /**
4324
+ * Set the audio gain, amplifying volume and enabling a maximum volume above 100%.
4325
+ *
4326
+ * @see {@link https://vidstack.io/docs/player/api/audio-gain}
4327
+ */
4328
+ setAudioGain(gain, trigger) {
4329
+ return this.#requestMgr.setAudioGain(gain, trigger);
4330
+ }
4331
+ destroy() {
4332
+ super.destroy();
4333
+ this.#media.remote.setPlayer(null);
4334
+ this.dispatch("destroy");
4335
+ }
4336
+ }
4337
+ const mediaplayer__proto = MediaPlayer.prototype;
4338
+ prop(mediaplayer__proto, "canPlayQueue");
4339
+ prop(mediaplayer__proto, "remoteControl");
4340
+ prop(mediaplayer__proto, "provider");
4341
+ prop(mediaplayer__proto, "controls");
4342
+ prop(mediaplayer__proto, "orientation");
4343
+ prop(mediaplayer__proto, "title");
4344
+ prop(mediaplayer__proto, "qualities");
4345
+ prop(mediaplayer__proto, "audioTracks");
4346
+ prop(mediaplayer__proto, "textTracks");
4347
+ prop(mediaplayer__proto, "textRenderers");
4348
+ prop(mediaplayer__proto, "duration");
4349
+ prop(mediaplayer__proto, "paused");
4350
+ prop(mediaplayer__proto, "muted");
4351
+ prop(mediaplayer__proto, "currentTime");
4352
+ prop(mediaplayer__proto, "volume");
4353
+ prop(mediaplayer__proto, "playbackRate");
4354
+ method(mediaplayer__proto, "play");
4355
+ method(mediaplayer__proto, "pause");
4356
+ method(mediaplayer__proto, "enterFullscreen");
4357
+ method(mediaplayer__proto, "exitFullscreen");
4358
+ method(mediaplayer__proto, "enterPictureInPicture");
4359
+ method(mediaplayer__proto, "exitPictureInPicture");
4360
+ method(mediaplayer__proto, "seekToLiveEdge");
4361
+ method(mediaplayer__proto, "startLoading");
4362
+ method(mediaplayer__proto, "startLoadingPoster");
4363
+ method(mediaplayer__proto, "requestAirPlay");
4364
+ method(mediaplayer__proto, "requestGoogleCast");
4365
+ method(mediaplayer__proto, "setAudioGain");
4366
+
4367
+ function resolveStreamTypeFromDASHManifest(manifestSrc, requestInit) {
4368
+ return fetch(manifestSrc, requestInit).then((res) => res.text()).then((manifest) => {
4369
+ return /type="static"/.test(manifest) ? "on-demand" : "live";
4370
+ });
4371
+ }
4372
+ function resolveStreamTypeFromHLSManifest(manifestSrc, requestInit) {
4373
+ return fetch(manifestSrc, requestInit).then((res) => res.text()).then((manifest) => {
4374
+ const renditionURI = resolveHLSRenditionURI(manifest);
4375
+ if (renditionURI) {
4376
+ return resolveStreamTypeFromHLSManifest(
4377
+ /^https?:/.test(renditionURI) ? renditionURI : new URL(renditionURI, manifestSrc).href,
4378
+ requestInit
4379
+ );
4380
+ }
4381
+ const streamType = /EXT-X-PLAYLIST-TYPE:\s*VOD/.test(manifest) ? "on-demand" : "live";
4382
+ if (streamType === "live" && resolveTargetDuration(manifest) >= 10 && (/#EXT-X-DVR-ENABLED:\s*true/.test(manifest) || manifest.includes("#EXT-X-DISCONTINUITY"))) {
4383
+ return "live:dvr";
4384
+ }
4385
+ return streamType;
4386
+ });
4387
+ }
4388
+ function resolveHLSRenditionURI(manifest) {
4389
+ const matches = manifest.match(/#EXT-X-STREAM-INF:[^\n]+(\n[^\n]+)*/g);
4390
+ return matches ? matches[0].split("\n")[1].trim() : null;
4391
+ }
4392
+ function resolveTargetDuration(manifest) {
4393
+ const lines = manifest.split("\n");
4394
+ for (const line of lines) {
4395
+ if (line.startsWith("#EXT-X-TARGETDURATION")) {
4396
+ const duration = parseFloat(line.split(":")[1]);
4397
+ if (!isNaN(duration)) {
4398
+ return duration;
4399
+ }
4400
+ }
4401
+ }
4402
+ return -1;
4403
+ }
4404
+
4405
+ const sourceTypes = /* @__PURE__ */ new Map();
4406
+ class SourceSelection {
4407
+ #initialize = false;
4408
+ #loaders;
4409
+ #domSources;
4410
+ #media;
4411
+ #loader;
4412
+ constructor(domSources, media, loader, customLoaders = []) {
4413
+ this.#domSources = domSources;
4414
+ this.#media = media;
4415
+ this.#loader = loader;
4416
+ const DASH_LOADER = new DASHProviderLoader(), HLS_LOADER = new HLSProviderLoader(), VIDEO_LOADER = new VideoProviderLoader(), AUDIO_LOADER = new AudioProviderLoader(), YOUTUBE_LOADER = new YouTubeProviderLoader(), VIMEO_LOADER = new VimeoProviderLoader(), EMBED_LOADERS = [YOUTUBE_LOADER, VIMEO_LOADER];
4417
+ this.#loaders = computed(() => {
4418
+ const remoteLoader = media.$state.remotePlaybackLoader();
4419
+ const loaders = media.$props.preferNativeHLS() ? [VIDEO_LOADER, AUDIO_LOADER, DASH_LOADER, HLS_LOADER, ...EMBED_LOADERS, ...customLoaders] : [HLS_LOADER, VIDEO_LOADER, AUDIO_LOADER, DASH_LOADER, ...EMBED_LOADERS, ...customLoaders];
4420
+ return remoteLoader ? [remoteLoader, ...loaders] : loaders;
4421
+ });
4422
+ const { $state } = media;
4423
+ $state.sources.set(normalizeSrc(media.$props.src()));
4424
+ for (const src of $state.sources()) {
4425
+ const loader2 = this.#loaders().find((loader3) => loader3.canPlay(src));
4426
+ if (!loader2) continue;
4427
+ const mediaType = loader2.mediaType(src);
4428
+ media.$state.source.set(src);
4429
+ media.$state.mediaType.set(mediaType);
4430
+ media.$state.inferredViewType.set(mediaType);
4431
+ this.#loader.set(loader2);
4432
+ this.#initialize = true;
4433
+ break;
4434
+ }
4435
+ }
4436
+ connect() {
4437
+ const loader = this.#loader();
4438
+ if (this.#initialize) {
4439
+ this.#notifySourceChange(this.#media.$state.source(), loader);
4440
+ this.#notifyLoaderChange(loader);
4441
+ this.#initialize = false;
4442
+ }
4443
+ effect(this.#onSourcesChange.bind(this));
4444
+ effect(this.#onSourceChange.bind(this));
4445
+ effect(this.#onSetup.bind(this));
4446
+ effect(this.#onLoadSource.bind(this));
4447
+ effect(this.#onLoadPoster.bind(this));
4448
+ }
4449
+ #onSourcesChange() {
4450
+ this.#media.notify("sources-change", [
4451
+ ...normalizeSrc(this.#media.$props.src()),
4452
+ ...this.#domSources()
4453
+ ]);
4454
+ }
4455
+ #onSourceChange() {
4456
+ const { $state } = this.#media;
4457
+ const sources = $state.sources(), currentSource = peek($state.source), newSource = this.#findNewSource(currentSource, sources), noMatch = sources[0]?.src && !newSource.src && !newSource.type;
4458
+ if (noMatch) {
4459
+ const { crossOrigin } = $state, credentials = getRequestCredentials(crossOrigin()), abort = new AbortController();
4460
+ Promise.all(
4461
+ sources.map(
4462
+ (source) => isString(source.src) && source.type === "?" ? fetch(source.src, {
4463
+ method: "HEAD",
4464
+ credentials,
4465
+ signal: abort.signal
4466
+ }).then((res) => {
4467
+ source.type = res.headers.get("content-type") || "??";
4468
+ sourceTypes.set(source.src, source.type);
4469
+ return source;
4470
+ }).catch(() => source) : source
4471
+ )
4472
+ ).then((sources2) => {
4473
+ if (abort.signal.aborted) return;
4474
+ const newSource2 = this.#findNewSource(peek($state.source), sources2);
4475
+ tick();
4476
+ if (!newSource2.src) {
4477
+ this.#media.notify("error", {
4478
+ message: "Failed to load resource.",
4479
+ code: 4
4480
+ });
4481
+ }
4482
+ });
4483
+ return () => abort.abort();
4484
+ }
4485
+ tick();
4486
+ }
4487
+ #findNewSource(currentSource, sources) {
4488
+ let newSource = { src: "", type: "" }, newLoader = null, triggerEvent = new DOMEvent("sources-change", { detail: { sources } }), loaders = this.#loaders(), { started, paused, currentTime, quality, savedState } = this.#media.$state;
4489
+ for (const src of sources) {
4490
+ const loader = loaders.find((loader2) => loader2.canPlay(src));
4491
+ if (loader) {
4492
+ newSource = src;
4493
+ newLoader = loader;
4494
+ break;
4495
+ }
4496
+ }
4497
+ if (isVideoQualitySrc(newSource)) {
4498
+ const currentQuality = quality(), sourceQuality = sources.find((s) => s.src === currentQuality?.src);
4499
+ if (peek(started)) {
4500
+ savedState.set({
4501
+ paused: peek(paused),
4502
+ currentTime: peek(currentTime)
4503
+ });
4504
+ } else {
4505
+ savedState.set(null);
4506
+ }
4507
+ if (sourceQuality) {
4508
+ newSource = sourceQuality;
4509
+ triggerEvent = new DOMEvent("quality-change", {
4510
+ detail: { quality: currentQuality }
4511
+ });
4512
+ }
4513
+ }
4514
+ if (!isSameSrc(currentSource, newSource)) {
4515
+ this.#notifySourceChange(newSource, newLoader, triggerEvent);
4516
+ }
4517
+ if (newLoader !== peek(this.#loader)) {
4518
+ this.#notifyLoaderChange(newLoader, triggerEvent);
4519
+ }
4520
+ return newSource;
4521
+ }
4522
+ #notifySourceChange(src, loader, trigger) {
4523
+ this.#media.notify("source-change", src, trigger);
4524
+ this.#media.notify("media-type-change", loader?.mediaType(src) || "unknown", trigger);
4525
+ }
4526
+ #notifyLoaderChange(loader, trigger) {
4527
+ this.#media.$providerSetup.set(false);
4528
+ this.#media.notify("provider-change", null, trigger);
4529
+ loader && peek(() => loader.preconnect?.(this.#media));
4530
+ this.#loader.set(loader);
4531
+ this.#media.notify("provider-loader-change", loader, trigger);
4532
+ }
4533
+ #onSetup() {
4534
+ const provider = this.#media.$provider();
4535
+ if (!provider || peek(this.#media.$providerSetup)) return;
4536
+ if (this.#media.$state.canLoad()) {
4537
+ scoped(() => provider.setup(), provider.scope);
4538
+ this.#media.$providerSetup.set(true);
4539
+ return;
4540
+ }
4541
+ peek(() => provider.preconnect?.());
4542
+ }
4543
+ #onLoadSource() {
4544
+ if (!this.#media.$providerSetup()) return;
4545
+ const provider = this.#media.$provider(), source = this.#media.$state.source(), crossOrigin = peek(this.#media.$state.crossOrigin), preferNativeHLS = peek(this.#media.$props.preferNativeHLS);
4546
+ if (isSameSrc(provider?.currentSrc, source)) {
4547
+ return;
4548
+ }
4549
+ if (this.#media.$state.canLoad()) {
4550
+ const abort = new AbortController();
4551
+ if (isHLSSrc(source)) {
4552
+ if (preferNativeHLS || !isHLSSupported()) {
4553
+ resolveStreamTypeFromHLSManifest(source.src, {
4554
+ credentials: getRequestCredentials(crossOrigin),
4555
+ signal: abort.signal
4556
+ }).then((streamType) => {
4557
+ this.#media.notify("stream-type-change", streamType);
4558
+ }).catch(noop);
4559
+ }
4560
+ } else if (isDASHSrc(source)) {
4561
+ resolveStreamTypeFromDASHManifest(source.src, {
4562
+ credentials: getRequestCredentials(crossOrigin),
4563
+ signal: abort.signal
4564
+ }).then((streamType) => {
4565
+ this.#media.notify("stream-type-change", streamType);
4566
+ }).catch(noop);
4567
+ } else {
4568
+ this.#media.notify("stream-type-change", "on-demand");
4569
+ }
4570
+ peek(() => {
4571
+ const preload = peek(this.#media.$state.preload);
4572
+ return provider?.loadSource(source, preload).catch((error) => {
4573
+ });
4574
+ });
4575
+ return () => abort.abort();
4576
+ }
4577
+ try {
4578
+ isString(source.src) && preconnect(new URL(source.src).origin);
4579
+ } catch (error) {
4580
+ }
4581
+ }
4582
+ #onLoadPoster() {
4583
+ const loader = this.#loader(), { providedPoster, source, canLoadPoster } = this.#media.$state;
4584
+ if (!loader || !loader.loadPoster || !source() || !canLoadPoster() || providedPoster()) return;
4585
+ const abort = new AbortController(), trigger = new DOMEvent("source-change", { detail: source });
4586
+ loader.loadPoster(source(), this.#media, abort).then((url) => {
4587
+ this.#media.notify("poster-change", url || "", trigger);
4588
+ }).catch(() => {
4589
+ this.#media.notify("poster-change", "", trigger);
4590
+ });
4591
+ return () => {
4592
+ abort.abort();
4593
+ };
4594
+ }
4595
+ }
4596
+ function normalizeSrc(src) {
4597
+ return (isArray(src) ? src : [src]).map((src2) => {
4598
+ if (isString(src2)) {
4599
+ return { src: src2, type: inferType(src2) };
4600
+ } else {
4601
+ return { ...src2, type: inferType(src2.src, src2.type) };
4602
+ }
4603
+ });
4604
+ }
4605
+ function inferType(src, type) {
4606
+ if (isString(type) && type.length) {
4607
+ return type;
4608
+ } else if (isString(src) && sourceTypes.has(src)) {
4609
+ return sourceTypes.get(src);
4610
+ } else if (!type && isHLSSrc({ src, type: "" })) {
4611
+ return "application/x-mpegurl";
4612
+ } else if (!type && isDASHSrc({ src, type: "" })) {
4613
+ return "application/dash+xml";
4614
+ } else if (!isString(src) || src.startsWith("blob:")) {
4615
+ return "video/object";
4616
+ } else if (src.includes("youtube") || src.includes("youtu.be")) {
4617
+ return "video/youtube";
4618
+ } else if (src.includes("vimeo") && !src.includes("progressive_redirect") && !src.includes(".m3u8")) {
4619
+ return "video/vimeo";
4620
+ }
4621
+ return "?";
4622
+ }
4623
+ function isSameSrc(a, b) {
4624
+ return a?.src === b?.src && a?.type === b?.type;
4625
+ }
4626
+
4627
+ class Tracks {
4628
+ #domTracks;
4629
+ #media;
4630
+ #prevTracks = [];
4631
+ constructor(domTracks, media) {
4632
+ this.#domTracks = domTracks;
4633
+ this.#media = media;
4634
+ effect(this.#onTracksChange.bind(this));
4635
+ }
4636
+ #onTracksChange() {
4637
+ const newTracks = this.#domTracks();
4638
+ for (const oldTrack of this.#prevTracks) {
4639
+ if (!newTracks.some((t) => t.id === oldTrack.id)) {
4640
+ const track = oldTrack.id && this.#media.textTracks.getById(oldTrack.id);
4641
+ if (track) this.#media.textTracks.remove(track);
4642
+ }
4643
+ }
4644
+ for (const newTrack of newTracks) {
4645
+ const id = newTrack.id || TextTrack.createId(newTrack);
4646
+ if (!this.#media.textTracks.getById(id)) {
4647
+ newTrack.id = id;
4648
+ this.#media.textTracks.add(newTrack);
4649
+ }
4650
+ }
4651
+ this.#prevTracks = newTracks;
4652
+ }
4653
+ }
4654
+
4655
+ class MediaProvider extends Component {
4656
+ static props = {
4657
+ loaders: []
4658
+ };
4659
+ static state = new State({
4660
+ loader: null
4661
+ });
4662
+ #media;
4663
+ #sources;
4664
+ #domSources = signal([]);
4665
+ #domTracks = signal([]);
4666
+ #loader = null;
4667
+ onSetup() {
4668
+ this.#media = useMediaContext();
4669
+ this.#sources = new SourceSelection(
4670
+ this.#domSources,
4671
+ this.#media,
4672
+ this.$state.loader,
4673
+ this.$props.loaders()
4674
+ );
4675
+ }
4676
+ onAttach(el) {
4677
+ el.setAttribute("data-media-provider", "");
4678
+ }
4679
+ onConnect(el) {
4680
+ this.#sources.connect();
4681
+ new Tracks(this.#domTracks, this.#media);
4682
+ const resize = new ResizeObserver(animationFrameThrottle(this.#onResize.bind(this)));
4683
+ resize.observe(el);
4684
+ const mutations = new MutationObserver(this.#onMutation.bind(this));
4685
+ mutations.observe(el, { attributes: true, childList: true });
4686
+ this.#onResize();
4687
+ this.#onMutation();
4688
+ onDispose(() => {
4689
+ resize.disconnect();
4690
+ mutations.disconnect();
4691
+ });
4692
+ }
4693
+ #loadRafId = -1;
4694
+ load(target) {
4695
+ target?.setAttribute("aria-hidden", "true");
4696
+ window.cancelAnimationFrame(this.#loadRafId);
4697
+ this.#loadRafId = requestAnimationFrame(() => this.#runLoader(target));
4698
+ onDispose(() => {
4699
+ window.cancelAnimationFrame(this.#loadRafId);
4700
+ });
4701
+ }
4702
+ #runLoader(target) {
4703
+ if (!this.scope) return;
4704
+ const loader = this.$state.loader(), { $provider } = this.#media;
4705
+ if (this.#loader === loader && loader?.target === target && peek($provider)) return;
4706
+ this.#destroyProvider();
4707
+ this.#loader = loader;
4708
+ if (loader) loader.target = target || null;
4709
+ if (!loader || !target) return;
4710
+ loader.load(this.#media).then((provider) => {
4711
+ if (!this.scope) return;
4712
+ if (peek(this.$state.loader) !== loader) return;
4713
+ this.#media.notify("provider-change", provider);
4714
+ });
4715
+ }
4716
+ onDestroy() {
4717
+ this.#loader = null;
4718
+ this.#destroyProvider();
4719
+ }
4720
+ #destroyProvider() {
4721
+ this.#media?.notify("provider-change", null);
4722
+ }
4723
+ #onResize() {
4724
+ if (!this.el) return;
4725
+ const { player, $state } = this.#media, width = this.el.offsetWidth, height = this.el.offsetHeight;
4726
+ if (!player) return;
4727
+ $state.mediaWidth.set(width);
4728
+ $state.mediaHeight.set(height);
4729
+ if (player.el) {
4730
+ setStyle(player.el, "--media-width", width + "px");
4731
+ setStyle(player.el, "--media-height", height + "px");
4732
+ }
4733
+ }
4734
+ #onMutation() {
4735
+ const sources = [], tracks = [], children = this.el.children;
4736
+ for (const el of children) {
4737
+ if (el.hasAttribute("data-vds")) continue;
4738
+ if (el instanceof HTMLSourceElement) {
4739
+ const src = {
4740
+ id: el.id,
4741
+ src: el.src,
4742
+ type: el.type
4743
+ };
4744
+ for (const prop of ["id", "src", "width", "height", "bitrate", "codec"]) {
4745
+ const value = el.getAttribute(`data-${prop}`);
4746
+ if (isString(value)) src[prop] = /id|src|codec/.test(prop) ? value : Number(value);
4747
+ }
4748
+ sources.push(src);
4749
+ } else if (el instanceof HTMLTrackElement) {
4750
+ const track = {
4751
+ src: el.src,
4752
+ kind: el.track.kind,
4753
+ language: el.srclang,
4754
+ label: el.label,
4755
+ default: el.default,
4756
+ type: el.getAttribute("data-type")
4757
+ };
4758
+ tracks.push({
4759
+ id: el.id || TextTrack.createId(track),
4760
+ ...track
4761
+ });
4762
+ }
4763
+ }
4764
+ this.#domSources.set(sources);
4765
+ this.#domTracks.set(tracks);
4766
+ tick();
4767
+ }
4768
+ }
4769
+ const mediaprovider__proto = MediaProvider.prototype;
4770
+ method(mediaprovider__proto, "load");
4771
+
4772
+ export { AudioProviderLoader, AudioTrackList, DASHProviderLoader, FullscreenController, HLSProviderLoader, List, LocalMediaStorage, MEDIA_KEY_SHORTCUTS, MediaControls, MediaPlayer, MediaProvider, MediaRemoteControl, ScreenOrientationController, TextRenderers, TextTrackList, VideoProviderLoader, VideoQualityList, VimeoProviderLoader, YouTubeProviderLoader, boundTime, canFullscreen, isAudioProvider, isDASHProvider, isGoogleCastProvider, isHLSProvider, isHTMLAudioElement, isHTMLIFrameElement, isHTMLMediaElement, isHTMLVideoElement, isVideoProvider, isVideoQualitySrc, isVimeoProvider, isYouTubeProvider, mediaState, softResetMediaState };