@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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @f8team/reel-core
|
|
2
2
|
|
|
3
|
+
## 1.0.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- b476560: Harden HLS playback against repeated fragment request storms by bounding HLS load retries, detecting looped fragment loads, aborting stuck loaders, and surfacing retryable player errors instead of leaving playback stuck in loading.
|
|
8
|
+
|
|
9
|
+
## 1.0.3
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- Stabilize YouTube playback state by preventing hidden native media timeupdate events from overriding the YouTube clock.
|
|
14
|
+
|
|
3
15
|
## 1.0.1
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
|
@@ -18,46 +30,46 @@
|
|
|
18
30
|
publishable as OSS.
|
|
19
31
|
|
|
20
32
|
## Public API additions (minor)
|
|
21
|
-
- **`@f8team/reel-core`**
|
|
33
|
+
- **`@f8team/reel-core`** - `Player.retry(): boolean` re-attempts the last
|
|
22
34
|
source. Returns `false` when there is nothing to retry. Useful for
|
|
23
35
|
"Try again" buttons on error overlays.
|
|
24
|
-
- **`@f8team/reel-core`**
|
|
36
|
+
- **`@f8team/reel-core`** - `SourceLoader.abort?()` (optional) lets the
|
|
25
37
|
orchestrator cancel in-flight network work on rapid `setSource` swaps
|
|
26
38
|
without tearing down the underlying media element. Both built-in HLS and
|
|
27
39
|
YouTube loaders implement it.
|
|
28
|
-
- **`@f8team/reel-react`**
|
|
40
|
+
- **`@f8team/reel-react`** - `<Player.Root labels>` prop + `defaultLabels` /
|
|
29
41
|
`vietnameseLabels` presets + `useLabels()` hook for ARIA / time-formatter
|
|
30
42
|
i18n.
|
|
31
|
-
- **`@f8team/reel-plugin-resume-position`**
|
|
43
|
+
- **`@f8team/reel-plugin-resume-position`** - `keyFn(source) => string | null`
|
|
32
44
|
(signed-URL-friendly), `saveIntervalMs` (default 10s, periodic save while
|
|
33
45
|
playing).
|
|
34
|
-
- **`@f8team/reel-plugin-auth-aware`**
|
|
46
|
+
- **`@f8team/reel-plugin-auth-aware`** - `allowlist` now accepts
|
|
35
47
|
`string | RegExp | (url: string) => boolean` per entry; new
|
|
36
48
|
`AuthAwareAllowlistEntry` exported type.
|
|
37
|
-
- **`@f8team/reel-themes`**
|
|
49
|
+
- **`@f8team/reel-themes`** - `f8-brand.css` opt-in layer for F8 orange
|
|
38
50
|
branding on top of any base theme.
|
|
39
51
|
|
|
40
52
|
## Production robustness (patch + minor)
|
|
41
|
-
- **HLS**
|
|
53
|
+
- **HLS** - listener-leak fix on per-segment XHRs; manifest-load timeout
|
|
42
54
|
(default 30s) so a stalled manifest doesn't pin the player in `loading`;
|
|
43
55
|
non-fatal HTTP 4xx/5xx now surface as load errors; native fallback path
|
|
44
56
|
cleans up listeners on early `detach()`.
|
|
45
|
-
- **YouTube**
|
|
57
|
+
- **YouTube** - bridges polled `getCurrentTime` / `getDuration` into the
|
|
46
58
|
core store and event bus (seek bars and time-aware plugins now tick on
|
|
47
59
|
YouTube sources); marks the underlying `<video>` via the
|
|
48
60
|
`data-reel-yt-hidden` attribute instead of mutating inline styles
|
|
49
61
|
(host CSS no longer fights the loader); warns once when the player's
|
|
50
62
|
parent is `position: static`.
|
|
51
|
-
- **createPlayer**
|
|
63
|
+
- **createPlayer** - autoplay / startTime no longer leak listeners across
|
|
52
64
|
re-attach cycles, and re-attach to an already-`ready` player now applies
|
|
53
65
|
them synchronously instead of waiting for a transition that already fired.
|
|
54
|
-
- **plugin-keyboard**
|
|
66
|
+
- **plugin-keyboard** - fixes container-scope fallback; skips `contenteditable`
|
|
55
67
|
regions, `role="textbox"`, and IME composition (no more accidental Space
|
|
56
68
|
toggles while typing in a rich-text editor).
|
|
57
|
-
- **plugin-resume-position**
|
|
69
|
+
- **plugin-resume-position** - SSR-safe (no-op when `window` is undefined);
|
|
58
70
|
also saves on `pagehide` / `visibilitychange→hidden` for iOS Safari, plus
|
|
59
71
|
periodic save while playing.
|
|
60
|
-
- **plugin-auth-aware**
|
|
72
|
+
- **plugin-auth-aware** - emits a one-time console warning when a string
|
|
61
73
|
allowlist entry ends with `.`, to surface a class of consumer mistakes
|
|
62
74
|
(`"https://api-gateway."` literal vs `/^https:\/\/api-gateway\./` regex).
|
|
63
75
|
|
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) {
|
|
@@ -113,16 +114,16 @@ function createPluginBus({ player, host }) {
|
|
|
113
114
|
if (findIndex(plugin.name) >= 0) {
|
|
114
115
|
throw new Error(`[@f8team/reel-core] plugin "${plugin.name}" is already registered`);
|
|
115
116
|
}
|
|
116
|
-
let
|
|
117
|
+
let setupResult;
|
|
117
118
|
try {
|
|
118
|
-
|
|
119
|
-
teardown = typeof result === "function" ? result : null;
|
|
119
|
+
setupResult = plugin.setup(player, host);
|
|
120
120
|
} catch (err) {
|
|
121
121
|
throw new Error(
|
|
122
122
|
`[@f8team/reel-core] plugin "${plugin.name}" setup threw: ${err.message}`,
|
|
123
123
|
{ cause: err }
|
|
124
124
|
);
|
|
125
125
|
}
|
|
126
|
+
const teardown = typeof setupResult === "function" ? setupResult : null;
|
|
126
127
|
entries.push({ plugin, teardown });
|
|
127
128
|
}
|
|
128
129
|
function unregister(name) {
|
|
@@ -266,6 +267,54 @@ function resolveWithCredentials(policy, url) {
|
|
|
266
267
|
}
|
|
267
268
|
|
|
268
269
|
// src/sources/hls.ts
|
|
270
|
+
var REPEATED_FRAGMENT_LOAD_LIMIT = 4;
|
|
271
|
+
var MEDIA_RECOVERY_LIMIT = 1;
|
|
272
|
+
function makeLoadPolicy(options) {
|
|
273
|
+
return {
|
|
274
|
+
default: {
|
|
275
|
+
maxTimeToFirstByteMs: options.maxTimeToFirstByteMs,
|
|
276
|
+
maxLoadTimeMs: options.maxLoadTimeMs,
|
|
277
|
+
timeoutRetry: options.timeoutRetry,
|
|
278
|
+
errorRetry: options.errorRetry
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
function makeLinearRetry(maxNumRetry, retryDelayMs, maxRetryDelayMs) {
|
|
283
|
+
return {
|
|
284
|
+
maxNumRetry,
|
|
285
|
+
retryDelayMs,
|
|
286
|
+
maxRetryDelayMs,
|
|
287
|
+
backoff: "linear"
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
function makeHlsLoadPolicies() {
|
|
291
|
+
return {
|
|
292
|
+
manifestLoadPolicy: makeLoadPolicy({
|
|
293
|
+
maxTimeToFirstByteMs: 1e4,
|
|
294
|
+
maxLoadTimeMs: 2e4,
|
|
295
|
+
timeoutRetry: makeLinearRetry(1, 0, 0),
|
|
296
|
+
errorRetry: makeLinearRetry(1, 1e3, 8e3)
|
|
297
|
+
}),
|
|
298
|
+
playlistLoadPolicy: makeLoadPolicy({
|
|
299
|
+
maxTimeToFirstByteMs: 1e4,
|
|
300
|
+
maxLoadTimeMs: 2e4,
|
|
301
|
+
timeoutRetry: makeLinearRetry(1, 0, 0),
|
|
302
|
+
errorRetry: makeLinearRetry(2, 1e3, 8e3)
|
|
303
|
+
}),
|
|
304
|
+
keyLoadPolicy: makeLoadPolicy({
|
|
305
|
+
maxTimeToFirstByteMs: 8e3,
|
|
306
|
+
maxLoadTimeMs: 2e4,
|
|
307
|
+
timeoutRetry: makeLinearRetry(1, 1e3, 8e3),
|
|
308
|
+
errorRetry: makeLinearRetry(2, 1e3, 8e3)
|
|
309
|
+
}),
|
|
310
|
+
fragLoadPolicy: makeLoadPolicy({
|
|
311
|
+
maxTimeToFirstByteMs: 1e4,
|
|
312
|
+
maxLoadTimeMs: 3e4,
|
|
313
|
+
timeoutRetry: makeLinearRetry(1, 0, 0),
|
|
314
|
+
errorRetry: makeLinearRetry(2, 1e3, 4e3)
|
|
315
|
+
})
|
|
316
|
+
};
|
|
317
|
+
}
|
|
269
318
|
var defaultLoad = () => {
|
|
270
319
|
return import('hls.js').then((m) => m.default);
|
|
271
320
|
};
|
|
@@ -290,13 +339,17 @@ var HlsLoader = class {
|
|
|
290
339
|
this.rejectAttach = null;
|
|
291
340
|
this.manifestTimeout = null;
|
|
292
341
|
this.aborted = false;
|
|
342
|
+
this.lastLoadingFragmentKey = null;
|
|
343
|
+
this.repeatedFragmentLoadCount = 0;
|
|
344
|
+
this.repeatedFragmentErrorFired = false;
|
|
345
|
+
this.mediaRecoveryCount = 0;
|
|
293
346
|
/** `false` idle; `'auto'` → first `LEVEL_SWITCHED` clears; otherwise await this level index. */
|
|
294
347
|
this.qualityAwait = false;
|
|
295
348
|
this.qualitySwitchTimer = null;
|
|
296
349
|
/** Tracks consecutive menu selections so redundant "Auto → Auto" skips the spinner. */
|
|
297
350
|
this.lastChosenQualityWasAuto = false;
|
|
298
351
|
/**
|
|
299
|
-
* `null`
|
|
352
|
+
* `null` - user chose ABR auto (menu shows "Auto"). Real hls.js still exposes
|
|
300
353
|
* `currentLevel >= 0` for the playing rendition; we must not mirror that into UI state.
|
|
301
354
|
*/
|
|
302
355
|
this.manualLevelIndex = null;
|
|
@@ -352,6 +405,7 @@ var HlsLoader = class {
|
|
|
352
405
|
attachWithHlsJs(Hls, video, source) {
|
|
353
406
|
const hls = new Hls({
|
|
354
407
|
enableWorker: true,
|
|
408
|
+
...makeHlsLoadPolicies(),
|
|
355
409
|
xhrSetup: (xhr, url) => this.installXhrWatcher(xhr, url, source)
|
|
356
410
|
});
|
|
357
411
|
this.hls = hls;
|
|
@@ -372,27 +426,44 @@ var HlsLoader = class {
|
|
|
372
426
|
this.onLevelEventuallyStabilized(...args);
|
|
373
427
|
this.refreshActiveQuality();
|
|
374
428
|
};
|
|
429
|
+
const onFragmentLoading = (...args) => {
|
|
430
|
+
this.onFragmentLoading(...args);
|
|
431
|
+
};
|
|
432
|
+
const onFragmentBuffered = (...args) => {
|
|
433
|
+
this.resetFragmentLoopGuardFromEvent(...args);
|
|
434
|
+
};
|
|
375
435
|
const onError = (..._args) => {
|
|
376
|
-
const data = _args
|
|
436
|
+
const data = this.getHlsEventData(_args);
|
|
377
437
|
const status = data.response?.code;
|
|
438
|
+
if (this.tryRecoverMediaError(Hls, data)) {
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
378
441
|
const isHttpError = typeof status === "number" && status >= 400;
|
|
379
|
-
|
|
380
|
-
|
|
442
|
+
const isTerminalHlsError = this.isTerminalHlsError(data);
|
|
443
|
+
if (data.fatal || isHttpError || isTerminalHlsError) {
|
|
444
|
+
const message = this.formatHlsErrorMessage(data, status);
|
|
445
|
+
this.stopHlsLoading();
|
|
381
446
|
this.options.onError?.({
|
|
447
|
+
code: this.getHlsErrorCode(data, status),
|
|
382
448
|
message,
|
|
383
449
|
cause: data,
|
|
384
450
|
status,
|
|
385
|
-
url:
|
|
451
|
+
url: this.getErrorUrl(data),
|
|
452
|
+
retryable: this.isRetryableHlsError(data, status)
|
|
386
453
|
});
|
|
387
454
|
settle(() => reject(new Error(message)));
|
|
388
455
|
}
|
|
389
456
|
};
|
|
390
457
|
hls.on(Hls.Events.MANIFEST_PARSED, onParsed);
|
|
391
458
|
hls.on(Hls.Events.LEVEL_SWITCHED, onSwitched);
|
|
459
|
+
hls.on(Hls.Events.FRAG_LOADING, onFragmentLoading);
|
|
460
|
+
hls.on(Hls.Events.FRAG_BUFFERED, onFragmentBuffered);
|
|
392
461
|
hls.on(Hls.Events.ERROR, onError);
|
|
393
462
|
this.detachListeners.push(
|
|
394
463
|
() => hls.off(Hls.Events.MANIFEST_PARSED, onParsed),
|
|
395
464
|
() => hls.off(Hls.Events.LEVEL_SWITCHED, onSwitched),
|
|
465
|
+
() => hls.off(Hls.Events.FRAG_LOADING, onFragmentLoading),
|
|
466
|
+
() => hls.off(Hls.Events.FRAG_BUFFERED, onFragmentBuffered),
|
|
396
467
|
() => hls.off(Hls.Events.ERROR, onError),
|
|
397
468
|
() => settle(() => reject(makeAbortError()))
|
|
398
469
|
);
|
|
@@ -400,6 +471,7 @@ var HlsLoader = class {
|
|
|
400
471
|
if (timeoutMs > 0) {
|
|
401
472
|
this.manifestTimeout = setTimeout(() => {
|
|
402
473
|
const message = `HLS manifest timeout after ${timeoutMs}ms`;
|
|
474
|
+
this.stopHlsLoading();
|
|
403
475
|
this.options.onError?.({ message, cause: { type: "timeout" }, url: source.src });
|
|
404
476
|
settle(() => reject(new Error(message)));
|
|
405
477
|
}, timeoutMs);
|
|
@@ -414,7 +486,7 @@ var HlsLoader = class {
|
|
|
414
486
|
}
|
|
415
487
|
/**
|
|
416
488
|
* Wire the readystatechange watcher and remove it as soon as the XHR
|
|
417
|
-
* completes
|
|
489
|
+
* completes - without this cleanup hls.js (which spawns one XHR per segment)
|
|
418
490
|
* leaks a closure per request (A1).
|
|
419
491
|
*/
|
|
420
492
|
installXhrWatcher(xhr, url, source) {
|
|
@@ -440,10 +512,149 @@ var HlsLoader = class {
|
|
|
440
512
|
};
|
|
441
513
|
xhr.addEventListener("readystatechange", watcher);
|
|
442
514
|
}
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
515
|
+
getHlsEventData(args) {
|
|
516
|
+
const data = args[1] ?? args[0];
|
|
517
|
+
return data !== null && typeof data === "object" ? data : {};
|
|
518
|
+
}
|
|
519
|
+
getFragmentUrl(data) {
|
|
520
|
+
return data.frag?.url ?? data.frag?.relurl ?? data.response?.url ?? data.url;
|
|
521
|
+
}
|
|
522
|
+
getErrorUrl(data) {
|
|
523
|
+
return this.getFragmentUrl(data) ?? this.currentSource?.src;
|
|
524
|
+
}
|
|
525
|
+
getFragmentKey(data) {
|
|
526
|
+
const url = this.getFragmentUrl(data);
|
|
527
|
+
const frag = data.frag;
|
|
528
|
+
if (!frag) return url ?? null;
|
|
529
|
+
const type = frag.type ?? "main";
|
|
530
|
+
const level = frag.level ?? "unknown-level";
|
|
531
|
+
const sn = frag.sn ?? "unknown-sn";
|
|
532
|
+
const cc = frag.cc ?? "unknown-cc";
|
|
533
|
+
const start = frag.start ?? frag.startPTS ?? "unknown-start";
|
|
534
|
+
return `${url ?? "unknown-url"}:${type}:${level}:${sn}:${cc}:${start}`;
|
|
535
|
+
}
|
|
536
|
+
isTerminalHlsError(data) {
|
|
537
|
+
switch (data.details) {
|
|
538
|
+
case "manifestParsingError":
|
|
539
|
+
case "manifestIncompatibleCodecsError":
|
|
540
|
+
case "levelEmptyError":
|
|
541
|
+
case "levelParsingError":
|
|
542
|
+
case "fragParsingError":
|
|
543
|
+
case "fragDecryptError":
|
|
544
|
+
case "keyLoadError":
|
|
545
|
+
case "keyLoadTimeOut":
|
|
546
|
+
case "bufferFullError":
|
|
547
|
+
return true;
|
|
548
|
+
default:
|
|
549
|
+
return false;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
formatHlsErrorMessage(data, status) {
|
|
553
|
+
if (data.fatal) {
|
|
554
|
+
const type2 = data.type ?? "unknown";
|
|
555
|
+
const details2 = data.details ? ` (${data.details})` : "";
|
|
556
|
+
return `HLS fatal error: ${type2}${details2}`;
|
|
557
|
+
}
|
|
558
|
+
if (typeof status === "number" && status >= 400) {
|
|
559
|
+
return `HLS load error: HTTP ${status}${data.details ? ` (${data.details})` : ""}`;
|
|
560
|
+
}
|
|
561
|
+
const type = data.type ?? "unknown";
|
|
562
|
+
const details = data.details ? ` (${data.details})` : "";
|
|
563
|
+
return `HLS error: ${type}${details}`;
|
|
564
|
+
}
|
|
565
|
+
getHlsErrorCode(data, status) {
|
|
566
|
+
if (status === 401 || status === 403) return "unauthorized";
|
|
567
|
+
switch (data.details) {
|
|
568
|
+
case "manifestParsingError":
|
|
569
|
+
case "manifestIncompatibleCodecsError":
|
|
570
|
+
case "levelParsingError":
|
|
571
|
+
case "fragParsingError":
|
|
572
|
+
case "fragDecryptError":
|
|
573
|
+
return "decode";
|
|
574
|
+
default:
|
|
575
|
+
return "network";
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
isRetryableHlsError(data, status) {
|
|
579
|
+
if (status === 401 || status === 403) return false;
|
|
580
|
+
if (typeof status === "number" && status >= 400 && status < 500) return false;
|
|
581
|
+
switch (data.details) {
|
|
582
|
+
case "manifestParsingError":
|
|
583
|
+
case "manifestIncompatibleCodecsError":
|
|
584
|
+
case "levelParsingError":
|
|
585
|
+
case "fragParsingError":
|
|
586
|
+
case "fragDecryptError":
|
|
587
|
+
return false;
|
|
588
|
+
default:
|
|
589
|
+
return true;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
isRecoverableMediaError(Hls, data) {
|
|
593
|
+
if (data.type !== Hls.ErrorTypes.MEDIA_ERROR && data.type !== "mediaError") {
|
|
594
|
+
return false;
|
|
595
|
+
}
|
|
596
|
+
if (!data.fatal) return false;
|
|
597
|
+
if (this.mediaRecoveryCount >= MEDIA_RECOVERY_LIMIT) return false;
|
|
598
|
+
if (typeof this.hls?.recoverMediaError !== "function") return false;
|
|
599
|
+
switch (data.details) {
|
|
600
|
+
case void 0:
|
|
601
|
+
case "bufferAppendError":
|
|
602
|
+
case "bufferAppendingError":
|
|
603
|
+
case "bufferStalledError":
|
|
604
|
+
case "bufferSeekOverHole":
|
|
605
|
+
case "bufferNudgeOnStall":
|
|
606
|
+
case "bufferFullError":
|
|
607
|
+
return true;
|
|
608
|
+
default:
|
|
609
|
+
return false;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
tryRecoverMediaError(Hls, data) {
|
|
613
|
+
if (!this.isRecoverableMediaError(Hls, data)) return false;
|
|
614
|
+
this.mediaRecoveryCount += 1;
|
|
615
|
+
this.resetFragmentLoopGuard();
|
|
616
|
+
this.completeQualityAwaitIfBusy();
|
|
617
|
+
try {
|
|
618
|
+
this.hls?.recoverMediaError?.();
|
|
619
|
+
return true;
|
|
620
|
+
} catch {
|
|
621
|
+
return false;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
onFragmentLoading(...args) {
|
|
625
|
+
if (this.repeatedFragmentErrorFired) return;
|
|
626
|
+
const data = this.getHlsEventData(args);
|
|
627
|
+
const key = this.getFragmentKey(data);
|
|
628
|
+
if (!key) return;
|
|
629
|
+
if (key === this.lastLoadingFragmentKey) {
|
|
630
|
+
this.repeatedFragmentLoadCount += 1;
|
|
631
|
+
} else {
|
|
632
|
+
this.lastLoadingFragmentKey = key;
|
|
633
|
+
this.repeatedFragmentLoadCount = 1;
|
|
634
|
+
}
|
|
635
|
+
if (this.repeatedFragmentLoadCount < REPEATED_FRAGMENT_LOAD_LIMIT) return;
|
|
636
|
+
this.repeatedFragmentErrorFired = true;
|
|
637
|
+
this.stopHlsLoading();
|
|
638
|
+
const url = this.getFragmentUrl(data);
|
|
639
|
+
this.options.onError?.({
|
|
640
|
+
code: "hls-fragment-loop",
|
|
641
|
+
message: `HLS fragment repeatedly loading: ${url ?? key}`,
|
|
642
|
+
cause: data,
|
|
643
|
+
url
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
resetFragmentLoopGuardFromEvent(...args) {
|
|
647
|
+
const data = this.getHlsEventData(args);
|
|
648
|
+
if (this.getFragmentKey(data)) {
|
|
649
|
+
this.resetFragmentLoopGuard();
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
resetFragmentLoopGuard() {
|
|
653
|
+
this.lastLoadingFragmentKey = null;
|
|
654
|
+
this.repeatedFragmentLoadCount = 0;
|
|
655
|
+
this.repeatedFragmentErrorFired = false;
|
|
656
|
+
}
|
|
657
|
+
abortLiveXhrs() {
|
|
447
658
|
for (const xhr of [...this.liveXhrs]) {
|
|
448
659
|
try {
|
|
449
660
|
xhr.abort();
|
|
@@ -451,6 +662,20 @@ var HlsLoader = class {
|
|
|
451
662
|
}
|
|
452
663
|
}
|
|
453
664
|
this.liveXhrs.clear();
|
|
665
|
+
}
|
|
666
|
+
stopHlsLoading() {
|
|
667
|
+
this.completeQualityAwaitIfBusy();
|
|
668
|
+
try {
|
|
669
|
+
this.hls?.stopLoad?.();
|
|
670
|
+
} catch {
|
|
671
|
+
}
|
|
672
|
+
this.abortLiveXhrs();
|
|
673
|
+
}
|
|
674
|
+
abort() {
|
|
675
|
+
if (this.aborted) return;
|
|
676
|
+
this.aborted = true;
|
|
677
|
+
this.clearManifestTimeout();
|
|
678
|
+
this.abortLiveXhrs();
|
|
454
679
|
const reject = this.rejectAttach;
|
|
455
680
|
this.rejectAttach = null;
|
|
456
681
|
reject?.(makeAbortError());
|
|
@@ -486,6 +711,8 @@ var HlsLoader = class {
|
|
|
486
711
|
this.aborted = false;
|
|
487
712
|
this.lastChosenQualityWasAuto = false;
|
|
488
713
|
this.manualLevelIndex = null;
|
|
714
|
+
this.mediaRecoveryCount = 0;
|
|
715
|
+
this.resetFragmentLoopGuard();
|
|
489
716
|
}
|
|
490
717
|
clearQualitySwitchTimerOnly() {
|
|
491
718
|
if (this.qualitySwitchTimer !== null) {
|
|
@@ -607,7 +834,7 @@ var HlsLoader = class {
|
|
|
607
834
|
const locked = this.qualities[this.manualLevelIndex] ?? null;
|
|
608
835
|
this.options.onActiveQuality?.(locked, false);
|
|
609
836
|
}
|
|
610
|
-
/** @internal
|
|
837
|
+
/** @internal - for tests. */
|
|
611
838
|
_getInternalState() {
|
|
612
839
|
return { source: this.currentSource, qualities: this.qualities };
|
|
613
840
|
}
|
|
@@ -1210,6 +1437,10 @@ function createPlayer(options = {}, internal = {}) {
|
|
|
1210
1437
|
let disposed = false;
|
|
1211
1438
|
let initialOptionsApplied = false;
|
|
1212
1439
|
let pendingPlay = false;
|
|
1440
|
+
const YT_PLAYER_STATE_PLAYING = 1;
|
|
1441
|
+
function getYouTubePlayer() {
|
|
1442
|
+
return activeLoader?._instance?.() ?? null;
|
|
1443
|
+
}
|
|
1213
1444
|
const registry = createSourceRegistry();
|
|
1214
1445
|
function emitUnauthorized(payload) {
|
|
1215
1446
|
bus.emit("unauthorized", payload);
|
|
@@ -1244,7 +1475,14 @@ function createPlayer(options = {}, internal = {}) {
|
|
|
1244
1475
|
onUnauthorized: (e) => emitUnauthorized(e),
|
|
1245
1476
|
onQualities: (q) => setQualities(q),
|
|
1246
1477
|
onActiveQuality: (q, auto) => setActiveQuality(q, auto),
|
|
1247
|
-
onError: (err) =>
|
|
1478
|
+
onError: (err) => {
|
|
1479
|
+
const playerError = createPlayerError({ ...err, code: err.code ?? "network" });
|
|
1480
|
+
if (status === "loading") {
|
|
1481
|
+
dispatch({ type: "loadFailed", error: playerError });
|
|
1482
|
+
return;
|
|
1483
|
+
}
|
|
1484
|
+
dispatch({ type: "runtimeError", error: playerError });
|
|
1485
|
+
},
|
|
1248
1486
|
onQualitySwitch: (active) => bus.emit("qualityswitch", { active })
|
|
1249
1487
|
})
|
|
1250
1488
|
);
|
|
@@ -1263,6 +1501,20 @@ function createPlayer(options = {}, internal = {}) {
|
|
|
1263
1501
|
onDuration: (duration) => {
|
|
1264
1502
|
store.setState({ duration });
|
|
1265
1503
|
bus.emit("durationchange", { duration });
|
|
1504
|
+
},
|
|
1505
|
+
onStateChange: (ytState) => {
|
|
1506
|
+
if (ytState === "playing") {
|
|
1507
|
+
bus.emit("play");
|
|
1508
|
+
if (status !== "playing" && status !== "loading") dispatch({ type: "play" });
|
|
1509
|
+
} else if (ytState === "paused") {
|
|
1510
|
+
bus.emit("pause");
|
|
1511
|
+
if (status === "playing") dispatch({ type: "pause" });
|
|
1512
|
+
} else if (ytState === "ended") {
|
|
1513
|
+
bus.emit("ended");
|
|
1514
|
+
if (status !== "ended") dispatch({ type: "ended" });
|
|
1515
|
+
} else if (ytState === "buffering") {
|
|
1516
|
+
bus.emit("buffering", { isBuffering: true });
|
|
1517
|
+
}
|
|
1266
1518
|
}
|
|
1267
1519
|
})
|
|
1268
1520
|
);
|
|
@@ -1388,6 +1640,14 @@ function createPlayer(options = {}, internal = {}) {
|
|
|
1388
1640
|
}
|
|
1389
1641
|
function runMediaPlay() {
|
|
1390
1642
|
if (!video) return;
|
|
1643
|
+
const yt = getYouTubePlayer();
|
|
1644
|
+
if (yt) {
|
|
1645
|
+
try {
|
|
1646
|
+
yt.playVideo();
|
|
1647
|
+
} catch {
|
|
1648
|
+
}
|
|
1649
|
+
return;
|
|
1650
|
+
}
|
|
1391
1651
|
const result = video.play();
|
|
1392
1652
|
if (result && typeof result.then === "function") {
|
|
1393
1653
|
result.catch((err) => {
|
|
@@ -1400,6 +1660,14 @@ function createPlayer(options = {}, internal = {}) {
|
|
|
1400
1660
|
}
|
|
1401
1661
|
function runMediaPause() {
|
|
1402
1662
|
if (!video) return;
|
|
1663
|
+
const yt = getYouTubePlayer();
|
|
1664
|
+
if (yt) {
|
|
1665
|
+
try {
|
|
1666
|
+
yt.pauseVideo();
|
|
1667
|
+
} catch {
|
|
1668
|
+
}
|
|
1669
|
+
return;
|
|
1670
|
+
}
|
|
1403
1671
|
try {
|
|
1404
1672
|
video.pause();
|
|
1405
1673
|
} catch {
|
|
@@ -1407,6 +1675,20 @@ function createPlayer(options = {}, internal = {}) {
|
|
|
1407
1675
|
}
|
|
1408
1676
|
function runMediaSeek(seconds) {
|
|
1409
1677
|
if (!video) return;
|
|
1678
|
+
const yt = getYouTubePlayer();
|
|
1679
|
+
if (yt) {
|
|
1680
|
+
try {
|
|
1681
|
+
yt.seekTo(seconds, true);
|
|
1682
|
+
store.setState({ currentTime: seconds });
|
|
1683
|
+
bus.emit("timeupdate", {
|
|
1684
|
+
currentTime: seconds,
|
|
1685
|
+
playedSeconds: seconds,
|
|
1686
|
+
duration: store.getState().duration
|
|
1687
|
+
});
|
|
1688
|
+
} catch {
|
|
1689
|
+
}
|
|
1690
|
+
return;
|
|
1691
|
+
}
|
|
1410
1692
|
try {
|
|
1411
1693
|
video.currentTime = seconds;
|
|
1412
1694
|
} catch {
|
|
@@ -1436,6 +1718,7 @@ function createPlayer(options = {}, internal = {}) {
|
|
|
1436
1718
|
bus.emit("durationchange", { duration });
|
|
1437
1719
|
});
|
|
1438
1720
|
on2("timeupdate", () => {
|
|
1721
|
+
if (getYouTubePlayer()) return;
|
|
1439
1722
|
const currentTime = target.currentTime;
|
|
1440
1723
|
store.setState({ currentTime, buffered: bufferedToRanges(target.buffered) });
|
|
1441
1724
|
bus.emit("timeupdate", {
|
|
@@ -1652,6 +1935,14 @@ function createPlayer(options = {}, internal = {}) {
|
|
|
1652
1935
|
}
|
|
1653
1936
|
function paused() {
|
|
1654
1937
|
if (!video) return true;
|
|
1938
|
+
const yt = getYouTubePlayer();
|
|
1939
|
+
if (yt) {
|
|
1940
|
+
try {
|
|
1941
|
+
return yt.getPlayerState() !== YT_PLAYER_STATE_PLAYING;
|
|
1942
|
+
} catch {
|
|
1943
|
+
return true;
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1655
1946
|
return video.paused;
|
|
1656
1947
|
}
|
|
1657
1948
|
function seekTo(seconds) {
|
|
@@ -1659,6 +1950,20 @@ function createPlayer(options = {}, internal = {}) {
|
|
|
1659
1950
|
if (!video) return;
|
|
1660
1951
|
const duration = Number.isFinite(video.duration) ? video.duration : Number.POSITIVE_INFINITY;
|
|
1661
1952
|
const clamped = Math.max(0, Math.min(seconds, duration));
|
|
1953
|
+
const yt = getYouTubePlayer();
|
|
1954
|
+
if (yt) {
|
|
1955
|
+
try {
|
|
1956
|
+
yt.seekTo(clamped, true);
|
|
1957
|
+
store.setState({ currentTime: clamped });
|
|
1958
|
+
bus.emit("timeupdate", {
|
|
1959
|
+
currentTime: clamped,
|
|
1960
|
+
playedSeconds: clamped,
|
|
1961
|
+
duration: store.getState().duration
|
|
1962
|
+
});
|
|
1963
|
+
} catch {
|
|
1964
|
+
}
|
|
1965
|
+
return;
|
|
1966
|
+
}
|
|
1662
1967
|
video.currentTime = clamped;
|
|
1663
1968
|
}
|
|
1664
1969
|
function syncVolumeMutedStoreFromVideo() {
|