@xhub-reels/sdk 0.1.14 → 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,6 +1669,60 @@ function skeletonCircle(size) {
1659
1669
  background: "rgba(255,255,255,0.1)"
1660
1670
  };
1661
1671
  }
1672
+ function DefaultPauseAction() {
1673
+ return /* @__PURE__ */ jsxRuntime.jsx(
1674
+ "div",
1675
+ {
1676
+ style: {
1677
+ width: 72,
1678
+ height: 72,
1679
+ borderRadius: "50%",
1680
+ background: "rgba(0, 0, 0, 0.55)",
1681
+ display: "flex",
1682
+ alignItems: "center",
1683
+ justifyContent: "center",
1684
+ pointerEvents: "none",
1685
+ // Inset the triangle visually — triangles look off-center without this
1686
+ paddingLeft: 6
1687
+ },
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
+ )
1705
+ }
1706
+ );
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
+ }
1662
1726
  function VideoSlot({
1663
1727
  item,
1664
1728
  index,
@@ -1668,10 +1732,11 @@ function VideoSlot({
1668
1732
  bufferTier,
1669
1733
  isMuted,
1670
1734
  onToggleMute,
1735
+ onAutoplayBlocked,
1671
1736
  showFps = false,
1672
1737
  renderOverlay,
1673
1738
  renderActions,
1674
- renderPauseIndicator
1739
+ renderPauseAction
1675
1740
  }) {
1676
1741
  const { optimisticManager, adapters } = useSDK();
1677
1742
  if (!isVideoItem(item)) {
@@ -1704,10 +1769,11 @@ function VideoSlot({
1704
1769
  bufferTier,
1705
1770
  isMuted,
1706
1771
  onToggleMute,
1772
+ onAutoplayBlocked,
1707
1773
  showFps,
1708
1774
  renderOverlay,
1709
1775
  renderActions,
1710
- renderPauseIndicator,
1776
+ renderPauseAction,
1711
1777
  optimisticManager,
1712
1778
  adapters
1713
1779
  }
@@ -1722,10 +1788,11 @@ function VideoSlotInner({
1722
1788
  bufferTier,
1723
1789
  isMuted,
1724
1790
  onToggleMute,
1791
+ onAutoplayBlocked,
1725
1792
  showFps,
1726
1793
  renderOverlay,
1727
1794
  renderActions,
1728
- renderPauseIndicator,
1795
+ renderPauseAction,
1729
1796
  optimisticManager,
1730
1797
  adapters
1731
1798
  }) {
@@ -1736,7 +1803,7 @@ function VideoSlotInner({
1736
1803
  const isHlsSource = sourceType === "hls";
1737
1804
  const hlsSrc = isHlsSource && shouldLoadSrc ? src : void 0;
1738
1805
  const mp4Src = !isHlsSource && shouldLoadSrc ? src : void 0;
1739
- const { isReady: hlsReady, isNativeHls } = useHls({
1806
+ const { isReady: hlsReady } = useHls({
1740
1807
  src: hlsSrc,
1741
1808
  videoRef,
1742
1809
  isActive,
@@ -1775,9 +1842,7 @@ function VideoSlotInner({
1775
1842
  }, [mp4Src, isActive, isPrefetch, isPreloaded, isHlsSource]);
1776
1843
  const isReady = isHlsSource ? hlsReady : mp4Ready;
1777
1844
  const [hasPlayedAhead, setHasPlayedAhead] = react.useState(false);
1778
- const canPlayAhead = isHlsSource && !isNativeHls;
1779
1845
  react.useEffect(() => {
1780
- if (!canPlayAhead) return;
1781
1846
  const video = videoRef.current;
1782
1847
  if (!video) return;
1783
1848
  if (isActive || !isReady) return;
@@ -1785,180 +1850,145 @@ function VideoSlotInner({
1785
1850
  const prevMuted = video.muted;
1786
1851
  video.muted = true;
1787
1852
  let cancelled = false;
1788
- let rafId = null;
1789
- let vfcHandle = null;
1790
- const pauseAfterDecode = () => {
1791
- if (cancelled) return;
1792
- video.pause();
1793
- video.currentTime = 0;
1794
- video.muted = prevMuted;
1795
- setHasPlayedAhead(true);
1796
- };
1797
1853
  const doPlayAhead = async () => {
1854
+ await acquirePlayAhead();
1798
1855
  try {
1799
1856
  await video.play();
1800
- if (cancelled) return;
1801
- if ("requestVideoFrameCallback" in video) {
1802
- vfcHandle = video.requestVideoFrameCallback(() => {
1803
- vfcHandle = null;
1804
- pauseAfterDecode();
1805
- });
1806
- } else {
1807
- rafId = requestAnimationFrame(() => {
1808
- rafId = requestAnimationFrame(() => {
1809
- rafId = null;
1810
- pauseAfterDecode();
1811
- });
1812
- });
1857
+ if (cancelled) {
1858
+ video.pause();
1859
+ releasePlayAhead();
1860
+ return;
1813
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);
1814
1872
  } catch {
1815
- video.muted = prevMuted;
1873
+ releasePlayAhead();
1816
1874
  }
1817
1875
  };
1818
1876
  doPlayAhead();
1819
1877
  return () => {
1820
1878
  cancelled = true;
1821
- if (rafId !== null) {
1822
- cancelAnimationFrame(rafId);
1823
- rafId = null;
1824
- }
1825
- if (vfcHandle !== null && "cancelVideoFrameCallback" in video) {
1826
- video.cancelVideoFrameCallback(vfcHandle);
1827
- vfcHandle = null;
1828
- }
1829
1879
  };
1830
- }, [canPlayAhead, isActive, isReady, hasPlayedAhead]);
1880
+ }, [isActive, isReady, hasPlayedAhead]);
1831
1881
  react.useEffect(() => {
1832
1882
  setHasPlayedAhead(false);
1833
1883
  }, [src]);
1834
1884
  const wasActiveRef = react.useRef(false);
1885
+ const [isManuallyPaused, setIsManuallyPaused] = react.useState(false);
1835
1886
  react.useEffect(() => {
1836
1887
  const video = videoRef.current;
1837
1888
  if (!video) return;
1838
1889
  let onReady = null;
1839
- let fallbackTimerId = null;
1840
- let pollId = null;
1841
- if (isActive) {
1842
- wasActiveRef.current = true;
1843
- const startPlay = () => {
1844
- if (onReady) {
1845
- video.removeEventListener("canplay", onReady);
1846
- video.removeEventListener("loadeddata", onReady);
1847
- video.removeEventListener("playing", onReady);
1848
- onReady = null;
1849
- }
1850
- if (fallbackTimerId !== null) {
1851
- clearTimeout(fallbackTimerId);
1852
- fallbackTimerId = null;
1853
- }
1854
- if (pollId !== null) {
1855
- clearInterval(pollId);
1856
- pollId = null;
1857
- }
1890
+ let cancelled = false;
1891
+ const attemptPlay = () => {
1892
+ if (cancelled) return;
1893
+ if (isMuted) {
1858
1894
  video.muted = true;
1859
- video.play().then(() => {
1860
- video.muted = isMuted;
1861
- }).catch(() => {
1862
- video.muted = isMuted;
1895
+ video.play().catch(() => {
1863
1896
  });
1864
- };
1865
- if (video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) {
1866
- startPlay();
1867
1897
  } else {
1868
- onReady = startPlay;
1869
- video.addEventListener("canplay", onReady, { once: true });
1870
- video.addEventListener("loadeddata", onReady, { once: true });
1871
- video.addEventListener("playing", onReady, { once: true });
1872
- if (isNativeHls && video.src && video.readyState < HTMLMediaElement.HAVE_CURRENT_DATA) {
1873
- video.load();
1874
- }
1875
- if (isNativeHls) {
1876
- pollId = setInterval(() => {
1877
- if (video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA && onReady) {
1878
- if (pollId !== null) {
1879
- clearInterval(pollId);
1880
- 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;
1881
1911
  }
1882
- startPlay();
1883
- }
1884
- }, 100);
1885
- }
1886
- fallbackTimerId = window.setTimeout(() => {
1887
- fallbackTimerId = null;
1888
- if (onReady) {
1889
- startPlay();
1912
+ onAutoplayBlocked?.();
1913
+ }).catch(() => {
1914
+ });
1890
1915
  }
1891
- }, isNativeHls ? 800 : 3e3);
1916
+ });
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 });
1892
1926
  }
1927
+ } else if (isActive && isManuallyPaused) {
1928
+ wasActiveRef.current = true;
1929
+ video.pause();
1893
1930
  } else if (wasActiveRef.current) {
1894
1931
  video.pause();
1895
1932
  video.currentTime = 0;
1896
1933
  wasActiveRef.current = false;
1934
+ setIsManuallyPaused(false);
1897
1935
  setHasPlayedAhead(false);
1898
1936
  } else if (!hasPlayedAhead) {
1899
1937
  video.pause();
1900
1938
  }
1901
1939
  return () => {
1902
- if (onReady) {
1903
- video.removeEventListener("canplay", onReady);
1904
- video.removeEventListener("loadeddata", onReady);
1905
- video.removeEventListener("playing", onReady);
1906
- }
1907
- if (fallbackTimerId !== null) {
1908
- clearTimeout(fallbackTimerId);
1909
- fallbackTimerId = null;
1910
- }
1911
- if (pollId !== null) {
1912
- clearInterval(pollId);
1913
- pollId = null;
1914
- }
1940
+ cancelled = true;
1941
+ if (onReady) video.removeEventListener("canplay", onReady);
1915
1942
  };
1916
- }, [isActive, isMuted, hasPlayedAhead, isNativeHls]);
1943
+ }, [isActive, isMuted, hasPlayedAhead, isManuallyPaused, onAutoplayBlocked]);
1917
1944
  react.useEffect(() => {
1918
1945
  const video = videoRef.current;
1919
1946
  if (!video) return;
1920
- if (isActive) {
1921
- video.muted = isMuted;
1922
- } else {
1923
- video.muted = true;
1924
- }
1947
+ video.muted = isActive ? isMuted : true;
1925
1948
  }, [isMuted, isActive]);
1926
- const [isActuallyPlaying, setIsActuallyPlaying] = react.useState(false);
1927
- react.useEffect(() => {
1928
- const video = videoRef.current;
1929
- if (!video) return;
1930
- const onPlaying = () => setIsActuallyPlaying(true);
1931
- const onPause = () => setIsActuallyPlaying(false);
1932
- const onEnded = () => setIsActuallyPlaying(false);
1933
- video.addEventListener("playing", onPlaying);
1934
- video.addEventListener("pause", onPause);
1935
- video.addEventListener("ended", onEnded);
1936
- return () => {
1937
- video.removeEventListener("playing", onPlaying);
1938
- video.removeEventListener("pause", onPause);
1939
- video.removeEventListener("ended", onEnded);
1940
- };
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 };
1941
1962
  }, []);
1942
- react.useEffect(() => {
1943
- if (!isActive) setIsActuallyPlaying(false);
1944
- }, [isActive]);
1945
- const showPosterOverlay = isActive ? !isReady && !isActuallyPlaying : canPlayAhead ? !hasPlayedAhead : !isReady;
1946
- const [isPaused, setIsPaused] = react.useState(false);
1947
- const handleTap = react.useCallback(() => {
1963
+ const handleClick = react.useCallback((e) => {
1964
+ if (e.button !== 0) return;
1965
+ if (!isActive) return;
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;
1973
+ }
1974
+ }
1975
+ tapStartRef.current = null;
1948
1976
  const video = videoRef.current;
1949
- if (!video || !isActive) return;
1950
- if (video.paused) {
1951
- video.play().catch(() => {
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(() => {
1952
1986
  });
1953
- setIsPaused(false);
1954
1987
  } else {
1988
+ setIsManuallyPaused(true);
1955
1989
  video.pause();
1956
- setIsPaused(true);
1957
1990
  }
1958
- }, [isActive]);
1959
- react.useEffect(() => {
1960
- if (isActive) setIsPaused(false);
1961
- }, [isActive]);
1991
+ }, [isActive, isManuallyPaused, isMuted]);
1962
1992
  const likeDelta = react.useSyncExternalStore(
1963
1993
  optimisticManager.store.subscribe,
1964
1994
  () => optimisticManager.getLikeDelta(item.id),
@@ -1976,12 +2006,36 @@ function VideoSlotInner({
1976
2006
  followState,
1977
2007
  share: () => adapters.interaction?.share?.(item.id),
1978
2008
  isMuted,
1979
- toggleMute: onToggleMute,
1980
- isPaused,
1981
- togglePause: handleTap,
2009
+ toggleMute: handleToggleMute,
1982
2010
  isActive,
1983
2011
  index
1984
- }), [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
+ }, []);
1985
2039
  return /* @__PURE__ */ jsxRuntime.jsxs(
1986
2040
  "div",
1987
2041
  {
@@ -1992,7 +2046,8 @@ function VideoSlotInner({
1992
2046
  background: "#111",
1993
2047
  overflow: "hidden"
1994
2048
  },
1995
- onClick: handleTap,
2049
+ onPointerDown: handlePointerDown,
2050
+ onClick: handleClick,
1996
2051
  children: [
1997
2052
  /* @__PURE__ */ jsxRuntime.jsx(
1998
2053
  "video",
@@ -2000,9 +2055,8 @@ function VideoSlotInner({
2000
2055
  ref: videoRef,
2001
2056
  src: mp4Src,
2002
2057
  loop: true,
2003
- muted: true,
2058
+ muted: isActive ? isMuted : true,
2004
2059
  playsInline: true,
2005
- autoPlay: isActive,
2006
2060
  preload: shouldLoadSrc ? "auto" : "none",
2007
2061
  style: {
2008
2062
  width: "100%",
@@ -2029,7 +2083,7 @@ function VideoSlotInner({
2029
2083
  }
2030
2084
  }
2031
2085
  ),
2032
- isPaused && /* @__PURE__ */ jsxRuntime.jsx(
2086
+ showMuteIndicator && /* @__PURE__ */ jsxRuntime.jsx(
2033
2087
  "div",
2034
2088
  {
2035
2089
  style: {
@@ -2037,26 +2091,32 @@ function VideoSlotInner({
2037
2091
  top: "50%",
2038
2092
  left: "50%",
2039
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,
2040
2102
  pointerEvents: "none",
2041
- zIndex: 5,
2042
- opacity: 0.3
2103
+ animation: "reels-sdk-fadeInOut 1.2s ease forwards"
2043
2104
  },
2044
- children: renderPauseIndicator ? renderPauseIndicator(isPaused) : /* @__PURE__ */ jsxRuntime.jsx(
2045
- "div",
2046
- {
2047
- style: {
2048
- background: "rgba(0,0,0,0.6)",
2049
- borderRadius: "50%",
2050
- width: 64,
2051
- height: 64,
2052
- display: "flex",
2053
- alignItems: "center",
2054
- justifyContent: "center",
2055
- fontSize: 28
2056
- },
2057
- children: "\u25B6\uFE0F"
2058
- }
2059
- )
2105
+ children: isMuted ? "\u{1F507}" : "\u{1F50A}"
2106
+ }
2107
+ ),
2108
+ isActive && isManuallyPaused && /* @__PURE__ */ jsxRuntime.jsx(
2109
+ "div",
2110
+ {
2111
+ style: {
2112
+ position: "absolute",
2113
+ top: "50%",
2114
+ left: "50%",
2115
+ transform: "translate(-50%, -50%)",
2116
+ pointerEvents: "none",
2117
+ animation: "reels-sdk-fadeIn 0.2s ease forwards"
2118
+ },
2119
+ children: renderPauseAction ? renderPauseAction(item, pauseActions) : /* @__PURE__ */ jsxRuntime.jsx(DefaultPauseAction, {})
2060
2120
  }
2061
2121
  ),
2062
2122
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -2064,13 +2124,11 @@ function VideoSlotInner({
2064
2124
  {
2065
2125
  style: {
2066
2126
  position: "absolute",
2067
- bottom: 0,
2127
+ bottom: 80,
2068
2128
  left: 16,
2069
2129
  right: 80,
2070
- paddingBottom: 16,
2071
2130
  pointerEvents: "none",
2072
- color: "#fff",
2073
- zIndex: 10
2131
+ color: "#fff"
2074
2132
  },
2075
2133
  children: renderOverlay ? renderOverlay(item, actions) : /* @__PURE__ */ jsxRuntime.jsx(DefaultOverlay, { item })
2076
2134
  }
@@ -2078,19 +2136,18 @@ function VideoSlotInner({
2078
2136
  /* @__PURE__ */ jsxRuntime.jsx(
2079
2137
  "div",
2080
2138
  {
2081
- onClick: (e) => e.stopPropagation(),
2082
2139
  style: {
2083
2140
  position: "absolute",
2084
- bottom: 0,
2141
+ bottom: 80,
2085
2142
  right: 16,
2086
- paddingBottom: 16,
2087
2143
  display: "flex",
2088
2144
  flexDirection: "column",
2089
2145
  gap: 20,
2090
- alignItems: "center",
2091
- pointerEvents: "auto",
2092
- 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.
2093
2149
  },
2150
+ onClick: (e) => e.stopPropagation(),
2094
2151
  children: renderActions ? renderActions(item, actions) : /* @__PURE__ */ jsxRuntime.jsx(DefaultActions, { item, actions })
2095
2152
  }
2096
2153
  ),
@@ -2140,6 +2197,7 @@ function FpsCounter() {
2140
2197
  }
2141
2198
  );
2142
2199
  }
2200
+ var RENDER_WINDOW_RADIUS = 3;
2143
2201
  var centerStyle = {
2144
2202
  height: "100dvh",
2145
2203
  display: "flex",
@@ -2151,7 +2209,7 @@ var centerStyle = {
2151
2209
  function ReelsFeed({
2152
2210
  renderOverlay,
2153
2211
  renderActions,
2154
- renderPauseIndicator,
2212
+ renderPauseAction,
2155
2213
  renderLoading,
2156
2214
  renderEmpty,
2157
2215
  renderError: _renderError,
@@ -2159,21 +2217,24 @@ function ReelsFeed({
2159
2217
  loadMoreThreshold = 5,
2160
2218
  onSlotChange,
2161
2219
  gestureConfig,
2162
- snapConfig
2220
+ snapConfig,
2221
+ initialMuted = true,
2222
+ onAutoplayBlocked
2163
2223
  }) {
2164
2224
  const { items, loading, loadInitial, loadMore, hasMore } = useFeed();
2225
+ const { adapters } = useSDK();
2165
2226
  const {
2166
- activeIndices,
2167
- warmIndices,
2168
2227
  focusedIndex,
2169
2228
  prefetchIndex,
2229
+ preloadQueue,
2170
2230
  setFocusedIndexImmediate,
2171
2231
  setTotalItems,
2172
2232
  shouldRenderVideo,
2173
2233
  isWarmAllocated,
2174
2234
  setPrefetchIndex
2175
2235
  } = useResource();
2176
- const [isMuted, setIsMuted] = react.useState(false);
2236
+ const [isMuted, setIsMuted] = react.useState(initialMuted);
2237
+ const [isDragMuted, setIsDragMuted] = react.useState(false);
2177
2238
  const containerRef = react.useRef(null);
2178
2239
  const slotCacheRef = react.useRef(/* @__PURE__ */ new Map());
2179
2240
  const activeIndexRef = react.useRef(0);
@@ -2189,6 +2250,14 @@ function ReelsFeed({
2189
2250
  react.useEffect(() => {
2190
2251
  setTotalItems(items.length);
2191
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]);
2192
2261
  react.useEffect(() => {
2193
2262
  if (items.length - focusedIndex <= loadMoreThreshold && hasMore && !loading) {
2194
2263
  loadMore();
@@ -2209,7 +2278,7 @@ function ReelsFeed({
2209
2278
  const observer = new MutationObserver(rebuild);
2210
2279
  observer.observe(container, { childList: true, subtree: true });
2211
2280
  return () => observer.disconnect();
2212
- }, [items.length]);
2281
+ }, [items.length, focusedIndex]);
2213
2282
  const containerHeight = react.useRef(
2214
2283
  typeof window !== "undefined" ? window.innerHeight : 800
2215
2284
  );
@@ -2300,6 +2369,12 @@ function ReelsFeed({
2300
2369
  animateBounceBack(targets);
2301
2370
  setPrefetchIndex(null);
2302
2371
  }, [animateBounceBack, setPrefetchIndex]);
2372
+ const handleDragStart = react.useCallback(() => {
2373
+ setIsDragMuted(true);
2374
+ }, []);
2375
+ const handleDragEnd = react.useCallback(() => {
2376
+ setTimeout(() => setIsDragMuted(false), 50);
2377
+ }, []);
2303
2378
  const { bind } = usePointerGesture({
2304
2379
  axis: "y",
2305
2380
  velocityThreshold: gestureConfig?.velocityThreshold ?? 0.3,
@@ -2312,7 +2387,9 @@ function ReelsFeed({
2312
2387
  },
2313
2388
  onDragThreshold: handleDragThreshold,
2314
2389
  onSnap: handleSnap,
2315
- onBounceBack: handleBounceBack
2390
+ onBounceBack: handleBounceBack,
2391
+ onDragStart: handleDragStart,
2392
+ onDragEnd: handleDragEnd
2316
2393
  });
2317
2394
  const getInitialTransformPx = react.useCallback(
2318
2395
  (index) => {
@@ -2354,58 +2431,58 @@ function ReelsFeed({
2354
2431
  70% { opacity: 1; transform: translate(-50%, -50%) scale(1); }
2355
2432
  100% { opacity: 0; transform: translate(-50%, -50%) scale(0.8); }
2356
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
+ }
2357
2438
  @keyframes reels-sdk-spin {
2358
2439
  to { transform: rotate(360deg); }
2359
2440
  }
2360
2441
  ` }),
2361
- (() => {
2362
- const windowIndices = /* @__PURE__ */ new Set([
2363
- ...activeIndices,
2364
- ...warmIndices
2365
- ]);
2366
- if (prefetchIndex !== null && prefetchIndex !== void 0) {
2367
- 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);
2368
2453
  }
2369
- return [...windowIndices].map((index) => {
2370
- const item = items[index];
2371
- if (!item) return null;
2372
- const isActive = index === focusedIndex;
2373
- const isPrefetchSlot = index === prefetchIndex;
2374
- const isWarm = isWarmAllocated(index);
2375
- const isVisible = shouldRenderVideo(index) || isPrefetchSlot;
2376
- const bufferTier = isActive ? "active" : isWarm ? "warm" : "hot";
2377
- return /* @__PURE__ */ jsxRuntime.jsx(
2378
- "div",
2379
- {
2380
- "data-slot-index": index,
2381
- style: {
2382
- position: "absolute",
2383
- inset: 0,
2384
- willChange: "transform",
2385
- transform: getInitialTransformPx(index)
2386
- },
2387
- children: isVisible ? /* @__PURE__ */ jsxRuntime.jsx(
2388
- VideoSlot,
2389
- {
2390
- item,
2391
- index,
2392
- isActive,
2393
- isPrefetch: isPrefetchSlot,
2394
- isPreloaded: !isActive && !isPrefetchSlot && isVisible,
2395
- bufferTier,
2396
- isMuted,
2397
- onToggleMute: handleToggleMute,
2398
- showFps: showFps && isActive,
2399
- renderOverlay,
2400
- renderActions,
2401
- renderPauseIndicator
2402
- }
2403
- ) : null
2404
- },
2405
- item.id
2406
- );
2407
- });
2408
- })()
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
+ })
2409
2486
  ]
2410
2487
  }
2411
2488
  );
@@ -2660,6 +2737,8 @@ var MockVideoLoader = class {
2660
2737
  this.preloaded.clear();
2661
2738
  this.loading.clear();
2662
2739
  }
2740
+ preloadMetadata(_url) {
2741
+ }
2663
2742
  };
2664
2743
  var MockDataSource = class {
2665
2744
  constructor(options = {}) {
@@ -3034,6 +3113,7 @@ exports.DEFAULT_PLAYER_CONFIG = DEFAULT_PLAYER_CONFIG;
3034
3113
  exports.DEFAULT_RESOURCE_CONFIG = DEFAULT_RESOURCE_CONFIG;
3035
3114
  exports.DefaultActions = DefaultActions;
3036
3115
  exports.DefaultOverlay = DefaultOverlay;
3116
+ exports.DefaultPauseAction = DefaultPauseAction;
3037
3117
  exports.DefaultSkeleton = DefaultSkeleton;
3038
3118
  exports.FeedManager = FeedManager;
3039
3119
  exports.HttpDataSource = HttpDataSource;