@f8team/reel-core 1.0.1 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -12
- package/dist/index.cjs +320 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +320 -15
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -58,7 +58,7 @@ interface CreateStoreOptions<_T> {
|
|
|
58
58
|
* Golden cases: G13 (unauthorized HLS), every error path in G1..G16.
|
|
59
59
|
*/
|
|
60
60
|
/** Stable error codes. The string literal IS the public contract. */
|
|
61
|
-
type PlayerErrorCode = "source" | "network" | "decode" | "unauthorized" | "unsupported" | "internal";
|
|
61
|
+
type PlayerErrorCode = "source" | "network" | "decode" | "unauthorized" | "unsupported" | "hls-fragment-loop" | "internal";
|
|
62
62
|
/**
|
|
63
63
|
* Structured player error. Adapters render the message; plugins can inspect
|
|
64
64
|
* `code`, `status`, and `retryable` to decide on retry/UI strategy.
|
|
@@ -639,7 +639,7 @@ interface PlayerOptions {
|
|
|
639
639
|
}
|
|
640
640
|
|
|
641
641
|
/**
|
|
642
|
-
* `createPlayer(options)`
|
|
642
|
+
* `createPlayer(options)` - wires every piece of `@f8team/reel-core` into a
|
|
643
643
|
* single `Player`. The factory is the one and only public entry point.
|
|
644
644
|
*
|
|
645
645
|
* Responsibilities:
|
|
@@ -658,7 +658,7 @@ interface PlayerOptions {
|
|
|
658
658
|
* unit tests.
|
|
659
659
|
*/
|
|
660
660
|
|
|
661
|
-
/** @internal
|
|
661
|
+
/** @internal - extra knobs surfaced for tests and adapters. */
|
|
662
662
|
interface CreatePlayerInternalOptions {
|
|
663
663
|
/** Override the microtask scheduler for the store. Tests use this. */
|
|
664
664
|
storeOptions?: CreateStoreOptions<PlayerState>;
|
package/dist/index.d.ts
CHANGED
|
@@ -58,7 +58,7 @@ interface CreateStoreOptions<_T> {
|
|
|
58
58
|
* Golden cases: G13 (unauthorized HLS), every error path in G1..G16.
|
|
59
59
|
*/
|
|
60
60
|
/** Stable error codes. The string literal IS the public contract. */
|
|
61
|
-
type PlayerErrorCode = "source" | "network" | "decode" | "unauthorized" | "unsupported" | "internal";
|
|
61
|
+
type PlayerErrorCode = "source" | "network" | "decode" | "unauthorized" | "unsupported" | "hls-fragment-loop" | "internal";
|
|
62
62
|
/**
|
|
63
63
|
* Structured player error. Adapters render the message; plugins can inspect
|
|
64
64
|
* `code`, `status`, and `retryable` to decide on retry/UI strategy.
|
|
@@ -639,7 +639,7 @@ interface PlayerOptions {
|
|
|
639
639
|
}
|
|
640
640
|
|
|
641
641
|
/**
|
|
642
|
-
* `createPlayer(options)`
|
|
642
|
+
* `createPlayer(options)` - wires every piece of `@f8team/reel-core` into a
|
|
643
643
|
* single `Player`. The factory is the one and only public entry point.
|
|
644
644
|
*
|
|
645
645
|
* Responsibilities:
|
|
@@ -658,7 +658,7 @@ interface PlayerOptions {
|
|
|
658
658
|
* unit tests.
|
|
659
659
|
*/
|
|
660
660
|
|
|
661
|
-
/** @internal
|
|
661
|
+
/** @internal - extra knobs surfaced for tests and adapters. */
|
|
662
662
|
interface CreatePlayerInternalOptions {
|
|
663
663
|
/** Override the microtask scheduler for the store. Tests use this. */
|
|
664
664
|
storeOptions?: CreateStoreOptions<PlayerState>;
|
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) {
|
|
@@ -111,16 +112,16 @@ function createPluginBus({ player, host }) {
|
|
|
111
112
|
if (findIndex(plugin.name) >= 0) {
|
|
112
113
|
throw new Error(`[@f8team/reel-core] plugin "${plugin.name}" is already registered`);
|
|
113
114
|
}
|
|
114
|
-
let
|
|
115
|
+
let setupResult;
|
|
115
116
|
try {
|
|
116
|
-
|
|
117
|
-
teardown = typeof result === "function" ? result : null;
|
|
117
|
+
setupResult = plugin.setup(player, host);
|
|
118
118
|
} catch (err) {
|
|
119
119
|
throw new Error(
|
|
120
120
|
`[@f8team/reel-core] plugin "${plugin.name}" setup threw: ${err.message}`,
|
|
121
121
|
{ cause: err }
|
|
122
122
|
);
|
|
123
123
|
}
|
|
124
|
+
const teardown = typeof setupResult === "function" ? setupResult : null;
|
|
124
125
|
entries.push({ plugin, teardown });
|
|
125
126
|
}
|
|
126
127
|
function unregister(name) {
|
|
@@ -264,6 +265,54 @@ function resolveWithCredentials(policy, url) {
|
|
|
264
265
|
}
|
|
265
266
|
|
|
266
267
|
// src/sources/hls.ts
|
|
268
|
+
var REPEATED_FRAGMENT_LOAD_LIMIT = 4;
|
|
269
|
+
var MEDIA_RECOVERY_LIMIT = 1;
|
|
270
|
+
function makeLoadPolicy(options) {
|
|
271
|
+
return {
|
|
272
|
+
default: {
|
|
273
|
+
maxTimeToFirstByteMs: options.maxTimeToFirstByteMs,
|
|
274
|
+
maxLoadTimeMs: options.maxLoadTimeMs,
|
|
275
|
+
timeoutRetry: options.timeoutRetry,
|
|
276
|
+
errorRetry: options.errorRetry
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
function makeLinearRetry(maxNumRetry, retryDelayMs, maxRetryDelayMs) {
|
|
281
|
+
return {
|
|
282
|
+
maxNumRetry,
|
|
283
|
+
retryDelayMs,
|
|
284
|
+
maxRetryDelayMs,
|
|
285
|
+
backoff: "linear"
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
function makeHlsLoadPolicies() {
|
|
289
|
+
return {
|
|
290
|
+
manifestLoadPolicy: makeLoadPolicy({
|
|
291
|
+
maxTimeToFirstByteMs: 1e4,
|
|
292
|
+
maxLoadTimeMs: 2e4,
|
|
293
|
+
timeoutRetry: makeLinearRetry(1, 0, 0),
|
|
294
|
+
errorRetry: makeLinearRetry(1, 1e3, 8e3)
|
|
295
|
+
}),
|
|
296
|
+
playlistLoadPolicy: makeLoadPolicy({
|
|
297
|
+
maxTimeToFirstByteMs: 1e4,
|
|
298
|
+
maxLoadTimeMs: 2e4,
|
|
299
|
+
timeoutRetry: makeLinearRetry(1, 0, 0),
|
|
300
|
+
errorRetry: makeLinearRetry(2, 1e3, 8e3)
|
|
301
|
+
}),
|
|
302
|
+
keyLoadPolicy: makeLoadPolicy({
|
|
303
|
+
maxTimeToFirstByteMs: 8e3,
|
|
304
|
+
maxLoadTimeMs: 2e4,
|
|
305
|
+
timeoutRetry: makeLinearRetry(1, 1e3, 8e3),
|
|
306
|
+
errorRetry: makeLinearRetry(2, 1e3, 8e3)
|
|
307
|
+
}),
|
|
308
|
+
fragLoadPolicy: makeLoadPolicy({
|
|
309
|
+
maxTimeToFirstByteMs: 1e4,
|
|
310
|
+
maxLoadTimeMs: 3e4,
|
|
311
|
+
timeoutRetry: makeLinearRetry(1, 0, 0),
|
|
312
|
+
errorRetry: makeLinearRetry(2, 1e3, 4e3)
|
|
313
|
+
})
|
|
314
|
+
};
|
|
315
|
+
}
|
|
267
316
|
var defaultLoad = () => {
|
|
268
317
|
return import('hls.js').then((m) => m.default);
|
|
269
318
|
};
|
|
@@ -288,13 +337,17 @@ var HlsLoader = class {
|
|
|
288
337
|
this.rejectAttach = null;
|
|
289
338
|
this.manifestTimeout = null;
|
|
290
339
|
this.aborted = false;
|
|
340
|
+
this.lastLoadingFragmentKey = null;
|
|
341
|
+
this.repeatedFragmentLoadCount = 0;
|
|
342
|
+
this.repeatedFragmentErrorFired = false;
|
|
343
|
+
this.mediaRecoveryCount = 0;
|
|
291
344
|
/** `false` idle; `'auto'` → first `LEVEL_SWITCHED` clears; otherwise await this level index. */
|
|
292
345
|
this.qualityAwait = false;
|
|
293
346
|
this.qualitySwitchTimer = null;
|
|
294
347
|
/** Tracks consecutive menu selections so redundant "Auto → Auto" skips the spinner. */
|
|
295
348
|
this.lastChosenQualityWasAuto = false;
|
|
296
349
|
/**
|
|
297
|
-
* `null`
|
|
350
|
+
* `null` - user chose ABR auto (menu shows "Auto"). Real hls.js still exposes
|
|
298
351
|
* `currentLevel >= 0` for the playing rendition; we must not mirror that into UI state.
|
|
299
352
|
*/
|
|
300
353
|
this.manualLevelIndex = null;
|
|
@@ -350,6 +403,7 @@ var HlsLoader = class {
|
|
|
350
403
|
attachWithHlsJs(Hls, video, source) {
|
|
351
404
|
const hls = new Hls({
|
|
352
405
|
enableWorker: true,
|
|
406
|
+
...makeHlsLoadPolicies(),
|
|
353
407
|
xhrSetup: (xhr, url) => this.installXhrWatcher(xhr, url, source)
|
|
354
408
|
});
|
|
355
409
|
this.hls = hls;
|
|
@@ -370,27 +424,44 @@ var HlsLoader = class {
|
|
|
370
424
|
this.onLevelEventuallyStabilized(...args);
|
|
371
425
|
this.refreshActiveQuality();
|
|
372
426
|
};
|
|
427
|
+
const onFragmentLoading = (...args) => {
|
|
428
|
+
this.onFragmentLoading(...args);
|
|
429
|
+
};
|
|
430
|
+
const onFragmentBuffered = (...args) => {
|
|
431
|
+
this.resetFragmentLoopGuardFromEvent(...args);
|
|
432
|
+
};
|
|
373
433
|
const onError = (..._args) => {
|
|
374
|
-
const data = _args
|
|
434
|
+
const data = this.getHlsEventData(_args);
|
|
375
435
|
const status = data.response?.code;
|
|
436
|
+
if (this.tryRecoverMediaError(Hls, data)) {
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
376
439
|
const isHttpError = typeof status === "number" && status >= 400;
|
|
377
|
-
|
|
378
|
-
|
|
440
|
+
const isTerminalHlsError = this.isTerminalHlsError(data);
|
|
441
|
+
if (data.fatal || isHttpError || isTerminalHlsError) {
|
|
442
|
+
const message = this.formatHlsErrorMessage(data, status);
|
|
443
|
+
this.stopHlsLoading();
|
|
379
444
|
this.options.onError?.({
|
|
445
|
+
code: this.getHlsErrorCode(data, status),
|
|
380
446
|
message,
|
|
381
447
|
cause: data,
|
|
382
448
|
status,
|
|
383
|
-
url:
|
|
449
|
+
url: this.getErrorUrl(data),
|
|
450
|
+
retryable: this.isRetryableHlsError(data, status)
|
|
384
451
|
});
|
|
385
452
|
settle(() => reject(new Error(message)));
|
|
386
453
|
}
|
|
387
454
|
};
|
|
388
455
|
hls.on(Hls.Events.MANIFEST_PARSED, onParsed);
|
|
389
456
|
hls.on(Hls.Events.LEVEL_SWITCHED, onSwitched);
|
|
457
|
+
hls.on(Hls.Events.FRAG_LOADING, onFragmentLoading);
|
|
458
|
+
hls.on(Hls.Events.FRAG_BUFFERED, onFragmentBuffered);
|
|
390
459
|
hls.on(Hls.Events.ERROR, onError);
|
|
391
460
|
this.detachListeners.push(
|
|
392
461
|
() => hls.off(Hls.Events.MANIFEST_PARSED, onParsed),
|
|
393
462
|
() => hls.off(Hls.Events.LEVEL_SWITCHED, onSwitched),
|
|
463
|
+
() => hls.off(Hls.Events.FRAG_LOADING, onFragmentLoading),
|
|
464
|
+
() => hls.off(Hls.Events.FRAG_BUFFERED, onFragmentBuffered),
|
|
394
465
|
() => hls.off(Hls.Events.ERROR, onError),
|
|
395
466
|
() => settle(() => reject(makeAbortError()))
|
|
396
467
|
);
|
|
@@ -398,6 +469,7 @@ var HlsLoader = class {
|
|
|
398
469
|
if (timeoutMs > 0) {
|
|
399
470
|
this.manifestTimeout = setTimeout(() => {
|
|
400
471
|
const message = `HLS manifest timeout after ${timeoutMs}ms`;
|
|
472
|
+
this.stopHlsLoading();
|
|
401
473
|
this.options.onError?.({ message, cause: { type: "timeout" }, url: source.src });
|
|
402
474
|
settle(() => reject(new Error(message)));
|
|
403
475
|
}, timeoutMs);
|
|
@@ -412,7 +484,7 @@ var HlsLoader = class {
|
|
|
412
484
|
}
|
|
413
485
|
/**
|
|
414
486
|
* Wire the readystatechange watcher and remove it as soon as the XHR
|
|
415
|
-
* completes
|
|
487
|
+
* completes - without this cleanup hls.js (which spawns one XHR per segment)
|
|
416
488
|
* leaks a closure per request (A1).
|
|
417
489
|
*/
|
|
418
490
|
installXhrWatcher(xhr, url, source) {
|
|
@@ -438,10 +510,149 @@ var HlsLoader = class {
|
|
|
438
510
|
};
|
|
439
511
|
xhr.addEventListener("readystatechange", watcher);
|
|
440
512
|
}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
513
|
+
getHlsEventData(args) {
|
|
514
|
+
const data = args[1] ?? args[0];
|
|
515
|
+
return data !== null && typeof data === "object" ? data : {};
|
|
516
|
+
}
|
|
517
|
+
getFragmentUrl(data) {
|
|
518
|
+
return data.frag?.url ?? data.frag?.relurl ?? data.response?.url ?? data.url;
|
|
519
|
+
}
|
|
520
|
+
getErrorUrl(data) {
|
|
521
|
+
return this.getFragmentUrl(data) ?? this.currentSource?.src;
|
|
522
|
+
}
|
|
523
|
+
getFragmentKey(data) {
|
|
524
|
+
const url = this.getFragmentUrl(data);
|
|
525
|
+
const frag = data.frag;
|
|
526
|
+
if (!frag) return url ?? null;
|
|
527
|
+
const type = frag.type ?? "main";
|
|
528
|
+
const level = frag.level ?? "unknown-level";
|
|
529
|
+
const sn = frag.sn ?? "unknown-sn";
|
|
530
|
+
const cc = frag.cc ?? "unknown-cc";
|
|
531
|
+
const start = frag.start ?? frag.startPTS ?? "unknown-start";
|
|
532
|
+
return `${url ?? "unknown-url"}:${type}:${level}:${sn}:${cc}:${start}`;
|
|
533
|
+
}
|
|
534
|
+
isTerminalHlsError(data) {
|
|
535
|
+
switch (data.details) {
|
|
536
|
+
case "manifestParsingError":
|
|
537
|
+
case "manifestIncompatibleCodecsError":
|
|
538
|
+
case "levelEmptyError":
|
|
539
|
+
case "levelParsingError":
|
|
540
|
+
case "fragParsingError":
|
|
541
|
+
case "fragDecryptError":
|
|
542
|
+
case "keyLoadError":
|
|
543
|
+
case "keyLoadTimeOut":
|
|
544
|
+
case "bufferFullError":
|
|
545
|
+
return true;
|
|
546
|
+
default:
|
|
547
|
+
return false;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
formatHlsErrorMessage(data, status) {
|
|
551
|
+
if (data.fatal) {
|
|
552
|
+
const type2 = data.type ?? "unknown";
|
|
553
|
+
const details2 = data.details ? ` (${data.details})` : "";
|
|
554
|
+
return `HLS fatal error: ${type2}${details2}`;
|
|
555
|
+
}
|
|
556
|
+
if (typeof status === "number" && status >= 400) {
|
|
557
|
+
return `HLS load error: HTTP ${status}${data.details ? ` (${data.details})` : ""}`;
|
|
558
|
+
}
|
|
559
|
+
const type = data.type ?? "unknown";
|
|
560
|
+
const details = data.details ? ` (${data.details})` : "";
|
|
561
|
+
return `HLS error: ${type}${details}`;
|
|
562
|
+
}
|
|
563
|
+
getHlsErrorCode(data, status) {
|
|
564
|
+
if (status === 401 || status === 403) return "unauthorized";
|
|
565
|
+
switch (data.details) {
|
|
566
|
+
case "manifestParsingError":
|
|
567
|
+
case "manifestIncompatibleCodecsError":
|
|
568
|
+
case "levelParsingError":
|
|
569
|
+
case "fragParsingError":
|
|
570
|
+
case "fragDecryptError":
|
|
571
|
+
return "decode";
|
|
572
|
+
default:
|
|
573
|
+
return "network";
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
isRetryableHlsError(data, status) {
|
|
577
|
+
if (status === 401 || status === 403) return false;
|
|
578
|
+
if (typeof status === "number" && status >= 400 && status < 500) return false;
|
|
579
|
+
switch (data.details) {
|
|
580
|
+
case "manifestParsingError":
|
|
581
|
+
case "manifestIncompatibleCodecsError":
|
|
582
|
+
case "levelParsingError":
|
|
583
|
+
case "fragParsingError":
|
|
584
|
+
case "fragDecryptError":
|
|
585
|
+
return false;
|
|
586
|
+
default:
|
|
587
|
+
return true;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
isRecoverableMediaError(Hls, data) {
|
|
591
|
+
if (data.type !== Hls.ErrorTypes.MEDIA_ERROR && data.type !== "mediaError") {
|
|
592
|
+
return false;
|
|
593
|
+
}
|
|
594
|
+
if (!data.fatal) return false;
|
|
595
|
+
if (this.mediaRecoveryCount >= MEDIA_RECOVERY_LIMIT) return false;
|
|
596
|
+
if (typeof this.hls?.recoverMediaError !== "function") return false;
|
|
597
|
+
switch (data.details) {
|
|
598
|
+
case void 0:
|
|
599
|
+
case "bufferAppendError":
|
|
600
|
+
case "bufferAppendingError":
|
|
601
|
+
case "bufferStalledError":
|
|
602
|
+
case "bufferSeekOverHole":
|
|
603
|
+
case "bufferNudgeOnStall":
|
|
604
|
+
case "bufferFullError":
|
|
605
|
+
return true;
|
|
606
|
+
default:
|
|
607
|
+
return false;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
tryRecoverMediaError(Hls, data) {
|
|
611
|
+
if (!this.isRecoverableMediaError(Hls, data)) return false;
|
|
612
|
+
this.mediaRecoveryCount += 1;
|
|
613
|
+
this.resetFragmentLoopGuard();
|
|
614
|
+
this.completeQualityAwaitIfBusy();
|
|
615
|
+
try {
|
|
616
|
+
this.hls?.recoverMediaError?.();
|
|
617
|
+
return true;
|
|
618
|
+
} catch {
|
|
619
|
+
return false;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
onFragmentLoading(...args) {
|
|
623
|
+
if (this.repeatedFragmentErrorFired) return;
|
|
624
|
+
const data = this.getHlsEventData(args);
|
|
625
|
+
const key = this.getFragmentKey(data);
|
|
626
|
+
if (!key) return;
|
|
627
|
+
if (key === this.lastLoadingFragmentKey) {
|
|
628
|
+
this.repeatedFragmentLoadCount += 1;
|
|
629
|
+
} else {
|
|
630
|
+
this.lastLoadingFragmentKey = key;
|
|
631
|
+
this.repeatedFragmentLoadCount = 1;
|
|
632
|
+
}
|
|
633
|
+
if (this.repeatedFragmentLoadCount < REPEATED_FRAGMENT_LOAD_LIMIT) return;
|
|
634
|
+
this.repeatedFragmentErrorFired = true;
|
|
635
|
+
this.stopHlsLoading();
|
|
636
|
+
const url = this.getFragmentUrl(data);
|
|
637
|
+
this.options.onError?.({
|
|
638
|
+
code: "hls-fragment-loop",
|
|
639
|
+
message: `HLS fragment repeatedly loading: ${url ?? key}`,
|
|
640
|
+
cause: data,
|
|
641
|
+
url
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
resetFragmentLoopGuardFromEvent(...args) {
|
|
645
|
+
const data = this.getHlsEventData(args);
|
|
646
|
+
if (this.getFragmentKey(data)) {
|
|
647
|
+
this.resetFragmentLoopGuard();
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
resetFragmentLoopGuard() {
|
|
651
|
+
this.lastLoadingFragmentKey = null;
|
|
652
|
+
this.repeatedFragmentLoadCount = 0;
|
|
653
|
+
this.repeatedFragmentErrorFired = false;
|
|
654
|
+
}
|
|
655
|
+
abortLiveXhrs() {
|
|
445
656
|
for (const xhr of [...this.liveXhrs]) {
|
|
446
657
|
try {
|
|
447
658
|
xhr.abort();
|
|
@@ -449,6 +660,20 @@ var HlsLoader = class {
|
|
|
449
660
|
}
|
|
450
661
|
}
|
|
451
662
|
this.liveXhrs.clear();
|
|
663
|
+
}
|
|
664
|
+
stopHlsLoading() {
|
|
665
|
+
this.completeQualityAwaitIfBusy();
|
|
666
|
+
try {
|
|
667
|
+
this.hls?.stopLoad?.();
|
|
668
|
+
} catch {
|
|
669
|
+
}
|
|
670
|
+
this.abortLiveXhrs();
|
|
671
|
+
}
|
|
672
|
+
abort() {
|
|
673
|
+
if (this.aborted) return;
|
|
674
|
+
this.aborted = true;
|
|
675
|
+
this.clearManifestTimeout();
|
|
676
|
+
this.abortLiveXhrs();
|
|
452
677
|
const reject = this.rejectAttach;
|
|
453
678
|
this.rejectAttach = null;
|
|
454
679
|
reject?.(makeAbortError());
|
|
@@ -484,6 +709,8 @@ var HlsLoader = class {
|
|
|
484
709
|
this.aborted = false;
|
|
485
710
|
this.lastChosenQualityWasAuto = false;
|
|
486
711
|
this.manualLevelIndex = null;
|
|
712
|
+
this.mediaRecoveryCount = 0;
|
|
713
|
+
this.resetFragmentLoopGuard();
|
|
487
714
|
}
|
|
488
715
|
clearQualitySwitchTimerOnly() {
|
|
489
716
|
if (this.qualitySwitchTimer !== null) {
|
|
@@ -605,7 +832,7 @@ var HlsLoader = class {
|
|
|
605
832
|
const locked = this.qualities[this.manualLevelIndex] ?? null;
|
|
606
833
|
this.options.onActiveQuality?.(locked, false);
|
|
607
834
|
}
|
|
608
|
-
/** @internal
|
|
835
|
+
/** @internal - for tests. */
|
|
609
836
|
_getInternalState() {
|
|
610
837
|
return { source: this.currentSource, qualities: this.qualities };
|
|
611
838
|
}
|
|
@@ -1208,6 +1435,10 @@ function createPlayer(options = {}, internal = {}) {
|
|
|
1208
1435
|
let disposed = false;
|
|
1209
1436
|
let initialOptionsApplied = false;
|
|
1210
1437
|
let pendingPlay = false;
|
|
1438
|
+
const YT_PLAYER_STATE_PLAYING = 1;
|
|
1439
|
+
function getYouTubePlayer() {
|
|
1440
|
+
return activeLoader?._instance?.() ?? null;
|
|
1441
|
+
}
|
|
1211
1442
|
const registry = createSourceRegistry();
|
|
1212
1443
|
function emitUnauthorized(payload) {
|
|
1213
1444
|
bus.emit("unauthorized", payload);
|
|
@@ -1242,7 +1473,14 @@ function createPlayer(options = {}, internal = {}) {
|
|
|
1242
1473
|
onUnauthorized: (e) => emitUnauthorized(e),
|
|
1243
1474
|
onQualities: (q) => setQualities(q),
|
|
1244
1475
|
onActiveQuality: (q, auto) => setActiveQuality(q, auto),
|
|
1245
|
-
onError: (err) =>
|
|
1476
|
+
onError: (err) => {
|
|
1477
|
+
const playerError = createPlayerError({ ...err, code: err.code ?? "network" });
|
|
1478
|
+
if (status === "loading") {
|
|
1479
|
+
dispatch({ type: "loadFailed", error: playerError });
|
|
1480
|
+
return;
|
|
1481
|
+
}
|
|
1482
|
+
dispatch({ type: "runtimeError", error: playerError });
|
|
1483
|
+
},
|
|
1246
1484
|
onQualitySwitch: (active) => bus.emit("qualityswitch", { active })
|
|
1247
1485
|
})
|
|
1248
1486
|
);
|
|
@@ -1261,6 +1499,20 @@ function createPlayer(options = {}, internal = {}) {
|
|
|
1261
1499
|
onDuration: (duration) => {
|
|
1262
1500
|
store.setState({ duration });
|
|
1263
1501
|
bus.emit("durationchange", { duration });
|
|
1502
|
+
},
|
|
1503
|
+
onStateChange: (ytState) => {
|
|
1504
|
+
if (ytState === "playing") {
|
|
1505
|
+
bus.emit("play");
|
|
1506
|
+
if (status !== "playing" && status !== "loading") dispatch({ type: "play" });
|
|
1507
|
+
} else if (ytState === "paused") {
|
|
1508
|
+
bus.emit("pause");
|
|
1509
|
+
if (status === "playing") dispatch({ type: "pause" });
|
|
1510
|
+
} else if (ytState === "ended") {
|
|
1511
|
+
bus.emit("ended");
|
|
1512
|
+
if (status !== "ended") dispatch({ type: "ended" });
|
|
1513
|
+
} else if (ytState === "buffering") {
|
|
1514
|
+
bus.emit("buffering", { isBuffering: true });
|
|
1515
|
+
}
|
|
1264
1516
|
}
|
|
1265
1517
|
})
|
|
1266
1518
|
);
|
|
@@ -1386,6 +1638,14 @@ function createPlayer(options = {}, internal = {}) {
|
|
|
1386
1638
|
}
|
|
1387
1639
|
function runMediaPlay() {
|
|
1388
1640
|
if (!video) return;
|
|
1641
|
+
const yt = getYouTubePlayer();
|
|
1642
|
+
if (yt) {
|
|
1643
|
+
try {
|
|
1644
|
+
yt.playVideo();
|
|
1645
|
+
} catch {
|
|
1646
|
+
}
|
|
1647
|
+
return;
|
|
1648
|
+
}
|
|
1389
1649
|
const result = video.play();
|
|
1390
1650
|
if (result && typeof result.then === "function") {
|
|
1391
1651
|
result.catch((err) => {
|
|
@@ -1398,6 +1658,14 @@ function createPlayer(options = {}, internal = {}) {
|
|
|
1398
1658
|
}
|
|
1399
1659
|
function runMediaPause() {
|
|
1400
1660
|
if (!video) return;
|
|
1661
|
+
const yt = getYouTubePlayer();
|
|
1662
|
+
if (yt) {
|
|
1663
|
+
try {
|
|
1664
|
+
yt.pauseVideo();
|
|
1665
|
+
} catch {
|
|
1666
|
+
}
|
|
1667
|
+
return;
|
|
1668
|
+
}
|
|
1401
1669
|
try {
|
|
1402
1670
|
video.pause();
|
|
1403
1671
|
} catch {
|
|
@@ -1405,6 +1673,20 @@ function createPlayer(options = {}, internal = {}) {
|
|
|
1405
1673
|
}
|
|
1406
1674
|
function runMediaSeek(seconds) {
|
|
1407
1675
|
if (!video) return;
|
|
1676
|
+
const yt = getYouTubePlayer();
|
|
1677
|
+
if (yt) {
|
|
1678
|
+
try {
|
|
1679
|
+
yt.seekTo(seconds, true);
|
|
1680
|
+
store.setState({ currentTime: seconds });
|
|
1681
|
+
bus.emit("timeupdate", {
|
|
1682
|
+
currentTime: seconds,
|
|
1683
|
+
playedSeconds: seconds,
|
|
1684
|
+
duration: store.getState().duration
|
|
1685
|
+
});
|
|
1686
|
+
} catch {
|
|
1687
|
+
}
|
|
1688
|
+
return;
|
|
1689
|
+
}
|
|
1408
1690
|
try {
|
|
1409
1691
|
video.currentTime = seconds;
|
|
1410
1692
|
} catch {
|
|
@@ -1434,6 +1716,7 @@ function createPlayer(options = {}, internal = {}) {
|
|
|
1434
1716
|
bus.emit("durationchange", { duration });
|
|
1435
1717
|
});
|
|
1436
1718
|
on2("timeupdate", () => {
|
|
1719
|
+
if (getYouTubePlayer()) return;
|
|
1437
1720
|
const currentTime = target.currentTime;
|
|
1438
1721
|
store.setState({ currentTime, buffered: bufferedToRanges(target.buffered) });
|
|
1439
1722
|
bus.emit("timeupdate", {
|
|
@@ -1650,6 +1933,14 @@ function createPlayer(options = {}, internal = {}) {
|
|
|
1650
1933
|
}
|
|
1651
1934
|
function paused() {
|
|
1652
1935
|
if (!video) return true;
|
|
1936
|
+
const yt = getYouTubePlayer();
|
|
1937
|
+
if (yt) {
|
|
1938
|
+
try {
|
|
1939
|
+
return yt.getPlayerState() !== YT_PLAYER_STATE_PLAYING;
|
|
1940
|
+
} catch {
|
|
1941
|
+
return true;
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1653
1944
|
return video.paused;
|
|
1654
1945
|
}
|
|
1655
1946
|
function seekTo(seconds) {
|
|
@@ -1657,6 +1948,20 @@ function createPlayer(options = {}, internal = {}) {
|
|
|
1657
1948
|
if (!video) return;
|
|
1658
1949
|
const duration = Number.isFinite(video.duration) ? video.duration : Number.POSITIVE_INFINITY;
|
|
1659
1950
|
const clamped = Math.max(0, Math.min(seconds, duration));
|
|
1951
|
+
const yt = getYouTubePlayer();
|
|
1952
|
+
if (yt) {
|
|
1953
|
+
try {
|
|
1954
|
+
yt.seekTo(clamped, true);
|
|
1955
|
+
store.setState({ currentTime: clamped });
|
|
1956
|
+
bus.emit("timeupdate", {
|
|
1957
|
+
currentTime: clamped,
|
|
1958
|
+
playedSeconds: clamped,
|
|
1959
|
+
duration: store.getState().duration
|
|
1960
|
+
});
|
|
1961
|
+
} catch {
|
|
1962
|
+
}
|
|
1963
|
+
return;
|
|
1964
|
+
}
|
|
1660
1965
|
video.currentTime = clamped;
|
|
1661
1966
|
}
|
|
1662
1967
|
function syncVolumeMutedStoreFromVideo() {
|