@xhub-reels/sdk 0.1.9 → 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
  };
@@ -1694,7 +1720,7 @@ function VideoSlotInner({
1694
1720
  const isHlsSource = sourceType === "hls";
1695
1721
  const hlsSrc = isHlsSource && shouldLoadSrc ? src : void 0;
1696
1722
  const mp4Src = !isHlsSource && shouldLoadSrc ? src : void 0;
1697
- const { isReady: hlsReady } = useHls({
1723
+ const { isReady: hlsReady, isNativeHls } = useHls({
1698
1724
  src: hlsSrc,
1699
1725
  videoRef,
1700
1726
  isActive,
@@ -1733,7 +1759,9 @@ function VideoSlotInner({
1733
1759
  }, [mp4Src, isActive, isPrefetch, isPreloaded, isHlsSource]);
1734
1760
  const isReady = isHlsSource ? hlsReady : mp4Ready;
1735
1761
  const [hasPlayedAhead, setHasPlayedAhead] = react.useState(false);
1762
+ const canPlayAhead = isHlsSource && !isNativeHls;
1736
1763
  react.useEffect(() => {
1764
+ if (!canPlayAhead) return;
1737
1765
  const video = videoRef.current;
1738
1766
  if (!video) return;
1739
1767
  if (isActive || !isReady) return;
@@ -1760,7 +1788,7 @@ function VideoSlotInner({
1760
1788
  return () => {
1761
1789
  cancelled = true;
1762
1790
  };
1763
- }, [isActive, isReady, hasPlayedAhead]);
1791
+ }, [canPlayAhead, isActive, isReady, hasPlayedAhead]);
1764
1792
  react.useEffect(() => {
1765
1793
  setHasPlayedAhead(false);
1766
1794
  }, [src]);
@@ -1771,16 +1799,30 @@ function VideoSlotInner({
1771
1799
  let onReady = null;
1772
1800
  if (isActive) {
1773
1801
  wasActiveRef.current = true;
1774
- video.muted = isMuted;
1775
- if (video.readyState >= HTMLMediaElement.HAVE_FUTURE_DATA) {
1776
- 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;
1777
1814
  });
1815
+ };
1816
+ if (video.readyState >= HTMLMediaElement.HAVE_FUTURE_DATA) {
1817
+ startPlay();
1778
1818
  } else {
1779
- onReady = () => {
1780
- video.play().catch(() => {
1781
- });
1782
- };
1819
+ onReady = startPlay;
1783
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
+ }
1784
1826
  }
1785
1827
  } else if (wasActiveRef.current) {
1786
1828
  video.pause();
@@ -1791,15 +1833,38 @@ function VideoSlotInner({
1791
1833
  video.pause();
1792
1834
  }
1793
1835
  return () => {
1794
- 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
+ }
1795
1841
  };
1796
- }, [isActive, isMuted, hasPlayedAhead]);
1842
+ }, [isActive, isMuted, hasPlayedAhead, isNativeHls]);
1797
1843
  react.useEffect(() => {
1798
1844
  const video = videoRef.current;
1799
1845
  if (!video) return;
1800
1846
  video.muted = isMuted;
1801
1847
  }, [isMuted]);
1802
- const showPosterOverlay = !isReady && !hasPlayedAhead;
1848
+ const [isActuallyPlaying, setIsActuallyPlaying] = react.useState(false);
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);
1858
+ return () => {
1859
+ video.removeEventListener("playing", onPlaying);
1860
+ video.removeEventListener("pause", onPause);
1861
+ video.removeEventListener("ended", onEnded);
1862
+ };
1863
+ }, []);
1864
+ react.useEffect(() => {
1865
+ if (!isActive) setIsActuallyPlaying(false);
1866
+ }, [isActive]);
1867
+ const showPosterOverlay = isActive ? !isReady && !isActuallyPlaying : canPlayAhead ? !hasPlayedAhead : !isReady;
1803
1868
  const [isPaused, setIsPaused] = react.useState(false);
