@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.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,6 +1663,60 @@ function skeletonCircle(size) {
1653
1663
  background: "rgba(255,255,255,0.1)"
1654
1664
  };
1655
1665
  }
1666
+ function DefaultPauseAction() {
1667
+ return /* @__PURE__ */ jsx(
1668
+ "div",
1669
+ {
1670
+ style: {
1671
+ width: 72,
1672
+ height: 72,
1673
+ borderRadius: "50%",
1674
+ background: "rgba(0, 0, 0, 0.55)",
1675
+ display: "flex",
1676
+ alignItems: "center",
1677
+ justifyContent: "center",
1678
+ pointerEvents: "none",
1679
+ // Inset the triangle visually — triangles look off-center without this
1680
+ paddingLeft: 6
1681
+ },
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
+ )
1699
+ }
1700
+ );
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
+ }
1656
1720
  function VideoSlot({
1657
1721
  item,
1658
1722
  index,
@@ -1662,10 +1726,11 @@ function VideoSlot({
1662
1726
  bufferTier,
1663
1727
  isMuted,
1664
1728
  onToggleMute,
1729
+ onAutoplayBlocked,
1665
1730
  showFps = false,
1666
1731
  renderOverlay,
1667
1732
  renderActions,
1668
- renderPauseIndicator
1733
+ renderPauseAction
1669
1734
  }) {
1670
1735
  const { optimisticManager, adapters } = useSDK();
1671
1736
  if (!isVideoItem(item)) {
@@ -1698,10 +1763,11 @@ function VideoSlot({
1698
1763
  bufferTier,
1699
1764
  isMuted,
1700
1765
  onToggleMute,
1766
+ onAutoplayBlocked,
1701
1767
  showFps,
1702
1768
  renderOverlay,
1703
1769
  renderActions,
1704
- renderPauseIndicator,
1770
+ renderPauseAction,
1705
1771
  optimisticManager,
1706
1772
  adapters
1707
1773
  }
@@ -1716,10 +1782,11 @@ function VideoSlotInner({
1716
1782
  bufferTier,
1717
1783
  isMuted,
1718
1784
  onToggleMute,
1785
+ onAutoplayBlocked,
1719
1786
  showFps,
1720
1787
  renderOverlay,
1721
1788
  renderActions,
1722
- renderPauseIndicator,
1789
+ renderPauseAction,
1723
1790
  optimisticManager,
1724
1791
  adapters
1725
1792
  }) {
@@ -1730,7 +1797,7 @@ function VideoSlotInner({
1730
1797
  const isHlsSource = sourceType === "hls";
1731
1798
  const hlsSrc = isHlsSource && shouldLoadSrc ? src : void 0;
1732
1799
  const mp4Src = !isHlsSource && shouldLoadSrc ? src : void 0;
1733
- const { isReady: hlsReady, isNativeHls } = useHls({
1800
+ const { isReady: hlsReady } = useHls({
1734
1801
  src: hlsSrc,
1735
1802
  videoRef,
1736
1803
  isActive,
@@ -1769,9 +1836,7 @@ function VideoSlotInner({
1769
1836
  }, [mp4Src, isActive, isPrefetch, isPreloaded, isHlsSource]);
1770
1837
  const isReady = isHlsSource ? hlsReady : mp4Ready;
1771
1838
  const [hasPlayedAhead, setHasPlayedAhead] = useState(false);
1772
- const canPlayAhead = isHlsSource && !isNativeHls;
1773
1839
  useEffect(() => {
1774
- if (!canPlayAhead) return;
1775
1840
  const video = videoRef.current;
1776
1841
  if (!video) return;
1777
1842
  if (isActive || !isReady) return;
@@ -1779,180 +1844,145 @@ function VideoSlotInner({
1779
1844
  const prevMuted = video.muted;
1780
1845
  video.muted = true;
1781
1846
  let cancelled = false;
1782
- let rafId = null;
1783
- let vfcHandle = null;
1784
- const pauseAfterDecode = () => {
1785
- if (cancelled) return;
1786
- video.pause();
1787
- video.currentTime = 0;
1788
- video.muted = prevMuted;
1789
- setHasPlayedAhead(true);
1790
- };
1791
1847
  const doPlayAhead = async () => {
1848
+ await acquirePlayAhead();
1792
1849
  try {
1793
1850
  await video.play();
1794
- if (cancelled) return;
1795
- if ("requestVideoFrameCallback" in video) {
1796
- vfcHandle = video.requestVideoFrameCallback(() => {
1797
- vfcHandle = null;
1798
- pauseAfterDecode();
1799
- });
1800
- } else {
1801
- rafId = requestAnimationFrame(() => {
1802
- rafId = requestAnimationFrame(() => {
1803
- rafId = null;
1804
- pauseAfterDecode();
1805
- });
1806
- });
1851
+ if (cancelled) {
1852
+ video.pause();
1853
+ releasePlayAhead();
1854
+ return;
1807
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);
1808
1866
  } catch {
1809
- video.muted = prevMuted;
1867
+ releasePlayAhead();
1810
1868
  }
1811
1869
  };
1812
1870
  doPlayAhead();
1813
1871
  return () => {
1814
1872
  cancelled = true;
1815
- if (rafId !== null) {
1816
- cancelAnimationFrame(rafId);
1817
- rafId = null;
1818
- }
1819
- if (vfcHandle !== null && "cancelVideoFrameCallback" in video) {
1820
- video.cancelVideoFrameCallback(vfcHandle);
1821
- vfcHandle = null;
1822
- }
1823
1873
  };
1824
- }, [canPlayAhead, isActive, isReady, hasPlayedAhead]);
1874
+ }, [isActive, isReady, hasPlayedAhead]);
1825
1875
  useEffect(() => {
1826
1876
  setHasPlayedAhead(false);
1827
1877
  }, [src]);
1828
1878
  const wasActiveRef = useRef(false);
1879
+ const [isManuallyPaused, setIsManuallyPaused] = useState(false);
1829
1880
  useEffect(() => {
1830
1881
  const video = videoRef.current;
1831
1882
  if (!video) return;
1832
1883
  let onReady = null;
1833
- let fallbackTimerId = null;
1834
- let pollId = null;
1835
- if (isActive) {
1836
- wasActiveRef.current = true;
1837
- const startPlay = () => {
1838
- if (onReady) {
1839
- video.removeEventListener("canplay", onReady);
1840
- video.removeEventListener("loadeddata", onReady);
1841
- video.removeEventListener("playing", onReady);
1842
- onReady = null;
1843
- }
1844
- if (fallbackTimerId !== null) {
1845
- clearTimeout(fallbackTimerId);
1846
- fallbackTimerId = null;
1847
- }
1848
- if (pollId !== null) {
1849
- clearInterval(pollId);
1850
- pollId = null;
1851
- }
1884
+ let cancelled = false;
1885
+ const attemptPlay = () => {
1886
+ if (cancelled) return;
1887
+ if (isMuted) {
1852
1888
  video.muted = true;
1853
- video.play().then(() => {
1854
- video.muted = isMuted;
1855
- }).catch(() => {
1856
- video.muted = isMuted;
1889
+ video.play().catch(() => {
1857
1890
  });
1858
- };
1859
- if (video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) {
1860
- startPlay();
1861
1891
  } else {
1862
- onReady = startPlay;
1863
- video.addEventListener("canplay", onReady, { once: true });
1864
- video.addEventListener("loadeddata", onReady, { once: true });
1865
- video.addEventListener("playing", onReady, { once: true });
1866
- if (isNativeHls && video.src && video.readyState < HTMLMediaElement.HAVE_CURRENT_DATA) {
1867
- video.load();
1868
- }
1869
- if (isNativeHls) {
1870
- pollId = setInterval(() => {
1871
- if (video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA && onReady) {
1872
- if (pollId !== null) {
1873
- clearInterval(pollId);
1874
- 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;
1875
1905
  }
1876
- startPlay();
1877
- }
1878
- }, 100);
1879
- }
1880
- fallbackTimerId = window.setTimeout(() => {
1881
- fallbackTimerId = null;
1882
- if (onReady) {
1883
- startPlay();
1906
+ onAutoplayBlocked?.();
1907
+ }).catch(() => {
1908
+ });
1884
1909
  }
1885
- }, isNativeHls ? 800 : 3e3);
1910
+ });
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 });
1886
1920
  }
1921
+ } else if (isActive && isManuallyPaused) {
1922
+ wasActiveRef.current = true;
1923
+ video.pause();
1887
1924
  } else if (wasActiveRef.current) {
1888
1925
  video.pause();
1889
1926
  video.currentTime = 0;
1890
1927
  wasActiveRef.current = false;
1928
+ setIsManuallyPaused(false);
1891
1929
  setHasPlayedAhead(false);
1892
1930
  } else if (!hasPlayedAhead) {
1893
1931
  video.pause();
1894
1932
  }
1895
1933
  return () => {
1896
- if (onReady) {
1897
- video.removeEventListener("canplay", onReady);
1898
- video.removeEventListener("loadeddata", onReady);
1899
- video.removeEventListener("playing", onReady);
1900
- }
1901
- if (fallbackTimerId !== null) {
1902
- clearTimeout(fallbackTimerId);
1903
- fallbackTimerId = null;
1904
- }
1905
- if (pollId !== null) {
1906
- clearInterval(pollId);
1907
- pollId = null;
1908
- }
1934
+ cancelled = true;
1935
+ if (onReady) video.removeEventListener("canplay", onReady);
1909
1936
  };
1910
- }, [isActive, isMuted, hasPlayedAhead, isNativeHls]);
1937
+ }, [isActive, isMuted, hasPlayedAhead, isManuallyPaused, onAutoplayBlocked]);
1911
1938
  useEffect(() => {
1912
1939
  const video = videoRef.current;
1913
1940
  if (!video) return;
1914
- if (isActive) {
1915
- video.muted = isMuted;
1916
- } else {
1917
- video.muted = true;
1918
- }
1941
+ video.muted = isActive ? isMuted : true;
1919
1942
  }, [isMuted, isActive]);
1920
- const [isActuallyPlaying, setIsActuallyPlaying] = useState(false);
1921
- useEffect(() => {
1922
- const video = videoRef.current;
1923
- if (!video) return;
1924
- const onPlaying = () => setIsActuallyPlaying(true);
1925
- const onPause = () => setIsActuallyPlaying(false);
1926
- const onEnded = () => setIsActuallyPlaying(false);
1927
- video.addEventListener("playing", onPlaying);
1928
- video.addEventListener("pause", onPause);
1929
- video.addEventListener("ended", onEnded);
1930
- return () => {
1931
- video.removeEventListener("playing", onPlaying);
1932
- video.removeEventListener("pause", onPause);
1933
- video.removeEventListener("ended", onEnded);
1934
- };
1943
+ const showPosterOverlay = !isReady && !hasPlayedAhead;
1944
+ const [showMuteIndicator, setShowMuteIndicator] = useState(false);
1945
+ const muteIndicatorTimer = useRef(null);
1946
+ const handleToggleMute = useCallback(() => {
1947
+ onToggleMute();
1948
+ setShowMuteIndicator(true);
1949
+ if (muteIndicatorTimer.current) clearTimeout(muteIndicatorTimer.current);
1950
+ muteIndicatorTimer.current = setTimeout(() => setShowMuteIndicator(false), 1200);
1951
+ }, [onToggleMute]);
1952
+ const tapStartRef = useRef(null);
1953
+ const TAP_SLOP_PX = 10;
1954
+ const handlePointerDown = useCallback((e) => {
1955
+ tapStartRef.current = { x: e.clientX, y: e.clientY };
1935
1956
  }, []);
1936
- useEffect(() => {
1937
- if (!isActive) setIsActuallyPlaying(false);
1938
- }, [isActive]);
1939
- const showPosterOverlay = isActive ? !isReady && !isActuallyPlaying : canPlayAhead ? !hasPlayedAhead : !isReady;
1940
- const [isPaused, setIsPaused] = useState(false);
1941
- const handleTap = useCallback(() => {
1957
+ const handleClick = useCallback((e) => {
1958
+ if (e.button !== 0) return;
1959
+ if (!isActive) return;
1960
+ const start = tapStartRef.current;
1961
+ if (start) {
1962
+ const dx = Math.abs(e.clientX - start.x);
1963
+ const dy = Math.abs(e.clientY - start.y);
1964
+ if (dx > TAP_SLOP_PX || dy > TAP_SLOP_PX) {
1965
+ tapStartRef.current = null;
1966
+ return;
1967
+ }
1968
+ }
1969
+ tapStartRef.current = null;
1942
1970
  const video = videoRef.current;
1943
- if (!video || !isActive) return;
1944
- if (video.paused) {
1945
- video.play().catch(() => {
1971
+ if (!video) return;
1972
+ if (video.paused || isManuallyPaused) {
1973
+ setIsManuallyPaused(false);
1974
+ video.muted = true;
1975
+ video.play().then(() => {
1976
+ requestAnimationFrame(() => {
1977
+ video.muted = isMuted;
1978
+ });
1979
+ }).catch(() => {
1946
1980
  });
1947
- setIsPaused(false);
1948
1981
  } else {
1982
+ setIsManuallyPaused(true);
1949
1983
  video.pause();
1950
- setIsPaused(true);
1951
1984
  }
1952
- }, [isActive]);
1953
- useEffect(() => {
1954
- if (isActive) setIsPaused(false);
1955
- }, [isActive]);
1985
+ }, [isActive, isManuallyPaused, isMuted]);
1956
1986
  const likeDelta = useSyncExternalStore(
1957
1987
  optimisticManager.store.subscribe,
1958
1988
  () => optimisticManager.getLikeDelta(item.id),
@@ -1970,12 +2000,36 @@ function VideoSlotInner({
1970
2000
  followState,
1971
2001
  share: () => adapters.interaction?.share?.(item.id),
1972
2002
  isMuted,
1973
- toggleMute: onToggleMute,
1974
- isPaused,
1975
- togglePause: handleTap,
2003
+ toggleMute: handleToggleMute,
1976
2004
  isActive,
1977
2005
  index
1978
- }), [item, likeDelta, followState, isMuted, isPaused, isActive, index, optimisticManager, adapters, onToggleMute, handleTap]);
2006
+ }), [item, likeDelta, followState, isMuted, isActive, index, optimisticManager, adapters, handleToggleMute]);
2007
+ const pauseActions = useMemo(() => ({
2008
+ ...actions,
2009
+ isPaused: isManuallyPaused,
2010
+ togglePlayPause: () => {
2011
+ const video = videoRef.current;
2012
+ if (!video) return;
2013
+ if (isManuallyPaused) {
2014
+ setIsManuallyPaused(false);
2015
+ video.muted = true;
2016
+ video.play().then(() => {
2017
+ requestAnimationFrame(() => {
2018
+ video.muted = isMuted;
2019
+ });
2020
+ }).catch(() => {
2021
+ });
2022
+ } else {
2023
+ setIsManuallyPaused(true);
2024
+ video.pause();
2025
+ }
2026
+ }
2027
+ }), [actions, isManuallyPaused, isMuted]);
2028
+ useEffect(() => {
2029
+ return () => {
2030
+ if (muteIndicatorTimer.current) clearTimeout(muteIndicatorTimer.current);
2031
+ };
2032
+ }, []);
1979
2033
  return /* @__PURE__ */ jsxs(
1980
2034
  "div",
1981
2035
  {
@@ -1986,7 +2040,8 @@ function VideoSlotInner({
1986
2040
  background: "#111",
1987
2041
  overflow: "hidden"
1988
2042
  },
1989
- onClick: handleTap,
2043
+ onPointerDown: handlePointerDown,
2044
+ onClick: handleClick,
1990
2045
  children: [
1991
2046
  /* @__PURE__ */ jsx(
1992
2047
  "video",
@@ -1994,9 +2049,8 @@ function VideoSlotInner({
1994
2049
  ref: videoRef,
1995
2050
  src: mp4Src,
1996
2051
  loop: true,
1997
- muted: true,
2052
+ muted: isActive ? isMuted : true,
1998
2053
  playsInline: true,
1999
- autoPlay: isActive,
2000
2054
  preload: shouldLoadSrc ? "auto" : "none",
2001
2055
  style: {
2002
2056
  width: "100%",
@@ -2023,7 +2077,7 @@ function VideoSlotInner({
2023
2077
  }
2024
2078
  }
2025
2079
  ),
2026
- isPaused && /* @__PURE__ */ jsx(
2080
+ showMuteIndicator && /* @__PURE__ */ jsx(
2027
2081
  "div",
2028
2082
  {
2029
2083
  style: {
@@ -2031,26 +2085,32 @@ function VideoSlotInner({
2031
2085
  top: "50%",
2032
2086
  left: "50%",
2033
2087
  transform: "translate(-50%, -50%)",
2088
+ background: "rgba(0,0,0,0.6)",
2089
+ borderRadius: "50%",
2090
+ width: 64,
2091
+ height: 64,
2092
+ display: "flex",
2093
+ alignItems: "center",
2094
+ justifyContent: "center",
2095
+ fontSize: 28,
2034
2096
  pointerEvents: "none",
2035
- zIndex: 5,
2036
- opacity: 0.3
2097
+ animation: "reels-sdk-fadeInOut 1.2s ease forwards"
2037
2098
  },
2038
- children: renderPauseIndicator ? renderPauseIndicator(isPaused) : /* @__PURE__ */ jsx(
2039
- "div",
2040
- {
2041
- style: {
2042
- background: "rgba(0,0,0,0.6)",
2043
- borderRadius: "50%",
2044
- width: 64,
2045
- height: 64,
2046
- display: "flex",
2047
- alignItems: "center",
2048
- justifyContent: "center",
2049
- fontSize: 28
2050
- },
2051
- children: "\u25B6\uFE0F"
2052
- }
2053
- )
2099
+ children: isMuted ? "\u{1F507}" : "\u{1F50A}"
2100
+ }
2101
+ ),
2102
+ isActive && isManuallyPaused && /* @__PURE__ */ jsx(
2103
+ "div",
2104
+ {
2105
+ style: {
2106
+ position: "absolute",
2107
+ top: "50%",
2108
+ left: "50%",
2109
+ transform: "translate(-50%, -50%)",
2110
+ pointerEvents: "none",
2111
+ animation: "reels-sdk-fadeIn 0.2s ease forwards"
2112
+ },
2113
+ children: renderPauseAction ? renderPauseAction(item, pauseActions) : /* @__PURE__ */ jsx(DefaultPauseAction, {})
2054
2114
  }
2055
2115
  ),
2056
2116
  /* @__PURE__ */ jsx(
@@ -2058,13 +2118,11 @@ function VideoSlotInner({
2058
2118
  {
2059
2119
  style: {
2060
2120
  position: "absolute",
2061
- bottom: 0,
2121
+ bottom: 80,
2062
2122
  left: 16,
2063
2123
  right: 80,
2064
- paddingBottom: 16,
2065
2124
  pointerEvents: "none",
2066
- color: "#fff",
2067
- zIndex: 10
2125
+ color: "#fff"
2068
2126
  },
2069
2127
  children: renderOverlay ? renderOverlay(item, actions) : /* @__PURE__ */ jsx(DefaultOverlay, { item })
2070
2128
  }
@@ -2072,19 +2130,18 @@ function VideoSlotInner({
2072
2130
  /* @__PURE__ */ jsx(
2073
2131
  "div",
2074
2132
  {
2075
- onClick: (e) => e.stopPropagation(),
2076
2133
  style: {
2077
2134
  position: "absolute",
2078
- bottom: 0,
2135
+ bottom: 80,
2079
2136
  right: 16,
2080
- paddingBottom: 16,
2081
2137
  display: "flex",
2082
2138
  flexDirection: "column",
2083
2139
  gap: 20,
2084
- alignItems: "center",
2085
- pointerEvents: "auto",
2086
- zIndex: 10
2140
+ alignItems: "center"
2141
+ // Actions must be clickable; stop propagation so taps don't
2142
+ // also trigger the video play/pause handler on the container.
2087
2143
  },
2144
+ onClick: (e) => e.stopPropagation(),
2088
2145
  children: renderActions ? renderActions(item, actions) : /* @__PURE__ */ jsx(DefaultActions, { item, actions })
2089
2146
  }
2090
2147
  ),
@@ -2134,6 +2191,7 @@ function FpsCounter() {
2134
2191
  }
2135
2192
  );
2136
2193
  }
2194
+ var RENDER_WINDOW_RADIUS = 3;
2137
2195
  var centerStyle = {
2138
2196
  height: "100dvh",
2139
2197
  display: "flex",
@@ -2145,7 +2203,7 @@ var centerStyle = {
2145
2203
  function ReelsFeed({
2146
2204
  renderOverlay,
2147
2205
  renderActions,
2148
- renderPauseIndicator,
2206
+ renderPauseAction,
2149
2207
  renderLoading,
2150
2208
  renderEmpty,
2151
2209
  renderError: _renderError,
@@ -2153,21 +2211,24 @@ function ReelsFeed({
2153
2211
  loadMoreThreshold = 5,
2154
2212
  onSlotChange,
2155
2213
  gestureConfig,
2156
- snapConfig
2214
+ snapConfig,
2215
+ initialMuted = true,
2216
+ onAutoplayBlocked
2157
2217
  }) {
2158
2218
  const { items, loading, loadInitial, loadMore, hasMore } = useFeed();
2219
+ const { adapters } = useSDK();
2159
2220
  const {
2160
- activeIndices,
2161
- warmIndices,
2162
2221
  focusedIndex,
2163
2222
  prefetchIndex,
2223
+ preloadQueue,
2164
2224
  setFocusedIndexImmediate,
2165
2225
  setTotalItems,
2166
2226
  shouldRenderVideo,
2167
2227
  isWarmAllocated,
2168
2228
  setPrefetchIndex
2169
2229
  } = useResource();
2170
- const [isMuted, setIsMuted] = useState(false);
2230
+ const [isMuted, setIsMuted] = useState(initialMuted);
2231
+ const [isDragMuted, setIsDragMuted] = useState(false);
2171
2232
  const containerRef = useRef(null);
2172
2233
  const slotCacheRef = useRef(/* @__PURE__ */ new Map());
2173
2234
  const activeIndexRef = useRef(0);
@@ -2183,6 +2244,14 @@ function ReelsFeed({
2183
2244
  useEffect(() => {
2184
2245
  setTotalItems(items.length);
2185
2246
  }, [items.length, setTotalItems]);
2247
+ useEffect(() => {
2248
+ for (const idx of preloadQueue) {
2249
+ const item = items[idx];
2250
+ if (item && isVideoItem(item) && item.source.type === "hls") {
2251
+ adapters.videoLoader?.preloadMetadata?.(item.source.url);
2252
+ }
2253
+ }
2254
+ }, [preloadQueue, items, adapters.videoLoader]);
2186
2255
  useEffect(() => {
2187
2256
  if (items.length - focusedIndex <= loadMoreThreshold && hasMore && !loading) {
2188
2257
  loadMore();
@@ -2203,7 +2272,7 @@ function ReelsFeed({
2203
2272
  const observer = new MutationObserver(rebuild);
2204
2273
  observer.observe(container, { childList: true, subtree: true });
2205
2274
  return () => observer.disconnect();
2206
- }, [items.length]);
2275
+ }, [items.length, focusedIndex]);
2207
2276
  const containerHeight = useRef(
2208
2277
  typeof window !== "undefined" ? window.innerHeight : 800
2209
2278
  );
@@ -2294,6 +2363,12 @@ function ReelsFeed({
2294
2363
  animateBounceBack(targets);
2295
2364
  setPrefetchIndex(null);
2296
2365
  }, [animateBounceBack, setPrefetchIndex]);
2366
+ const handleDragStart = useCallback(() => {
2367
+ setIsDragMuted(true);
2368
+ }, []);
2369
+ const handleDragEnd = useCallback(() => {
2370
+ setTimeout(() => setIsDragMuted(false), 50);
2371
+ }, []);
2297
2372
  const { bind } = usePointerGesture({
2298
2373
  axis: "y",
2299
2374
  velocityThreshold: gestureConfig?.velocityThreshold ?? 0.3,
@@ -2306,7 +2381,9 @@ function ReelsFeed({
2306
2381
  },
2307
2382
  onDragThreshold: handleDragThreshold,
2308
2383
  onSnap: handleSnap,
2309
- onBounceBack: handleBounceBack
2384
+ onBounceBack: handleBounceBack,
2385
+ onDragStart: handleDragStart,
2386
+ onDragEnd: handleDragEnd
2310
2387
  });
2311
2388
  const getInitialTransformPx = useCallback(
2312
2389
  (index) => {
@@ -2348,58 +2425,58 @@ function ReelsFeed({
2348
2425
  70% { opacity: 1; transform: translate(-50%, -50%) scale(1); }
2349
2426
  100% { opacity: 0; transform: translate(-50%, -50%) scale(0.8); }
2350
2427
  }
2428
+ @keyframes reels-sdk-fadeIn {
2429
+ from { opacity: 0; transform: translate(-50%, -50%) scale(0.8); }
2430
+ to { opacity: 1; transform: translate(-50%, -50%) scale(1); }
2431
+ }
2351
2432
  @keyframes reels-sdk-spin {
2352
2433
  to { transform: rotate(360deg); }
2353
2434
  }
2354
2435
  ` }),
2355
- (() => {
2356
- const windowIndices = /* @__PURE__ */ new Set([
2357
- ...activeIndices,
2358
- ...warmIndices
2359
- ]);
2360
- if (prefetchIndex !== null && prefetchIndex !== void 0) {
2361
- windowIndices.add(prefetchIndex);
2436
+ items.map((item, index) => {
2437
+ const distFromFocus = Math.abs(index - focusedIndex);
2438
+ const wrapperStyle = {
2439
+ position: "absolute",
2440
+ inset: 0,
2441
+ contain: "layout style paint",
2442
+ willChange: distFromFocus <= 1 ? "transform" : "auto",
2443
+ transform: getInitialTransformPx(index)
2444
+ };
2445
+ if (distFromFocus > RENDER_WINDOW_RADIUS) {
2446
+ return /* @__PURE__ */ jsx("div", { "data-slot-index": index, style: wrapperStyle }, item.id);
2362
2447
  }
2363
- return [...windowIndices].map((index) => {
2364
- const item = items[index];
2365
- if (!item) return null;
2366
- const isActive = index === focusedIndex;
2367
- const isPrefetchSlot = index === prefetchIndex;
2368
- const isWarm = isWarmAllocated(index);
2369
- const isVisible = shouldRenderVideo(index) || isPrefetchSlot;
2370
- const bufferTier = isActive ? "active" : isWarm ? "warm" : "hot";
2371
- return /* @__PURE__ */ jsx(
2372
- "div",
2373
- {
2374
- "data-slot-index": index,
2375
- style: {
2376
- position: "absolute",
2377
- inset: 0,
2378
- willChange: "transform",
2379
- transform: getInitialTransformPx(index)
2380
- },
2381
- children: isVisible ? /* @__PURE__ */ jsx(
2382
- VideoSlot,
2383
- {
2384
- item,
2385
- index,
2386
- isActive,
2387
- isPrefetch: isPrefetchSlot,
2388
- isPreloaded: !isActive && !isPrefetchSlot && isVisible,
2389
- bufferTier,
2390
- isMuted,
2391
- onToggleMute: handleToggleMute,
2392
- showFps: showFps && isActive,
2393
- renderOverlay,
2394
- renderActions,
2395
- renderPauseIndicator
2396
- }
2397
- ) : null
2398
- },
2399
- item.id
2400
- );
2401
- });
2402
- })()
2448
+ const isActive = index === focusedIndex;
2449
+ const isPrefetch = index === prefetchIndex;
2450
+ const isWarm = isWarmAllocated(index);
2451
+ const isVisible = shouldRenderVideo(index) || isPrefetch;
2452
+ const bufferTier = isActive ? "active" : isWarm ? "warm" : "hot";
2453
+ return /* @__PURE__ */ jsx(
2454
+ "div",
2455
+ {
2456
+ "data-slot-index": index,
2457
+ style: wrapperStyle,
2458
+ children: /* @__PURE__ */ jsx(
2459
+ VideoSlot,
2460
+ {
2461
+ item,
2462
+ index,
2463
+ isActive,
2464
+ isPrefetch,
2465
+ isPreloaded: !isActive && !isPrefetch && isVisible,
2466
+ bufferTier,
2467
+ isMuted: isMuted || isDragMuted,
2468
+ onToggleMute: handleToggleMute,
2469
+ onAutoplayBlocked,
2470
+ showFps: showFps && isActive,
2471
+ renderOverlay,
2472
+ renderActions,
2473
+ renderPauseAction
2474
+ }
2475
+ )
2476
+ },
2477
+ item.id
2478
+ );
2479
+ })
2403
2480
  ]
2404
2481
  }
2405
2482
  );
@@ -2654,6 +2731,8 @@ var MockVideoLoader = class {
2654
2731
  this.preloaded.clear();
2655
2732
  this.loading.clear();
2656
2733
  }
2734
+ preloadMetadata(_url) {
2735
+ }
2657
2736
  };
2658
2737
  var MockDataSource = class {
2659
2738
  constructor(options = {}) {
@@ -3023,4 +3102,4 @@ var HttpError = class extends Error {
3023
3102
  }
3024
3103
  };
3025
3104
 
3026
- export { DEFAULT_FEED_CONFIG, DEFAULT_PLAYER_CONFIG, DEFAULT_RESOURCE_CONFIG, DefaultActions, 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 };
3105
+ 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 };