@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 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`** — `Player.retry(): boolean` re-attempts the last
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`** — `SourceLoader.abort?()` (optional) lets the
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`** — `<Player.Root labels>` prop + `defaultLabels` /
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`** — `keyFn(source) => string | null`
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`** — `allowlist` now accepts
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`** — `f8-brand.css` opt-in layer for F8 orange
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** — listener-leak fix on per-segment XHRs; manifest-load timeout
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** — bridges polled `getCurrentTime` / `getDuration` into the
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** — autoplay / startTime no longer leak listeners across
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** — fixes container-scope fallback; skips `contenteditable`
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** — SSR-safe (no-op when `window` is undefined);
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** — emits a one-time console warning when a string
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 teardown = null;
117
+ let setupResult;
117
118
  try {
118
- const result = plugin.setup(player, host);
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` — user chose ABR auto (menu shows "Auto"). Real hls.js still exposes
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[1] ?? {};
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
- if (data.fatal || isHttpError) {
380
- const message = data.fatal ? `HLS fatal error: ${data.type ?? "unknown"}` : `HLS load error: HTTP ${status}`;
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: data.response?.url ?? data.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 — without this cleanup hls.js (which spawns one XHR per segment)
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
- abort() {
444
- if (this.aborted) return;
445
- this.aborted = true;
446
- this.clearManifestTimeout();
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 — for tests. */
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) => applyError(createPlayerError({ ...err, code: "network" })),
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() {