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