@xhub-reels/sdk 0.1.8 → 0.1.10

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
@@ -1392,6 +1392,15 @@ function useHls(options) {
1392
1392
  return;
1393
1393
  }
1394
1394
  if (!isActive && !isPrefetch) {
1395
+ if (isNative) {
1396
+ if (video.src) {
1397
+ video.removeAttribute("src");
1398
+ video.load();
1399
+ }
1400
+ setIsReady(false);
1401
+ currentSrcRef.current = void 0;
1402
+ return;
1403
+ }
1395
1404
  destroy();
1396
1405
  setIsReady(false);
1397
1406
  canPlayFiredRef.current = false;
@@ -1399,20 +1408,36 @@ function useHls(options) {
1399
1408
  return;
1400
1409
  }
1401
1410
  if (isNative) {
1402
- if (video.src !== src) {
1403
- video.src = src;
1411
+ if (currentSrcRef.current === src) {
1412
+ if (video.readyState >= HTMLMediaElement.HAVE_FUTURE_DATA) {
1413
+ setIsReady(true);
1414
+ return void 0;
1415
+ }
1416
+ const handleCanPlayReuse = () => setIsReady(true);
1417
+ video.addEventListener("canplay", handleCanPlayReuse, { once: true });
1418
+ video.addEventListener("loadeddata", handleCanPlayReuse, { once: true });
1419
+ video.addEventListener("playing", handleCanPlayReuse, { once: true });
1420
+ return () => {
1421
+ video.removeEventListener("canplay", handleCanPlayReuse);
1422
+ video.removeEventListener("loadeddata", handleCanPlayReuse);
1423
+ video.removeEventListener("playing", handleCanPlayReuse);
1424
+ };
1404
1425
  }
1426
+ video.src = src;
1427
+ currentSrcRef.current = src;
1405
1428
  if (video.readyState >= HTMLMediaElement.HAVE_FUTURE_DATA) {
1406
1429
  setIsReady(true);
1407
- currentSrcRef.current = src;
1408
- return;
1430
+ return void 0;
1409
1431
  }
1410
1432
  setIsReady(false);
1411
- currentSrcRef.current = src;
1412
1433
  const handleCanPlay2 = () => setIsReady(true);
1413
1434
  video.addEventListener("canplay", handleCanPlay2, { once: true });
1435
+ video.addEventListener("loadeddata", handleCanPlay2, { once: true });
1436
+ video.addEventListener("playing", handleCanPlay2, { once: true });
1414
1437
  return () => {
1415
1438
  video.removeEventListener("canplay", handleCanPlay2);
1439
+ video.removeEventListener("loadeddata", handleCanPlay2);
1440
+ video.removeEventListener("playing", handleCanPlay2);
1416
1441
  };
1417
1442
  }
1418
1443
  if (!isHlsSupported) {
@@ -1499,6 +1524,7 @@ function useHls(options) {
1499
1524
  }, [bufferTier]);
1500
1525
  return {
1501
1526
  isHlsJs,
1527
+ isNativeHls: isNative,
1502
1528
  isReady,
1503
1529
  destroy
1504
1530
  };
@@ -1628,7 +1654,8 @@ function VideoSlot({
1628
1654
  onToggleMute,
1629
1655
  showFps = false,
1630
1656
  renderOverlay,
1631
- renderActions
1657
+ renderActions,
1658
+ renderPauseIndicator
1632
1659
  }) {
1633
1660
  const { optimisticManager, adapters } = useSDK();
1634
1661
  if (!isVideoItem(item)) {
@@ -1664,6 +1691,7 @@ function VideoSlot({
1664
1691
  showFps,
1665
1692
  renderOverlay,
1666
1693
  renderActions,
1694
+ renderPauseIndicator,
1667
1695
  optimisticManager,
1668
1696
  adapters
1669
1697
  }
@@ -1681,6 +1709,7 @@ function VideoSlotInner({
1681
1709
  showFps,
1682
1710
  renderOverlay,
1683
1711
  renderActions,
1712
+ renderPauseIndicator,
1684
1713
  optimisticManager,
1685
1714
  adapters
1686
1715
  }) {
@@ -1691,7 +1720,7 @@ function VideoSlotInner({
1691
1720
  const isHlsSource = sourceType === "hls";
1692
1721
  const hlsSrc = isHlsSource && shouldLoadSrc ? src : void 0;
1693
1722
  const mp4Src = !isHlsSource && shouldLoadSrc ? src : void 0;
1694
- const { isReady: hlsReady } = useHls({
1723
+ const { isReady: hlsReady, isNativeHls } = useHls({
1695
1724
  src: hlsSrc,
1696
1725
  videoRef,
1697
1726
  isActive,
@@ -1730,7 +1759,9 @@ function VideoSlotInner({
1730
1759
  }, [mp4Src, isActive, isPrefetch, isPreloaded, isHlsSource]);
1731
1760
  const isReady = isHlsSource ? hlsReady : mp4Ready;
1732
1761
  const [hasPlayedAhead, setHasPlayedAhead] = react.useState(false);
1762
+ const canPlayAhead = isHlsSource && !isNativeHls;
1733
1763
  react.useEffect(() => {
1764
+ if (!canPlayAhead) return;
1734
1765
  const video = videoRef.current;
1735
1766
  if (!video) return;
1736
1767
  if (isActive || !isReady) return;
@@ -1757,7 +1788,7 @@ function VideoSlotInner({
1757
1788
  return () => {
1758
1789
  cancelled = true;
1759
1790
  };
1760
- }, [isActive, isReady, hasPlayedAhead]);
1791
+ }, [canPlayAhead, isActive, isReady, hasPlayedAhead]);
1761
1792
  react.useEffect(() => {
1762
1793
  setHasPlayedAhead(false);
1763
1794
  }, [src]);
@@ -1768,16 +1799,30 @@ function VideoSlotInner({
1768
1799
  let onReady = null;
1769
1800
  if (isActive) {
1770
1801
  wasActiveRef.current = true;
1771
- video.muted = isMuted;
1772
- if (video.readyState >= HTMLMediaElement.HAVE_FUTURE_DATA) {
1773
- video.play().catch(() => {
1802
+ const startPlay = () => {
1803
+ if (onReady) {
1804
+ video.removeEventListener("canplay", onReady);
1805
+ video.removeEventListener("loadeddata", onReady);
1806
+ video.removeEventListener("playing", onReady);
1807
+ onReady = null;
1808
+ }
1809
+ video.muted = true;
1810
+ video.play().then(() => {
1811
+ video.muted = isMuted;
1812
+ }).catch(() => {
1813
+ video.muted = isMuted;
1774
1814
  });
1815
+ };
1816
+ if (video.readyState >= HTMLMediaElement.HAVE_FUTURE_DATA) {
1817
+ startPlay();
1775
1818
  } else {
1776
- onReady = () => {
1777
- video.play().catch(() => {
1778
- });
1779
- };
1819
+ onReady = startPlay;
1780
1820
  video.addEventListener("canplay", onReady, { once: true });
1821
+ video.addEventListener("loadeddata", onReady, { once: true });
1822
+ video.addEventListener("playing", onReady, { once: true });
1823
+ if (video.readyState === HTMLMediaElement.HAVE_NOTHING && isNativeHls && video.src) {
1824
+ video.load();
1825
+ }
1781
1826
  }
1782
1827
  } else if (wasActiveRef.current) {
1783
1828
  video.pause();
@@ -1788,28 +1833,54 @@ function VideoSlotInner({
1788
1833
  video.pause();
1789
1834
  }
1790
1835
  return () => {
1791
- if (onReady) video.removeEventListener("canplay", onReady);
1836
+ if (onReady) {
1837
+ video.removeEventListener("canplay", onReady);
1838
+ video.removeEventListener("loadeddata", onReady);
1839
+ video.removeEventListener("playing", onReady);
1840
+ }
1792
1841
  };
1793
- }, [isActive, isMuted, hasPlayedAhead]);
1842
+ }, [isActive, isMuted, hasPlayedAhead, isNativeHls]);
1794
1843
  react.useEffect(() => {
1795
1844
  const video = videoRef.current;
1796
1845
  if (!video) return;
1797
1846
  video.muted = isMuted;
1798
1847
  }, [isMuted]);
1799
- const showPosterOverlay = !isReady && !hasPlayedAhead;
1800
- const [showMuteIndicator, setShowMuteIndicator] = react.useState(false);
1801
- const muteIndicatorTimer = react.useRef(null);
1802
- const handleTap = react.useCallback(() => {
1803
- onToggleMute();
1804
- setShowMuteIndicator(true);
1805
- if (muteIndicatorTimer.current) clearTimeout(muteIndicatorTimer.current);
1806
- muteIndicatorTimer.current = setTimeout(() => setShowMuteIndicator(false), 1200);
1807
- }, [onToggleMute]);
1848
+ const [isActuallyPlaying, setIsActuallyPlaying] = react.useState(false);
1808
1849
  react.useEffect(() => {
1850
+ const video = videoRef.current;
1851
+ if (!video) return;
1852
+ const onPlaying = () => setIsActuallyPlaying(true);
1853
+ const onPause = () => setIsActuallyPlaying(false);
1854
+ const onEnded = () => setIsActuallyPlaying(false);
1855
+ video.addEventListener("playing", onPlaying);
1856
+ video.addEventListener("pause", onPause);
1857
+ video.addEventListener("ended", onEnded);
1809
1858
  return () => {
1810
- if (muteIndicatorTimer.current) clearTimeout(muteIndicatorTimer.current);
1859
+ video.removeEventListener("playing", onPlaying);
1860
+ video.removeEventListener("pause", onPause);
1861
+ video.removeEventListener("ended", onEnded);
1811
1862
  };
1812
1863
  }, []);
1864
+ react.useEffect(() => {
1865
+ if (!isActive) setIsActuallyPlaying(false);
1866
+ }, [isActive]);
1867
+ const showPosterOverlay = isActive ? !isReady && !isActuallyPlaying : canPlayAhead ? !hasPlayedAhead : !isReady;
1868
+ const [isPaused, setIsPaused] = react.useState(false);
1869
+ const handleTap = react.useCallback(() => {
1870
+ const video = videoRef.current;
1871
+ if (!video || !isActive) return;
1872
+ if (video.paused) {
1873
+ video.play().catch(() => {
1874
+ });
1875
+ setIsPaused(false);
1876
+ } else {
1877
+ video.pause();
1878
+ setIsPaused(true);
1879
+ }
1880
+ }, [isActive]);
1881
+ react.useEffect(() => {
1882
+ if (isActive) setIsPaused(false);
1883
+ }, [isActive]);
1813
1884
  const likeDelta = react.useSyncExternalStore(
1814
1885
  optimisticManager.store.subscribe,
1815
1886
  () => optimisticManager.getLikeDelta(item.id),
@@ -1828,9 +1899,11 @@ function VideoSlotInner({
1828
1899
  share: () => adapters.interaction?.share?.(item.id),
1829
1900
  isMuted,
1830
1901
  toggleMute: onToggleMute,
1902
+ isPaused,
1903
+ togglePause: handleTap,
1831
1904
  isActive,
1832
1905
  index
1833
- }), [item, likeDelta, followState, isMuted, isActive, index, optimisticManager, adapters, onToggleMute]);
1906
+ }), [item, likeDelta, followState, isMuted, isPaused, isActive, index, optimisticManager, adapters, onToggleMute, handleTap]);
1834
1907
  return /* @__PURE__ */ jsxRuntime.jsxs(
1835
1908
  "div",
1836
1909
  {
@@ -1877,7 +1950,7 @@ function VideoSlotInner({
1877
1950
  }
1878
1951
  }
1879
1952
  ),
1880
- showMuteIndicator && /* @__PURE__ */ jsxRuntime.jsx(
1953
+ isPaused && /* @__PURE__ */ jsxRuntime.jsx(
1881
1954
  "div",
1882
1955
  {
1883
1956
  style: {
@@ -1885,18 +1958,26 @@ function VideoSlotInner({
1885
1958
  top: "50%",
1886
1959
  left: "50%",
1887
1960
  transform: "translate(-50%, -50%)",
1888
- background: "rgba(0,0,0,0.6)",
1889
- borderRadius: "50%",
1890
- width: 64,
1891
- height: 64,
1892
- display: "flex",
1893
- alignItems: "center",
1894
- justifyContent: "center",
1895
- fontSize: 28,
1896
1961
  pointerEvents: "none",
1897
- animation: "fadeInOut 1.2s ease forwards"
1962
+ zIndex: 5,
1963
+ opacity: 0.3
1898
1964
  },
1899
- children: isMuted ? "\u{1F507}" : "\u{1F50A}"
1965
+ children: renderPauseIndicator ? renderPauseIndicator(isPaused) : /* @__PURE__ */ jsxRuntime.jsx(
1966
+ "div",
1967
+ {
1968
+ style: {
1969
+ background: "rgba(0,0,0,0.6)",
1970
+ borderRadius: "50%",
1971
+ width: 64,
1972
+ height: 64,
1973
+ display: "flex",
1974
+ alignItems: "center",
1975
+ justifyContent: "center",
1976
+ fontSize: 28
1977
+ },
1978
+ children: "\u25B6\uFE0F"
1979
+ }
1980
+ )
1900
1981
  }
1901
1982
  ),
1902
1983
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -1909,7 +1990,8 @@ function VideoSlotInner({
1909
1990
  right: 80,
1910
1991
  paddingBottom: 16,
1911
1992
  pointerEvents: "none",
1912
- color: "#fff"
1993
+ color: "#fff",
1994
+ zIndex: 10
1913
1995
  },
1914
1996
  children: renderOverlay ? renderOverlay(item, actions) : /* @__PURE__ */ jsxRuntime.jsx(DefaultOverlay, { item })
1915
1997
  }
@@ -1917,6 +1999,7 @@ function VideoSlotInner({
1917
1999
  /* @__PURE__ */ jsxRuntime.jsx(
1918
2000
  "div",
1919
2001
  {
2002
+ onClick: (e) => e.stopPropagation(),
1920
2003
  style: {
1921
2004
  position: "absolute",
1922
2005
  bottom: 0,
@@ -1925,7 +2008,9 @@ function VideoSlotInner({
1925
2008
  display: "flex",
1926
2009
  flexDirection: "column",
1927
2010
  gap: 20,
1928
- alignItems: "center"
2011
+ alignItems: "center",
2012
+ pointerEvents: "auto",
2013
+ zIndex: 10
1929
2014
  },
1930
2015
  children: renderActions ? renderActions(item, actions) : /* @__PURE__ */ jsxRuntime.jsx(DefaultActions, { item, actions })
1931
2016
  }
@@ -1987,6 +2072,7 @@ var centerStyle = {
1987
2072
  function ReelsFeed({
1988
2073
  renderOverlay,
1989
2074
  renderActions,
2075
+ renderPauseIndicator,
1990
2076
  renderLoading,
1991
2077
  renderEmpty,
1992
2078
  renderError: _renderError,
@@ -2006,7 +2092,7 @@ function ReelsFeed({
2006
2092
  isWarmAllocated,
2007
2093
  setPrefetchIndex
2008
2094
  } = useResource();
2009
- const [isMuted, setIsMuted] = react.useState(true);
2095
+ const [isMuted, setIsMuted] = react.useState(false);
2010
2096
  const containerRef = react.useRef(null);
2011
2097
  const slotCacheRef = react.useRef(/* @__PURE__ */ new Map());
2012
2098
  const activeIndexRef = react.useRef(0);
@@ -2220,7 +2306,8 @@ function ReelsFeed({
2220
2306
  onToggleMute: handleToggleMute,
2221
2307
  showFps: showFps && isActive,
2222
2308
  renderOverlay,
2223
- renderActions
2309
+ renderActions,
2310
+ renderPauseIndicator
2224
2311
  }
2225
2312
  )
2226
2313
  },
package/dist/index.d.cts CHANGED
@@ -302,6 +302,10 @@ interface SlotActions {
302
302
  isMuted: boolean;
303
303
  /** Toggle global mute */
304
304
  toggleMute: () => void;
305
+ /** Whether the active video is currently paused */
306
+ isPaused: boolean;
307
+ /** Toggle pause/play on the active video */
308
+ togglePause: () => void;
305
309
  /** Whether this slot is the active playing slot */
306
310
  isActive: boolean;
307
311
  /** Slot index in feed */
@@ -323,6 +327,13 @@ interface ReelsFeedProps {
323
327
  * If not provided, SDK uses DefaultActions showing like/comment/share emojis.
324
328
  */
325
329
  renderActions?: (item: ContentItem, actions: SlotActions) => ReactNode;
330
+ /**
331
+ * Custom pause/play indicator rendered when user taps to pause/play.
332
+ * Receives isPaused state. Return ReactNode.
333
+ * Positioned absolute center by SDK.
334
+ * If not provided, SDK uses a default ▶️/⏸️ emoji indicator.
335
+ */
336
+ renderPauseIndicator?: (isPaused: boolean) => ReactNode;
326
337
  /**
327
338
  * Custom loading skeleton shown during initial feed load.
328
339
  * If not provided, SDK uses DefaultSkeleton with shimmer animation.
@@ -713,7 +724,7 @@ interface ReelsProviderProps {
713
724
  declare function ReelsProvider({ children, adapters, debug }: ReelsProviderProps): react_jsx_runtime.JSX.Element;
714
725
  declare function useSDK(): SDKContextValue;
715
726
 
716
- declare function ReelsFeed({ renderOverlay, renderActions, renderLoading, renderEmpty, renderError: _renderError, showFps, loadMoreThreshold, onSlotChange, gestureConfig, snapConfig, }: ReelsFeedProps): string | number | bigint | boolean | Iterable<react.ReactNode> | Promise<string | number | bigint | boolean | react.ReactPortal | react.ReactElement<unknown, string | react.JSXElementConstructor<any>> | Iterable<react.ReactNode> | null | undefined> | react_jsx_runtime.JSX.Element | null | undefined;
727
+ declare function ReelsFeed({ renderOverlay, renderActions, renderPauseIndicator, renderLoading, renderEmpty, renderError: _renderError, showFps, loadMoreThreshold, onSlotChange, gestureConfig, snapConfig, }: ReelsFeedProps): string | number | bigint | boolean | Iterable<react.ReactNode> | Promise<string | number | bigint | boolean | react.ReactPortal | react.ReactElement<unknown, string | react.JSXElementConstructor<any>> | Iterable<react.ReactNode> | null | undefined> | react_jsx_runtime.JSX.Element | null | undefined;
717
728
 
718
729
  /**
719
730
  * useHls — React hook for hls.js lifecycle management (3-Tier buffer support)
@@ -764,6 +775,12 @@ interface UseHlsOptions {
764
775
  interface UseHlsReturn {
765
776
  /** Whether hls.js is being used (false = native HLS on Safari) */
766
777
  isHlsJs: boolean;
778
+ /**
779
+ * Whether the device uses native HLS (Safari / iOS WebView).
780
+ * When true, play-ahead (video.play() on non-active slots) must be skipped —
781
+ * iOS only allows one concurrently-playing video element at a time.
782
+ */
783
+ isNativeHls: boolean;
767
784
  /** Whether the video has buffered enough data to play without black flash */
768
785
  isReady: boolean;
769
786
  /** Destroy the HLS instance manually (also called automatically on unmount) */
@@ -783,8 +800,9 @@ interface VideoSlotProps {
783
800
  showFps?: boolean;
784
801
  renderOverlay?: (item: ContentItem, actions: SlotActions) => ReactNode;
785
802
  renderActions?: (item: ContentItem, actions: SlotActions) => ReactNode;
803
+ renderPauseIndicator?: (isPaused: boolean) => ReactNode;
786
804
  }
787
- declare function VideoSlot({ item, index, isActive, isPrefetch, isPreloaded, bufferTier, isMuted, onToggleMute, showFps, renderOverlay, renderActions, }: VideoSlotProps): react_jsx_runtime.JSX.Element;
805
+ declare function VideoSlot({ item, index, isActive, isPrefetch, isPreloaded, bufferTier, isMuted, onToggleMute, showFps, renderOverlay, renderActions, renderPauseIndicator, }: VideoSlotProps): react_jsx_runtime.JSX.Element;
788
806
 
789
807
  declare function DefaultOverlay({ item }: {
790
808
  item: ContentItem;
package/dist/index.d.ts CHANGED
@@ -302,6 +302,10 @@ interface SlotActions {
302
302
  isMuted: boolean;
303
303
  /** Toggle global mute */
304
304
  toggleMute: () => void;
305
+ /** Whether the active video is currently paused */
306
+ isPaused: boolean;
307
+ /** Toggle pause/play on the active video */
308
+ togglePause: () => void;
305
309
  /** Whether this slot is the active playing slot */
306
310
  isActive: boolean;
307
311
  /** Slot index in feed */
@@ -323,6 +327,13 @@ interface ReelsFeedProps {
323
327
  * If not provided, SDK uses DefaultActions showing like/comment/share emojis.
324
328
  */
325
329
  renderActions?: (item: ContentItem, actions: SlotActions) => ReactNode;
330
+ /**
331
+ * Custom pause/play indicator rendered when user taps to pause/play.
332
+ * Receives isPaused state. Return ReactNode.
333
+ * Positioned absolute center by SDK.
334
+ * If not provided, SDK uses a default ▶️/⏸️ emoji indicator.
335
+ */
336
+ renderPauseIndicator?: (isPaused: boolean) => ReactNode;
326
337
  /**
327
338
  * Custom loading skeleton shown during initial feed load.
328
339
  * If not provided, SDK uses DefaultSkeleton with shimmer animation.
@@ -713,7 +724,7 @@ interface ReelsProviderProps {
713
724
  declare function ReelsProvider({ children, adapters, debug }: ReelsProviderProps): react_jsx_runtime.JSX.Element;
714
725
  declare function useSDK(): SDKContextValue;
715
726
 
716
- declare function ReelsFeed({ renderOverlay, renderActions, renderLoading, renderEmpty, renderError: _renderError, showFps, loadMoreThreshold, onSlotChange, gestureConfig, snapConfig, }: ReelsFeedProps): string | number | bigint | boolean | Iterable<react.ReactNode> | Promise<string | number | bigint | boolean | react.ReactPortal | react.ReactElement<unknown, string | react.JSXElementConstructor<any>> | Iterable<react.ReactNode> | null | undefined> | react_jsx_runtime.JSX.Element | null | undefined;
727
+ declare function ReelsFeed({ renderOverlay, renderActions, renderPauseIndicator, renderLoading, renderEmpty, renderError: _renderError, showFps, loadMoreThreshold, onSlotChange, gestureConfig, snapConfig, }: ReelsFeedProps): string | number | bigint | boolean | Iterable<react.ReactNode> | Promise<string | number | bigint | boolean | react.ReactPortal | react.ReactElement<unknown, string | react.JSXElementConstructor<any>> | Iterable<react.ReactNode> | null | undefined> | react_jsx_runtime.JSX.Element | null | undefined;
717
728
 
718
729
  /**
719
730
  * useHls — React hook for hls.js lifecycle management (3-Tier buffer support)
@@ -764,6 +775,12 @@ interface UseHlsOptions {
764
775
  interface UseHlsReturn {
765
776
  /** Whether hls.js is being used (false = native HLS on Safari) */
766
777
  isHlsJs: boolean;
778
+ /**
779
+ * Whether the device uses native HLS (Safari / iOS WebView).
780
+ * When true, play-ahead (video.play() on non-active slots) must be skipped —
781
+ * iOS only allows one concurrently-playing video element at a time.
782
+ */
783
+ isNativeHls: boolean;
767
784
  /** Whether the video has buffered enough data to play without black flash */
768
785
  isReady: boolean;
769
786
  /** Destroy the HLS instance manually (also called automatically on unmount) */
@@ -783,8 +800,9 @@ interface VideoSlotProps {
783
800
  showFps?: boolean;
784
801
  renderOverlay?: (item: ContentItem, actions: SlotActions) => ReactNode;
785
802
  renderActions?: (item: ContentItem, actions: SlotActions) => ReactNode;
803
+ renderPauseIndicator?: (isPaused: boolean) => ReactNode;
786
804
  }
787
- declare function VideoSlot({ item, index, isActive, isPrefetch, isPreloaded, bufferTier, isMuted, onToggleMute, showFps, renderOverlay, renderActions, }: VideoSlotProps): react_jsx_runtime.JSX.Element;
805
+ declare function VideoSlot({ item, index, isActive, isPrefetch, isPreloaded, bufferTier, isMuted, onToggleMute, showFps, renderOverlay, renderActions, renderPauseIndicator, }: VideoSlotProps): react_jsx_runtime.JSX.Element;
788
806
 
789
807
  declare function DefaultOverlay({ item }: {
790
808
  item: ContentItem;
package/dist/index.js CHANGED
@@ -1386,6 +1386,15 @@ function useHls(options) {
1386
1386
  return;
1387
1387
  }
1388
1388
  if (!isActive && !isPrefetch) {
1389
+ if (isNative) {
1390
+ if (video.src) {
1391
+ video.removeAttribute("src");
1392
+ video.load();
1393
+ }
1394
+ setIsReady(false);
1395
+ currentSrcRef.current = void 0;
1396
+ return;
1397
+ }
1389
1398
  destroy();
1390
1399
  setIsReady(false);
1391
1400
  canPlayFiredRef.current = false;
@@ -1393,20 +1402,36 @@ function useHls(options) {
1393
1402
  return;
1394
1403
  }
1395
1404
  if (isNative) {
1396
- if (video.src !== src) {
1397
- video.src = src;
1405
+ if (currentSrcRef.current === src) {
1406
+ if (video.readyState >= HTMLMediaElement.HAVE_FUTURE_DATA) {
1407
+ setIsReady(true);
1408
+ return void 0;
1409
+ }
1410
+ const handleCanPlayReuse = () => setIsReady(true);
1411
+ video.addEventListener("canplay", handleCanPlayReuse, { once: true });
1412
+ video.addEventListener("loadeddata", handleCanPlayReuse, { once: true });
1413
+ video.addEventListener("playing", handleCanPlayReuse, { once: true });
1414
+ return () => {
1415
+ video.removeEventListener("canplay", handleCanPlayReuse);
1416
+ video.removeEventListener("loadeddata", handleCanPlayReuse);
1417
+ video.removeEventListener("playing", handleCanPlayReuse);
1418
+ };
1398
1419
  }
1420
+ video.src = src;
1421
+ currentSrcRef.current = src;
1399
1422
  if (video.readyState >= HTMLMediaElement.HAVE_FUTURE_DATA) {
1400
1423
  setIsReady(true);
1401
- currentSrcRef.current = src;
1402
- return;
1424
+ return void 0;
1403
1425
  }
1404
1426
  setIsReady(false);
1405
- currentSrcRef.current = src;
1406
1427
  const handleCanPlay2 = () => setIsReady(true);
1407
1428
  video.addEventListener("canplay", handleCanPlay2, { once: true });
1429
+ video.addEventListener("loadeddata", handleCanPlay2, { once: true });
1430
+ video.addEventListener("playing", handleCanPlay2, { once: true });
1408
1431
  return () => {
1409
1432
  video.removeEventListener("canplay", handleCanPlay2);
1433
+ video.removeEventListener("loadeddata", handleCanPlay2);
1434
+ video.removeEventListener("playing", handleCanPlay2);
1410
1435
  };
1411
1436
  }
1412
1437
  if (!isHlsSupported) {
@@ -1493,6 +1518,7 @@ function useHls(options) {
1493
1518
  }, [bufferTier]);
1494
1519
  return {
1495
1520
  isHlsJs,
1521
+ isNativeHls: isNative,
1496
1522
  isReady,
1497
1523
  destroy
1498
1524
  };
@@ -1622,7 +1648,8 @@ function VideoSlot({
1622
1648
  onToggleMute,
1623
1649
  showFps = false,
1624
1650
  renderOverlay,
1625
- renderActions
1651
+ renderActions,
1652
+ renderPauseIndicator
1626
1653
  }) {
1627
1654
  const { optimisticManager, adapters } = useSDK();
1628
1655
  if (!isVideoItem(item)) {
@@ -1658,6 +1685,7 @@ function VideoSlot({
1658
1685
  showFps,
1659
1686
  renderOverlay,
1660
1687
  renderActions,
1688
+ renderPauseIndicator,
1661
1689
  optimisticManager,
1662
1690
  adapters
1663
1691
  }
@@ -1675,6 +1703,7 @@ function VideoSlotInner({
1675
1703
  showFps,
1676
1704
  renderOverlay,
1677
1705
  renderActions,
1706
+ renderPauseIndicator,
1678
1707
  optimisticManager,
1679
1708
  adapters
1680
1709
  }) {
@@ -1685,7 +1714,7 @@ function VideoSlotInner({
1685
1714
  const isHlsSource = sourceType === "hls";
1686
1715
  const hlsSrc = isHlsSource && shouldLoadSrc ? src : void 0;
1687
1716
  const mp4Src = !isHlsSource && shouldLoadSrc ? src : void 0;
1688
- const { isReady: hlsReady } = useHls({
1717
+ const { isReady: hlsReady, isNativeHls } = useHls({
1689
1718
  src: hlsSrc,
1690
1719
  videoRef,
1691
1720
  isActive,
@@ -1724,7 +1753,9 @@ function VideoSlotInner({
1724
1753
  }, [mp4Src, isActive, isPrefetch, isPreloaded, isHlsSource]);
1725
1754
  const isReady = isHlsSource ? hlsReady : mp4Ready;
1726
1755
  const [hasPlayedAhead, setHasPlayedAhead] = useState(false);
1756
+ const canPlayAhead = isHlsSource && !isNativeHls;
1727
1757
  useEffect(() => {
1758
+ if (!canPlayAhead) return;
1728
1759
  const video = videoRef.current;
1729
1760
  if (!video) return;
1730
1761
  if (isActive || !isReady) return;
@@ -1751,7 +1782,7 @@ function VideoSlotInner({
1751
1782
  return () => {
1752
1783
  cancelled = true;
1753
1784
  };
1754
- }, [isActive, isReady, hasPlayedAhead]);
1785
+ }, [canPlayAhead, isActive, isReady, hasPlayedAhead]);
1755
1786
  useEffect(() => {
1756
1787
  setHasPlayedAhead(false);
1757
1788
  }, [src]);
@@ -1762,16 +1793,30 @@ function VideoSlotInner({
1762
1793
  let onReady = null;
1763
1794
  if (isActive) {
1764
1795
  wasActiveRef.current = true;
1765
- video.muted = isMuted;
1766
- if (video.readyState >= HTMLMediaElement.HAVE_FUTURE_DATA) {
1767
- video.play().catch(() => {
1796
+ const startPlay = () => {
1797
+ if (onReady) {
1798
+ video.removeEventListener("canplay", onReady);
1799
+ video.removeEventListener("loadeddata", onReady);
1800
+ video.removeEventListener("playing", onReady);
1801
+ onReady = null;
1802
+ }
1803
+ video.muted = true;
1804
+ video.play().then(() => {
1805
+ video.muted = isMuted;
1806
+ }).catch(() => {
1807
+ video.muted = isMuted;
1768
1808
  });
1809
+ };
1810
+ if (video.readyState >= HTMLMediaElement.HAVE_FUTURE_DATA) {
1811
+ startPlay();
1769
1812
  } else {
1770
- onReady = () => {
1771
- video.play().catch(() => {
1772
- });
1773
- };
1813
+ onReady = startPlay;
1774
1814
  video.addEventListener("canplay", onReady, { once: true });
1815
+ video.addEventListener("loadeddata", onReady, { once: true });
1816
+ video.addEventListener("playing", onReady, { once: true });
1817
+ if (video.readyState === HTMLMediaElement.HAVE_NOTHING && isNativeHls && video.src) {
1818
+ video.load();
1819
+ }
1775
1820
  }
1776
1821
  } else if (wasActiveRef.current) {
1777
1822
  video.pause();
@@ -1782,28 +1827,54 @@ function VideoSlotInner({
1782
1827
  video.pause();
1783
1828
  }
1784
1829
  return () => {
1785
- if (onReady) video.removeEventListener("canplay", onReady);
1830
+ if (onReady) {
1831
+ video.removeEventListener("canplay", onReady);
1832
+ video.removeEventListener("loadeddata", onReady);
1833
+ video.removeEventListener("playing", onReady);
1834
+ }
1786
1835
  };
1787
- }, [isActive, isMuted, hasPlayedAhead]);
1836
+ }, [isActive, isMuted, hasPlayedAhead, isNativeHls]);
1788
1837
  useEffect(() => {
1789
1838
  const video = videoRef.current;
1790
1839
  if (!video) return;
1791
1840
  video.muted = isMuted;
1792
1841
  }, [isMuted]);
1793
- const showPosterOverlay = !isReady && !hasPlayedAhead;
1794
- const [showMuteIndicator, setShowMuteIndicator] = useState(false);
1795
- const muteIndicatorTimer = useRef(null);
1796
- const handleTap = useCallback(() => {
1797
- onToggleMute();
1798
- setShowMuteIndicator(true);
1799
- if (muteIndicatorTimer.current) clearTimeout(muteIndicatorTimer.current);
1800
- muteIndicatorTimer.current = setTimeout(() => setShowMuteIndicator(false), 1200);
1801
- }, [onToggleMute]);
1842
+ const [isActuallyPlaying, setIsActuallyPlaying] = useState(false);
1802
1843
  useEffect(() => {
1844
+ const video = videoRef.current;
1845
+ if (!video) return;
1846
+ const onPlaying = () => setIsActuallyPlaying(true);
1847
+ const onPause = () => setIsActuallyPlaying(false);
1848
+ const onEnded = () => setIsActuallyPlaying(false);
1849
+ video.addEventListener("playing", onPlaying);
1850
+ video.addEventListener("pause", onPause);
1851
+ video.addEventListener("ended", onEnded);
1803
1852
  return () => {
1804
- if (muteIndicatorTimer.current) clearTimeout(muteIndicatorTimer.current);
1853
+ video.removeEventListener("playing", onPlaying);
1854
+ video.removeEventListener("pause", onPause);
1855
+ video.removeEventListener("ended", onEnded);
1805
1856
  };
1806
1857
  }, []);
1858
+ useEffect(() => {
1859
+ if (!isActive) setIsActuallyPlaying(false);
1860
+ }, [isActive]);
1861
+ const showPosterOverlay = isActive ? !isReady && !isActuallyPlaying : canPlayAhead ? !hasPlayedAhead : !isReady;
1862
+ const [isPaused, setIsPaused] = useState(false);
1863
+ const handleTap = useCallback(() => {
1864
+ const video = videoRef.current;
1865
+ if (!video || !isActive) return;
1866
+ if (video.paused) {
1867
+ video.play().catch(() => {
1868
+ });
1869
+ setIsPaused(false);
1870
+ } else {
1871
+ video.pause();
1872
+ setIsPaused(true);
1873
+ }
1874
+ }, [isActive]);
1875
+ useEffect(() => {
1876
+ if (isActive) setIsPaused(false);
1877
+ }, [isActive]);
1807
1878
  const likeDelta = useSyncExternalStore(
1808
1879
  optimisticManager.store.subscribe,
1809
1880
  () => optimisticManager.getLikeDelta(item.id),
@@ -1822,9 +1893,11 @@ function VideoSlotInner({
1822
1893
  share: () => adapters.interaction?.share?.(item.id),
1823
1894
  isMuted,
1824
1895
  toggleMute: onToggleMute,
1896
+ isPaused,
1897
+ togglePause: handleTap,
1825
1898
  isActive,
1826
1899
  index
1827
- }), [item, likeDelta, followState, isMuted, isActive, index, optimisticManager, adapters, onToggleMute]);
1900
+ }), [item, likeDelta, followState, isMuted, isPaused, isActive, index, optimisticManager, adapters, onToggleMute, handleTap]);
1828
1901
  return /* @__PURE__ */ jsxs(
1829
1902
  "div",
1830
1903
  {
@@ -1871,7 +1944,7 @@ function VideoSlotInner({
1871
1944
  }
1872
1945
  }
1873
1946
  ),
1874
- showMuteIndicator && /* @__PURE__ */ jsx(
1947
+ isPaused && /* @__PURE__ */ jsx(
1875
1948
  "div",
1876
1949
  {
1877
1950
  style: {
@@ -1879,18 +1952,26 @@ function VideoSlotInner({
1879
1952
  top: "50%",
1880
1953
  left: "50%",
1881
1954
  transform: "translate(-50%, -50%)",
1882
- background: "rgba(0,0,0,0.6)",
1883
- borderRadius: "50%",
1884
- width: 64,
1885
- height: 64,
1886
- display: "flex",
1887
- alignItems: "center",
1888
- justifyContent: "center",
1889
- fontSize: 28,
1890
1955
  pointerEvents: "none",
1891
- animation: "fadeInOut 1.2s ease forwards"
1956
+ zIndex: 5,
1957
+ opacity: 0.3
1892
1958
  },
1893
- children: isMuted ? "\u{1F507}" : "\u{1F50A}"
1959
+ children: renderPauseIndicator ? renderPauseIndicator(isPaused) : /* @__PURE__ */ jsx(
1960
+ "div",
1961
+ {
1962
+ style: {
1963
+ background: "rgba(0,0,0,0.6)",
1964
+ borderRadius: "50%",
1965
+ width: 64,
1966
+ height: 64,
1967
+ display: "flex",
1968
+ alignItems: "center",
1969
+ justifyContent: "center",
1970
+ fontSize: 28
1971
+ },
1972
+ children: "\u25B6\uFE0F"
1973
+ }
1974
+ )
1894
1975
  }
1895
1976
  ),
1896
1977
  /* @__PURE__ */ jsx(
@@ -1903,7 +1984,8 @@ function VideoSlotInner({
1903
1984
  right: 80,
1904
1985
  paddingBottom: 16,
1905
1986
  pointerEvents: "none",
1906
- color: "#fff"
1987
+ color: "#fff",
1988
+ zIndex: 10
1907
1989
  },
1908
1990
  children: renderOverlay ? renderOverlay(item, actions) : /* @__PURE__ */ jsx(DefaultOverlay, { item })
1909
1991
  }
@@ -1911,6 +1993,7 @@ function VideoSlotInner({
1911
1993
  /* @__PURE__ */ jsx(
1912
1994
  "div",
1913
1995
  {
1996
+ onClick: (e) => e.stopPropagation(),
1914
1997
  style: {
1915
1998
  position: "absolute",
1916
1999
  bottom: 0,
@@ -1919,7 +2002,9 @@ function VideoSlotInner({
1919
2002
  display: "flex",
1920
2003
  flexDirection: "column",
1921
2004
  gap: 20,
1922
- alignItems: "center"
2005
+ alignItems: "center",
2006
+ pointerEvents: "auto",
2007
+ zIndex: 10
1923
2008
  },
1924
2009
  children: renderActions ? renderActions(item, actions) : /* @__PURE__ */ jsx(DefaultActions, { item, actions })
1925
2010
  }
@@ -1981,6 +2066,7 @@ var centerStyle = {
1981
2066
  function ReelsFeed({
1982
2067
  renderOverlay,
1983
2068
  renderActions,
2069
+ renderPauseIndicator,
1984
2070
  renderLoading,
1985
2071
  renderEmpty,
1986
2072
  renderError: _renderError,
@@ -2000,7 +2086,7 @@ function ReelsFeed({
2000
2086
  isWarmAllocated,
2001
2087
  setPrefetchIndex
2002
2088
  } = useResource();
2003
- const [isMuted, setIsMuted] = useState(true);
2089
+ const [isMuted, setIsMuted] = useState(false);
2004
2090
  const containerRef = useRef(null);
2005
2091
  const slotCacheRef = useRef(/* @__PURE__ */ new Map());
2006
2092
  const activeIndexRef = useRef(0);
@@ -2214,7 +2300,8 @@ function ReelsFeed({
2214
2300
  onToggleMute: handleToggleMute,
2215
2301
  showFps: showFps && isActive,
2216
2302
  renderOverlay,
2217
- renderActions
2303
+ renderActions,
2304
+ renderPauseIndicator
2218
2305
  }
2219
2306
  )
2220
2307
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xhub-reels/sdk",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "High-performance Short Video / Reels SDK for React — optimized for Flutter WebView",
5
5
  "license": "MIT",
6
6
  "type": "module",