@xhub-reels/sdk 0.1.15 → 0.1.18

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.js CHANGED
@@ -827,6 +827,21 @@ var ResourceGovernor = class {
827
827
  isPreloading(index) {
828
828
  return this.store.getState().preloadQueue.includes(index);
829
829
  }
830
+ /**
831
+ * Returns the buffer tier (1–4) for a given index.
832
+ * - 1: Active (playing, 10s buffer)
833
+ * - 2: Hot (±bufferWindow, 2s buffer, instant swap)
834
+ * - 3: Warm (manifest + 1 segment, ~300ms show)
835
+ * - 4: Cold (preload queue — manifest in HTTP cache, no DOM)
836
+ */
837
+ getTier(index) {
838
+ const { focusedIndex, activeAllocations, warmAllocations, preloadQueue } = this.store.getState();
839
+ if (index === focusedIndex) return 1;
840
+ if (activeAllocations.has(index)) return 2;
841
+ if (warmAllocations.has(index)) return 3;
842
+ if (preloadQueue.includes(index)) return 4;
843
+ return null;
844
+ }
830
845
  getActiveAllocations() {
831
846
  return [...this.store.getState().activeAllocations];
832
847
  }
@@ -899,7 +914,9 @@ function usePointerGesture(config = {}) {
899
914
  containerSize,
900
915
  dragThresholdRatio = 0.5,
901
916
  onSnap,
902
- onBounceBack
917
+ onBounceBack,
918
+ onDragStart,
919
+ onDragEnd
903
920
  } = config;
904
921
  const isDraggingRef = useRef(false);
905
922
  const dragOffsetRef = useRef(0);
@@ -916,6 +933,8 @@ function usePointerGesture(config = {}) {
916
933
  const onDragThresholdRef = useRef(onDragThreshold);
917
934
  const onSnapRef = useRef(onSnap);
918
935
  const onBounceBackRef = useRef(onBounceBack);
936
+ const onDragStartRef = useRef(onDragStart);
937
+ const onDragEndRef = useRef(onDragEnd);
919
938
  const disabledRef = useRef(disabled);
920
939
  const containerSizeRef = useRef(containerSize);
921
940
  const dragThresholdRatioRef = useRef(dragThresholdRatio);
@@ -924,6 +943,8 @@ function usePointerGesture(config = {}) {
924
943
  onDragThresholdRef.current = onDragThreshold;
925
944
  onSnapRef.current = onSnap;
926
945
  onBounceBackRef.current = onBounceBack;
946
+ onDragStartRef.current = onDragStart;
947
+ onDragEndRef.current = onDragEnd;
927
948
  disabledRef.current = disabled;
928
949
  containerSizeRef.current = containerSize;
929
950
  dragThresholdRatioRef.current = dragThresholdRatio;
@@ -990,6 +1011,7 @@ function usePointerGesture(config = {}) {
990
1011
  window.removeEventListener("pointermove", handlePointerMove);
991
1012
  window.removeEventListener("pointerup", handlePointerUp);
992
1013
  window.removeEventListener("pointercancel", handlePointerUp);
1014
+ onDragEndRef.current?.();
993
1015
  const offset = dragOffsetRef.current;
994
1016
  const velocity = velocityRef.current;
995
1017
  const shouldSnap = Math.abs(velocity) > velocityThreshold || Math.abs(offset) > distanceThreshold;
@@ -1021,6 +1043,7 @@ function usePointerGesture(config = {}) {
1021
1043
  lastTimeRef.current = performance.now();
1022
1044
  velocityRef.current = 0;
1023
1045
  dragOffsetRef.current = 0;
1046
+ onDragStartRef.current?.();
1024
1047
  e.currentTarget.setPointerCapture(e.pointerId);
1025
1048
  window.addEventListener("pointermove", handlePointerMove, { passive: true });
1026
1049
  window.addEventListener("pointerup", handlePointerUp);
@@ -1238,6 +1261,7 @@ function useResource() {
1238
1261
  const networkType = useResourceSelector((s) => s.networkType);
1239
1262
  const isActive = useResourceSelector((s) => s.isActive);
1240
1263
  const prefetchIndex = useResourceSelector((s) => s.prefetchIndex);
1264
+ const preloadQueue = useResourceSelector((s) => s.preloadQueue);
1241
1265
  const activeIndices = useMemo(() => [...activeAllocations], [activeAllocations]);
1242
1266
  const warmIndices = useMemo(() => [...warmAllocations], [warmAllocations]);
1243
1267
  const setFocusedIndex = useCallback(
@@ -1268,6 +1292,10 @@ function useResource() {
1268
1292
  (i) => resourceGovernor.setPrefetchIndex(i),
1269
1293
  [resourceGovernor]
1270
1294
  );
1295
+ const getTier = useCallback(
1296
+ (index) => resourceGovernor.getTier(index),
1297
+ [resourceGovernor]
1298
+ );
1271
1299
  return {
1272
1300
  activeIndices,
1273
1301
  warmIndices,
@@ -1276,13 +1304,15 @@ function useResource() {
1276
1304
  networkType,
1277
1305
  isActive,
1278
1306
  prefetchIndex,
1307
+ preloadQueue,
1279
1308
  setFocusedIndex,
1280
1309
  setFocusedIndexImmediate,
1281
1310
  setTotalItems,
1282
1311
  shouldRenderVideo,
1283
1312
  isAllocated,
1284
1313
  isWarmAllocated,
1285
- setPrefetchIndex
1314
+ setPrefetchIndex,
1315
+ getTier
1286
1316
  };
1287
1317
  }
1288
1318
  var ACTIVE_HLS_DEFAULTS = {
@@ -1358,18 +1388,9 @@ function mapHlsError(data) {
1358
1388
  }
1359
1389
  function useHls(options) {
1360
1390
  const { src, videoRef, isActive, isPrefetch, bufferTier = "active", hlsConfig, onError } = options;
1361
- const [isHlsJs, setIsHlsJs] = useState(false);
1362
- const [isNativeHls, setIsNativeHls] = useState(false);
1363
- useEffect(() => {
1364
- const hlsSupported = Hls.isSupported();
1365
- const native = supportsNativeHls();
1366
- setIsHlsJs(hlsSupported && !native);
1367
- setIsNativeHls(native);
1368
- }, []);
1369
- const isHlsJsRef = useRef(false);
1370
- const isNativeRef = useRef(false);
1371
- isHlsJsRef.current = isHlsJs;
1372
- isNativeRef.current = isNativeHls;
1391
+ const isHlsSupported = typeof window !== "undefined" && Hls.isSupported();
1392
+ const isNative = supportsNativeHls();
1393
+ const isHlsJs = isHlsSupported && !isNative;
1373
1394
  const [isReady, setIsReady] = useState(false);
1374
1395
  const hlsRef = useRef(null);
1375
1396
  const onErrorRef = useRef(onError);
@@ -1394,18 +1415,7 @@ function useHls(options) {
1394
1415
  currentSrcRef.current = void 0;
1395
1416
  return;
1396
1417
  }
1397
- const isNative = isNativeRef.current;
1398
- const isHlsSupported = isHlsJsRef.current;
1399
1418
  if (!isActive && !isPrefetch) {
1400
- if (isNative) {
1401
- if (video.src) {
1402
- video.removeAttribute("src");
1403
- video.load();
1404
- }
1405
- setIsReady(false);
1406
- currentSrcRef.current = void 0;
1407
- return;
1408
- }
1409
1419
  destroy();
1410
1420
  setIsReady(false);
1411
1421
  canPlayFiredRef.current = false;
@@ -1413,45 +1423,40 @@ function useHls(options) {
1413
1423
  return;
1414
1424
  }
1415
1425
  if (isNative) {
1416
- if (currentSrcRef.current === src) {
1417
- if (video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) {
1418
- setIsReady(true);
1419
- return void 0;
1420
- }
1421
- const handleCanPlayReuse = () => setIsReady(true);
1422
- video.addEventListener("canplay", handleCanPlayReuse, { once: true });
1423
- video.addEventListener("loadeddata", handleCanPlayReuse, { once: true });
1424
- video.addEventListener("playing", handleCanPlayReuse, { once: true });
1425
- if (video.readyState < HTMLMediaElement.HAVE_CURRENT_DATA) {
1426
- video.load();
1427
- }
1428
- return () => {
1429
- video.removeEventListener("canplay", handleCanPlayReuse);
1430
- video.removeEventListener("loadeddata", handleCanPlayReuse);
1431
- video.removeEventListener("playing", handleCanPlayReuse);
1432
- };
1433
- }
1434
- video.src = src;
1435
- currentSrcRef.current = src;
1436
- if (!isActive) {
1426
+ if (video.src !== src) {
1427
+ video.src = src;
1437
1428
  video.load();
1438
1429
  }
1439
- if (video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) {
1430
+ if (!video.hasAttribute("webkit-playsinline")) {
1431
+ video.setAttribute("webkit-playsinline", "");
1432
+ }
1433
+ if (video.readyState >= HTMLMediaElement.HAVE_FUTURE_DATA) {
1440
1434
  setIsReady(true);
1441
- return void 0;
1435
+ currentSrcRef.current = src;
1436
+ return;
1442
1437
  }
1443
1438
  setIsReady(false);
1444
- const handleCanPlay2 = () => setIsReady(true);
1439
+ canPlayFiredRef.current = false;
1440
+ currentSrcRef.current = src;
1441
+ const handleCanPlay2 = () => {
1442
+ canPlayFiredRef.current = true;
1443
+ setIsReady(true);
1444
+ };
1445
+ const handleLoadedData = () => {
1446
+ if (!canPlayFiredRef.current) {
1447
+ canPlayFiredRef.current = true;
1448
+ setIsReady(true);
1449
+ }
1450
+ };
1445
1451
  video.addEventListener("canplay", handleCanPlay2, { once: true });
1446
- video.addEventListener("loadeddata", handleCanPlay2, { once: true });
1447
- video.addEventListener("playing", handleCanPlay2, { once: true });
1452
+ video.addEventListener("loadeddata", handleLoadedData, { once: true });
1448
1453
  return () => {
1449
1454
  video.removeEventListener("canplay", handleCanPlay2);
1450
- video.removeEventListener("loadeddata", handleCanPlay2);
1451
- video.removeEventListener("playing", handleCanPlay2);
1455
+ video.removeEventListener("loadeddata", handleLoadedData);
1452
1456
  };
1453
1457
  }
1454
1458
  if (!isHlsSupported) {
1459
+ onErrorRef.current?.("UNKNOWN", "HLS playback not supported in this browser");
1455
1460
  return;
1456
1461
  }
1457
1462
  if (hlsRef.current && currentSrcRef.current === src) {
@@ -1515,7 +1520,7 @@ function useHls(options) {
1515
1520
  currentSrcRef.current = void 0;
1516
1521
  }
1517
1522
  };
1518
- }, [src, isActive, isPrefetch, isHlsJs, isNativeHls]);
1523
+ }, [src, isActive, isPrefetch]);
1519
1524
  useEffect(() => {
1520
1525
  const hls = hlsRef.current;
1521
1526
  if (!hls) {
@@ -1531,10 +1536,15 @@ function useHls(options) {
1531
1536
  for (const key of configKeys) {
1532
1537
  hlsAnyConfig[key] = newConfig[key];
1533
1538
  }
1539
+ if (prevTier === "warm" && bufferTier === "active") {
1540
+ try {
1541
+ hls.startLoad();
1542
+ } catch {
1543
+ }
1544
+ }
1534
1545
  }, [bufferTier]);
1535
1546
  return {
1536
1547
  isHlsJs,
1537
- isNativeHls,
1538
1548
  isReady,
1539
1549
  destroy
1540
1550
  };
@@ -1653,43 +1663,60 @@ function skeletonCircle(size) {
1653
1663
  background: "rgba(255,255,255,0.1)"
1654
1664
  };
1655
1665
  }
1656
- function DefaultDoubleTap({ isDoubleTap }) {
1657
- if (!isDoubleTap) return null;
1658
- return /* @__PURE__ */ jsxs(
1666
+ function DefaultPauseAction() {
1667
+ return /* @__PURE__ */ jsx(
1659
1668
  "div",
1660
1669
  {
1661
1670
  style: {
1662
- position: "absolute",
1663
- inset: 0,
1671
+ width: 72,
1672
+ height: 72,
1673
+ borderRadius: "50%",
1674
+ background: "rgba(0, 0, 0, 0.55)",
1664
1675
  display: "flex",
1665
1676
  alignItems: "center",
1666
1677
  justifyContent: "center",
1667
1678
  pointerEvents: "none",
1668
- zIndex: 20
1679
+ // Inset the triangle visually — triangles look off-center without this
1680
+ paddingLeft: 6
1669
1681
  },
1670
- children: [
1671
- /* @__PURE__ */ jsx(
1672
- "div",
1673
- {
1674
- style: {
1675
- fontSize: 80,
1676
- animation: "reels-sdk-doubleTapHeart 0.6s ease forwards"
1677
- },
1678
- children: "\u2764\uFE0F"
1679
- }
1680
- ),
1681
- /* @__PURE__ */ jsx("style", { children: `
1682
- @keyframes reels-sdk-doubleTapHeart {
1683
- 0% { opacity: 0; transform: scale(0.5); }
1684
- 30% { opacity: 1; transform: scale(1.2); }
1685
- 60% { opacity: 1; transform: scale(1.0); }
1686
- 100% { opacity: 0; transform: scale(1.1); }
1687
- }
1688
- ` })
1689
- ]
1682
+ children: /* @__PURE__ */ jsx(
1683
+ "svg",
1684
+ {
1685
+ width: "32",
1686
+ height: "32",
1687
+ viewBox: "0 0 32 32",
1688
+ fill: "none",
1689
+ xmlns: "http://www.w3.org/2000/svg",
1690
+ children: /* @__PURE__ */ jsx(
1691
+ "path",
1692
+ {
1693
+ d: "M8 5.5L27 16L8 26.5V5.5Z",
1694
+ fill: "white"
1695
+ }
1696
+ )
1697
+ }
1698
+ )
1690
1699
  }
1691
1700
  );
1692
1701
  }
1702
+ var PLAY_AHEAD_MAX_CONCURRENT = 2;
1703
+ var _playAheadActive = 0;
1704
+ var _playAheadQueue = [];
1705
+ function acquirePlayAhead() {
1706
+ if (_playAheadActive < PLAY_AHEAD_MAX_CONCURRENT) {
1707
+ _playAheadActive++;
1708
+ return Promise.resolve();
1709
+ }
1710
+ return new Promise((resolve) => _playAheadQueue.push(resolve));
1711
+ }
1712
+ function releasePlayAhead() {
1713
+ _playAheadActive = Math.max(0, _playAheadActive - 1);
1714
+ const next = _playAheadQueue.shift();
1715
+ if (next) {
1716
+ _playAheadActive++;
1717
+ next();
1718
+ }
1719
+ }
1693
1720
  function VideoSlot({
1694
1721
  item,
1695
1722
  index,
@@ -1699,11 +1726,11 @@ function VideoSlot({
1699
1726
  bufferTier,
1700
1727
  isMuted,
1701
1728
  onToggleMute,
1729
+ onAutoplayBlocked,
1702
1730
  showFps = false,
1703
1731
  renderOverlay,
1704
1732
  renderActions,
1705
- renderPauseIndicator,
1706
- renderDoubleTap
1733
+ renderPauseAction
1707
1734
  }) {
1708
1735
  const { optimisticManager, adapters } = useSDK();
1709
1736
  if (!isVideoItem(item)) {
@@ -1736,11 +1763,11 @@ function VideoSlot({
1736
1763
  bufferTier,
1737
1764
  isMuted,
1738
1765
  onToggleMute,
1766
+ onAutoplayBlocked,
1739
1767
  showFps,
1740
1768
  renderOverlay,
1741
1769
  renderActions,
1742
- renderPauseIndicator,
1743
- renderDoubleTap,
1770
+ renderPauseAction,
1744
1771
  optimisticManager,
1745
1772
  adapters
1746
1773
  }
@@ -1755,11 +1782,11 @@ function VideoSlotInner({
1755
1782
  bufferTier,
1756
1783
  isMuted,
1757
1784
  onToggleMute,
1785
+ onAutoplayBlocked,
1758
1786
  showFps,
1759
1787
  renderOverlay,
1760
1788
  renderActions,
1761
- renderPauseIndicator,
1762
- renderDoubleTap,
1789
+ renderPauseAction,
1763
1790
  optimisticManager,
1764
1791
  adapters
1765
1792
  }) {
@@ -1770,7 +1797,7 @@ function VideoSlotInner({
1770
1797
  const isHlsSource = sourceType === "hls";
1771
1798
  const hlsSrc = isHlsSource && shouldLoadSrc ? src : void 0;
1772
1799
  const mp4Src = !isHlsSource && shouldLoadSrc ? src : void 0;
1773
- const { isReady: hlsReady, isNativeHls } = useHls({
1800
+ const { isReady: hlsReady } = useHls({
1774
1801
  src: hlsSrc,
1775
1802
  videoRef,
1776
1803
  isActive,
@@ -1809,9 +1836,7 @@ function VideoSlotInner({
1809
1836
  }, [mp4Src, isActive, isPrefetch, isPreloaded, isHlsSource]);
1810
1837
  const isReady = isHlsSource ? hlsReady : mp4Ready;
1811
1838
  const [hasPlayedAhead, setHasPlayedAhead] = useState(false);
1812
- const canPlayAhead = isHlsSource && !isNativeHls;
1813
1839
  useEffect(() => {
1814
- if (!canPlayAhead) return;
1815
1840
  const video = videoRef.current;
1816
1841
  if (!video) return;
1817
1842
  if (isActive || !isReady) return;
@@ -1819,206 +1844,146 @@ function VideoSlotInner({
1819
1844
  const prevMuted = video.muted;
1820
1845
  video.muted = true;
1821
1846
  let cancelled = false;
1822
- let rafId = null;
1823
- let vfcHandle = null;
1824
- const pauseAfterDecode = () => {
1825
- if (cancelled) return;
1826
- video.pause();
1827
- video.currentTime = 0;
1828
- video.muted = prevMuted;
1829
- setHasPlayedAhead(true);
1830
- };
1831
1847
  const doPlayAhead = async () => {
1848
+ await acquirePlayAhead();
1832
1849
  try {
1833
1850
  await video.play();
1834
- if (cancelled) return;
1835
- if ("requestVideoFrameCallback" in video) {
1836
- vfcHandle = video.requestVideoFrameCallback(() => {
1837
- vfcHandle = null;
1838
- pauseAfterDecode();
1839
- });
1840
- } else {
1841
- rafId = requestAnimationFrame(() => {
1842
- rafId = requestAnimationFrame(() => {
1843
- rafId = null;
1844
- pauseAfterDecode();
1845
- });
1846
- });
1851
+ if (cancelled) {
1852
+ video.pause();
1853
+ releasePlayAhead();
1854
+ return;
1847
1855
  }
1856
+ const pauseAfterDecode = () => {
1857
+ video.pause();
1858
+ video.currentTime = 0;
1859
+ video.muted = prevMuted;
1860
+ releasePlayAhead();
1861
+ if (!cancelled) {
1862
+ setHasPlayedAhead(true);
1863
+ }
1864
+ };
1865
+ setTimeout(pauseAfterDecode, 50);
1848
1866
  } catch {
1849
- video.muted = prevMuted;
1867
+ releasePlayAhead();
1850
1868
  }
1851
1869
  };
1852
1870
  doPlayAhead();
1853
1871
  return () => {
1854
1872
  cancelled = true;
1855
- if (rafId !== null) {
1856
- cancelAnimationFrame(rafId);
1857
- rafId = null;
1858
- }
1859
- if (vfcHandle !== null && "cancelVideoFrameCallback" in video) {
1860
- video.cancelVideoFrameCallback(vfcHandle);
1861
- vfcHandle = null;
1862
- }
1863
1873
  };
1864
- }, [canPlayAhead, isActive, isReady, hasPlayedAhead]);
1874
+ }, [isActive, isReady, hasPlayedAhead]);
1865
1875
  useEffect(() => {
1866
1876
  setHasPlayedAhead(false);
1867
1877
  }, [src]);
1868
1878
  const wasActiveRef = useRef(false);
1879
+ const [isManuallyPaused, setIsManuallyPaused] = useState(false);
1869
1880
  useEffect(() => {
1870
1881
  const video = videoRef.current;
1871
1882
  if (!video) return;
1872
1883
  let onReady = null;
1873
- let fallbackTimerId = null;
1874
- let pollId = null;
1875
- if (isActive) {
1876
- wasActiveRef.current = true;
1877
- const startPlay = () => {
1878
- if (onReady) {
1879
- video.removeEventListener("canplay", onReady);
1880
- video.removeEventListener("loadeddata", onReady);
1881
- video.removeEventListener("playing", onReady);
1882
- onReady = null;
1883
- }
1884
- if (fallbackTimerId !== null) {
1885
- clearTimeout(fallbackTimerId);
1886
- fallbackTimerId = null;
1887
- }
1888
- if (pollId !== null) {
1889
- clearInterval(pollId);
1890
- pollId = null;
1891
- }
1884
+ let cancelled = false;
1885
+ const attemptPlay = () => {
1886
+ if (cancelled) return;
1887
+ if (isMuted) {
1892
1888
  video.muted = true;
1893
- video.play().then(() => {
1894
- video.muted = isMuted;
1895
- }).catch(() => {
1896
- video.muted = isMuted;
1889
+ video.play().catch(() => {
1897
1890
  });
1898
- };
1899
- if (video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) {
1900
- startPlay();
1901
1891
  } else {
1902
- onReady = startPlay;
1903
- video.addEventListener("canplay", onReady, { once: true });
1904
- video.addEventListener("loadeddata", onReady, { once: true });
1905
- video.addEventListener("playing", onReady, { once: true });
1906
- if (isNativeHls && video.src && video.readyState < HTMLMediaElement.HAVE_CURRENT_DATA) {
1907
- video.load();
1908
- }
1909
- if (isNativeHls) {
1910
- pollId = setInterval(() => {
1911
- if (video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA && onReady) {
1912
- if (pollId !== null) {
1913
- clearInterval(pollId);
1914
- pollId = null;
1892
+ video.muted = false;
1893
+ video.play().then(() => {
1894
+ if (cancelled) {
1895
+ video.pause();
1896
+ }
1897
+ }).catch((err) => {
1898
+ if (cancelled) return;
1899
+ if (err.name === "NotAllowedError") {
1900
+ video.muted = true;
1901
+ video.play().then(() => {
1902
+ if (cancelled) {
1903
+ video.pause();
1904
+ return;
1915
1905
  }
1916
- startPlay();
1917
- }
1918
- }, 100);
1919
- }
1920
- fallbackTimerId = window.setTimeout(() => {
1921
- fallbackTimerId = null;
1922
- if (onReady) {
1923
- startPlay();
1906
+ onAutoplayBlocked?.();
1907
+ }).catch(() => {
1908
+ });
1924
1909
  }
1925
- }, isNativeHls ? 800 : 3e3);
1910
+ });
1926
1911
  }
1912
+ };
1913
+ if (isActive && !isManuallyPaused) {
1914
+ wasActiveRef.current = true;
1915
+ if (video.readyState >= HTMLMediaElement.HAVE_FUTURE_DATA) {
1916
+ attemptPlay();
1917
+ } else {
1918
+ onReady = attemptPlay;
1919
+ video.addEventListener("canplay", onReady, { once: true });
1920
+ }
1921
+ } else if (isActive && isManuallyPaused) {
1922
+ wasActiveRef.current = true;
1923
+ video.pause();
1927
1924
  } else if (wasActiveRef.current) {
1928
1925
  video.pause();
1929
1926
  video.currentTime = 0;
1930
1927
  wasActiveRef.current = false;
1928
+ setIsManuallyPaused(false);
1931
1929
  setHasPlayedAhead(false);
1932
1930
  } else if (!hasPlayedAhead) {
1933
1931
  video.pause();
1934
1932
  }
1935
1933
  return () => {
1936
- if (onReady) {
1937
- video.removeEventListener("canplay", onReady);
1938
- video.removeEventListener("loadeddata", onReady);
1939
- video.removeEventListener("playing", onReady);
1940
- }
1941
- if (fallbackTimerId !== null) {
1942
- clearTimeout(fallbackTimerId);
1943
- fallbackTimerId = null;
1944
- }
1945
- if (pollId !== null) {
1946
- clearInterval(pollId);
1947
- pollId = null;
1948
- }
1934
+ cancelled = true;
1935
+ if (onReady) video.removeEventListener("canplay", onReady);
1949
1936
  };
1950
- }, [isActive, isMuted, hasPlayedAhead, isNativeHls]);
1937
+ }, [isActive, isMuted, hasPlayedAhead, isManuallyPaused, onAutoplayBlocked]);
1951
1938
  useEffect(() => {
1952
1939
  const video = videoRef.current;
1953
1940
  if (!video) return;
1954
- if (isActive) {
1955
- video.muted = isMuted;
1956
- } else {
1957
- video.muted = true;
1958
- }
1941
+ video.muted = isActive ? isMuted : true;
1959
1942
  }, [isMuted, isActive]);
1960
- const [isActuallyPlaying, setIsActuallyPlaying] = useState(false);
1961
- useEffect(() => {
1962
- const video = videoRef.current;
1963
- if (!video) return;
1964
- const onPlaying = () => setIsActuallyPlaying(true);
1965
- const onPause = () => setIsActuallyPlaying(false);
1966
- const onEnded = () => setIsActuallyPlaying(false);
1967
- video.addEventListener("playing", onPlaying);
1968
- video.addEventListener("pause", onPause);
1969
- video.addEventListener("ended", onEnded);
1970
- return () => {
1971
- video.removeEventListener("playing", onPlaying);
1972
- video.removeEventListener("pause", onPause);
1973
- video.removeEventListener("ended", onEnded);
1974
- };
1943
+ const showPosterOverlay = !isReady && !hasPlayedAhead;
1944
+ const isPreDecoded = hasPlayedAhead;
1945
+ const [showMuteIndicator, setShowMuteIndicator] = useState(false);
1946
+ const muteIndicatorTimer = useRef(null);
1947
+ const handleToggleMute = useCallback(() => {
1948
+ onToggleMute();
1949
+ setShowMuteIndicator(true);
1950
+ if (muteIndicatorTimer.current) clearTimeout(muteIndicatorTimer.current);
1951
+ muteIndicatorTimer.current = setTimeout(() => setShowMuteIndicator(false), 1200);
1952
+ }, [onToggleMute]);
1953
+ const tapStartRef = useRef(null);
1954
+ const TAP_SLOP_PX = 10;
1955
+ const handlePointerDown = useCallback((e) => {
1956
+ tapStartRef.current = { x: e.clientX, y: e.clientY };
1975
1957
  }, []);
1976
- useEffect(() => {
1977
- if (!isActive) setIsActuallyPlaying(false);
1978
- }, [isActive]);
1979
- const showPosterOverlay = isActive ? !isReady && !isActuallyPlaying : canPlayAhead ? !hasPlayedAhead : !isReady;
1980
- const [isPaused, setIsPaused] = useState(false);
1981
- const [isDoubleTap, setIsDoubleTap] = useState(false);
1982
- const lastTapTimeRef = useRef(0);
1983
- const doubleTapTimerRef = useRef(null);
1984
- const handleTap = useCallback(() => {
1958
+ const handleClick = useCallback((e) => {
1959
+ if (e.button !== 0) return;
1985
1960
  if (!isActive) return;
1986
- const now = Date.now();
1987
- const delta = now - lastTapTimeRef.current;
1988
- lastTapTimeRef.current = now;
1989
- if (delta < 300) {
1990
- if (doubleTapTimerRef.current !== null) {
1991
- clearTimeout(doubleTapTimerRef.current);
1992
- doubleTapTimerRef.current = null;
1961
+ const start = tapStartRef.current;
1962
+ if (start) {
1963
+ const dx = Math.abs(e.clientX - start.x);
1964
+ const dy = Math.abs(e.clientY - start.y);
1965
+ if (dx > TAP_SLOP_PX || dy > TAP_SLOP_PX) {
1966
+ tapStartRef.current = null;
1967
+ return;
1993
1968
  }
1994
- setIsDoubleTap(true);
1995
- setTimeout(() => setIsDoubleTap(false), 600);
1969
+ }
1970
+ tapStartRef.current = null;
1971
+ const video = videoRef.current;
1972
+ if (!video) return;
1973
+ if (video.paused || isManuallyPaused) {
1974
+ setIsManuallyPaused(false);
1975
+ video.muted = true;
1976
+ video.play().then(() => {
1977
+ requestAnimationFrame(() => {
1978
+ video.muted = isMuted;
1979
+ });
1980
+ }).catch(() => {
1981
+ });
1996
1982
  } else {
1997
- doubleTapTimerRef.current = setTimeout(() => {
1998
- doubleTapTimerRef.current = null;
1999
- const video = videoRef.current;
2000
- if (!video) return;
2001
- if (video.paused) {
2002
- video.play().catch(() => {
2003
- });
2004
- setIsPaused(false);
2005
- } else {
2006
- video.pause();
2007
- setIsPaused(true);
2008
- }
2009
- }, 300);
1983
+ setIsManuallyPaused(true);
1984
+ video.pause();
2010
1985
  }
2011
- }, [isActive]);
2012
- useEffect(() => {
2013
- return () => {
2014
- if (doubleTapTimerRef.current !== null) {
2015
- clearTimeout(doubleTapTimerRef.current);
2016
- }
2017
- };
2018
- }, []);
2019
- useEffect(() => {
2020
- if (isActive) setIsPaused(false);
2021
- }, [isActive]);
1986
+ }, [isActive, isManuallyPaused, isMuted]);
2022
1987
  const likeDelta = useSyncExternalStore(
2023
1988
  optimisticManager.store.subscribe,
2024
1989
  () => optimisticManager.getLikeDelta(item.id),
@@ -2036,12 +2001,36 @@ function VideoSlotInner({
2036
2001
  followState,
2037
2002
  share: () => adapters.interaction?.share?.(item.id),
2038
2003
  isMuted,
2039
- toggleMute: onToggleMute,
2040
- isPaused,
2041
- togglePause: handleTap,
2004
+ toggleMute: handleToggleMute,
2042
2005
  isActive,
2043
2006
  index
2044
- }), [item, likeDelta, followState, isMuted, isPaused, isActive, index, optimisticManager, adapters, onToggleMute, handleTap]);
2007
+ }), [item, likeDelta, followState, isMuted, isActive, index, optimisticManager, adapters, handleToggleMute]);
2008
+ const pauseActions = useMemo(() => ({
2009
+ ...actions,
2010
+ isPaused: isManuallyPaused,
2011
+ togglePlayPause: () => {
2012
+ const video = videoRef.current;
2013
+ if (!video) return;
2014
+ if (isManuallyPaused) {
2015
+ setIsManuallyPaused(false);
2016
+ video.muted = true;
2017
+ video.play().then(() => {
2018
+ requestAnimationFrame(() => {
2019
+ video.muted = isMuted;
2020
+ });
2021
+ }).catch(() => {
2022
+ });
2023
+ } else {
2024
+ setIsManuallyPaused(true);
2025
+ video.pause();
2026
+ }
2027
+ }
2028
+ }), [actions, isManuallyPaused, isMuted]);
2029
+ useEffect(() => {
2030
+ return () => {
2031
+ if (muteIndicatorTimer.current) clearTimeout(muteIndicatorTimer.current);
2032
+ };
2033
+ }, []);
2045
2034
  return /* @__PURE__ */ jsxs(
2046
2035
  "div",
2047
2036
  {
@@ -2052,7 +2041,8 @@ function VideoSlotInner({
2052
2041
  background: "#111",
2053
2042
  overflow: "hidden"
2054
2043
  },
2055
- onClick: handleTap,
2044
+ onPointerDown: handlePointerDown,
2045
+ onClick: handleClick,
2056
2046
  children: [
2057
2047
  /* @__PURE__ */ jsx(
2058
2048
  "video",
@@ -2060,21 +2050,21 @@ function VideoSlotInner({
2060
2050
  ref: videoRef,
2061
2051
  src: mp4Src,
2062
2052
  loop: true,
2063
- muted: true,
2053
+ muted: isActive ? isMuted : true,
2064
2054
  playsInline: true,
2065
- autoPlay: isActive,
2066
2055
  preload: shouldLoadSrc ? "auto" : "none",
2067
2056
  style: {
2068
2057
  width: "100%",
2069
2058
  height: "100%",
2070
2059
  objectFit: "cover",
2071
- // Hide video until ready to avoid black frame flash
2060
+ // Hide video until ready to avoid black frame flash.
2061
+ // When pre-decoded, skip transition — first frame is already on canvas.
2072
2062
  opacity: showPosterOverlay ? 0 : 1,
2073
- transition: "opacity 0.15s ease"
2063
+ transition: isPreDecoded ? "none" : "opacity 0.15s ease"
2074
2064
  }
2075
2065
  }
2076
2066
  ),
2077
- item.poster && /* @__PURE__ */ jsx(
2067
+ item.poster && !isPreDecoded && /* @__PURE__ */ jsx(
2078
2068
  "div",
2079
2069
  {
2080
2070
  style: {
@@ -2089,19 +2079,29 @@ function VideoSlotInner({
2089
2079
  }
2090
2080
  }
2091
2081
  ),
2092
- (isDoubleTap || renderDoubleTap) && /* @__PURE__ */ jsx(
2082
+ showMuteIndicator && /* @__PURE__ */ jsx(
2093
2083
  "div",
2094
2084
  {
2095
2085
  style: {
2096
2086
  position: "absolute",
2097
- inset: 0,
2087
+ top: "50%",
2088
+ left: "50%",
2089
+ transform: "translate(-50%, -50%)",
2090
+ background: "rgba(0,0,0,0.6)",
2091
+ borderRadius: "50%",
2092
+ width: 64,
2093
+ height: 64,
2094
+ display: "flex",
2095
+ alignItems: "center",
2096
+ justifyContent: "center",
2097
+ fontSize: 28,
2098
2098
  pointerEvents: "none",
2099
- zIndex: 20
2099
+ animation: "reels-sdk-fadeInOut 1.2s ease forwards"
2100
2100
  },
2101
- children: renderDoubleTap ? renderDoubleTap(isDoubleTap) : /* @__PURE__ */ jsx(DefaultDoubleTap, { isDoubleTap })
2101
+ children: isMuted ? "\u{1F507}" : "\u{1F50A}"
2102
2102
  }
2103
2103
  ),
2104
- isPaused && /* @__PURE__ */ jsx(
2104
+ isActive && isManuallyPaused && /* @__PURE__ */ jsx(
2105
2105
  "div",
2106
2106
  {
2107
2107
  style: {
@@ -2110,25 +2110,9 @@ function VideoSlotInner({
2110
2110
  left: "50%",
2111
2111
  transform: "translate(-50%, -50%)",
2112
2112
  pointerEvents: "none",
2113
- zIndex: 5,
2114
- opacity: 0.3
2113
+ animation: "reels-sdk-fadeIn 0.2s ease forwards"
2115
2114
  },
2116
- children: renderPauseIndicator ? renderPauseIndicator(isPaused) : /* @__PURE__ */ jsx(
2117
- "div",
2118
- {
2119
- style: {
2120
- background: "rgba(0,0,0,0.6)",
2121
- borderRadius: "50%",
2122
- width: 64,
2123
- height: 64,
2124
- display: "flex",
2125
- alignItems: "center",
2126
- justifyContent: "center",
2127
- fontSize: 28
2128
- },
2129
- children: "\u25B6\uFE0F"
2130
- }
2131
- )
2115
+ children: renderPauseAction ? renderPauseAction(item, pauseActions) : /* @__PURE__ */ jsx(DefaultPauseAction, {})
2132
2116
  }
2133
2117
  ),
2134
2118
  /* @__PURE__ */ jsx(
@@ -2136,13 +2120,11 @@ function VideoSlotInner({
2136
2120
  {
2137
2121
  style: {
2138
2122
  position: "absolute",
2139
- bottom: 0,
2123
+ bottom: 80,
2140
2124
  left: 16,
2141
2125
  right: 80,
2142
- paddingBottom: 16,
2143
2126
  pointerEvents: "none",
2144
- color: "#fff",
2145
- zIndex: 10
2127
+ color: "#fff"
2146
2128
  },
2147
2129
  children: renderOverlay ? renderOverlay(item, actions) : /* @__PURE__ */ jsx(DefaultOverlay, { item })
2148
2130
  }
@@ -2150,19 +2132,18 @@ function VideoSlotInner({
2150
2132
  /* @__PURE__ */ jsx(
2151
2133
  "div",
2152
2134
  {
2153
- onClick: (e) => e.stopPropagation(),
2154
2135
  style: {
2155
2136
  position: "absolute",
2156
- bottom: 0,
2137
+ bottom: 80,
2157
2138
  right: 16,
2158
- paddingBottom: 16,
2159
2139
  display: "flex",
2160
2140
  flexDirection: "column",
2161
2141
  gap: 20,
2162
- alignItems: "center",
2163
- pointerEvents: "auto",
2164
- zIndex: 10
2142
+ alignItems: "center"
2143
+ // Actions must be clickable; stop propagation so taps don't
2144
+ // also trigger the video play/pause handler on the container.
2165
2145
  },
2146
+ onClick: (e) => e.stopPropagation(),
2166
2147
  children: renderActions ? renderActions(item, actions) : /* @__PURE__ */ jsx(DefaultActions, { item, actions })
2167
2148
  }
2168
2149
  ),
@@ -2212,6 +2193,7 @@ function FpsCounter() {
2212
2193
  }
2213
2194
  );
2214
2195
  }
2196
+ var RENDER_WINDOW_RADIUS = 3;
2215
2197
  var centerStyle = {
2216
2198
  height: "100dvh",
2217
2199
  display: "flex",
@@ -2223,8 +2205,7 @@ var centerStyle = {
2223
2205
  function ReelsFeed({
2224
2206
  renderOverlay,
2225
2207
  renderActions,
2226
- renderPauseIndicator,
2227
- renderDoubleTap,
2208
+ renderPauseAction,
2228
2209
  renderLoading,
2229
2210
  renderEmpty,
2230
2211
  renderError: _renderError,
@@ -2232,21 +2213,24 @@ function ReelsFeed({
2232
2213
  loadMoreThreshold = 5,
2233
2214
  onSlotChange,
2234
2215
  gestureConfig,
2235
- snapConfig
2216
+ snapConfig,
2217
+ initialMuted = true,
2218
+ onAutoplayBlocked
2236
2219
  }) {
2237
2220
  const { items, loading, loadInitial, loadMore, hasMore } = useFeed();
2221
+ const { adapters } = useSDK();
2238
2222
  const {
2239
- activeIndices,
2240
- warmIndices,
2241
2223
  focusedIndex,
2242
2224
  prefetchIndex,
2225
+ preloadQueue,
2243
2226
  setFocusedIndexImmediate,
2244
2227
  setTotalItems,
2245
2228
  shouldRenderVideo,
2246
2229
  isWarmAllocated,
2247
2230
  setPrefetchIndex
2248
2231
  } = useResource();
2249
- const [isMuted, setIsMuted] = useState(false);
2232
+ const [isMuted, setIsMuted] = useState(initialMuted);
2233
+ const [isDragMuted, setIsDragMuted] = useState(false);
2250
2234
  const containerRef = useRef(null);
2251
2235
  const slotCacheRef = useRef(/* @__PURE__ */ new Map());
2252
2236
  const activeIndexRef = useRef(0);
@@ -2262,6 +2246,14 @@ function ReelsFeed({
2262
2246
  useEffect(() => {
2263
2247
  setTotalItems(items.length);
2264
2248
  }, [items.length, setTotalItems]);
2249
+ useEffect(() => {
2250
+ for (const idx of preloadQueue) {
2251
+ const item = items[idx];
2252
+ if (item && isVideoItem(item) && item.source.type === "hls") {
2253
+ adapters.videoLoader?.preloadMetadata?.(item.source.url);
2254
+ }
2255
+ }
2256
+ }, [preloadQueue, items, adapters.videoLoader]);
2265
2257
  useEffect(() => {
2266
2258
  if (items.length - focusedIndex <= loadMoreThreshold && hasMore && !loading) {
2267
2259
  loadMore();
@@ -2282,7 +2274,7 @@ function ReelsFeed({
2282
2274
  const observer = new MutationObserver(rebuild);
2283
2275
  observer.observe(container, { childList: true, subtree: true });
2284
2276
  return () => observer.disconnect();
2285
- }, [items.length]);
2277
+ }, [items.length, focusedIndex]);
2286
2278
  const containerHeight = useRef(
2287
2279
  typeof window !== "undefined" ? window.innerHeight : 800
2288
2280
  );
@@ -2373,6 +2365,12 @@ function ReelsFeed({
2373
2365
  animateBounceBack(targets);
2374
2366
  setPrefetchIndex(null);
2375
2367
  }, [animateBounceBack, setPrefetchIndex]);
2368
+ const handleDragStart = useCallback(() => {
2369
+ setIsDragMuted(true);
2370
+ }, []);
2371
+ const handleDragEnd = useCallback(() => {
2372
+ setTimeout(() => setIsDragMuted(false), 50);
2373
+ }, []);
2376
2374
  const { bind } = usePointerGesture({
2377
2375
  axis: "y",
2378
2376
  velocityThreshold: gestureConfig?.velocityThreshold ?? 0.3,
@@ -2385,7 +2383,9 @@ function ReelsFeed({
2385
2383
  },
2386
2384
  onDragThreshold: handleDragThreshold,
2387
2385
  onSnap: handleSnap,
2388
- onBounceBack: handleBounceBack
2386
+ onBounceBack: handleBounceBack,
2387
+ onDragStart: handleDragStart,
2388
+ onDragEnd: handleDragEnd
2389
2389
  });
2390
2390
  const getInitialTransformPx = useCallback(
2391
2391
  (index) => {
@@ -2427,59 +2427,58 @@ function ReelsFeed({
2427
2427
  70% { opacity: 1; transform: translate(-50%, -50%) scale(1); }
2428
2428
  100% { opacity: 0; transform: translate(-50%, -50%) scale(0.8); }
2429
2429
  }
2430
+ @keyframes reels-sdk-fadeIn {
2431
+ from { opacity: 0; transform: translate(-50%, -50%) scale(0.8); }
2432
+ to { opacity: 1; transform: translate(-50%, -50%) scale(1); }
2433
+ }
2430
2434
  @keyframes reels-sdk-spin {
2431
2435
  to { transform: rotate(360deg); }
2432
2436
  }
2433
2437
  ` }),
2434
- (() => {
2435
- const windowIndices = /* @__PURE__ */ new Set([
2436
- ...activeIndices,
2437
- ...warmIndices
2438
- ]);
2439
- if (prefetchIndex !== null && prefetchIndex !== void 0) {
2440
- windowIndices.add(prefetchIndex);
2438
+ items.map((item, index) => {
2439
+ const distFromFocus = Math.abs(index - focusedIndex);
2440
+ const wrapperStyle = {
2441
+ position: "absolute",
2442
+ inset: 0,
2443
+ contain: "layout style paint",
2444
+ willChange: distFromFocus <= 1 ? "transform" : "auto",
2445
+ transform: getInitialTransformPx(index)
2446
+ };
2447
+ if (distFromFocus > RENDER_WINDOW_RADIUS) {
2448
+ return /* @__PURE__ */ jsx("div", { "data-slot-index": index, style: wrapperStyle }, item.id);
2441
2449
  }
2442
- return [...windowIndices].map((index) => {
2443
- const item = items[index];
2444
- if (!item) return null;
2445
- const isActive = index === focusedIndex;
2446
- const isPrefetchSlot = index === prefetchIndex;
2447
- const isWarm = isWarmAllocated(index);
2448
- const isVisible = shouldRenderVideo(index) || isPrefetchSlot;
2449
- const bufferTier = isActive ? "active" : isWarm ? "warm" : "hot";
2450
- return /* @__PURE__ */ jsx(
2451
- "div",
2452
- {
2453
- "data-slot-index": index,
2454
- style: {
2455
- position: "absolute",
2456
- inset: 0,
2457
- willChange: "transform",
2458
- transform: getInitialTransformPx(index)
2459
- },
2460
- children: isVisible ? /* @__PURE__ */ jsx(
2461
- VideoSlot,
2462
- {
2463
- item,
2464
- index,
2465
- isActive,
2466
- isPrefetch: isPrefetchSlot,
2467
- isPreloaded: !isActive && !isPrefetchSlot && isVisible,
2468
- bufferTier,
2469
- isMuted,
2470
- onToggleMute: handleToggleMute,
2471
- showFps: showFps && isActive,
2472
- renderOverlay,
2473
- renderActions,
2474
- renderPauseIndicator,
2475
- renderDoubleTap
2476
- }
2477
- ) : null
2478
- },
2479
- item.id
2480
- );
2481
- });
2482
- })()
2450
+ const isActive = index === focusedIndex;
2451
+ const isPrefetch = index === prefetchIndex;
2452
+ const isWarm = isWarmAllocated(index);
2453
+ const isVisible = shouldRenderVideo(index) || isPrefetch;
2454
+ const bufferTier = isActive ? "active" : isWarm ? "warm" : "hot";
2455
+ return /* @__PURE__ */ jsx(
2456
+ "div",
2457
+ {
2458
+ "data-slot-index": index,
2459
+ style: wrapperStyle,
2460
+ children: /* @__PURE__ */ jsx(
2461
+ VideoSlot,
2462
+ {
2463
+ item,
2464
+ index,
2465
+ isActive,
2466
+ isPrefetch,
2467
+ isPreloaded: !isActive && !isPrefetch && isVisible,
2468
+ bufferTier,
2469
+ isMuted: isMuted || isDragMuted,
2470
+ onToggleMute: handleToggleMute,
2471
+ onAutoplayBlocked,
2472
+ showFps: showFps && isActive,
2473
+ renderOverlay,
2474
+ renderActions,
2475
+ renderPauseAction
2476
+ }
2477
+ )
2478
+ },
2479
+ item.id
2480
+ );
2481
+ })
2483
2482
  ]
2484
2483
  }
2485
2484
  );
@@ -2734,6 +2733,8 @@ var MockVideoLoader = class {
2734
2733
  this.preloaded.clear();
2735
2734
  this.loading.clear();
2736
2735
  }
2736
+ preloadMetadata(_url) {
2737
+ }
2737
2738
  };
2738
2739
  var MockDataSource = class {
2739
2740
  constructor(options = {}) {
@@ -3103,4 +3104,4 @@ var HttpError = class extends Error {
3103
3104
  }
3104
3105
  };
3105
3106
 
3106
- export { DEFAULT_FEED_CONFIG, DEFAULT_PLAYER_CONFIG, DEFAULT_RESOURCE_CONFIG, DefaultActions, DefaultDoubleTap, DefaultOverlay, DefaultSkeleton, FeedManager, HttpDataSource, HttpError, MockAnalytics, MockCommentAdapter, MockDataSource, MockInteraction, MockLogger, MockNetworkAdapter, MockSessionStorage, MockVideoLoader, OptimisticManager, PlayerEngine, PlayerStatus, ReelsFeed, ReelsProvider, ResourceGovernor, VALID_TRANSITIONS, VideoSlot, canPause, canPlay, canSeek, isArticle, isValidTransition, isVideoItem, useFeed, useFeedSelector, useHls, usePlayer, usePlayerSelector, usePointerGesture, useResource, useResourceSelector, useSDK, useSnapAnimation };
3107
+ export { DEFAULT_FEED_CONFIG, DEFAULT_PLAYER_CONFIG, DEFAULT_RESOURCE_CONFIG, DefaultActions, DefaultOverlay, DefaultPauseAction, DefaultSkeleton, FeedManager, HttpDataSource, HttpError, MockAnalytics, MockCommentAdapter, MockDataSource, MockInteraction, MockLogger, MockNetworkAdapter, MockSessionStorage, MockVideoLoader, OptimisticManager, PlayerEngine, PlayerStatus, ReelsFeed, ReelsProvider, ResourceGovernor, VALID_TRANSITIONS, VideoSlot, canPause, canPlay, canSeek, isArticle, isValidTransition, isVideoItem, useFeed, useFeedSelector, useHls, usePlayer, usePlayerSelector, usePointerGesture, useResource, useResourceSelector, useSDK, useSnapAnimation };