@xhub-reels/sdk 0.1.15 → 0.1.17

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.cjs CHANGED
@@ -833,6 +833,21 @@ var ResourceGovernor = class {
833
833
  isPreloading(index) {
834
834
  return this.store.getState().preloadQueue.includes(index);
835
835
  }
836
+ /**
837
+ * Returns the buffer tier (1–4) for a given index.
838
+ * - 1: Active (playing, 10s buffer)
839
+ * - 2: Hot (±bufferWindow, 2s buffer, instant swap)
840
+ * - 3: Warm (manifest + 1 segment, ~300ms show)
841
+ * - 4: Cold (preload queue — manifest in HTTP cache, no DOM)
842
+ */
843
+ getTier(index) {
844
+ const { focusedIndex, activeAllocations, warmAllocations, preloadQueue } = this.store.getState();
845
+ if (index === focusedIndex) return 1;
846
+ if (activeAllocations.has(index)) return 2;
847
+ if (warmAllocations.has(index)) return 3;
848
+ if (preloadQueue.includes(index)) return 4;
849
+ return null;
850
+ }
836
851
  getActiveAllocations() {
837
852
  return [...this.store.getState().activeAllocations];
838
853
  }
@@ -905,7 +920,9 @@ function usePointerGesture(config = {}) {
905
920
  containerSize,
906
921
  dragThresholdRatio = 0.5,
907
922
  onSnap,
908
- onBounceBack
923
+ onBounceBack,
924
+ onDragStart,
925
+ onDragEnd
909
926
  } = config;
910
927
  const isDraggingRef = react.useRef(false);
911
928
  const dragOffsetRef = react.useRef(0);
@@ -922,6 +939,8 @@ function usePointerGesture(config = {}) {
922
939
  const onDragThresholdRef = react.useRef(onDragThreshold);
923
940
  const onSnapRef = react.useRef(onSnap);
924
941
  const onBounceBackRef = react.useRef(onBounceBack);
942
+ const onDragStartRef = react.useRef(onDragStart);
943
+ const onDragEndRef = react.useRef(onDragEnd);
925
944
  const disabledRef = react.useRef(disabled);
926
945
  const containerSizeRef = react.useRef(containerSize);
927
946
  const dragThresholdRatioRef = react.useRef(dragThresholdRatio);
@@ -930,6 +949,8 @@ function usePointerGesture(config = {}) {
930
949
  onDragThresholdRef.current = onDragThreshold;
931
950
  onSnapRef.current = onSnap;
932
951
  onBounceBackRef.current = onBounceBack;
952
+ onDragStartRef.current = onDragStart;
953
+ onDragEndRef.current = onDragEnd;
933
954
  disabledRef.current = disabled;
934
955
  containerSizeRef.current = containerSize;
935
956
  dragThresholdRatioRef.current = dragThresholdRatio;
@@ -996,6 +1017,7 @@ function usePointerGesture(config = {}) {
996
1017
  window.removeEventListener("pointermove", handlePointerMove);
997
1018
  window.removeEventListener("pointerup", handlePointerUp);
998
1019
  window.removeEventListener("pointercancel", handlePointerUp);
1020
+ onDragEndRef.current?.();
999
1021
  const offset = dragOffsetRef.current;
1000
1022
  const velocity = velocityRef.current;
1001
1023
  const shouldSnap = Math.abs(velocity) > velocityThreshold || Math.abs(offset) > distanceThreshold;
@@ -1027,6 +1049,7 @@ function usePointerGesture(config = {}) {
1027
1049
  lastTimeRef.current = performance.now();
1028
1050
  velocityRef.current = 0;
1029
1051
  dragOffsetRef.current = 0;
1052
+ onDragStartRef.current?.();
1030
1053
  e.currentTarget.setPointerCapture(e.pointerId);
1031
1054
  window.addEventListener("pointermove", handlePointerMove, { passive: true });
1032
1055
  window.addEventListener("pointerup", handlePointerUp);
@@ -1244,6 +1267,7 @@ function useResource() {
1244
1267
  const networkType = useResourceSelector((s) => s.networkType);
1245
1268
  const isActive = useResourceSelector((s) => s.isActive);
1246
1269
  const prefetchIndex = useResourceSelector((s) => s.prefetchIndex);
1270
+ const preloadQueue = useResourceSelector((s) => s.preloadQueue);
1247
1271
  const activeIndices = react.useMemo(() => [...activeAllocations], [activeAllocations]);
1248
1272
  const warmIndices = react.useMemo(() => [...warmAllocations], [warmAllocations]);
1249
1273
  const setFocusedIndex = react.useCallback(
@@ -1274,6 +1298,10 @@ function useResource() {
1274
1298
  (i) => resourceGovernor.setPrefetchIndex(i),
1275
1299
  [resourceGovernor]
1276
1300
  );
1301
+ const getTier = react.useCallback(
1302
+ (index) => resourceGovernor.getTier(index),
1303
+ [resourceGovernor]
1304
+ );
1277
1305
  return {
1278
1306
  activeIndices,
1279
1307
  warmIndices,
@@ -1282,13 +1310,15 @@ function useResource() {
1282
1310
  networkType,
1283
1311
  isActive,
1284
1312
  prefetchIndex,
1313
+ preloadQueue,
1285
1314
  setFocusedIndex,
1286
1315
  setFocusedIndexImmediate,
1287
1316
  setTotalItems,
1288
1317
  shouldRenderVideo,
1289
1318
  isAllocated,
1290
1319
  isWarmAllocated,
1291
- setPrefetchIndex
1320
+ setPrefetchIndex,
1321
+ getTier
1292
1322
  };
1293
1323
  }
1294
1324
  var ACTIVE_HLS_DEFAULTS = {
@@ -1364,18 +1394,9 @@ function mapHlsError(data) {
1364
1394
  }
1365
1395
  function useHls(options) {
1366
1396
  const { src, videoRef, isActive, isPrefetch, bufferTier = "active", hlsConfig, onError } = options;
1367
- const [isHlsJs, setIsHlsJs] = react.useState(false);
1368
- const [isNativeHls, setIsNativeHls] = react.useState(false);
1369
- react.useEffect(() => {
1370
- const hlsSupported = Hls__default.default.isSupported();
1371
- const native = supportsNativeHls();
1372
- setIsHlsJs(hlsSupported && !native);
1373
- setIsNativeHls(native);
1374
- }, []);
1375
- const isHlsJsRef = react.useRef(false);
1376
- const isNativeRef = react.useRef(false);
1377
- isHlsJsRef.current = isHlsJs;
1378
- isNativeRef.current = isNativeHls;
1397
+ const isHlsSupported = typeof window !== "undefined" && Hls__default.default.isSupported();
1398
+ const isNative = supportsNativeHls();
1399
+ const isHlsJs = isHlsSupported && !isNative;
1379
1400
  const [isReady, setIsReady] = react.useState(false);
1380
1401
  const hlsRef = react.useRef(null);
1381
1402
  const onErrorRef = react.useRef(onError);
@@ -1400,18 +1421,7 @@ function useHls(options) {
1400
1421
  currentSrcRef.current = void 0;
1401
1422
  return;
1402
1423
  }
1403
- const isNative = isNativeRef.current;
1404
- const isHlsSupported = isHlsJsRef.current;
1405
1424
  if (!isActive && !isPrefetch) {
1406
- if (isNative) {
1407
- if (video.src) {
1408
- video.removeAttribute("src");
1409
- video.load();
1410
- }
1411
- setIsReady(false);
1412
- currentSrcRef.current = void 0;
1413
- return;
1414
- }
1415
1425
  destroy();
1416
1426
  setIsReady(false);
1417
1427
  canPlayFiredRef.current = false;
@@ -1419,45 +1429,40 @@ function useHls(options) {
1419
1429
  return;
1420
1430
  }
1421
1431
  if (isNative) {
1422
- if (currentSrcRef.current === src) {
1423
- if (video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) {
1424
- setIsReady(true);
1425
- return void 0;
1426
- }
1427
- const handleCanPlayReuse = () => setIsReady(true);
1428
- video.addEventListener("canplay", handleCanPlayReuse, { once: true });
1429
- video.addEventListener("loadeddata", handleCanPlayReuse, { once: true });
1430
- video.addEventListener("playing", handleCanPlayReuse, { once: true });
1431
- if (video.readyState < HTMLMediaElement.HAVE_CURRENT_DATA) {
1432
- video.load();
1433
- }
1434
- return () => {
1435
- video.removeEventListener("canplay", handleCanPlayReuse);
1436
- video.removeEventListener("loadeddata", handleCanPlayReuse);
1437
- video.removeEventListener("playing", handleCanPlayReuse);
1438
- };
1439
- }
1440
- video.src = src;
1441
- currentSrcRef.current = src;
1442
- if (!isActive) {
1432
+ if (video.src !== src) {
1433
+ video.src = src;
1443
1434
  video.load();
1444
1435
  }
1445
- if (video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) {
1436
+ if (!video.hasAttribute("webkit-playsinline")) {
1437
+ video.setAttribute("webkit-playsinline", "");
1438
+ }
1439
+ if (video.readyState >= HTMLMediaElement.HAVE_FUTURE_DATA) {
1446
1440
  setIsReady(true);
1447
- return void 0;
1441
+ currentSrcRef.current = src;
1442
+ return;
1448
1443
  }
1449
1444
  setIsReady(false);
1450
- const handleCanPlay2 = () => setIsReady(true);
1445
+ canPlayFiredRef.current = false;
1446
+ currentSrcRef.current = src;
1447
+ const handleCanPlay2 = () => {
1448
+ canPlayFiredRef.current = true;
1449
+ setIsReady(true);
1450
+ };
1451
+ const handleLoadedData = () => {
1452
+ if (!canPlayFiredRef.current) {
1453
+ canPlayFiredRef.current = true;
1454
+ setIsReady(true);
1455
+ }
1456
+ };
1451
1457
  video.addEventListener("canplay", handleCanPlay2, { once: true });
1452
- video.addEventListener("loadeddata", handleCanPlay2, { once: true });
1453
- video.addEventListener("playing", handleCanPlay2, { once: true });
1458
+ video.addEventListener("loadeddata", handleLoadedData, { once: true });
1454
1459
  return () => {
1455
1460
  video.removeEventListener("canplay", handleCanPlay2);
1456
- video.removeEventListener("loadeddata", handleCanPlay2);
1457
- video.removeEventListener("playing", handleCanPlay2);
1461
+ video.removeEventListener("loadeddata", handleLoadedData);
1458
1462
  };
1459
1463
  }
1460
1464
  if (!isHlsSupported) {
1465
+ onErrorRef.current?.("UNKNOWN", "HLS playback not supported in this browser");
1461
1466
  return;
1462
1467
  }
1463
1468
  if (hlsRef.current && currentSrcRef.current === src) {
@@ -1521,7 +1526,7 @@ function useHls(options) {
1521
1526
  currentSrcRef.current = void 0;
1522
1527
  }
1523
1528
  };
1524
- }, [src, isActive, isPrefetch, isHlsJs, isNativeHls]);
1529
+ }, [src, isActive, isPrefetch]);
1525
1530
  react.useEffect(() => {
1526
1531
  const hls = hlsRef.current;
1527
1532
  if (!hls) {
@@ -1537,10 +1542,15 @@ function useHls(options) {
1537
1542
  for (const key of configKeys) {
1538
1543
  hlsAnyConfig[key] = newConfig[key];
1539
1544
  }
1545
+ if (prevTier === "warm" && bufferTier === "active") {
1546
+ try {
1547
+ hls.startLoad();
1548
+ } catch {
1549
+ }
1550
+ }
1540
1551
  }, [bufferTier]);
1541
1552
  return {
1542
1553
  isHlsJs,
1543
- isNativeHls,
1544
1554
  isReady,
1545
1555
  destroy
1546
1556
  };
@@ -1659,43 +1669,60 @@ function skeletonCircle(size) {
1659
1669
  background: "rgba(255,255,255,0.1)"
1660
1670
  };
1661
1671
  }
1662
- function DefaultDoubleTap({ isDoubleTap }) {
1663
- if (!isDoubleTap) return null;
1664
- return /* @__PURE__ */ jsxRuntime.jsxs(
1672
+ function DefaultPauseAction() {
1673
+ return /* @__PURE__ */ jsxRuntime.jsx(
1665
1674
  "div",
1666
1675
  {
1667
1676
  style: {
1668
- position: "absolute",
1669
- inset: 0,
1677
+ width: 72,
1678
+ height: 72,
1679
+ borderRadius: "50%",
1680
+ background: "rgba(0, 0, 0, 0.55)",
1670
1681
  display: "flex",
1671
1682
  alignItems: "center",
1672
1683
  justifyContent: "center",
1673
1684
  pointerEvents: "none",
1674
- zIndex: 20
1685
+ // Inset the triangle visually — triangles look off-center without this
1686
+ paddingLeft: 6
1675
1687
  },
1676
- children: [
1677
- /* @__PURE__ */ jsxRuntime.jsx(
1678
- "div",
1679
- {
1680
- style: {
1681
- fontSize: 80,
1682
- animation: "reels-sdk-doubleTapHeart 0.6s ease forwards"
1683
- },
1684
- children: "\u2764\uFE0F"
1685
- }
1686
- ),
1687
- /* @__PURE__ */ jsxRuntime.jsx("style", { children: `
1688
- @keyframes reels-sdk-doubleTapHeart {
1689
- 0% { opacity: 0; transform: scale(0.5); }
1690
- 30% { opacity: 1; transform: scale(1.2); }
1691
- 60% { opacity: 1; transform: scale(1.0); }
1692
- 100% { opacity: 0; transform: scale(1.1); }
1693
- }
1694
- ` })
1695
- ]
1688
+ children: /* @__PURE__ */ jsxRuntime.jsx(
1689
+ "svg",
1690
+ {
1691
+ width: "32",
1692
+ height: "32",
1693
+ viewBox: "0 0 32 32",
1694
+ fill: "none",
1695
+ xmlns: "http://www.w3.org/2000/svg",
1696
+ children: /* @__PURE__ */ jsxRuntime.jsx(
1697
+ "path",
1698
+ {
1699
+ d: "M8 5.5L27 16L8 26.5V5.5Z",
1700
+ fill: "white"
1701
+ }
1702
+ )
1703
+ }
1704
+ )
1696
1705
  }
1697
1706
  );
1698
1707
  }
1708
+ var PLAY_AHEAD_MAX_CONCURRENT = 2;
1709
+ var _playAheadActive = 0;
1710
+ var _playAheadQueue = [];
1711
+ function acquirePlayAhead() {
1712
+ if (_playAheadActive < PLAY_AHEAD_MAX_CONCURRENT) {
1713
+ _playAheadActive++;
1714
+ return Promise.resolve();
1715
+ }
1716
+ return new Promise((resolve) => _playAheadQueue.push(resolve));
1717
+ }
1718
+ function releasePlayAhead() {
1719
+ _playAheadActive = Math.max(0, _playAheadActive - 1);
1720
+ const next = _playAheadQueue.shift();
1721
+ if (next) {
1722
+ _playAheadActive++;
1723
+ next();
1724
+ }
1725
+ }
1699
1726
  function VideoSlot({
1700
1727
  item,
1701
1728
  index,
@@ -1705,11 +1732,11 @@ function VideoSlot({
1705
1732
  bufferTier,
1706
1733
  isMuted,
1707
1734
  onToggleMute,
1735
+ onAutoplayBlocked,
1708
1736
  showFps = false,
1709
1737
  renderOverlay,
1710
1738
  renderActions,
1711
- renderPauseIndicator,
1712
- renderDoubleTap
1739
+ renderPauseAction
1713
1740
  }) {
1714
1741
  const { optimisticManager, adapters } = useSDK();
1715
1742
  if (!isVideoItem(item)) {
@@ -1742,11 +1769,11 @@ function VideoSlot({
1742
1769
  bufferTier,
1743
1770
  isMuted,
1744
1771
  onToggleMute,
1772
+ onAutoplayBlocked,
1745
1773
  showFps,
1746
1774
  renderOverlay,
1747
1775
  renderActions,
1748
- renderPauseIndicator,
1749
- renderDoubleTap,
1776
+ renderPauseAction,
1750
1777
  optimisticManager,
1751
1778
  adapters
1752
1779
  }
@@ -1761,11 +1788,11 @@ function VideoSlotInner({
1761
1788
  bufferTier,
1762
1789
  isMuted,
1763
1790
  onToggleMute,
1791
+ onAutoplayBlocked,
1764
1792
  showFps,
1765
1793
  renderOverlay,
1766
1794
  renderActions,
1767
- renderPauseIndicator,
1768
- renderDoubleTap,
1795
+ renderPauseAction,
1769
1796
  optimisticManager,
1770
1797
  adapters
1771
1798
  }) {
@@ -1776,7 +1803,7 @@ function VideoSlotInner({
1776
1803
  const isHlsSource = sourceType === "hls";
1777
1804
  const hlsSrc = isHlsSource && shouldLoadSrc ? src : void 0;
1778
1805
  const mp4Src = !isHlsSource && shouldLoadSrc ? src : void 0;
1779
- const { isReady: hlsReady, isNativeHls } = useHls({
1806
+ const { isReady: hlsReady } = useHls({
1780
1807
  src: hlsSrc,
1781
1808
  videoRef,
1782
1809
  isActive,
@@ -1815,9 +1842,7 @@ function VideoSlotInner({
1815
1842
  }, [mp4Src, isActive, isPrefetch, isPreloaded, isHlsSource]);
1816
1843
  const isReady = isHlsSource ? hlsReady : mp4Ready;
1817
1844
  const [hasPlayedAhead, setHasPlayedAhead] = react.useState(false);
1818
- const canPlayAhead = isHlsSource && !isNativeHls;
1819
1845
  react.useEffect(() => {
1820
- if (!canPlayAhead) return;
1821
1846
  const video = videoRef.current;
1822
1847
  if (!video) return;
1823
1848
  if (isActive || !isReady) return;
@@ -1825,206 +1850,145 @@ function VideoSlotInner({
1825
1850
  const prevMuted = video.muted;
1826
1851
  video.muted = true;
1827
1852
  let cancelled = false;
1828
- let rafId = null;
1829
- let vfcHandle = null;
1830
- const pauseAfterDecode = () => {
1831
- if (cancelled) return;
1832
- video.pause();
1833
- video.currentTime = 0;
1834
- video.muted = prevMuted;
1835
- setHasPlayedAhead(true);
1836
- };
1837
1853
  const doPlayAhead = async () => {
1854
+ await acquirePlayAhead();
1838
1855
  try {
1839
1856
  await video.play();
1840
- if (cancelled) return;
1841
- if ("requestVideoFrameCallback" in video) {
1842
- vfcHandle = video.requestVideoFrameCallback(() => {
1843
- vfcHandle = null;
1844
- pauseAfterDecode();
1845
- });
1846
- } else {
1847
- rafId = requestAnimationFrame(() => {
1848
- rafId = requestAnimationFrame(() => {
1849
- rafId = null;
1850
- pauseAfterDecode();
1851
- });
1852
- });
1857
+ if (cancelled) {
1858
+ video.pause();
1859
+ releasePlayAhead();
1860
+ return;
1853
1861
  }
1862
+ const pauseAfterDecode = () => {
1863
+ video.pause();
1864
+ video.currentTime = 0;
1865
+ video.muted = prevMuted;
1866
+ releasePlayAhead();
1867
+ if (!cancelled) {
1868
+ setHasPlayedAhead(true);
1869
+ }
1870
+ };
1871
+ setTimeout(pauseAfterDecode, 50);
1854
1872
  } catch {
1855
- video.muted = prevMuted;
1873
+ releasePlayAhead();
1856
1874
  }
1857
1875
  };
1858
1876
  doPlayAhead();
1859
1877
  return () => {
1860
1878
  cancelled = true;
1861
- if (rafId !== null) {
1862
- cancelAnimationFrame(rafId);
1863
- rafId = null;
1864
- }
1865
- if (vfcHandle !== null && "cancelVideoFrameCallback" in video) {
1866
- video.cancelVideoFrameCallback(vfcHandle);
1867
- vfcHandle = null;
1868
- }
1869
1879
  };
1870
- }, [canPlayAhead, isActive, isReady, hasPlayedAhead]);
1880
+ }, [isActive, isReady, hasPlayedAhead]);
1871
1881
  react.useEffect(() => {
1872
1882
  setHasPlayedAhead(false);
1873
1883
  }, [src]);
1874
1884
  const wasActiveRef = react.useRef(false);
1885
+ const [isManuallyPaused, setIsManuallyPaused] = react.useState(false);
1875
1886
  react.useEffect(() => {
1876
1887
  const video = videoRef.current;
1877
1888
  if (!video) return;
1878
1889
  let onReady = null;
1879
- let fallbackTimerId = null;
1880
- let pollId = null;
1881
- if (isActive) {
1882
- wasActiveRef.current = true;
1883
- const startPlay = () => {
1884
- if (onReady) {
1885
- video.removeEventListener("canplay", onReady);
1886
- video.removeEventListener("loadeddata", onReady);
1887
- video.removeEventListener("playing", onReady);
1888
- onReady = null;
1889
- }
1890
- if (fallbackTimerId !== null) {
1891
- clearTimeout(fallbackTimerId);
1892
- fallbackTimerId = null;
1893
- }
1894
- if (pollId !== null) {
1895
- clearInterval(pollId);
1896
- pollId = null;
1897
- }
1890
+ let cancelled = false;
1891
+ const attemptPlay = () => {
1892
+ if (cancelled) return;
1893
+ if (isMuted) {
1898
1894
  video.muted = true;
1899
- video.play().then(() => {
1900
- video.muted = isMuted;
1901
- }).catch(() => {
1902
- video.muted = isMuted;
1895
+ video.play().catch(() => {
1903
1896
  });
1904
- };
1905
- if (video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) {
1906
- startPlay();
1907
1897
  } else {
1908
- onReady = startPlay;
1909
- video.addEventListener("canplay", onReady, { once: true });
1910
- video.addEventListener("loadeddata", onReady, { once: true });
1911
- video.addEventListener("playing", onReady, { once: true });
1912
- if (isNativeHls && video.src && video.readyState < HTMLMediaElement.HAVE_CURRENT_DATA) {
1913
- video.load();
1914
- }
1915
- if (isNativeHls) {
1916
- pollId = setInterval(() => {
1917
- if (video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA && onReady) {
1918
- if (pollId !== null) {
1919
- clearInterval(pollId);
1920
- pollId = null;
1898
+ video.muted = false;
1899
+ video.play().then(() => {
1900
+ if (cancelled) {
1901
+ video.pause();
1902
+ }
1903
+ }).catch((err) => {
1904
+ if (cancelled) return;
1905
+ if (err.name === "NotAllowedError") {
1906
+ video.muted = true;
1907
+ video.play().then(() => {
1908
+ if (cancelled) {
1909
+ video.pause();
1910
+ return;
1921
1911
  }
1922
- startPlay();
1923
- }
1924
- }, 100);
1925
- }
1926
- fallbackTimerId = window.setTimeout(() => {
1927
- fallbackTimerId = null;
1928
- if (onReady) {
1929
- startPlay();
1912
+ onAutoplayBlocked?.();
1913
+ }).catch(() => {
1914
+ });
1930
1915
  }
1931
- }, isNativeHls ? 800 : 3e3);
1916
+ });
1932
1917
  }
1918
+ };
1919
+ if (isActive && !isManuallyPaused) {
1920
+ wasActiveRef.current = true;
1921
+ if (video.readyState >= HTMLMediaElement.HAVE_FUTURE_DATA) {
1922
+ attemptPlay();
1923
+ } else {
1924
+ onReady = attemptPlay;
1925
+ video.addEventListener("canplay", onReady, { once: true });
1926
+ }
1927
+ } else if (isActive && isManuallyPaused) {
1928
+ wasActiveRef.current = true;
1929
+ video.pause();
1933
1930
  } else if (wasActiveRef.current) {
1934
1931
  video.pause();
1935
1932
  video.currentTime = 0;
1936
1933
  wasActiveRef.current = false;
1934
+ setIsManuallyPaused(false);
1937
1935
  setHasPlayedAhead(false);
1938
1936
  } else if (!hasPlayedAhead) {
1939
1937
  video.pause();
1940
1938
  }
1941
1939
  return () => {
1942
- if (onReady) {
1943
- video.removeEventListener("canplay", onReady);
1944
- video.removeEventListener("loadeddata", onReady);
1945
- video.removeEventListener("playing", onReady);
1946
- }
1947
- if (fallbackTimerId !== null) {
1948
- clearTimeout(fallbackTimerId);
1949
- fallbackTimerId = null;
1950
- }
1951
- if (pollId !== null) {
1952
- clearInterval(pollId);
1953
- pollId = null;
1954
- }
1940
+ cancelled = true;
1941
+ if (onReady) video.removeEventListener("canplay", onReady);
1955
1942
  };
1956
- }, [isActive, isMuted, hasPlayedAhead, isNativeHls]);
1943
+ }, [isActive, isMuted, hasPlayedAhead, isManuallyPaused, onAutoplayBlocked]);
1957
1944
  react.useEffect(() => {
1958
1945
  const video = videoRef.current;
1959
1946
  if (!video) return;
1960
- if (isActive) {
1961
- video.muted = isMuted;
1962
- } else {
1963
- video.muted = true;
1964
- }
1947
+ video.muted = isActive ? isMuted : true;
1965
1948
  }, [isMuted, isActive]);
1966
- const [isActuallyPlaying, setIsActuallyPlaying] = react.useState(false);
1967
- react.useEffect(() => {
1968
- const video = videoRef.current;
1969
- if (!video) return;
1970
- const onPlaying = () => setIsActuallyPlaying(true);
1971
- const onPause = () => setIsActuallyPlaying(false);
1972
- const onEnded = () => setIsActuallyPlaying(false);
1973
- video.addEventListener("playing", onPlaying);
1974
- video.addEventListener("pause", onPause);
1975
- video.addEventListener("ended", onEnded);
1976
- return () => {
1977
- video.removeEventListener("playing", onPlaying);
1978
- video.removeEventListener("pause", onPause);
1979
- video.removeEventListener("ended", onEnded);
1980
- };
1949
+ const showPosterOverlay = !isReady && !hasPlayedAhead;
1950
+ const [showMuteIndicator, setShowMuteIndicator] = react.useState(false);
1951
+ const muteIndicatorTimer = react.useRef(null);
1952
+ const handleToggleMute = react.useCallback(() => {
1953
+ onToggleMute();
1954
+ setShowMuteIndicator(true);
1955
+ if (muteIndicatorTimer.current) clearTimeout(muteIndicatorTimer.current);
1956
+ muteIndicatorTimer.current = setTimeout(() => setShowMuteIndicator(false), 1200);
1957
+ }, [onToggleMute]);
1958
+ const tapStartRef = react.useRef(null);
1959
+ const TAP_SLOP_PX = 10;
1960
+ const handlePointerDown = react.useCallback((e) => {
1961
+ tapStartRef.current = { x: e.clientX, y: e.clientY };
1981
1962
  }, []);
1982
- react.useEffect(() => {
1983
- if (!isActive) setIsActuallyPlaying(false);
1984
- }, [isActive]);
1985
- const showPosterOverlay = isActive ? !isReady && !isActuallyPlaying : canPlayAhead ? !hasPlayedAhead : !isReady;
1986
- const [isPaused, setIsPaused] = react.useState(false);
1987
- const [isDoubleTap, setIsDoubleTap] = react.useState(false);
1988
- const lastTapTimeRef = react.useRef(0);
1989
- const doubleTapTimerRef = react.useRef(null);
1990
- const handleTap = react.useCallback(() => {
1963
+ const handleClick = react.useCallback((e) => {
1964
+ if (e.button !== 0) return;
1991
1965
  if (!isActive) return;
1992
- const now = Date.now();
1993
- const delta = now - lastTapTimeRef.current;
1994
- lastTapTimeRef.current = now;
1995
- if (delta < 300) {
1996
- if (doubleTapTimerRef.current !== null) {
1997
- clearTimeout(doubleTapTimerRef.current);
1998
- doubleTapTimerRef.current = null;
1966
+ const start = tapStartRef.current;
1967
+ if (start) {
1968
+ const dx = Math.abs(e.clientX - start.x);
1969
+ const dy = Math.abs(e.clientY - start.y);
1970
+ if (dx > TAP_SLOP_PX || dy > TAP_SLOP_PX) {
1971
+ tapStartRef.current = null;
1972
+ return;
1999
1973
  }
2000
- setIsDoubleTap(true);
2001
- setTimeout(() => setIsDoubleTap(false), 600);
1974
+ }
1975
+ tapStartRef.current = null;
1976
+ const video = videoRef.current;
1977
+ if (!video) return;
1978
+ if (video.paused || isManuallyPaused) {
1979
+ setIsManuallyPaused(false);
1980
+ video.muted = true;
1981
+ video.play().then(() => {
1982
+ requestAnimationFrame(() => {
1983
+ video.muted = isMuted;
1984
+ });
1985
+ }).catch(() => {
1986
+ });
2002
1987
  } else {
2003
- doubleTapTimerRef.current = setTimeout(() => {
2004
- doubleTapTimerRef.current = null;
2005
- const video = videoRef.current;
2006
- if (!video) return;
2007
- if (video.paused) {
2008
- video.play().catch(() => {
2009
- });
2010
- setIsPaused(false);
2011
- } else {
2012
- video.pause();
2013
- setIsPaused(true);
2014
- }
2015
- }, 300);
1988
+ setIsManuallyPaused(true);
1989
+ video.pause();
2016
1990
  }
2017
- }, [isActive]);
2018
- react.useEffect(() => {
2019
- return () => {
2020
- if (doubleTapTimerRef.current !== null) {
2021
- clearTimeout(doubleTapTimerRef.current);
2022
- }
2023
- };
2024
- }, []);
2025
- react.useEffect(() => {
2026
- if (isActive) setIsPaused(false);
2027
- }, [isActive]);
1991
+ }, [isActive, isManuallyPaused, isMuted]);
2028
1992
  const likeDelta = react.useSyncExternalStore(
2029
1993
  optimisticManager.store.subscribe,
2030
1994
  () => optimisticManager.getLikeDelta(item.id),
@@ -2042,12 +2006,36 @@ function VideoSlotInner({
2042
2006
  followState,
2043
2007
  share: () => adapters.interaction?.share?.(item.id),
2044
2008
  isMuted,
2045
- toggleMute: onToggleMute,
2046
- isPaused,
2047
- togglePause: handleTap,
2009
+ toggleMute: handleToggleMute,
2048
2010
  isActive,
2049
2011
  index
2050
- }), [item, likeDelta, followState, isMuted, isPaused, isActive, index, optimisticManager, adapters, onToggleMute, handleTap]);
2012
+ }), [item, likeDelta, followState, isMuted, isActive, index, optimisticManager, adapters, handleToggleMute]);
2013
+ const pauseActions = react.useMemo(() => ({
2014
+ ...actions,
2015
+ isPaused: isManuallyPaused,
2016
+ togglePlayPause: () => {
2017
+ const video = videoRef.current;
2018
+ if (!video) return;
2019
+ if (isManuallyPaused) {
2020
+ setIsManuallyPaused(false);
2021
+ video.muted = true;
2022
+ video.play().then(() => {
2023
+ requestAnimationFrame(() => {
2024
+ video.muted = isMuted;
2025
+ });
2026
+ }).catch(() => {
2027
+ });
2028
+ } else {
2029
+ setIsManuallyPaused(true);
2030
+ video.pause();
2031
+ }
2032
+ }
2033
+ }), [actions, isManuallyPaused, isMuted]);
2034
+ react.useEffect(() => {
2035
+ return () => {
2036
+ if (muteIndicatorTimer.current) clearTimeout(muteIndicatorTimer.current);
2037
+ };
2038
+ }, []);
2051
2039
  return /* @__PURE__ */ jsxRuntime.jsxs(
2052
2040
  "div",
2053
2041
  {
@@ -2058,7 +2046,8 @@ function VideoSlotInner({
2058
2046
  background: "#111",
2059
2047
  overflow: "hidden"
2060
2048
  },
2061
- onClick: handleTap,
2049
+ onPointerDown: handlePointerDown,
2050
+ onClick: handleClick,
2062
2051
  children: [
2063
2052
  /* @__PURE__ */ jsxRuntime.jsx(
2064
2053
  "video",
@@ -2066,9 +2055,8 @@ function VideoSlotInner({
2066
2055
  ref: videoRef,
2067
2056
  src: mp4Src,
2068
2057
  loop: true,
2069
- muted: true,
2058
+ muted: isActive ? isMuted : true,
2070
2059
  playsInline: true,
2071
- autoPlay: isActive,
2072
2060
  preload: shouldLoadSrc ? "auto" : "none",
2073
2061
  style: {
2074
2062
  width: "100%",
@@ -2095,19 +2083,29 @@ function VideoSlotInner({
2095
2083
  }
2096
2084
  }
2097
2085
  ),
2098
- (isDoubleTap || renderDoubleTap) && /* @__PURE__ */ jsxRuntime.jsx(
2086
+ showMuteIndicator && /* @__PURE__ */ jsxRuntime.jsx(
2099
2087
  "div",
2100
2088
  {
2101
2089
  style: {
2102
2090
  position: "absolute",
2103
- inset: 0,
2091
+ top: "50%",
2092
+ left: "50%",
2093
+ transform: "translate(-50%, -50%)",
2094
+ background: "rgba(0,0,0,0.6)",
2095
+ borderRadius: "50%",
2096
+ width: 64,
2097
+ height: 64,
2098
+ display: "flex",
2099
+ alignItems: "center",
2100
+ justifyContent: "center",
2101
+ fontSize: 28,
2104
2102
  pointerEvents: "none",
2105
- zIndex: 20
2103
+ animation: "reels-sdk-fadeInOut 1.2s ease forwards"
2106
2104
  },
2107
- children: renderDoubleTap ? renderDoubleTap(isDoubleTap) : /* @__PURE__ */ jsxRuntime.jsx(DefaultDoubleTap, { isDoubleTap })
2105
+ children: isMuted ? "\u{1F507}" : "\u{1F50A}"
2108
2106
  }
2109
2107
  ),
2110
- isPaused && /* @__PURE__ */ jsxRuntime.jsx(
2108
+ isActive && isManuallyPaused && /* @__PURE__ */ jsxRuntime.jsx(
2111
2109
  "div",
2112
2110
  {
2113
2111
  style: {
@@ -2116,25 +2114,9 @@ function VideoSlotInner({
2116
2114
  left: "50%",
2117
2115
  transform: "translate(-50%, -50%)",
2118
2116
  pointerEvents: "none",
2119
- zIndex: 5,
2120
- opacity: 0.3
2117
+ animation: "reels-sdk-fadeIn 0.2s ease forwards"
2121
2118
  },
2122
- children: renderPauseIndicator ? renderPauseIndicator(isPaused) : /* @__PURE__ */ jsxRuntime.jsx(
2123
- "div",
2124
- {
2125
- style: {
2126
- background: "rgba(0,0,0,0.6)",
2127
- borderRadius: "50%",
2128
- width: 64,
2129
- height: 64,
2130
- display: "flex",
2131
- alignItems: "center",
2132
- justifyContent: "center",
2133
- fontSize: 28
2134
- },
2135
- children: "\u25B6\uFE0F"
2136
- }
2137
- )
2119
+ children: renderPauseAction ? renderPauseAction(item, pauseActions) : /* @__PURE__ */ jsxRuntime.jsx(DefaultPauseAction, {})
2138
2120
  }
2139
2121
  ),
2140
2122
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -2142,13 +2124,11 @@ function VideoSlotInner({
2142
2124
  {
2143
2125
  style: {
2144
2126
  position: "absolute",
2145
- bottom: 0,
2127
+ bottom: 80,
2146
2128
  left: 16,
2147
2129
  right: 80,
2148
- paddingBottom: 16,
2149
2130
  pointerEvents: "none",
2150
- color: "#fff",
2151
- zIndex: 10
2131
+ color: "#fff"
2152
2132
  },
2153
2133
  children: renderOverlay ? renderOverlay(item, actions) : /* @__PURE__ */ jsxRuntime.jsx(DefaultOverlay, { item })
2154
2134
  }
@@ -2156,19 +2136,18 @@ function VideoSlotInner({
2156
2136
  /* @__PURE__ */ jsxRuntime.jsx(
2157
2137
  "div",
2158
2138
  {
2159
- onClick: (e) => e.stopPropagation(),
2160
2139
  style: {
2161
2140
  position: "absolute",
2162
- bottom: 0,
2141
+ bottom: 80,
2163
2142
  right: 16,
2164
- paddingBottom: 16,
2165
2143
  display: "flex",
2166
2144
  flexDirection: "column",
2167
2145
  gap: 20,
2168
- alignItems: "center",
2169
- pointerEvents: "auto",
2170
- zIndex: 10
2146
+ alignItems: "center"
2147
+ // Actions must be clickable; stop propagation so taps don't
2148
+ // also trigger the video play/pause handler on the container.
2171
2149
  },
2150
+ onClick: (e) => e.stopPropagation(),
2172
2151
  children: renderActions ? renderActions(item, actions) : /* @__PURE__ */ jsxRuntime.jsx(DefaultActions, { item, actions })
2173
2152
  }
2174
2153
  ),
@@ -2218,6 +2197,7 @@ function FpsCounter() {
2218
2197
  }
2219
2198
  );
2220
2199
  }
2200
+ var RENDER_WINDOW_RADIUS = 3;
2221
2201
  var centerStyle = {
2222
2202
  height: "100dvh",
2223
2203
  display: "flex",
@@ -2229,8 +2209,7 @@ var centerStyle = {
2229
2209
  function ReelsFeed({
2230
2210
  renderOverlay,
2231
2211
  renderActions,
2232
- renderPauseIndicator,
2233
- renderDoubleTap,
2212
+ renderPauseAction,
2234
2213
  renderLoading,
2235
2214
  renderEmpty,
2236
2215
  renderError: _renderError,
@@ -2238,21 +2217,24 @@ function ReelsFeed({
2238
2217
  loadMoreThreshold = 5,
2239
2218
  onSlotChange,
2240
2219
  gestureConfig,
2241
- snapConfig
2220
+ snapConfig,
2221
+ initialMuted = true,
2222
+ onAutoplayBlocked
2242
2223
  }) {
2243
2224
  const { items, loading, loadInitial, loadMore, hasMore } = useFeed();
2225
+ const { adapters } = useSDK();
2244
2226
  const {
2245
- activeIndices,
2246
- warmIndices,
2247
2227
  focusedIndex,
2248
2228
  prefetchIndex,
2229
+ preloadQueue,
2249
2230
  setFocusedIndexImmediate,
2250
2231
  setTotalItems,
2251
2232
  shouldRenderVideo,
2252
2233
  isWarmAllocated,
2253
2234
  setPrefetchIndex
2254
2235
  } = useResource();
2255
- const [isMuted, setIsMuted] = react.useState(false);
2236
+ const [isMuted, setIsMuted] = react.useState(initialMuted);
2237
+ const [isDragMuted, setIsDragMuted] = react.useState(false);
2256
2238
  const containerRef = react.useRef(null);
2257
2239
  const slotCacheRef = react.useRef(/* @__PURE__ */ new Map());
2258
2240
  const activeIndexRef = react.useRef(0);
@@ -2268,6 +2250,14 @@ function ReelsFeed({
2268
2250
  react.useEffect(() => {
2269
2251
  setTotalItems(items.length);
2270
2252
  }, [items.length, setTotalItems]);
2253
+ react.useEffect(() => {
2254
+ for (const idx of preloadQueue) {
2255
+ const item = items[idx];
2256
+ if (item && isVideoItem(item) && item.source.type === "hls") {
2257
+ adapters.videoLoader?.preloadMetadata?.(item.source.url);
2258
+ }
2259
+ }
2260
+ }, [preloadQueue, items, adapters.videoLoader]);
2271
2261
  react.useEffect(() => {
2272
2262
  if (items.length - focusedIndex <= loadMoreThreshold && hasMore && !loading) {
2273
2263
  loadMore();
@@ -2288,7 +2278,7 @@ function ReelsFeed({
2288
2278
  const observer = new MutationObserver(rebuild);
2289
2279
  observer.observe(container, { childList: true, subtree: true });
2290
2280
  return () => observer.disconnect();
2291
- }, [items.length]);
2281
+ }, [items.length, focusedIndex]);
2292
2282
  const containerHeight = react.useRef(
2293
2283
  typeof window !== "undefined" ? window.innerHeight : 800
2294
2284
  );
@@ -2379,6 +2369,12 @@ function ReelsFeed({
2379
2369
  animateBounceBack(targets);
2380
2370
  setPrefetchIndex(null);
2381
2371
  }, [animateBounceBack, setPrefetchIndex]);
2372
+ const handleDragStart = react.useCallback(() => {
2373
+ setIsDragMuted(true);
2374
+ }, []);
2375
+ const handleDragEnd = react.useCallback(() => {
2376
+ setTimeout(() => setIsDragMuted(false), 50);
2377
+ }, []);
2382
2378
  const { bind } = usePointerGesture({
2383
2379
  axis: "y",
2384
2380
  velocityThreshold: gestureConfig?.velocityThreshold ?? 0.3,
@@ -2391,7 +2387,9 @@ function ReelsFeed({
2391
2387
  },
2392
2388
  onDragThreshold: handleDragThreshold,
2393
2389
  onSnap: handleSnap,
2394
- onBounceBack: handleBounceBack
2390
+ onBounceBack: handleBounceBack,
2391
+ onDragStart: handleDragStart,
2392
+ onDragEnd: handleDragEnd
2395
2393
  });
2396
2394
  const getInitialTransformPx = react.useCallback(
2397
2395
  (index) => {
@@ -2433,59 +2431,58 @@ function ReelsFeed({
2433
2431
  70% { opacity: 1; transform: translate(-50%, -50%) scale(1); }
2434
2432
  100% { opacity: 0; transform: translate(-50%, -50%) scale(0.8); }
2435
2433
  }
2434
+ @keyframes reels-sdk-fadeIn {
2435
+ from { opacity: 0; transform: translate(-50%, -50%) scale(0.8); }
2436
+ to { opacity: 1; transform: translate(-50%, -50%) scale(1); }
2437
+ }
2436
2438
  @keyframes reels-sdk-spin {
2437
2439
  to { transform: rotate(360deg); }
2438
2440
  }
2439
2441
  ` }),
2440
- (() => {
2441
- const windowIndices = /* @__PURE__ */ new Set([
2442
- ...activeIndices,
2443
- ...warmIndices
2444
- ]);
2445
- if (prefetchIndex !== null && prefetchIndex !== void 0) {
2446
- windowIndices.add(prefetchIndex);
2442
+ items.map((item, index) => {
2443
+ const distFromFocus = Math.abs(index - focusedIndex);
2444
+ const wrapperStyle = {
2445
+ position: "absolute",
2446
+ inset: 0,
2447
+ contain: "layout style paint",
2448
+ willChange: distFromFocus <= 1 ? "transform" : "auto",
2449
+ transform: getInitialTransformPx(index)
2450
+ };
2451
+ if (distFromFocus > RENDER_WINDOW_RADIUS) {
2452
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { "data-slot-index": index, style: wrapperStyle }, item.id);
2447
2453
  }
2448
- return [...windowIndices].map((index) => {
2449
- const item = items[index];
2450
- if (!item) return null;
2451
- const isActive = index === focusedIndex;
2452
- const isPrefetchSlot = index === prefetchIndex;
2453
- const isWarm = isWarmAllocated(index);
2454
- const isVisible = shouldRenderVideo(index) || isPrefetchSlot;
2455
- const bufferTier = isActive ? "active" : isWarm ? "warm" : "hot";
2456
- return /* @__PURE__ */ jsxRuntime.jsx(
2457
- "div",
2458
- {
2459
- "data-slot-index": index,
2460
- style: {
2461
- position: "absolute",
2462
- inset: 0,
2463
- willChange: "transform",
2464
- transform: getInitialTransformPx(index)
2465
- },
2466
- children: isVisible ? /* @__PURE__ */ jsxRuntime.jsx(
2467
- VideoSlot,
2468
- {
2469
- item,
2470
- index,
2471
- isActive,
2472
- isPrefetch: isPrefetchSlot,
2473
- isPreloaded: !isActive && !isPrefetchSlot && isVisible,
2474
- bufferTier,
2475
- isMuted,
2476
- onToggleMute: handleToggleMute,
2477
- showFps: showFps && isActive,
2478
- renderOverlay,
2479
- renderActions,
2480
- renderPauseIndicator,
2481
- renderDoubleTap
2482
- }
2483
- ) : null
2484
- },
2485
- item.id
2486
- );
2487
- });
2488
- })()
2454
+ const isActive = index === focusedIndex;
2455
+ const isPrefetch = index === prefetchIndex;
2456
+ const isWarm = isWarmAllocated(index);
2457
+ const isVisible = shouldRenderVideo(index) || isPrefetch;
2458
+ const bufferTier = isActive ? "active" : isWarm ? "warm" : "hot";
2459
+ return /* @__PURE__ */ jsxRuntime.jsx(
2460
+ "div",
2461
+ {
2462
+ "data-slot-index": index,
2463
+ style: wrapperStyle,
2464
+ children: /* @__PURE__ */ jsxRuntime.jsx(
2465
+ VideoSlot,
2466
+ {
2467
+ item,
2468
+ index,
2469
+ isActive,
2470
+ isPrefetch,
2471
+ isPreloaded: !isActive && !isPrefetch && isVisible,
2472
+ bufferTier,
2473
+ isMuted: isMuted || isDragMuted,
2474
+ onToggleMute: handleToggleMute,
2475
+ onAutoplayBlocked,
2476
+ showFps: showFps && isActive,
2477
+ renderOverlay,
2478
+ renderActions,
2479
+ renderPauseAction
2480
+ }
2481
+ )
2482
+ },
2483
+ item.id
2484
+ );
2485
+ })
2489
2486
  ]
2490
2487
  }
2491
2488
  );
@@ -2740,6 +2737,8 @@ var MockVideoLoader = class {
2740
2737
  this.preloaded.clear();
2741
2738
  this.loading.clear();
2742
2739
  }
2740
+ preloadMetadata(_url) {
2741
+ }
2743
2742
  };
2744
2743
  var MockDataSource = class {
2745
2744
  constructor(options = {}) {
@@ -3113,8 +3112,8 @@ exports.DEFAULT_FEED_CONFIG = DEFAULT_FEED_CONFIG;
3113
3112
  exports.DEFAULT_PLAYER_CONFIG = DEFAULT_PLAYER_CONFIG;
3114
3113
  exports.DEFAULT_RESOURCE_CONFIG = DEFAULT_RESOURCE_CONFIG;
3115
3114
  exports.DefaultActions = DefaultActions;
3116
- exports.DefaultDoubleTap = DefaultDoubleTap;
3117
3115
  exports.DefaultOverlay = DefaultOverlay;
3116
+ exports.DefaultPauseAction = DefaultPauseAction;
3118
3117
  exports.DefaultSkeleton = DefaultSkeleton;
3119
3118
  exports.FeedManager = FeedManager;
3120
3119
  exports.HttpDataSource = HttpDataSource;