@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/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)` — wires every piece of `@f8team/reel-core` into a
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 — extra knobs surfaced for tests and adapters. */
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)` — wires every piece of `@f8team/reel-core` into a
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 — extra knobs surfaced for tests and adapters. */
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 teardown = null;
115
+ let setupResult;
115
116
  try {
116
- const result = plugin.setup(player, host);
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` — user chose ABR auto (menu shows "Auto"). Real hls.js still exposes
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[1] ?? {};
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
- if (data.fatal || isHttpError) {
378
- const message = data.fatal ? `HLS fatal error: ${data.type ?? "unknown"}` : `HLS load error: HTTP ${status}`;
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: data.response?.url ?? data.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 — without this cleanup hls.js (which spawns one XHR per segment)
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
- abort() {
442
- if (this.aborted) return;
443
- this.aborted = true;
444
- this.clearManifestTimeout();
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 — for tests. */
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) => applyError(createPlayerError({ ...err, code: "network" })),
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() {