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