1804
1869
  const handleTap = react.useCallback(() => {
1805
1870
  const video = videoRef.current;
package/dist/index.d.cts CHANGED
@@ -775,6 +775,12 @@ interface UseHlsOptions {
775
775
  interface UseHlsReturn {
776
776
  /** Whether hls.js is being used (false = native HLS on Safari) */
777
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;
778
784
  /** Whether the video has buffered enough data to play without black flash */
779
785
  isReady: boolean;
780
786
  /** Destroy the HLS instance manually (also called automatically on unmount) */
package/dist/index.d.ts CHANGED
@@ -775,6 +775,12 @@ interface UseHlsOptions {
775
775
  interface UseHlsReturn {
776
776
  /** Whether hls.js is being used (false = native HLS on Safari) */
777
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;
778
784
  /** Whether the video has buffered enough data to play without black flash */
779
785
  isReady: boolean;
780
786
  /** Destroy the HLS instance manually (also called automatically on unmount) */
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
  };
@@ -1688,7 +1714,7 @@ function VideoSlotInner({
1688
1714
  const isHlsSource = sourceType === "hls";
1689
1715
  const hlsSrc = isHlsSource && shouldLoadSrc ? src : void 0;
1690
1716
  const mp4Src = !isHlsSource && shouldLoadSrc ? src : void 0;
1691
- const { isReady: hlsReady } = useHls({
1717
+ const { isReady: hlsReady, isNativeHls } = useHls({
1692
1718
  src: hlsSrc,
1693
1719
  videoRef,
1694
1720
  isActive,
@@ -1727,7 +1753,9 @@ function VideoSlotInner({
1727
1753
  }, [mp4Src, isActive, isPrefetch, isPreloaded, isHlsSource]);
1728
1754
  const isReady = isHlsSource ? hlsReady : mp4Ready;
1729
1755
  const [hasPlayedAhead, setHasPlayedAhead] = useState(false);
1756
+ const canPlayAhead = isHlsSource && !isNativeHls;
1730
1757
  useEffect(() => {
1758
+ if (!canPlayAhead) return;
1731
1759
  const video = videoRef.current;
1732
1760
  if (!video) return;
1733
1761
  if (isActive || !isReady) return;
@@ -1754,7 +1782,7 @@ function VideoSlotInner({
1754
1782
  return () => {
1755
1783
  cancelled = true;
1756
1784
  };
1757
- }, [isActive, isReady, hasPlayedAhead]);
1785
+ }, [canPlayAhead, isActive, isReady, hasPlayedAhead]);
1758
1786
  useEffect(() => {
1759
1787
  setHasPlayedAhead(false);
1760
1788
  }, [src]);
@@ -1765,16 +1793,30 @@ function VideoSlotInner({
1765
1793
  let onReady = null;
1766
1794
  if (isActive) {
1767
1795
  wasActiveRef.current = true;
1768
- video.muted = isMuted;
1769
- if (video.readyState >= HTMLMediaElement.HAVE_FUTURE_DATA) {
1770
- 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;
1771
1808
  });
1809
+ };
1810
+ if (video.readyState >= HTMLMediaElement.HAVE_FUTURE_DATA) {
1811
+ startPlay();
1772
1812
  } else {
1773
- onReady = () => {
1774
- video.play().catch(() => {
1775
- });
1776
- };
1813
+ onReady = startPlay;
1777
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
+ }
1778
1820
  }
1779
1821
  } else if (wasActiveRef.current) {
1780
1822
  video.pause();
@@ -1785,15 +1827,38 @@ function VideoSlotInner({
1785
1827
  video.pause();
1786
1828
  }
1787
1829
  return () => {
1788
- 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
+ }
1789
1835
  };
1790
- }, [isActive, isMuted, hasPlayedAhead]);
1836
+ }, [isActive, isMuted, hasPlayedAhead, isNativeHls]);
1791
1837
  useEffect(() => {
1792
1838
  const video = videoRef.current;
1793
1839
  if (!video) return;
1794
1840
  video.muted = isMuted;
1795
1841
  }, [isMuted]);
1796
- const showPosterOverlay = !isReady && !hasPlayedAhead;
1842
+ const [isActuallyPlaying, setIsActuallyPlaying] = useState(false);
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);
1852
+ return () => {
1853
+ video.removeEventListener("playing", onPlaying);
1854
+ video.removeEventListener("pause", onPause);
1855
+ video.removeEventListener("ended", onEnded);
1856
+ };
1857
+ }, []);
1858
+ useEffect(() => {
1859
+ if (!isActive) setIsActuallyPlaying(false);
1860
+ }, [isActive]);
1861
+ const showPosterOverlay = isActive ? !isReady && !isActuallyPlaying : canPlayAhead ? !hasPlayedAhead : !isReady;
1797
1862
  const [isPaused, setIsPaused] = useState(false);
1798
1863
  const handleTap = useCallback(() => {
1799
1864
  const video = videoRef.current;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xhub-reels/sdk",
3
- "version": "0.1.9",
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",