@xhub-reels/sdk 0.1.13 → 0.1.15

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
@@ -1420,7 +1420,7 @@ function useHls(options) {
1420
1420
  }
1421
1421
  if (isNative) {
1422
1422
  if (currentSrcRef.current === src) {
1423
- if (video.readyState >= HTMLMediaElement.HAVE_FUTURE_DATA) {
1423
+ if (video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) {
1424
1424
  setIsReady(true);
1425
1425
  return void 0;
1426
1426
  }
@@ -1428,7 +1428,7 @@ function useHls(options) {
1428
1428
  video.addEventListener("canplay", handleCanPlayReuse, { once: true });
1429
1429
  video.addEventListener("loadeddata", handleCanPlayReuse, { once: true });
1430
1430
  video.addEventListener("playing", handleCanPlayReuse, { once: true });
1431
- if (isActive && video.readyState < HTMLMediaElement.HAVE_FUTURE_DATA) {
1431
+ if (video.readyState < HTMLMediaElement.HAVE_CURRENT_DATA) {
1432
1432
  video.load();
1433
1433
  }
1434
1434
  return () => {
@@ -1439,7 +1439,10 @@ function useHls(options) {
1439
1439
  }
1440
1440
  video.src = src;
1441
1441
  currentSrcRef.current = src;
1442
- if (video.readyState >= HTMLMediaElement.HAVE_FUTURE_DATA) {
1442
+ if (!isActive) {
1443
+ video.load();
1444
+ }
1445
+ if (video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) {
1443
1446
  setIsReady(true);
1444
1447
  return void 0;
1445
1448
  }
@@ -1656,6 +1659,43 @@ function skeletonCircle(size) {
1656
1659
  background: "rgba(255,255,255,0.1)"
1657
1660
  };
1658
1661
  }
1662
+ function DefaultDoubleTap({ isDoubleTap }) {
1663
+ if (!isDoubleTap) return null;
1664
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1665
+ "div",
1666
+ {
1667
+ style: {
1668
+ position: "absolute",
1669
+ inset: 0,
1670
+ display: "flex",
1671
+ alignItems: "center",
1672
+ justifyContent: "center",
1673
+ pointerEvents: "none",
1674
+ zIndex: 20
1675
+ },
1676
+ children: [
1677
+ /* @__PURE__ */ jsxRuntime.jsx(
1678
+ "div",
1679
+ {
1680
+ style: {
1681
+ fontSize: 80,
1682
+ animation: "reels-sdk-doubleTapHeart 0.6s ease forwards"
1683
+ },
1684
+ children: "\u2764\uFE0F"
1685
+ }
1686
+ ),
1687
+ /* @__PURE__ */ jsxRuntime.jsx("style", { children: `
1688
+ @keyframes reels-sdk-doubleTapHeart {
1689
+ 0% { opacity: 0; transform: scale(0.5); }
1690
+ 30% { opacity: 1; transform: scale(1.2); }
1691
+ 60% { opacity: 1; transform: scale(1.0); }
1692
+ 100% { opacity: 0; transform: scale(1.1); }
1693
+ }
1694
+ ` })
1695
+ ]
1696
+ }
1697
+ );
1698
+ }
1659
1699
  function VideoSlot({
1660
1700
  item,
1661
1701
  index,
@@ -1668,7 +1708,8 @@ function VideoSlot({
1668
1708
  showFps = false,
1669
1709
  renderOverlay,
1670
1710
  renderActions,
1671
- renderPauseIndicator
1711
+ renderPauseIndicator,
1712
+ renderDoubleTap
1672
1713
  }) {
1673
1714
  const { optimisticManager, adapters } = useSDK();
1674
1715
  if (!isVideoItem(item)) {
@@ -1705,6 +1746,7 @@ function VideoSlot({
1705
1746
  renderOverlay,
1706
1747
  renderActions,
1707
1748
  renderPauseIndicator,
1749
+ renderDoubleTap,
1708
1750
  optimisticManager,
1709
1751
  adapters
1710
1752
  }
@@ -1723,6 +1765,7 @@ function VideoSlotInner({
1723
1765
  renderOverlay,
1724
1766
  renderActions,
1725
1767
  renderPauseIndicator,
1768
+ renderDoubleTap,
1726
1769
  optimisticManager,
1727
1770
  adapters
1728
1771
  }) {
@@ -1834,6 +1877,7 @@ function VideoSlotInner({
1834
1877
  if (!video) return;
1835
1878
  let onReady = null;
1836
1879
  let fallbackTimerId = null;
1880
+ let pollId = null;
1837
1881
  if (isActive) {
1838
1882
  wasActiveRef.current = true;
1839
1883
  const startPlay = () => {
@@ -1847,6 +1891,10 @@ function VideoSlotInner({
1847
1891
  clearTimeout(fallbackTimerId);
1848
1892
  fallbackTimerId = null;
1849
1893
  }
1894
+ if (pollId !== null) {
1895
+ clearInterval(pollId);
1896
+ pollId = null;
1897
+ }
1850
1898
  video.muted = true;
1851
1899
  video.play().then(() => {
1852
1900
  video.muted = isMuted;
@@ -1854,22 +1902,33 @@ function VideoSlotInner({
1854
1902
  video.muted = isMuted;
1855
1903
  });
1856
1904
  };
1857
- if (video.readyState >= HTMLMediaElement.HAVE_FUTURE_DATA) {
1905
+ if (video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) {
1858
1906
  startPlay();
1859
1907
  } else {
1860
1908
  onReady = startPlay;
1861
1909
  video.addEventListener("canplay", onReady, { once: true });
1862
1910
  video.addEventListener("loadeddata", onReady, { once: true });
1863
1911
  video.addEventListener("playing", onReady, { once: true });
1864
- if (isNativeHls && video.src && video.readyState < HTMLMediaElement.HAVE_FUTURE_DATA) {
1912
+ if (isNativeHls && video.src && video.readyState < HTMLMediaElement.HAVE_CURRENT_DATA) {
1865
1913
  video.load();
1866
1914
  }
1915
+ if (isNativeHls) {
1916
+ pollId = setInterval(() => {
1917
+ if (video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA && onReady) {
1918
+ if (pollId !== null) {
1919
+ clearInterval(pollId);
1920
+ pollId = null;
1921
+ }
1922
+ startPlay();
1923
+ }
1924
+ }, 100);
1925
+ }
1867
1926
  fallbackTimerId = window.setTimeout(() => {
1868
1927
  fallbackTimerId = null;
1869
1928
  if (onReady) {
1870
1929
  startPlay();
1871
1930
  }
1872
- }, 3e3);
1931
+ }, isNativeHls ? 800 : 3e3);
1873
1932
  }
1874
1933
  } else if (wasActiveRef.current) {
1875
1934
  video.pause();
@@ -1889,6 +1948,10 @@ function VideoSlotInner({
1889
1948
  clearTimeout(fallbackTimerId);
1890
1949
  fallbackTimerId = null;
1891
1950
  }
1951
+ if (pollId !== null) {
1952
+ clearInterval(pollId);
1953
+ pollId = null;
1954
+ }
1892
1955
  };
1893
1956
  }, [isActive, isMuted, hasPlayedAhead, isNativeHls]);
1894
1957
  react.useEffect(() => {
@@ -1921,18 +1984,44 @@ function VideoSlotInner({
1921
1984
  }, [isActive]);
1922
1985
  const showPosterOverlay = isActive ? !isReady && !isActuallyPlaying : canPlayAhead ? !hasPlayedAhead : !isReady;
1923
1986
  const [isPaused, setIsPaused] = react.useState(false);
1987
+ const [isDoubleTap, setIsDoubleTap] = react.useState(false);
1988
+ const lastTapTimeRef = react.useRef(0);
1989
+ const doubleTapTimerRef = react.useRef(null);
1924
1990
  const handleTap = react.useCallback(() => {
1925
- const video = videoRef.current;
1926
- if (!video || !isActive) return;
1927
- if (video.paused) {
1928
- video.play().catch(() => {
1929
- });
1930
- setIsPaused(false);
1991
+ if (!isActive) return;
1992
+ const now = Date.now();
1993
+ const delta = now - lastTapTimeRef.current;
1994
+ lastTapTimeRef.current = now;
1995
+ if (delta < 300) {
1996
+ if (doubleTapTimerRef.current !== null) {
1997
+ clearTimeout(doubleTapTimerRef.current);
1998
+ doubleTapTimerRef.current = null;
1999
+ }
2000
+ setIsDoubleTap(true);
2001
+ setTimeout(() => setIsDoubleTap(false), 600);
1931
2002
  } else {
1932
- video.pause();
1933
- setIsPaused(true);
2003
+ doubleTapTimerRef.current = setTimeout(() => {
2004
+ doubleTapTimerRef.current = null;
2005
+ const video = videoRef.current;
2006
+ if (!video) return;
2007
+ if (video.paused) {
2008
+ video.play().catch(() => {
2009
+ });
2010
+ setIsPaused(false);
2011
+ } else {
2012
+ video.pause();
2013
+ setIsPaused(true);
2014
+ }
2015
+ }, 300);
1934
2016
  }
1935
2017
  }, [isActive]);
2018
+ react.useEffect(() => {
2019
+ return () => {
2020
+ if (doubleTapTimerRef.current !== null) {
2021
+ clearTimeout(doubleTapTimerRef.current);
2022
+ }
2023
+ };
2024
+ }, []);
1936
2025
  react.useEffect(() => {
1937
2026
  if (isActive) setIsPaused(false);
1938
2027
  }, [isActive]);
@@ -2006,6 +2095,18 @@ function VideoSlotInner({
2006
2095
  }
2007
2096
  }
2008
2097
  ),
2098
+ (isDoubleTap || renderDoubleTap) && /* @__PURE__ */ jsxRuntime.jsx(
2099
+ "div",
2100
+ {
2101
+ style: {
2102
+ position: "absolute",
2103
+ inset: 0,
2104
+ pointerEvents: "none",
2105
+ zIndex: 20
2106
+ },
2107
+ children: renderDoubleTap ? renderDoubleTap(isDoubleTap) : /* @__PURE__ */ jsxRuntime.jsx(DefaultDoubleTap, { isDoubleTap })
2108
+ }
2109
+ ),
2009
2110
  isPaused && /* @__PURE__ */ jsxRuntime.jsx(
2010
2111
  "div",
2011
2112
  {
@@ -2129,6 +2230,7 @@ function ReelsFeed({
2129
2230
  renderOverlay,
2130
2231
  renderActions,
2131
2232
  renderPauseIndicator,
2233
+ renderDoubleTap,
2132
2234
  renderLoading,
2133
2235
  renderEmpty,
2134
2236
  renderError: _renderError,
@@ -2375,7 +2477,8 @@ function ReelsFeed({
2375
2477
  showFps: showFps && isActive,
2376
2478
  renderOverlay,
2377
2479
  renderActions,
2378
- renderPauseIndicator
2480
+ renderPauseIndicator,
2481
+ renderDoubleTap
2379
2482
  }
2380
2483
  ) : null
2381
2484
  },
@@ -3010,6 +3113,7 @@ exports.DEFAULT_FEED_CONFIG = DEFAULT_FEED_CONFIG;
3010
3113
  exports.DEFAULT_PLAYER_CONFIG = DEFAULT_PLAYER_CONFIG;
3011
3114
  exports.DEFAULT_RESOURCE_CONFIG = DEFAULT_RESOURCE_CONFIG;
3012
3115
  exports.DefaultActions = DefaultActions;
3116
+ exports.DefaultDoubleTap = DefaultDoubleTap;
3013
3117
  exports.DefaultOverlay = DefaultOverlay;
3014
3118
  exports.DefaultSkeleton = DefaultSkeleton;
3015
3119
  exports.FeedManager = FeedManager;
package/dist/index.d.cts CHANGED
@@ -334,6 +334,13 @@ interface ReelsFeedProps {
334
334
  * If not provided, SDK uses a default ▶️/⏸️ emoji indicator.
335
335
  */
336
336
  renderPauseIndicator?: (isPaused: boolean) => ReactNode;
337
+ /**
338
+ * Custom double-tap overlay rendered when user double-taps a video slot.
339
+ * Receives isDoubleTap boolean (true for the duration of the animation window).
340
+ * Return ReactNode — positioned absolute center (full inset) by SDK.
341
+ * If not provided, SDK uses DefaultDoubleTap (animated ❤️ heart).
342
+ */
343
+ renderDoubleTap?: (isDoubleTap: boolean) => ReactNode;
337
344
  /**
338
345
  * Custom loading skeleton shown during initial feed load.
339
346
  * If not provided, SDK uses DefaultSkeleton with shimmer animation.
@@ -724,7 +731,7 @@ interface ReelsProviderProps {
724
731
  declare function ReelsProvider({ children, adapters, debug }: ReelsProviderProps): react_jsx_runtime.JSX.Element;
725
732
  declare function useSDK(): SDKContextValue;
726
733
 
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;
734
+ declare function ReelsFeed({ renderOverlay, renderActions, renderPauseIndicator, renderDoubleTap, 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;
728
735
 
729
736
  /**
730
737
  * useHls — React hook for hls.js lifecycle management (3-Tier buffer support)
@@ -801,8 +808,9 @@ interface VideoSlotProps {
801
808
  renderOverlay?: (item: ContentItem, actions: SlotActions) => ReactNode;
802
809
  renderActions?: (item: ContentItem, actions: SlotActions) => ReactNode;
803
810
  renderPauseIndicator?: (isPaused: boolean) => ReactNode;
811
+ renderDoubleTap?: (isDoubleTap: boolean) => ReactNode;
804
812
  }
805
- declare function VideoSlot({ item, index, isActive, isPrefetch, isPreloaded, bufferTier, isMuted, onToggleMute, showFps, renderOverlay, renderActions, renderPauseIndicator, }: VideoSlotProps): react_jsx_runtime.JSX.Element;
813
+ declare function VideoSlot({ item, index, isActive, isPrefetch, isPreloaded, bufferTier, isMuted, onToggleMute, showFps, renderOverlay, renderActions, renderPauseIndicator, renderDoubleTap, }: VideoSlotProps): react_jsx_runtime.JSX.Element;
806
814
 
807
815
  declare function DefaultOverlay({ item }: {
808
816
  item: ContentItem;
@@ -820,6 +828,16 @@ declare function DefaultActions({ item, actions }: {
820
828
  */
821
829
  declare function DefaultSkeleton(): react_jsx_runtime.JSX.Element;
822
830
 
831
+ /**
832
+ * DefaultDoubleTap — Default double-tap heart animation overlay
833
+ *
834
+ * Shown when user double-taps a video slot and no renderDoubleTap is provided.
835
+ * Renders an animated ❤️ that fades in/scales up then fades out.
836
+ */
837
+ declare function DefaultDoubleTap({ isDoubleTap }: {
838
+ isDoubleTap: boolean;
839
+ }): react_jsx_runtime.JSX.Element | null;
840
+
823
841
  declare function useFeedSelector<T>(selector: (state: FeedState) => T): T;
824
842
  declare function useFeed(): {
825
843
  items: NonNullable<ContentItem | undefined>[];
@@ -1090,4 +1108,4 @@ declare class HttpError extends Error {
1090
1108
  constructor(status: number, message: string, body?: string | undefined);
1091
1109
  }
1092
1110
 
1093
- export { type Article, type ArticleImage, type Author, type BufferTier, type CommentItem, type CommentPage, type ContentItem, type ContentStats, DEFAULT_FEED_CONFIG, DEFAULT_PLAYER_CONFIG, DEFAULT_RESOURCE_CONFIG, DefaultActions, DefaultOverlay, DefaultSkeleton, type FeedConfig, type FeedError, FeedManager, type FeedPage, type FeedState, HttpDataSource, type HttpDataSourceConfig, HttpError, type IAnalytics, type ICommentAdapter, type IDataSource, type IInteraction, type ILogger, type INetworkAdapter, type ISessionStorage, type IVideoLoader, type InteractionState, type LogLevel, MockAnalytics, MockCommentAdapter, MockDataSource, MockInteraction, MockLogger, MockNetworkAdapter, MockSessionStorage, MockVideoLoader, type NetworkType, OptimisticManager, type PlayerConfig, PlayerEngine, type PlayerError, type PlayerEvent, type PlayerEventListener, type PlayerState, PlayerStatus, type PointerGestureConfig, type PreloadResult, type PreloadStatus, ReelsFeed, type ReelsFeedProps, ReelsProvider, type ReelsProviderProps, type ResourceConfig, ResourceGovernor, type ResourceState, type SDKAdapters, type SDKContextValue, type SlotActions, type SnapTarget, type UseHlsOptions, type UseHlsReturn, VALID_TRANSITIONS, type VideoItem, type VideoQuality, VideoSlot, type VideoSource, canPause, canPlay, canSeek, isArticle, isValidTransition, isVideoItem, useFeed, useFeedSelector, useHls, usePlayer, usePlayerSelector, usePointerGesture, useResource, useResourceSelector, useSDK, useSnapAnimation };
1111
+ export { type Article, type ArticleImage, type Author, type BufferTier, type CommentItem, type CommentPage, type ContentItem, type ContentStats, DEFAULT_FEED_CONFIG, DEFAULT_PLAYER_CONFIG, DEFAULT_RESOURCE_CONFIG, DefaultActions, DefaultDoubleTap, DefaultOverlay, DefaultSkeleton, type FeedConfig, type FeedError, FeedManager, type FeedPage, type FeedState, HttpDataSource, type HttpDataSourceConfig, HttpError, type IAnalytics, type ICommentAdapter, type IDataSource, type IInteraction, type ILogger, type INetworkAdapter, type ISessionStorage, type IVideoLoader, type InteractionState, type LogLevel, MockAnalytics, MockCommentAdapter, MockDataSource, MockInteraction, MockLogger, MockNetworkAdapter, MockSessionStorage, MockVideoLoader, type NetworkType, OptimisticManager, type PlayerConfig, PlayerEngine, type PlayerError, type PlayerEvent, type PlayerEventListener, type PlayerState, PlayerStatus, type PointerGestureConfig, type PreloadResult, type PreloadStatus, ReelsFeed, type ReelsFeedProps, ReelsProvider, type ReelsProviderProps, type ResourceConfig, ResourceGovernor, type ResourceState, type SDKAdapters, type SDKContextValue, type SlotActions, type SnapTarget, type UseHlsOptions, type UseHlsReturn, VALID_TRANSITIONS, type VideoItem, type VideoQuality, VideoSlot, type VideoSource, canPause, canPlay, canSeek, isArticle, isValidTransition, isVideoItem, useFeed, useFeedSelector, useHls, usePlayer, usePlayerSelector, usePointerGesture, useResource, useResourceSelector, useSDK, useSnapAnimation };
package/dist/index.d.ts CHANGED
@@ -334,6 +334,13 @@ interface ReelsFeedProps {
334
334
  * If not provided, SDK uses a default ▶️/⏸️ emoji indicator.
335
335
  */
336
336
  renderPauseIndicator?: (isPaused: boolean) => ReactNode;
337
+ /**
338
+ * Custom double-tap overlay rendered when user double-taps a video slot.
339
+ * Receives isDoubleTap boolean (true for the duration of the animation window).
340
+ * Return ReactNode — positioned absolute center (full inset) by SDK.
341
+ * If not provided, SDK uses DefaultDoubleTap (animated ❤️ heart).
342
+ */
343
+ renderDoubleTap?: (isDoubleTap: boolean) => ReactNode;
337
344
  /**
338
345
  * Custom loading skeleton shown during initial feed load.
339
346
  * If not provided, SDK uses DefaultSkeleton with shimmer animation.
@@ -724,7 +731,7 @@ interface ReelsProviderProps {
724
731
  declare function ReelsProvider({ children, adapters, debug }: ReelsProviderProps): react_jsx_runtime.JSX.Element;
725
732
  declare function useSDK(): SDKContextValue;
726
733
 
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;
734
+ declare function ReelsFeed({ renderOverlay, renderActions, renderPauseIndicator, renderDoubleTap, 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;
728
735
 
729
736
  /**
730
737
  * useHls — React hook for hls.js lifecycle management (3-Tier buffer support)
@@ -801,8 +808,9 @@ interface VideoSlotProps {
801
808
  renderOverlay?: (item: ContentItem, actions: SlotActions) => ReactNode;
802
809
  renderActions?: (item: ContentItem, actions: SlotActions) => ReactNode;
803
810
  renderPauseIndicator?: (isPaused: boolean) => ReactNode;
811
+ renderDoubleTap?: (isDoubleTap: boolean) => ReactNode;
804
812
  }
805
- declare function VideoSlot({ item, index, isActive, isPrefetch, isPreloaded, bufferTier, isMuted, onToggleMute, showFps, renderOverlay, renderActions, renderPauseIndicator, }: VideoSlotProps): react_jsx_runtime.JSX.Element;
813
+ declare function VideoSlot({ item, index, isActive, isPrefetch, isPreloaded, bufferTier, isMuted, onToggleMute, showFps, renderOverlay, renderActions, renderPauseIndicator, renderDoubleTap, }: VideoSlotProps): react_jsx_runtime.JSX.Element;
806
814
 
807
815
  declare function DefaultOverlay({ item }: {
808
816
  item: ContentItem;
@@ -820,6 +828,16 @@ declare function DefaultActions({ item, actions }: {
820
828
  */
821
829
  declare function DefaultSkeleton(): react_jsx_runtime.JSX.Element;
822
830
 
831
+ /**
832
+ * DefaultDoubleTap — Default double-tap heart animation overlay
833
+ *
834
+ * Shown when user double-taps a video slot and no renderDoubleTap is provided.
835
+ * Renders an animated ❤️ that fades in/scales up then fades out.
836
+ */
837
+ declare function DefaultDoubleTap({ isDoubleTap }: {
838
+ isDoubleTap: boolean;
839
+ }): react_jsx_runtime.JSX.Element | null;
840
+
823
841
  declare function useFeedSelector<T>(selector: (state: FeedState) => T): T;
824
842
  declare function useFeed(): {
825
843
  items: NonNullable<ContentItem | undefined>[];
@@ -1090,4 +1108,4 @@ declare class HttpError extends Error {
1090
1108
  constructor(status: number, message: string, body?: string | undefined);
1091
1109
  }
1092
1110
 
1093
- export { type Article, type ArticleImage, type Author, type BufferTier, type CommentItem, type CommentPage, type ContentItem, type ContentStats, DEFAULT_FEED_CONFIG, DEFAULT_PLAYER_CONFIG, DEFAULT_RESOURCE_CONFIG, DefaultActions, DefaultOverlay, DefaultSkeleton, type FeedConfig, type FeedError, FeedManager, type FeedPage, type FeedState, HttpDataSource, type HttpDataSourceConfig, HttpError, type IAnalytics, type ICommentAdapter, type IDataSource, type IInteraction, type ILogger, type INetworkAdapter, type ISessionStorage, type IVideoLoader, type InteractionState, type LogLevel, MockAnalytics, MockCommentAdapter, MockDataSource, MockInteraction, MockLogger, MockNetworkAdapter, MockSessionStorage, MockVideoLoader, type NetworkType, OptimisticManager, type PlayerConfig, PlayerEngine, type PlayerError, type PlayerEvent, type PlayerEventListener, type PlayerState, PlayerStatus, type PointerGestureConfig, type PreloadResult, type PreloadStatus, ReelsFeed, type ReelsFeedProps, ReelsProvider, type ReelsProviderProps, type ResourceConfig, ResourceGovernor, type ResourceState, type SDKAdapters, type SDKContextValue, type SlotActions, type SnapTarget, type UseHlsOptions, type UseHlsReturn, VALID_TRANSITIONS, type VideoItem, type VideoQuality, VideoSlot, type VideoSource, canPause, canPlay, canSeek, isArticle, isValidTransition, isVideoItem, useFeed, useFeedSelector, useHls, usePlayer, usePlayerSelector, usePointerGesture, useResource, useResourceSelector, useSDK, useSnapAnimation };
1111
+ export { type Article, type ArticleImage, type Author, type BufferTier, type CommentItem, type CommentPage, type ContentItem, type ContentStats, DEFAULT_FEED_CONFIG, DEFAULT_PLAYER_CONFIG, DEFAULT_RESOURCE_CONFIG, DefaultActions, DefaultDoubleTap, DefaultOverlay, DefaultSkeleton, type FeedConfig, type FeedError, FeedManager, type FeedPage, type FeedState, HttpDataSource, type HttpDataSourceConfig, HttpError, type IAnalytics, type ICommentAdapter, type IDataSource, type IInteraction, type ILogger, type INetworkAdapter, type ISessionStorage, type IVideoLoader, type InteractionState, type LogLevel, MockAnalytics, MockCommentAdapter, MockDataSource, MockInteraction, MockLogger, MockNetworkAdapter, MockSessionStorage, MockVideoLoader, type NetworkType, OptimisticManager, type PlayerConfig, PlayerEngine, type PlayerError, type PlayerEvent, type PlayerEventListener, type PlayerState, PlayerStatus, type PointerGestureConfig, type PreloadResult, type PreloadStatus, ReelsFeed, type ReelsFeedProps, ReelsProvider, type ReelsProviderProps, type ResourceConfig, ResourceGovernor, type ResourceState, type SDKAdapters, type SDKContextValue, type SlotActions, type SnapTarget, type UseHlsOptions, type UseHlsReturn, VALID_TRANSITIONS, type VideoItem, type VideoQuality, VideoSlot, type VideoSource, canPause, canPlay, canSeek, isArticle, isValidTransition, isVideoItem, useFeed, useFeedSelector, useHls, usePlayer, usePlayerSelector, usePointerGesture, useResource, useResourceSelector, useSDK, useSnapAnimation };
package/dist/index.js CHANGED
@@ -1414,7 +1414,7 @@ function useHls(options) {
1414
1414
  }
1415
1415
  if (isNative) {
1416
1416
  if (currentSrcRef.current === src) {
1417
- if (video.readyState >= HTMLMediaElement.HAVE_FUTURE_DATA) {
1417
+ if (video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) {
1418
1418
  setIsReady(true);
1419
1419
  return void 0;
1420
1420
  }
@@ -1422,7 +1422,7 @@ function useHls(options) {
1422
1422
  video.addEventListener("canplay", handleCanPlayReuse, { once: true });
1423
1423
  video.addEventListener("loadeddata", handleCanPlayReuse, { once: true });
1424
1424
  video.addEventListener("playing", handleCanPlayReuse, { once: true });
1425
- if (isActive && video.readyState < HTMLMediaElement.HAVE_FUTURE_DATA) {
1425
+ if (video.readyState < HTMLMediaElement.HAVE_CURRENT_DATA) {
1426
1426
  video.load();
1427
1427
  }
1428
1428
  return () => {
@@ -1433,7 +1433,10 @@ function useHls(options) {
1433
1433
  }
1434
1434
  video.src = src;
1435
1435
  currentSrcRef.current = src;
1436
- if (video.readyState >= HTMLMediaElement.HAVE_FUTURE_DATA) {
1436
+ if (!isActive) {
1437
+ video.load();
1438
+ }
1439
+ if (video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) {
1437
1440
  setIsReady(true);
1438
1441
  return void 0;
1439
1442
  }
@@ -1650,6 +1653,43 @@ function skeletonCircle(size) {
1650
1653
  background: "rgba(255,255,255,0.1)"
1651
1654
  };
1652
1655
  }
1656
+ function DefaultDoubleTap({ isDoubleTap }) {
1657
+ if (!isDoubleTap) return null;
1658
+ return /* @__PURE__ */ jsxs(
1659
+ "div",
1660
+ {
1661
+ style: {
1662
+ position: "absolute",
1663
+ inset: 0,
1664
+ display: "flex",
1665
+ alignItems: "center",
1666
+ justifyContent: "center",
1667
+ pointerEvents: "none",
1668
+ zIndex: 20
1669
+ },
1670
+ children: [
1671
+ /* @__PURE__ */ jsx(
1672
+ "div",
1673
+ {
1674
+ style: {
1675
+ fontSize: 80,
1676
+ animation: "reels-sdk-doubleTapHeart 0.6s ease forwards"
1677
+ },
1678
+ children: "\u2764\uFE0F"
1679
+ }
1680
+ ),
1681
+ /* @__PURE__ */ jsx("style", { children: `
1682
+ @keyframes reels-sdk-doubleTapHeart {
1683
+ 0% { opacity: 0; transform: scale(0.5); }
1684
+ 30% { opacity: 1; transform: scale(1.2); }
1685
+ 60% { opacity: 1; transform: scale(1.0); }
1686
+ 100% { opacity: 0; transform: scale(1.1); }
1687
+ }
1688
+ ` })
1689
+ ]
1690
+ }
1691
+ );
1692
+ }
1653
1693
  function VideoSlot({
1654
1694
  item,
1655
1695
  index,
@@ -1662,7 +1702,8 @@ function VideoSlot({
1662
1702
  showFps = false,
1663
1703
  renderOverlay,
1664
1704
  renderActions,
1665
- renderPauseIndicator
1705
+ renderPauseIndicator,
1706
+ renderDoubleTap
1666
1707
  }) {
1667
1708
  const { optimisticManager, adapters } = useSDK();
1668
1709
  if (!isVideoItem(item)) {
@@ -1699,6 +1740,7 @@ function VideoSlot({
1699
1740
  renderOverlay,
1700
1741
  renderActions,
1701
1742
  renderPauseIndicator,
1743
+ renderDoubleTap,
1702
1744
  optimisticManager,
1703
1745
  adapters
1704
1746
  }
@@ -1717,6 +1759,7 @@ function VideoSlotInner({
1717
1759
  renderOverlay,
1718
1760
  renderActions,
1719
1761
  renderPauseIndicator,
1762
+ renderDoubleTap,
1720
1763
  optimisticManager,
1721
1764
  adapters
1722
1765
  }) {
@@ -1828,6 +1871,7 @@ function VideoSlotInner({
1828
1871
  if (!video) return;
1829
1872
  let onReady = null;
1830
1873
  let fallbackTimerId = null;
1874
+ let pollId = null;
1831
1875
  if (isActive) {
1832
1876
  wasActiveRef.current = true;
1833
1877
  const startPlay = () => {
@@ -1841,6 +1885,10 @@ function VideoSlotInner({
1841
1885
  clearTimeout(fallbackTimerId);
1842
1886
  fallbackTimerId = null;
1843
1887
  }
1888
+ if (pollId !== null) {
1889
+ clearInterval(pollId);
1890
+ pollId = null;
1891
+ }
1844
1892
  video.muted = true;
1845
1893
  video.play().then(() => {
1846
1894
  video.muted = isMuted;
@@ -1848,22 +1896,33 @@ function VideoSlotInner({
1848
1896
  video.muted = isMuted;
1849
1897
  });
1850
1898
  };
1851
- if (video.readyState >= HTMLMediaElement.HAVE_FUTURE_DATA) {
1899
+ if (video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) {
1852
1900
  startPlay();
1853
1901
  } else {
1854
1902
  onReady = startPlay;
1855
1903
  video.addEventListener("canplay", onReady, { once: true });
1856
1904
  video.addEventListener("loadeddata", onReady, { once: true });
1857
1905
  video.addEventListener("playing", onReady, { once: true });
1858
- if (isNativeHls && video.src && video.readyState < HTMLMediaElement.HAVE_FUTURE_DATA) {
1906
+ if (isNativeHls && video.src && video.readyState < HTMLMediaElement.HAVE_CURRENT_DATA) {
1859
1907
  video.load();
1860
1908
  }
1909
+ if (isNativeHls) {
1910
+ pollId = setInterval(() => {
1911
+ if (video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA && onReady) {
1912
+ if (pollId !== null) {
1913
+ clearInterval(pollId);
1914
+ pollId = null;
1915
+ }
1916
+ startPlay();
1917
+ }
1918
+ }, 100);
1919
+ }
1861
1920
  fallbackTimerId = window.setTimeout(() => {
1862
1921
  fallbackTimerId = null;
1863
1922
  if (onReady) {
1864
1923
  startPlay();
1865
1924
  }
1866
- }, 3e3);
1925
+ }, isNativeHls ? 800 : 3e3);
1867
1926
  }
1868
1927
  } else if (wasActiveRef.current) {
1869
1928
  video.pause();
@@ -1883,6 +1942,10 @@ function VideoSlotInner({
1883
1942
  clearTimeout(fallbackTimerId);
1884
1943
  fallbackTimerId = null;
1885
1944
  }
1945
+ if (pollId !== null) {
1946
+ clearInterval(pollId);
1947
+ pollId = null;
1948
+ }
1886
1949
  };
1887
1950
  }, [isActive, isMuted, hasPlayedAhead, isNativeHls]);
1888
1951
  useEffect(() => {
@@ -1915,18 +1978,44 @@ function VideoSlotInner({
1915
1978
  }, [isActive]);
1916
1979
  const showPosterOverlay = isActive ? !isReady && !isActuallyPlaying : canPlayAhead ? !hasPlayedAhead : !isReady;
1917
1980
  const [isPaused, setIsPaused] = useState(false);
1981
+ const [isDoubleTap, setIsDoubleTap] = useState(false);
1982
+ const lastTapTimeRef = useRef(0);
1983
+ const doubleTapTimerRef = useRef(null);
1918
1984
  const handleTap = useCallback(() => {
1919
- const video = videoRef.current;
1920
- if (!video || !isActive) return;
1921
- if (video.paused) {
1922
- video.play().catch(() => {
1923
- });
1924
- setIsPaused(false);
1985
+ if (!isActive) return;
1986
+ const now = Date.now();
1987
+ const delta = now - lastTapTimeRef.current;
1988
+ lastTapTimeRef.current = now;
1989
+ if (delta < 300) {
1990
+ if (doubleTapTimerRef.current !== null) {
1991
+ clearTimeout(doubleTapTimerRef.current);
1992
+ doubleTapTimerRef.current = null;
1993
+ }
1994
+ setIsDoubleTap(true);
1995
+ setTimeout(() => setIsDoubleTap(false), 600);
1925
1996
  } else {
1926
- video.pause();
1927
- setIsPaused(true);
1997
+ doubleTapTimerRef.current = setTimeout(() => {
1998
+ doubleTapTimerRef.current = null;
1999
+ const video = videoRef.current;
2000
+ if (!video) return;
2001
+ if (video.paused) {
2002
+ video.play().catch(() => {
2003
+ });
2004
+ setIsPaused(false);
2005
+ } else {
2006
+ video.pause();
2007
+ setIsPaused(true);
2008
+ }
2009
+ }, 300);
1928
2010
  }
1929
2011
  }, [isActive]);
2012
+ useEffect(() => {
2013
+ return () => {
2014
+ if (doubleTapTimerRef.current !== null) {
2015
+ clearTimeout(doubleTapTimerRef.current);
2016
+ }
2017
+ };
2018
+ }, []);
1930
2019
  useEffect(() => {
1931
2020
  if (isActive) setIsPaused(false);
1932
2021
  }, [isActive]);
@@ -2000,6 +2089,18 @@ function VideoSlotInner({
2000
2089
  }
2001
2090
  }
2002
2091
  ),
2092
+ (isDoubleTap || renderDoubleTap) && /* @__PURE__ */ jsx(
2093
+ "div",
2094
+ {
2095
+ style: {
2096
+ position: "absolute",
2097
+ inset: 0,
2098
+ pointerEvents: "none",
2099
+ zIndex: 20
2100
+ },
2101
+ children: renderDoubleTap ? renderDoubleTap(isDoubleTap) : /* @__PURE__ */ jsx(DefaultDoubleTap, { isDoubleTap })
2102
+ }
2103
+ ),
2003
2104
  isPaused && /* @__PURE__ */ jsx(
2004
2105
  "div",
2005
2106
  {
@@ -2123,6 +2224,7 @@ function ReelsFeed({
2123
2224
  renderOverlay,
2124
2225
  renderActions,
2125
2226
  renderPauseIndicator,
2227
+ renderDoubleTap,
2126
2228
  renderLoading,
2127
2229
  renderEmpty,
2128
2230
  renderError: _renderError,
@@ -2369,7 +2471,8 @@ function ReelsFeed({
2369
2471
  showFps: showFps && isActive,
2370
2472
  renderOverlay,
2371
2473
  renderActions,
2372
- renderPauseIndicator
2474
+ renderPauseIndicator,
2475
+ renderDoubleTap
2373
2476
  }
2374
2477
  ) : null
2375
2478
  },
@@ -3000,4 +3103,4 @@ var HttpError = class extends Error {
3000
3103
  }
3001
3104
  };
3002
3105
 
3003
- 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 };
3106
+ export { DEFAULT_FEED_CONFIG, DEFAULT_PLAYER_CONFIG, DEFAULT_RESOURCE_CONFIG, DefaultActions, DefaultDoubleTap, 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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xhub-reels/sdk",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "description": "High-performance Short Video / Reels SDK for React — optimized for Flutter WebView",
5
5
  "license": "MIT",
6
6
  "type": "module",