@f8team/reel-core 1.0.1 → 1.0.3

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,11 @@
1
1
  # @f8team/reel-core
2
2
 
3
+ ## 1.0.3
4
+
5
+ ### Patch Changes
6
+
7
+ - Stabilize YouTube playback state by preventing hidden native media timeupdate events from overriding the YouTube clock.
8
+
3
9
  ## 1.0.1
4
10
 
5
11
  ### Patch Changes
@@ -18,46 +24,46 @@
18
24
  publishable as OSS.
19
25
 
20
26
  ## Public API additions (minor)
21
- - **`@f8team/reel-core`** — `Player.retry(): boolean` re-attempts the last
27
+ - **`@f8team/reel-core`** - `Player.retry(): boolean` re-attempts the last
22
28
  source. Returns `false` when there is nothing to retry. Useful for
23
29
  "Try again" buttons on error overlays.
24
- - **`@f8team/reel-core`** — `SourceLoader.abort?()` (optional) lets the
30
+ - **`@f8team/reel-core`** - `SourceLoader.abort?()` (optional) lets the
25
31
  orchestrator cancel in-flight network work on rapid `setSource` swaps
26
32
  without tearing down the underlying media element. Both built-in HLS and
27
33
  YouTube loaders implement it.
28
- - **`@f8team/reel-react`** — `<Player.Root labels>` prop + `defaultLabels` /
34
+ - **`@f8team/reel-react`** - `<Player.Root labels>` prop + `defaultLabels` /
29
35
  `vietnameseLabels` presets + `useLabels()` hook for ARIA / time-formatter
30
36
  i18n.
31
- - **`@f8team/reel-plugin-resume-position`** — `keyFn(source) => string | null`
37
+ - **`@f8team/reel-plugin-resume-position`** - `keyFn(source) => string | null`
32
38
  (signed-URL-friendly), `saveIntervalMs` (default 10s, periodic save while
33
39
  playing).
34
- - **`@f8team/reel-plugin-auth-aware`** — `allowlist` now accepts
40
+ - **`@f8team/reel-plugin-auth-aware`** - `allowlist` now accepts
35
41
  `string | RegExp | (url: string) => boolean` per entry; new
36
42
  `AuthAwareAllowlistEntry` exported type.
37
- - **`@f8team/reel-themes`** — `f8-brand.css` opt-in layer for F8 orange
43
+ - **`@f8team/reel-themes`** - `f8-brand.css` opt-in layer for F8 orange
38
44
  branding on top of any base theme.
39
45
 
40
46
  ## Production robustness (patch + minor)
41
- - **HLS** — listener-leak fix on per-segment XHRs; manifest-load timeout
47
+ - **HLS** - listener-leak fix on per-segment XHRs; manifest-load timeout
42
48
  (default 30s) so a stalled manifest doesn't pin the player in `loading`;
43
49
  non-fatal HTTP 4xx/5xx now surface as load errors; native fallback path
44
50
  cleans up listeners on early `detach()`.
45
- - **YouTube** — bridges polled `getCurrentTime` / `getDuration` into the
51
+ - **YouTube** - bridges polled `getCurrentTime` / `getDuration` into the
46
52
  core store and event bus (seek bars and time-aware plugins now tick on
47
53
  YouTube sources); marks the underlying `<video>` via the
48
54
  `data-reel-yt-hidden` attribute instead of mutating inline styles
49
55
  (host CSS no longer fights the loader); warns once when the player's
50
56
  parent is `position: static`.
51
- - **createPlayer** — autoplay / startTime no longer leak listeners across
57
+ - **createPlayer** - autoplay / startTime no longer leak listeners across
52
58
  re-attach cycles, and re-attach to an already-`ready` player now applies
53
59
  them synchronously instead of waiting for a transition that already fired.
54
- - **plugin-keyboard** — fixes container-scope fallback; skips `contenteditable`
60
+ - **plugin-keyboard** - fixes container-scope fallback; skips `contenteditable`
55
61
  regions, `role="textbox"`, and IME composition (no more accidental Space
56
62
  toggles while typing in a rich-text editor).
57
- - **plugin-resume-position** — SSR-safe (no-op when `window` is undefined);
63
+ - **plugin-resume-position** - SSR-safe (no-op when `window` is undefined);
58
64
  also saves on `pagehide` / `visibilitychange→hidden` for iOS Safari, plus
59
65
  periodic save while playing.
60
- - **plugin-auth-aware** — emits a one-time console warning when a string
66
+ - **plugin-auth-aware** - emits a one-time console warning when a string
61
67
  allowlist entry ends with `.`, to surface a class of consumer mistakes
62
68
  (`"https://api-gateway."` literal vs `/^https:\/\/api-gateway\./` regex).
63
69
 
package/dist/index.cjs CHANGED
@@ -113,16 +113,16 @@ function createPluginBus({ player, host }) {
113
113
  if (findIndex(plugin.name) >= 0) {
114
114
  throw new Error(`[@f8team/reel-core] plugin "${plugin.name}" is already registered`);
115
115
  }
116
- let teardown = null;
116
+ let setupResult;
117
117
  try {
118
- const result = plugin.setup(player, host);
119
- teardown = typeof result === "function" ? result : null;
118
+ setupResult = plugin.setup(player, host);
120
119
  } catch (err) {
121
120
  throw new Error(
122
121
  `[@f8team/reel-core] plugin "${plugin.name}" setup threw: ${err.message}`,
123
122
  { cause: err }
124
123
  );
125
124
  }
125
+ const teardown = typeof setupResult === "function" ? setupResult : null;
126
126
  entries.push({ plugin, teardown });
127
127
  }
