@f8team/reel-core 1.0.3 → 1.0.5
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/CHANGELOG.md +16 -0
- package/dist/index.cjs +443 -79
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +43 -16
- package/dist/index.d.ts +43 -16
- package/dist/index.js +442 -80
- package/dist/index.js.map +1 -1
- package/package.json +4 -1
package/dist/index.cjs
CHANGED
|
@@ -7,6 +7,7 @@ var RETRYABLE_BY_DEFAULT = {
|
|
|
7
7
|
decode: false,
|
|
8
8
|
unauthorized: false,
|
|
9
9
|
unsupported: false,
|
|
10
|
+
"hls-fragment-loop": true,
|
|
10
11
|
internal: false
|
|
11
12
|
};
|
|
12
13
|
function createPlayerError(input) {
|
|
@@ -213,12 +214,58 @@ function createControlsRegistry() {
|
|
|
213
214
|
}
|
|
214
215
|
return { contribute, snapshot, clear };
|
|
215
216
|
}
|
|
216
|
-
function
|
|
217
|
+
function createSourceTransformRegistry() {
|
|
218
|
+
const transforms = /* @__PURE__ */ new Set();
|
|
219
|
+
function register(transform) {
|
|
220
|
+
transforms.add(transform);
|
|
221
|
+
return () => transforms.delete(transform);
|
|
222
|
+
}
|
|
223
|
+
function apply(source) {
|
|
224
|
+
let current = source;
|
|
225
|
+
for (const transform of transforms) {
|
|
226
|
+
current = transform(current);
|
|
227
|
+
}
|
|
228
|
+
return current;
|
|
229
|
+
}
|
|
230
|
+
return { register, apply };
|
|
231
|
+
}
|
|
232
|
+
function createMediaAttachmentRegistry() {
|
|
233
|
+
let current = null;
|
|
234
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
235
|
+
function get() {
|
|
236
|
+
return current;
|
|
237
|
+
}
|
|
238
|
+
function set(attachment) {
|
|
239
|
+
current = attachment;
|
|
240
|
+
for (const listener of listeners) listener(current);
|
|
241
|
+
}
|
|
242
|
+
function subscribe(listener) {
|
|
243
|
+
listeners.add(listener);
|
|
244
|
+
if (current) listener(current);
|
|
245
|
+
return () => listeners.delete(listener);
|
|
246
|
+
}
|
|
247
|
+
return { get, set, subscribe };
|
|
248
|
+
}
|
|
249
|
+
function createPluginHost({
|
|
250
|
+
commands,
|
|
251
|
+
controls,
|
|
252
|
+
store,
|
|
253
|
+
bus,
|
|
254
|
+
sourceTransforms = createSourceTransformRegistry(),
|
|
255
|
+
mediaAttachments = createMediaAttachmentRegistry()
|
|
256
|
+
}) {
|
|
217
257
|
return {
|
|
218
258
|
controls: {
|
|
219
|
-
contribute:
|
|
259
|
+
contribute: controls.contribute
|
|
220
260
|
},
|
|
221
261
|
commands,
|
|
262
|
+
sources: {
|
|
263
|
+
transform: sourceTransforms.register
|
|
264
|
+
},
|
|
265
|
+
media: {
|
|
266
|
+
get: mediaAttachments.get,
|
|
267
|
+
subscribe: mediaAttachments.subscribe
|
|
268
|
+
},
|
|
222
269
|
store,
|
|
223
270
|
emit: (event, payload) => {
|
|
224
271
|
bus.emit(event, payload);
|
|
@@ -227,6 +274,21 @@ function createPluginHost({ commands, controls, store, bus }) {
|
|
|
227
274
|
}
|
|
228
275
|
|
|
229
276
|
// src/util/url.ts
|
|
277
|
+
function areSourceDescriptorsEquivalent(previous, next) {
|
|
278
|
+
if (previous === next) return true;
|
|
279
|
+
if (!previous || !next) return false;
|
|
280
|
+
const previousThumbnails = previous.thumbnails;
|
|
281
|
+
const nextThumbnails = next.thumbnails;
|
|
282
|
+
return previous.src === next.src && (previous.type || "auto") === (next.type || "auto") && previous.withCredentials === next.withCredentials && previous.tracks === next.tracks && (previousThumbnails === nextThumbnails || previousThumbnails?.src === nextThumbnails?.src && previousThumbnails?.withCredentials === nextThumbnails?.withCredentials);
|
|
283
|
+
}
|
|
284
|
+
function isSafeMediaUrl(input) {
|
|
285
|
+
if (!input?.trim()) return false;
|
|
286
|
+
try {
|
|
287
|
+
return /^(https?|blob|data):$/.test(new URL(input, "https://reel.invalid").protocol);
|
|
288
|
+
} catch {
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
230
292
|
function looksLikeHls(input) {
|
|
231
293
|
if (!input) return false;
|
|
232
294
|
const lower = input.toLowerCase();
|
|
@@ -266,6 +328,54 @@ function resolveWithCredentials(policy, url) {
|
|
|
266
328
|
}
|
|
267
329
|
|
|
268
330
|
// src/sources/hls.ts
|
|
331
|
+
var REPEATED_FRAGMENT_LOAD_LIMIT = 4;
|
|
332
|
+
var MEDIA_RECOVERY_LIMIT = 1;
|
|
333
|
+
function makeLoadPolicy(options) {
|
|
334
|
+
return {
|
|
335
|
+
default: {
|
|
336
|
+
maxTimeToFirstByteMs: options.maxTimeToFirstByteMs,
|
|
337
|
+
maxLoadTimeMs: options.maxLoadTimeMs,
|
|
338
|
+
timeoutRetry: options.timeoutRetry,
|
|
339
|
+
errorRetry: options.errorRetry
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
function makeLinearRetry(maxNumRetry, retryDelayMs, maxRetryDelayMs) {
|
|
344
|
+
return {
|
|
345
|
+
maxNumRetry,
|
|
346
|
+
retryDelayMs,
|
|
347
|
+
maxRetryDelayMs,
|
|
348
|
+
backoff: "linear"
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
function makeHlsLoadPolicies() {
|
|
352
|
+
return {
|
|
353
|
+
manifestLoadPolicy: makeLoadPolicy({
|
|
354
|
+
maxTimeToFirstByteMs: 1e4,
|
|
355
|
+
maxLoadTimeMs: 2e4,
|
|
356
|
+
timeoutRetry: makeLinearRetry(1, 0, 0),
|
|
357
|
+
errorRetry: makeLinearRetry(1, 1e3, 8e3)
|
|
358
|
+
}),
|
|
359
|
+
playlistLoadPolicy: makeLoadPolicy({
|
|
360
|
+
maxTimeToFirstByteMs: 1e4,
|
|
361
|
+
maxLoadTimeMs: 2e4,
|
|
362
|
+
timeoutRetry: makeLinearRetry(1, 0, 0),
|
|
363
|
+
errorRetry: makeLinearRetry(2, 1e3, 8e3)
|
|
364
|
+
}),
|
|
365
|
+
keyLoadPolicy: makeLoadPolicy({
|
|
366
|
+
maxTimeToFirstByteMs: 8e3,
|
|
367
|
+
maxLoadTimeMs: 2e4,
|
|
368
|
+
timeoutRetry: makeLinearRetry(1, 1e3, 8e3),
|
|
369
|
+
errorRetry: makeLinearRetry(2, 1e3, 8e3)
|
|
370
|
+
}),
|
|
371
|
+
fragLoadPolicy: makeLoadPolicy({
|
|
372
|
+
maxTimeToFirstByteMs: 1e4,
|
|
373
|
+
maxLoadTimeMs: 3e4,
|
|
374
|
+
timeoutRetry: makeLinearRetry(1, 0, 0),
|
|
375
|
+
errorRetry: makeLinearRetry(2, 1e3, 4e3)
|
|
376
|
+
})
|
|
377
|
+
};
|
|
378
|
+
}
|
|
269
379
|
var defaultLoad = () => {
|
|
270
380
|
return import('hls.js').then((m) => m.default);
|
|
271
381
|
};
|
|
@@ -290,13 +400,17 @@ var HlsLoader = class {
|
|
|
290
400
|
this.rejectAttach = null;
|
|
291
401
|
this.manifestTimeout = null;
|
|
292
402
|
this.aborted = false;
|
|
403
|
+
this.lastLoadingFragmentKey = null;
|
|
404
|
+
this.repeatedFragmentLoadCount = 0;
|
|
405
|
+
this.repeatedFragmentErrorFired = false;
|
|
406
|
+
this.mediaRecoveryCount = 0;
|
|
293
407
|
/** `false` idle; `'auto'` → first `LEVEL_SWITCHED` clears; otherwise await this level index. */
|
|
294
408
|
this.qualityAwait = false;
|
|
295
409
|
this.qualitySwitchTimer = null;
|
|
296
410
|
/** Tracks consecutive menu selections so redundant "Auto → Auto" skips the spinner. */
|
|
297
411
|
this.lastChosenQualityWasAuto = false;
|
|
298
412
|
/**
|
|
299
|
-
* `null`
|
|
413
|
+
* `null` - user chose ABR auto (menu shows "Auto"). Real hls.js still exposes
|
|
300
414
|
* `currentLevel >= 0` for the playing rendition; we must not mirror that into UI state.
|
|
301
415
|
*/
|
|
302
416
|
this.manualLevelIndex = null;
|
|
@@ -352,6 +466,7 @@ var HlsLoader = class {
|
|
|
352
466
|
attachWithHlsJs(Hls, video, source) {
|
|
353
467
|
const hls = new Hls({
|
|
354
468
|
enableWorker: true,
|
|
469
|
+
...makeHlsLoadPolicies(),
|
|
355
470
|
xhrSetup: (xhr, url) => this.installXhrWatcher(xhr, url, source)
|
|
356
471
|
});
|
|
357
472
|
this.hls = hls;
|
|
@@ -372,27 +487,44 @@ var HlsLoader = class {
|
|
|
372
487
|
this.onLevelEventuallyStabilized(...args);
|
|
373
488
|
this.refreshActiveQuality();
|
|
374
489
|
};
|
|
490
|
+
const onFragmentLoading = (...args) => {
|
|
491
|
+
this.onFragmentLoading(...args);
|
|
492
|
+
};
|
|
493
|
+
const onFragmentBuffered = (...args) => {
|
|
494
|
+
this.resetFragmentLoopGuardFromEvent(...args);
|
|
495
|
+
};
|
|
375
496
|
const onError = (..._args) => {
|
|
376
|
-
const data = _args
|
|
497
|
+
const data = this.getHlsEventData(_args);
|
|
377
498
|
const status = data.response?.code;
|
|
499
|
+
if (this.tryRecoverMediaError(Hls, data)) {
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
378
502
|
const isHttpError = typeof status === "number" && status >= 400;
|
|
379
|
-
|
|
380
|
-
|
|
503
|
+
const isTerminalHlsError = this.isTerminalHlsError(data);
|
|
504
|
+
if (data.fatal || isHttpError || isTerminalHlsError) {
|
|
505
|
+
const message = this.formatHlsErrorMessage(data, status);
|
|
506
|
+
this.stopHlsLoading();
|
|
381
507
|
this.options.onError?.({
|
|
508
|
+
code: this.getHlsErrorCode(data, status),
|
|
382
509
|
message,
|
|
383
510
|
cause: data,
|
|
384
511
|
status,
|
|
385
|
-
url:
|
|
512
|
+
url: this.getErrorUrl(data),
|
|
513
|
+
retryable: this.isRetryableHlsError(data, status)
|
|
386
514
|
});
|
|
387
515
|
settle(() => reject(new Error(message)));
|
|
388
516
|
}
|
|
389
517
|
};
|
|
390
518
|
hls.on(Hls.Events.MANIFEST_PARSED, onParsed);
|
|
391
519
|
hls.on(Hls.Events.LEVEL_SWITCHED, onSwitched);
|
|
520
|
+
hls.on(Hls.Events.FRAG_LOADING, onFragmentLoading);
|
|
521
|
+
hls.on(Hls.Events.FRAG_BUFFERED, onFragmentBuffered);
|
|
392
522
|
hls.on(Hls.Events.ERROR, onError);
|
|
393
523
|
this.detachListeners.push(
|
|
394
524
|
() => hls.off(Hls.Events.MANIFEST_PARSED, onParsed),
|
|
395
525
|
() => hls.off(Hls.Events.LEVEL_SWITCHED, onSwitched),
|
|
526
|
+
() => hls.off(Hls.Events.FRAG_LOADING, onFragmentLoading),
|
|
527
|
+
() => hls.off(Hls.Events.FRAG_BUFFERED, onFragmentBuffered),
|
|
396
528
|
() => hls.off(Hls.Events.ERROR, onError),
|
|
397
529
|
() => settle(() => reject(makeAbortError()))
|
|
398
530
|
);
|
|
@@ -400,6 +532,7 @@ var HlsLoader = class {
|
|
|
400
532
|
if (timeoutMs > 0) {
|
|
401
533
|
this.manifestTimeout = setTimeout(() => {
|
|
402
534
|
const message = `HLS manifest timeout after ${timeoutMs}ms`;
|
|
535
|
+
this.stopHlsLoading();
|
|
403
536
|
this.options.onError?.({ message, cause: { type: "timeout" }, url: source.src });
|
|
404
537
|
settle(() => reject(new Error(message)));
|
|
405
538
|
}, timeoutMs);
|
|
@@ -414,7 +547,7 @@ var HlsLoader = class {
|
|
|
414
547
|
}
|
|
415
548
|
/**
|
|
416
549
|
* Wire the readystatechange watcher and remove it as soon as the XHR
|
|
417
|
-
* completes
|
|
550
|
+
* completes - without this cleanup hls.js (which spawns one XHR per segment)
|
|
418
551
|
* leaks a closure per request (A1).
|
|
419
552
|
*/
|
|
420
553
|
installXhrWatcher(xhr, url, source) {
|
|
@@ -440,10 +573,149 @@ var HlsLoader = class {
|
|
|
440
573
|
};
|
|
441
574
|
xhr.addEventListener("readystatechange", watcher);
|
|
442
575
|
}
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
576
|
+
getHlsEventData(args) {
|
|
577
|
+
const data = args[1] ?? args[0];
|
|
578
|
+
return data !== null && typeof data === "object" ? data : {};
|
|
579
|
+
}
|
|
580
|
+
getFragmentUrl(data) {
|
|
581
|
+
return data.frag?.url ?? data.frag?.relurl ?? data.response?.url ?? data.url;
|
|
582
|
+
}
|
|
583
|
+
getErrorUrl(data) {
|
|
584
|
+
return this.getFragmentUrl(data) ?? this.currentSource?.src;
|
|
585
|
+
}
|
|
586
|
+
getFragmentKey(data) {
|
|
587
|
+
const url = this.getFragmentUrl(data);
|
|
588
|
+
const frag = data.frag;
|
|
589
|
+
if (!frag) return url ?? null;
|
|
590
|
+
const type = frag.type ?? "main";
|
|
591
|
+
const level = frag.level ?? "unknown-level";
|
|
592
|
+
const sn = frag.sn ?? "unknown-sn";
|
|
593
|
+
const cc = frag.cc ?? "unknown-cc";
|
|
594
|
+
const start = frag.start ?? frag.startPTS ?? "unknown-start";
|
|
595
|
+
return `${url ?? "unknown-url"}:${type}:${level}:${sn}:${cc}:${start}`;
|
|
596
|
+
}
|
|
597
|
+
isTerminalHlsError(data) {
|
|
598
|
+
switch (data.details) {
|
|
599
|
+
case "manifestParsingError":
|
|
600
|
+
case "manifestIncompatibleCodecsError":
|
|
601
|
+
case "levelEmptyError":
|
|
602
|
+
case "levelParsingError":
|
|
603
|
+
case "fragParsingError":
|
|
604
|
+
case "fragDecryptError":
|
|
605
|
+
case "keyLoadError":
|
|
606
|
+
case "keyLoadTimeOut":
|
|
607
|
+
case "bufferFullError":
|
|
608
|
+
return true;
|
|
609
|
+
default:
|
|
610
|
+
return false;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
formatHlsErrorMessage(data, status) {
|
|
614
|
+
if (data.fatal) {
|
|
615
|
+
const type2 = data.type ?? "unknown";
|
|
616
|
+
const details2 = data.details ? ` (${data.details})` : "";
|
|
617
|
+
return `HLS fatal error: ${type2}${details2}`;
|
|
618
|
+
}
|
|
619
|
+
if (typeof status === "number" && status >= 400) {
|
|
620
|
+
return `HLS load error: HTTP ${status}${data.details ? ` (${data.details})` : ""}`;
|
|
621
|
+
}
|
|
622
|
+
const type = data.type ?? "unknown";
|
|
623
|
+
const details = data.details ? ` (${data.details})` : "";
|
|
624
|
+
return `HLS error: ${type}${details}`;
|
|
625
|
+
}
|
|
626
|
+
getHlsErrorCode(data, status) {
|
|
627
|
+
if (status === 401 || status === 403) return "unauthorized";
|
|
628
|
+
switch (data.details) {
|
|
629
|
+
case "manifestParsingError":
|
|
630
|
+
case "manifestIncompatibleCodecsError":
|
|
631
|
+
case "levelParsingError":
|
|
632
|
+
case "fragParsingError":
|
|
633
|
+
case "fragDecryptError":
|
|
634
|
+
return "decode";
|
|
635
|
+
default:
|
|
636
|
+
return "network";
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
isRetryableHlsError(data, status) {
|
|
640
|
+
if (status === 401 || status === 403) return false;
|
|
641
|
+
if (typeof status === "number" && status >= 400 && status < 500) return false;
|
|
642
|
+
switch (data.details) {
|
|
643
|
+
case "manifestParsingError":
|
|
644
|
+
case "manifestIncompatibleCodecsError":
|
|
645
|
+
case "levelParsingError":
|
|
646
|
+
case "fragParsingError":
|
|
647
|
+
case "fragDecryptError":
|
|
648
|
+
return false;
|
|
649
|
+
default:
|
|
650
|
+
return true;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
isRecoverableMediaError(Hls, data) {
|
|
654
|
+
if (data.type !== Hls.ErrorTypes.MEDIA_ERROR && data.type !== "mediaError") {
|
|
655
|
+
return false;
|
|
656
|
+
}
|
|
657
|
+
if (!data.fatal) return false;
|
|
658
|
+
if (this.mediaRecoveryCount >= MEDIA_RECOVERY_LIMIT) return false;
|
|
659
|
+
if (typeof this.hls?.recoverMediaError !== "function") return false;
|
|
660
|
+
switch (data.details) {
|
|
661
|
+
case void 0:
|
|
662
|
+
case "bufferAppendError":
|
|
663
|
+
case "bufferAppendingError":
|
|
664
|
+
case "bufferStalledError":
|
|
665
|
+
case "bufferSeekOverHole":
|
|
666
|
+
case "bufferNudgeOnStall":
|
|
667
|
+
case "bufferFullError":
|
|
668
|
+
return true;
|
|
669
|
+
default:
|
|
670
|
+
return false;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
tryRecoverMediaError(Hls, data) {
|
|
674
|
+
if (!this.isRecoverableMediaError(Hls, data)) return false;
|
|
675
|
+
this.mediaRecoveryCount += 1;
|
|
676
|
+
this.resetFragmentLoopGuard();
|
|
677
|
+
this.completeQualityAwaitIfBusy();
|
|
678
|
+
try {
|
|
679
|
+
this.hls?.recoverMediaError?.();
|
|
680
|
+
return true;
|
|
681
|
+
} catch {
|
|
682
|
+
return false;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
onFragmentLoading(...args) {
|
|
686
|
+
if (this.repeatedFragmentErrorFired) return;
|
|
687
|
+
const data = this.getHlsEventData(args);
|
|
688
|
+
const key = this.getFragmentKey(data);
|
|
689
|
+
if (!key) return;
|
|
690
|
+
if (key === this.lastLoadingFragmentKey) {
|
|
691
|
+
this.repeatedFragmentLoadCount += 1;
|
|
692
|
+
} else {
|
|
693
|
+
this.lastLoadingFragmentKey = key;
|
|
694
|
+
this.repeatedFragmentLoadCount = 1;
|
|
695
|
+
}
|
|
696
|
+
if (this.repeatedFragmentLoadCount < REPEATED_FRAGMENT_LOAD_LIMIT) return;
|
|
697
|
+
this.repeatedFragmentErrorFired = true;
|
|
698
|
+
this.stopHlsLoading();
|
|
699
|
+
const url = this.getFragmentUrl(data);
|
|
700
|
+
this.options.onError?.({
|
|
701
|
+
code: "hls-fragment-loop",
|
|
702
|
+
message: `HLS fragment repeatedly loading: ${url ?? key}`,
|
|
703
|
+
cause: data,
|
|
704
|
+
url
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
resetFragmentLoopGuardFromEvent(...args) {
|
|
708
|
+
const data = this.getHlsEventData(args);
|
|
709
|
+
if (this.getFragmentKey(data)) {
|
|
710
|
+
this.resetFragmentLoopGuard();
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
resetFragmentLoopGuard() {
|
|
714
|
+
this.lastLoadingFragmentKey = null;
|
|
715
|
+
this.repeatedFragmentLoadCount = 0;
|
|
716
|
+
this.repeatedFragmentErrorFired = false;
|
|
717
|
+
}
|
|
718
|
+
abortLiveXhrs() {
|
|
447
719
|
for (const xhr of [...this.liveXhrs]) {
|
|
448
720
|
try {
|
|
449
721
|
xhr.abort();
|
|
@@ -451,6 +723,20 @@ var HlsLoader = class {
|
|
|
451
723
|
}
|
|
452
724
|
}
|
|
453
725
|
this.liveXhrs.clear();
|
|
726
|
+
}
|
|
727
|
+
stopHlsLoading() {
|
|
728
|
+
this.completeQualityAwaitIfBusy();
|
|
729
|
+
try {
|
|
730
|
+
this.hls?.stopLoad?.();
|
|
731
|
+
} catch {
|
|
732
|
+
}
|
|
733
|
+
this.abortLiveXhrs();
|
|
734
|
+
}
|
|
735
|
+
abort() {
|
|
736
|
+
if (this.aborted) return;
|
|
737
|
+
this.aborted = true;
|
|
738
|
+
this.clearManifestTimeout();
|
|
739
|
+
this.abortLiveXhrs();
|
|
454
740
|
const reject = this.rejectAttach;
|
|
455
741
|
this.rejectAttach = null;
|
|
456
742
|
reject?.(makeAbortError());
|
|
@@ -486,6 +772,8 @@ var HlsLoader = class {
|
|
|
486
772
|
this.aborted = false;
|
|
487
773
|
this.lastChosenQualityWasAuto = false;
|
|
488
774
|
this.manualLevelIndex = null;
|
|
775
|
+
this.mediaRecoveryCount = 0;
|
|
776
|
+
this.resetFragmentLoopGuard();
|
|
489
777
|
}
|
|
490
778
|
clearQualitySwitchTimerOnly() {
|
|
491
779
|
if (this.qualitySwitchTimer !== null) {
|
|
@@ -607,7 +895,7 @@ var HlsLoader = class {
|
|
|
607
895
|
const locked = this.qualities[this.manualLevelIndex] ?? null;
|
|
608
896
|
this.options.onActiveQuality?.(locked, false);
|
|
609
897
|
}
|
|
610
|
-
/** @internal
|
|
898
|
+
/** @internal - for tests. */
|
|
611
899
|
_getInternalState() {
|
|
612
900
|
return { source: this.currentSource, qualities: this.qualities };
|
|
613
901
|
}
|
|
@@ -629,6 +917,7 @@ var NativeLoader = class {
|
|
|
629
917
|
constructor() {
|
|
630
918
|
this.video = null;
|
|
631
919
|
this.cleanups = [];
|
|
920
|
+
this.rejectPending = null;
|
|
632
921
|
}
|
|
633
922
|
async attach(video, source) {
|
|
634
923
|
this.detach();
|
|
@@ -636,12 +925,14 @@ var NativeLoader = class {
|
|
|
636
925
|
return new Promise((resolve, reject) => {
|
|
637
926
|
const onMeta = () => {
|
|
638
927
|
cleanupListeners();
|
|
928
|
+
this.rejectPending = null;
|
|
639
929
|
resolve();
|
|
640
930
|
};
|
|
641
931
|
const onError = () => {
|
|
642
932
|
cleanupListeners();
|
|
643
933
|
const err = video.error;
|
|
644
934
|
const message = err ? `MediaError code=${err.code} message=${err.message}` : "Native loader failed";
|
|
935
|
+
this.rejectPending = null;
|
|
645
936
|
reject(new Error(message));
|
|
646
937
|
};
|
|
647
938
|
const cleanupListeners = () => {
|
|
@@ -651,17 +942,28 @@ var NativeLoader = class {
|
|
|
651
942
|
video.addEventListener("loadedmetadata", onMeta);
|
|
652
943
|
video.addEventListener("error", onError);
|
|
653
944
|
this.cleanups.push(cleanupListeners);
|
|
945
|
+
this.rejectPending = reject;
|
|
654
946
|
video.src = source.src;
|
|
655
947
|
video.load();
|
|
656
948
|
});
|
|
657
949
|
}
|
|
658
|
-
|
|
950
|
+
abort() {
|
|
951
|
+
const reject = this.rejectPending;
|
|
952
|
+
this.rejectPending = null;
|
|
659
953
|
while (this.cleanups.length) {
|
|
660
954
|
try {
|
|
661
955
|
this.cleanups.pop()?.();
|
|
662
956
|
} catch {
|
|
663
957
|
}
|
|
664
958
|
}
|
|
959
|
+
if (reject) {
|
|
960
|
+
const error = new Error("Native source load aborted");
|
|
961
|
+
error.name = "AbortError";
|
|
962
|
+
reject(error);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
detach() {
|
|
966
|
+
this.abort();
|
|
665
967
|
if (this.video) {
|
|
666
968
|
try {
|
|
667
969
|
this.video.removeAttribute("src");
|
|
@@ -795,7 +1097,9 @@ var YouTubeLoader = class {
|
|
|
795
1097
|
}
|
|
796
1098
|
const host = document.createElement("div");
|
|
797
1099
|
host.setAttribute("data-reel-yt-host", "");
|
|
798
|
-
host.style.cssText = "position:absolute;inset:0;
|
|
1100
|
+
host.style.cssText = "position:absolute;inset:0;overflow:hidden;background:#000";
|
|
1101
|
+
const mount = document.createElement("div");
|
|
1102
|
+
host.appendChild(mount);
|
|
799
1103
|
parent.appendChild(host);
|
|
800
1104
|
this.host = host;
|
|
801
1105
|
video.setAttribute(YT_HIDDEN_ATTR, "");
|
|
@@ -808,7 +1112,7 @@ var YouTubeLoader = class {
|
|
|
808
1112
|
run();
|
|
809
1113
|
};
|
|
810
1114
|
try {
|
|
811
|
-
this.yt = new YT.Player(
|
|
1115
|
+
this.yt = new YT.Player(mount, {
|
|
812
1116
|
videoId: id,
|
|
813
1117
|
// 100% so the iframe fills the host div rather than using default px dimensions.
|
|
814
1118
|
width: "100%",
|
|
@@ -824,7 +1128,7 @@ var YouTubeLoader = class {
|
|
|
824
1128
|
onReady: () => {
|
|
825
1129
|
const iframe = host.querySelector("iframe");
|
|
826
1130
|
if (iframe) {
|
|
827
|
-
iframe.style.cssText = "width:100%;height:100%;border:none;display:block
|
|
1131
|
+
iframe.style.cssText = "width:100%;height:100%;border:none;display:block";
|
|
828
1132
|
}
|
|
829
1133
|
try {
|
|
830
1134
|
this.duration = this.yt?.getDuration() ?? 0;
|
|
@@ -1156,21 +1460,6 @@ function createStore(initial, options = {}) {
|
|
|
1156
1460
|
}
|
|
1157
1461
|
|
|
1158
1462
|
// src/createPlayer.ts
|
|
1159
|
-
var NATIVE_EVENTS = [
|
|
1160
|
-
"loadedmetadata",
|
|
1161
|
-
"durationchange",
|
|
1162
|
-
"timeupdate",
|
|
1163
|
-
"play",
|
|
1164
|
-
"pause",
|
|
1165
|
-
"ended",
|
|
1166
|
-
"error",
|
|
1167
|
-
"volumechange",
|
|
1168
|
-
"ratechange",
|
|
1169
|
-
"seeking",
|
|
1170
|
-
"seeked",
|
|
1171
|
-
"waiting",
|
|
1172
|
-
"playing"
|
|
1173
|
-
];
|
|
1174
1463
|
function makeInitialState(options) {
|
|
1175
1464
|
return {
|
|
1176
1465
|
status: "idle",
|
|
@@ -1203,13 +1492,42 @@ function createPlayer(options = {}, internal = {}) {
|
|
|
1203
1492
|
const bus = createEventBus();
|
|
1204
1493
|
const commands = createCommandRegistry();
|
|
1205
1494
|
const controls = createControlsRegistry();
|
|
1495
|
+
const sourceTransforms = createSourceTransformRegistry();
|
|
1496
|
+
const mediaAttachments = createMediaAttachmentRegistry();
|
|
1206
1497
|
let status = "idle";
|
|
1207
1498
|
let video = null;
|
|
1208
1499
|
let activeLoader = null;
|
|
1209
1500
|
let pendingAttachAbort = null;
|
|
1501
|
+
let pendingSourceLoad = null;
|
|
1502
|
+
let latestSourceLoadPromise = null;
|
|
1210
1503
|
let disposed = false;
|
|
1211
|
-
let initialOptionsApplied = false;
|
|
1212
1504
|
let pendingPlay = false;
|
|
1505
|
+
function createPendingSourceLoad() {
|
|
1506
|
+
let resolve;
|
|
1507
|
+
let reject;
|
|
1508
|
+
const promise = new Promise((onResolve, onReject) => {
|
|
1509
|
+
resolve = onResolve;
|
|
1510
|
+
reject = onReject;
|
|
1511
|
+
});
|
|
1512
|
+
void promise.catch(() => void 0);
|
|
1513
|
+
latestSourceLoadPromise = promise;
|
|
1514
|
+
return { promise, resolve, reject };
|
|
1515
|
+
}
|
|
1516
|
+
function resolvePendingSourceLoad() {
|
|
1517
|
+
const load = pendingSourceLoad;
|
|
1518
|
+
if (!load) return;
|
|
1519
|
+
pendingSourceLoad = null;
|
|
1520
|
+
load.resolve();
|
|
1521
|
+
}
|
|
1522
|
+
function rejectPendingSourceLoad(reason) {
|
|
1523
|
+
const load = pendingSourceLoad;
|
|
1524
|
+
if (!load) return;
|
|
1525
|
+
pendingSourceLoad = null;
|
|
1526
|
+
load.reject(reason);
|
|
1527
|
+
}
|
|
1528
|
+
function createAbortError() {
|
|
1529
|
+
return Object.assign(new Error("Aborted"), { name: "AbortError" });
|
|
1530
|
+
}
|
|
1213
1531
|
const YT_PLAYER_STATE_PLAYING = 1;
|
|
1214
1532
|
function getYouTubePlayer() {
|
|
1215
1533
|
return activeLoader?._instance?.() ?? null;
|
|
@@ -1232,11 +1550,12 @@ function createPlayer(options = {}, internal = {}) {
|
|
|
1232
1550
|
bus.emit("qualitychange", { quality, auto });
|
|
1233
1551
|
}
|
|
1234
1552
|
commands.add("hls:setQuality", (level) => {
|
|
1235
|
-
if (level
|
|
1236
|
-
|
|
1237
|
-
} else if (level !== null && typeof level === "object" && typeof level.height === "number") {
|
|
1238
|
-
bus.emit("qualitypreference", { qualityHeight: level.height });
|
|
1553
|
+
if (level !== null && (typeof level !== "object" || typeof level.height !== "number")) {
|
|
1554
|
+
return;
|
|
1239
1555
|
}
|
|
1556
|
+
bus.emit("qualitypreference", {
|
|
1557
|
+
qualityHeight: level === null ? null : level.height
|
|
1558
|
+
});
|
|
1240
1559
|
const loader = activeLoader;
|
|
1241
1560
|
if (typeof loader?.setQuality === "function") {
|
|
1242
1561
|
loader.setQuality(level === null ? "auto" : level);
|
|
@@ -1248,7 +1567,14 @@ function createPlayer(options = {}, internal = {}) {
|
|
|
1248
1567
|
onUnauthorized: (e) => emitUnauthorized(e),
|
|
1249
1568
|
onQualities: (q) => setQualities(q),
|
|
1250
1569
|
onActiveQuality: (q, auto) => setActiveQuality(q, auto),
|
|
1251
|
-
onError: (err) =>
|
|
1570
|
+
onError: (err) => {
|
|
1571
|
+
const playerError = createPlayerError({ ...err, code: err.code ?? "network" });
|
|
1572
|
+
if (status === "loading") {
|
|
1573
|
+
dispatch({ type: "loadFailed", error: playerError });
|
|
1574
|
+
return;
|
|
1575
|
+
}
|
|
1576
|
+
dispatch({ type: "runtimeError", error: playerError });
|
|
1577
|
+
},
|
|
1252
1578
|
onQualitySwitch: (active) => bus.emit("qualityswitch", { active })
|
|
1253
1579
|
})
|
|
1254
1580
|
);
|
|
@@ -1290,7 +1616,9 @@ function createPlayer(options = {}, internal = {}) {
|
|
|
1290
1616
|
commands,
|
|
1291
1617
|
controls,
|
|
1292
1618
|
store: { getState: store.getState, subscribe: store.subscribe },
|
|
1293
|
-
bus
|
|
1619
|
+
bus,
|
|
1620
|
+
sourceTransforms,
|
|
1621
|
+
mediaAttachments
|
|
1294
1622
|
});
|
|
1295
1623
|
const plugins = createPluginBus({ player: playerRef, host });
|
|
1296
1624
|
function dispatch(event) {
|
|
@@ -1302,6 +1630,10 @@ function createPlayer(options = {}, internal = {}) {
|
|
|
1302
1630
|
for (const effect of result.effects) applyEffect(effect);
|
|
1303
1631
|
if (prev !== "ready" && status === "ready") {
|
|
1304
1632
|
bus.emit("ready", { duration: store.getState().duration });
|
|
1633
|
+
resolvePendingSourceLoad();
|
|
1634
|
+
}
|
|
1635
|
+
if (event.type === "loadFailed") {
|
|
1636
|
+
rejectPendingSourceLoad(event.error);
|
|
1305
1637
|
}
|
|
1306
1638
|
}
|
|
1307
1639
|
function applyEffect(effect) {
|
|
@@ -1345,15 +1677,29 @@ function createPlayer(options = {}, internal = {}) {
|
|
|
1345
1677
|
activeQuality: null
|
|
1346
1678
|
});
|
|
1347
1679
|
bus.emit("qualitychange", { quality: null, auto: true });
|
|
1680
|
+
if (!isSafeMediaUrl(source.src) || source.tracks?.some((track) => !isSafeMediaUrl(track.src)) || source.thumbnails && !isSafeMediaUrl(source.thumbnails.src)) {
|
|
1681
|
+
if (video) {
|
|
1682
|
+
pendingSourceLoad = createPendingSourceLoad();
|
|
1683
|
+
}
|
|
1684
|
+
const error = createPlayerError({
|
|
1685
|
+
code: "unsupported",
|
|
1686
|
+
message: "Invalid URL"
|
|
1687
|
+
});
|
|
1688
|
+
dispatch({ type: "loadFailed", error });
|
|
1689
|
+
return;
|
|
1690
|
+
}
|
|
1348
1691
|
if (!video) return;
|
|
1692
|
+
const sourceLoad = createPendingSourceLoad();
|
|
1693
|
+
pendingSourceLoad = sourceLoad;
|
|
1349
1694
|
const provider = registry.resolve(source);
|
|
1350
1695
|
if (!provider) {
|
|
1696
|
+
const error = createPlayerError({
|
|
1697
|
+
code: "unsupported",
|
|
1698
|
+
message: `No source provider for ${source.src}`
|
|
1699
|
+
});
|
|
1351
1700
|
dispatch({
|
|
1352
1701
|
type: "loadFailed",
|
|
1353
|
-
error
|
|
1354
|
-
code: "unsupported",
|
|
1355
|
-
message: `No source provider for ${source.src}`
|
|
1356
|
-
})
|
|
1702
|
+
error
|
|
1357
1703
|
});
|
|
1358
1704
|
return;
|
|
1359
1705
|
}
|
|
@@ -1364,8 +1710,13 @@ function createPlayer(options = {}, internal = {}) {
|
|
|
1364
1710
|
aborted = true;
|
|
1365
1711
|
try {
|
|
1366
1712
|
loader.abort?.();
|
|
1367
|
-
} catch (
|
|
1368
|
-
console.error(
|
|
1713
|
+
} catch (error) {
|
|
1714
|
+
console.error(error);
|
|
1715
|
+
}
|
|
1716
|
+
if (pendingSourceLoad === sourceLoad) {
|
|
1717
|
+
rejectPendingSourceLoad(createAbortError());
|
|
1718
|
+
} else {
|
|
1719
|
+
sourceLoad.reject(createAbortError());
|
|
1369
1720
|
}
|
|
1370
1721
|
};
|
|
1371
1722
|
loader.attach(video, source).then(() => {
|
|
@@ -1467,8 +1818,6 @@ function createPlayer(options = {}, internal = {}) {
|
|
|
1467
1818
|
target.addEventListener(name, handler);
|
|
1468
1819
|
nativeListeners.push(() => target.removeEventListener(name, handler));
|
|
1469
1820
|
}
|
|
1470
|
-
for (const _ of NATIVE_EVENTS) {
|
|
1471
|
-
}
|
|
1472
1821
|
on2("loadedmetadata", () => {
|
|
1473
1822
|
store.setState({
|
|
1474
1823
|
duration: Number.isFinite(target.duration) ? target.duration : 0,
|
|
@@ -1539,17 +1888,24 @@ function createPlayer(options = {}, internal = {}) {
|
|
|
1539
1888
|
on2("playing", () => {
|
|
1540
1889
|
bus.emit("buffering", { isBuffering: false });
|
|
1541
1890
|
});
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
};
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1891
|
+
on2("enterpictureinpicture", () => {
|
|
1892
|
+
store.setState({ pip: true });
|
|
1893
|
+
bus.emit("pipchange", { pip: true });
|
|
1894
|
+
});
|
|
1895
|
+
on2("leavepictureinpicture", () => {
|
|
1896
|
+
store.setState({ pip: false });
|
|
1897
|
+
bus.emit("pipchange", { pip: false });
|
|
1898
|
+
});
|
|
1899
|
+
const ownerDocument = target.ownerDocument;
|
|
1900
|
+
const onFullscreenChange = () => {
|
|
1901
|
+
const isFullscreen = !!ownerDocument.fullscreenElement?.contains(target);
|
|
1902
|
+
store.setState({ fullscreen: isFullscreen });
|
|
1903
|
+
bus.emit("fullscreenchange", { fullscreen: isFullscreen });
|
|
1904
|
+
};
|
|
1905
|
+
ownerDocument.addEventListener("fullscreenchange", onFullscreenChange);
|
|
1906
|
+
nativeListeners.push(
|
|
1907
|
+
() => ownerDocument.removeEventListener("fullscreenchange", onFullscreenChange)
|
|
1908
|
+
);
|
|
1553
1909
|
}
|
|
1554
1910
|
function unbindNativeEvents() {
|
|
1555
1911
|
while (nativeListeners.length) {
|
|
@@ -1559,24 +1915,18 @@ function createPlayer(options = {}, internal = {}) {
|
|
|
1559
1915
|
}
|
|
1560
1916
|
}
|
|
1561
1917
|
}
|
|
1562
|
-
function
|
|
1563
|
-
if (
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1918
|
+
function applyAttachOptions() {
|
|
1919
|
+
if (!video) return;
|
|
1920
|
+
const state = store.getState();
|
|
1921
|
+
video.volume = state.volume;
|
|
1922
|
+
video.muted = state.muted;
|
|
1923
|
+
video.playbackRate = state.playbackRate;
|
|
1567
1924
|
if (options.poster) video.poster = options.poster;
|
|
1568
1925
|
if (options.preload) video.preload = options.preload;
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
if ("crossOrigin" in options) {
|
|
1574
|
-
if (options.crossOrigin) video.crossOrigin = options.crossOrigin;
|
|
1575
|
-
} else {
|
|
1576
|
-
video.crossOrigin = "anonymous";
|
|
1577
|
-
}
|
|
1578
|
-
if (options.loop) video.loop = true;
|
|
1579
|
-
initialOptionsApplied = true;
|
|
1926
|
+
video.playsInline = options.playsInline ?? true;
|
|
1927
|
+
video.toggleAttribute("webkit-playsinline", video.playsInline);
|
|
1928
|
+
video.crossOrigin = "crossOrigin" in options ? options.crossOrigin ?? null : "anonymous";
|
|
1929
|
+
video.loop = !!options.loop;
|
|
1580
1930
|
}
|
|
1581
1931
|
const attachDisposers = [];
|
|
1582
1932
|
function flushAttachDisposers() {
|
|
@@ -1600,12 +1950,21 @@ function createPlayer(options = {}, internal = {}) {
|
|
|
1600
1950
|
}
|
|
1601
1951
|
async function attach(el) {
|
|
1602
1952
|
if (disposed) throw new Error("[@f8team/reel-core] cannot attach: player disposed");
|
|
1603
|
-
if (video === el)
|
|
1953
|
+
if (video === el) {
|
|
1954
|
+
await latestSourceLoadPromise;
|
|
1955
|
+
return;
|
|
1956
|
+
}
|
|
1604
1957
|
if (video) detach();
|
|
1605
1958
|
video = el;
|
|
1959
|
+
mediaAttachments.set({
|
|
1960
|
+
video: el,
|
|
1961
|
+
container: el.closest("[data-reel]") ?? el.parentElement ?? el
|
|
1962
|
+
});
|
|
1606
1963
|
bindNativeEvents(el);
|
|
1607
|
-
|
|
1608
|
-
|
|
1964
|
+
applyAttachOptions();
|
|
1965
|
+
const currentSource = store.getState().source;
|
|
1966
|
+
if (currentSource) setSource(currentSource);
|
|
1967
|
+
const sourceLoadPromise = latestSourceLoadPromise;
|
|
1609
1968
|
const wantStartTime = typeof options.startTime === "number" && options.startTime > 0;
|
|
1610
1969
|
const wantAutoplay = !!options.autoplay && options.autoplay !== "off" && !!options.source;
|
|
1611
1970
|
if (status === "ready") {
|
|
@@ -1627,6 +1986,7 @@ function createPlayer(options = {}, internal = {}) {
|
|
|
1627
1986
|
attachDisposers.push(off2);
|
|
1628
1987
|
}
|
|
1629
1988
|
}
|
|
1989
|
+
await sourceLoadPromise;
|
|
1630
1990
|
}
|
|
1631
1991
|
function detach() {
|
|
1632
1992
|
pendingAttachAbort?.();
|
|
@@ -1640,6 +2000,7 @@ function createPlayer(options = {}, internal = {}) {
|
|
|
1640
2000
|
}
|
|
1641
2001
|
activeLoader = null;
|
|
1642
2002
|
}
|
|
2003
|
+
mediaAttachments.set(null);
|
|
1643
2004
|
video = null;
|
|
1644
2005
|
}
|
|
1645
2006
|
function dispose() {
|
|
@@ -1654,7 +2015,8 @@ function createPlayer(options = {}, internal = {}) {
|
|
|
1654
2015
|
}
|
|
1655
2016
|
function setSource(source) {
|
|
1656
2017
|
pendingPlay = false;
|
|
1657
|
-
|
|
2018
|
+
latestSourceLoadPromise = null;
|
|
2019
|
+
dispatch({ type: "setSource", source: source ? sourceTransforms.apply(source) : null });
|
|
1658
2020
|
}
|
|
1659
2021
|
function getSource() {
|
|
1660
2022
|
return store.getState().source;
|
|
@@ -1662,7 +2024,7 @@ function createPlayer(options = {}, internal = {}) {
|
|
|
1662
2024
|
function retry() {
|
|
1663
2025
|
const current = store.getState().source;
|
|
1664
2026
|
if (!current) return false;
|
|
1665
|
-
|
|
2027
|
+
setSource(current);
|
|
1666
2028
|
return true;
|
|
1667
2029
|
}
|
|
1668
2030
|
async function play() {
|
|
@@ -2070,11 +2432,13 @@ exports.THEME_PRESETS = THEME_PRESETS;
|
|
|
2070
2432
|
exports.TOKEN_TO_CSS_VAR = TOKEN_TO_CSS_VAR;
|
|
2071
2433
|
exports.announce = announce;
|
|
2072
2434
|
exports.applyTheme = applyTheme;
|
|
2435
|
+
exports.areSourceDescriptorsEquivalent = areSourceDescriptorsEquivalent;
|
|
2073
2436
|
exports.createFocusTrap = createFocusTrap;
|
|
2074
2437
|
exports.createPlayer = createPlayer;
|
|
2075
2438
|
exports.definePlugin = definePlugin;
|
|
2076
2439
|
exports.detectSourceType = detectSourceType;
|
|
2077
2440
|
exports.formatTime = formatTime;
|
|
2441
|
+
exports.isSafeMediaUrl = isSafeMediaUrl;
|
|
2078
2442
|
exports.looksLikeHls = looksLikeHls;
|
|
2079
2443
|
exports.looksLikeProgressive = looksLikeProgressive;
|
|
2080
2444
|
exports.looksLikeYouTube = looksLikeYouTube;
|