@f8team/reel-core 1.0.3 → 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 +6 -0
- package/dist/index.cjs +246 -12
- 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 +246 -12
- 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) {
|
|
@@ -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
|
}
|
|
@@ -1246,7 +1473,14 @@ function createPlayer(options = {}, internal = {}) {
|
|
|
1246
1473
|
onUnauthorized: (e) => emitUnauthorized(e),
|
|
1247
1474
|
onQualities: (q) => setQualities(q),
|
|
1248
1475
|
onActiveQuality: (q, auto) => setActiveQuality(q, auto),
|
|
1249
|
-
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
|
+
},
|
|
1250
1484
|
onQualitySwitch: (active) => bus.emit("qualityswitch", { active })
|
|
1251
1485
|
})
|
|
1252
1486
|
);
|