128
128
  function unregister(name) {
@@ -1210,6 +1210,10 @@ function createPlayer(options = {}, internal = {}) {
1210
1210
  let disposed = false;
1211
1211
  let initialOptionsApplied = false;
1212
1212
  let pendingPlay = false;
1213
+ const YT_PLAYER_STATE_PLAYING = 1;
1214
+ function getYouTubePlayer() {
1215
+ return activeLoader?._instance?.() ?? null;
1216
+ }
1213
1217
  const registry = createSourceRegistry();
1214
1218
  function emitUnauthorized(payload) {
1215
1219
  bus.emit("unauthorized", payload);
@@ -1263,6 +1267,20 @@ function createPlayer(options = {}, internal = {}) {
1263
1267
  onDuration: (duration) => {
1264
1268
  store.setState({ duration });
1265
1269
  bus.emit("durationchange", { duration });
1270
+ },
1271
+ onStateChange: (ytState) => {
1272
+ if (ytState === "playing") {
1273
+ bus.emit("play");
1274
+ if (status !== "playing" && status !== "loading") dispatch({ type: "play" });
1275
+ } else if (ytState === "paused") {
1276
+ bus.emit("pause");
1277
+ if (status === "playing") dispatch({ type: "pause" });
1278
+ } else if (ytState === "ended") {
1279
+ bus.emit("ended");
1280
+ if (status !== "ended") dispatch({ type: "ended" });
1281
+ } else if (ytState === "buffering") {
1282
+ bus.emit("buffering", { isBuffering: true });
1283
+ }
1266
1284
  }
1267
1285
  })
1268
1286
  );
@@ -1388,6 +1406,14 @@ function createPlayer(options = {}, internal = {}) {
1388
1406
  }
1389
1407
  function runMediaPlay() {
1390
1408
  if (!video) return;
1409
+ const yt = getYouTubePlayer();
1410
+ if (yt) {
1411
+ try {
1412
+ yt.playVideo();
1413
+ } catch {
1414
+ }
1415
+ return;
1416
+ }
1391
1417
  const result = video.play();
1392
1418
  if (result && typeof result.then === "function") {
1393
1419
  result.catch((err) => {
@@ -1400,6 +1426,14 @@ function createPlayer(options = {}, internal = {}) {
1400
1426
  }
1401
1427
  function runMediaPause() {
1402
1428
  if (!video) return;
1429
+ const yt = getYouTubePlayer();
1430
+ if (yt) {
1431
+ try {
1432
+ yt.pauseVideo();
1433
+ } catch {
1434
+ }
1435
+ return;
1436
+ }
1403
1437
  try {
1404
1438
  video.pause();
1405
1439
  } catch {
@@ -1407,6 +1441,20 @@ function createPlayer(options = {}, internal = {}) {
1407
1441
  }
1408
1442
  function runMediaSeek(seconds) {
1409
1443
  if (!video) return;
1444
+ const yt = getYouTubePlayer();
1445
+ if (yt) {
1446
+ try {
1447
+ yt.seekTo(seconds, true);
1448
+ store.setState({ currentTime: seconds });
1449
+ bus.emit("timeupdate", {
1450
+ currentTime: seconds,
1451
+ playedSeconds: seconds,
1452
+ duration: store.getState().duration
1453
+ });
1454
+ } catch {
1455
+ }
1456
+ return;
1457
+ }
1410
1458
  try {
1411
1459
  video.currentTime = seconds;
1412
1460
  } catch {
@@ -1436,6 +1484,7 @@ function createPlayer(options = {}, internal = {}) {
1436
1484
  bus.emit("durationchange", { duration });
1437
1485
  });
1438
1486
  on2("timeupdate", () => {
1487
+ if (getYouTubePlayer()) return;
1439
1488
  const currentTime = target.currentTime;
1440
1489
  store.setState({ currentTime, buffered: bufferedToRanges(target.buffered) });
1441
1490
  bus.emit("timeupdate", {
@@ -1652,6 +1701,14 @@ function createPlayer(options = {}, internal = {}) {
1652
1701
  }
1653
1702
  function paused() {
1654
1703
  if (!video) return true;
1704
+ const yt = getYouTubePlayer();
1705
+ if (yt) {
1706
+ try {
1707
+ return yt.getPlayerState() !== YT_PLAYER_STATE_PLAYING;
1708
+ } catch {
1709
+ return true;
1710
+ }
1711
+ }
1655
1712
  return video.paused;
1656
1713
  }
1657
1714
  function seekTo(seconds) {
@@ -1659,6 +1716,20 @@ function createPlayer(options = {}, internal = {}) {
1659
1716
  if (!video) return;
1660
1717
  const duration = Number.isFinite(video.duration) ? video.duration : Number.POSITIVE_INFINITY;
1661
1718
  const clamped = Math.max(0, Math.min(seconds, duration));
1719
+ const yt = getYouTubePlayer();
1720
+ if (yt) {
1721
+ try {
1722
+ yt.seekTo(clamped, true);
1723
+ store.setState({ currentTime: clamped });
1724
+ bus.emit("timeupdate", {
1725
+ currentTime: clamped,
1726
+ playedSeconds: clamped,
1727
+ duration: store.getState().duration
1728
+ });
1729
+ } catch {
1730
+ }
1731
+ return;
1732
+ }
1662
1733
  video.currentTime = clamped;
1663
1734
  }
1664
1735
  function syncVolumeMutedStoreFromVideo() {