@xhub-reels/sdk 0.2.1 → 0.2.5

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/README.md CHANGED
@@ -111,48 +111,6 @@ function SwipeableFeed() {
111
111
  }
112
112
  ```
113
113
 
114
- ## Thumbnail grid
115
-
116
- Headless pre-drawer browsing UX. Mount inside `<ReelsProvider>`. Host provides the
117
- card visuals via `renderThumbnail`; the SDK ships no default card.
118
-
119
- ```tsx
120
- import { ReelsFeedThumbnail } from 'xhub-reels-sdk';
121
-
122
- <ReelsFeedThumbnail
123
- renderThumbnail={(videoData) => (
124
- <article className="aspect-[3/2] rounded-lg overflow-hidden">
125
- <img src={videoData.poster} alt="" />
126
- <p>@{videoData.author.name}</p>
127
- </article>
128
- )}
129
- onThumbnailClick={(id) => openDrawer(id)}
130
- />
131
- ```
132
-
133
- Opt-in hover prefetch (warms HLS manifest via `adapters.videoLoader.preloadMetadata`):
134
-
135
- ```tsx
136
- <ReelsFeedThumbnail prefetchOnHover renderThumbnail={...} />
137
- ```
138
-
139
- On click the SDK calls `ResourceGovernor.setFocusedIndexImmediate(index)` before
140
- invoking `onThumbnailClick`, so opening `<ReelsFeed>` lands on the tapped item
141
- without a scroll-to-index flash. Disable with `setFocusOnClick={false}`.
142
-
143
- Custom slots for the zero-items branches:
144
-
145
- ```tsx
146
- <ReelsFeedThumbnail
147
- renderThumbnail={(item) => <Card item={item} />}
148
- renderLoading={() => <SkeletonGrid />}
149
- renderEmpty={() => <EmptyState />}
150
- renderError={({ message, retry }) => (
151
- <ErrorBox message={message} onRetry={retry} />
152
- )}
153
- />
154
- ```
155
-
156
114
  ## Architecture
157
115
 
158
116
  ```
package/dist/index.cjs CHANGED
@@ -1447,7 +1447,7 @@ function useHls(options) {
1447
1447
  setIsReady(false);
1448
1448
  canPlayFiredRef.current = false;
1449
1449
  currentSrcRef.current = src;
1450
- const handleCanPlay2 = () => {
1450
+ const handleCanPlay = () => {
1451
1451
  canPlayFiredRef.current = true;
1452
1452
  setIsReady(true);
1453
1453
  };
@@ -1457,10 +1457,10 @@ function useHls(options) {
1457
1457
  setIsReady(true);
1458
1458
  }
1459
1459
  };
1460
- video.addEventListener("canplay", handleCanPlay2, { once: true });
1460
+ video.addEventListener("canplay", handleCanPlay, { once: true });
1461
1461
  video.addEventListener("loadeddata", handleLoadedData, { once: true });
1462
1462
  return () => {
1463
- video.removeEventListener("canplay", handleCanPlay2);
1463
+ video.removeEventListener("canplay", handleCanPlay);
1464
1464
  video.removeEventListener("loadeddata", handleLoadedData);
1465
1465
  };
1466
1466
  }
@@ -1470,13 +1470,21 @@ function useHls(options) {
1470
1470
  }
1471
1471
  if (hlsRef.current && currentSrcRef.current === src) {
1472
1472
  if (!canPlayFiredRef.current) {
1473
- const handleCanPlay2 = () => {
1473
+ if (video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) {
1474
+ canPlayFiredRef.current = true;
1475
+ setIsReady(true);
1476
+ return void 0;
1477
+ }
1478
+ const handleReady2 = () => {
1479
+ if (canPlayFiredRef.current) return;
1474
1480
  canPlayFiredRef.current = true;
1475
1481
  setIsReady(true);
1476
1482
  };
1477
- video.addEventListener("canplay", handleCanPlay2, { once: true });
1483
+ video.addEventListener("loadeddata", handleReady2, { once: true });
1484
+ video.addEventListener("canplay", handleReady2, { once: true });
1478
1485
  return () => {
1479
- video.removeEventListener("canplay", handleCanPlay2);
1486
+ video.removeEventListener("loadeddata", handleReady2);
1487
+ video.removeEventListener("canplay", handleReady2);
1480
1488
  };
1481
1489
  }
1482
1490
  return void 0;
@@ -1513,15 +1521,18 @@ function useHls(options) {
1513
1521
  const mapped = mapHlsError(data);
1514
1522
  onErrorRef.current?.(mapped.code, mapped.message);
1515
1523
  });
1516
- const handleCanPlay = () => {
1524
+ const handleReady = () => {
1525
+ if (canPlayFiredRef.current) return;
1517
1526
  canPlayFiredRef.current = true;
1518
1527
  setIsReady(true);
1519
1528
  };
1520
- video.addEventListener("canplay", handleCanPlay, { once: true });
1529
+ video.addEventListener("loadeddata", handleReady, { once: true });
1530
+ video.addEventListener("canplay", handleReady, { once: true });
1521
1531
  hls.attachMedia(video);
1522
1532
  hls.loadSource(src);
1523
1533
  return () => {
1524
- video.removeEventListener("canplay", handleCanPlay);
1534
+ video.removeEventListener("loadeddata", handleReady);
1535
+ video.removeEventListener("canplay", handleReady);
1525
1536
  if (hlsRef.current === hls) {
1526
1537
  hls.destroy();
1527
1538
  hlsRef.current = null;
@@ -1892,11 +1903,18 @@ function VideoSlotInner({
1892
1903
  };
1893
1904
  if (isActive && !isManuallyPaused) {
1894
1905
  wasActiveRef.current = true;
1895
- if (video.readyState >= HTMLMediaElement.HAVE_FUTURE_DATA) {
1906
+ if (video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) {
1896
1907
  attemptPlay();
1897
1908
  } else {
1898
- onReady = attemptPlay;
1899
- video.addEventListener("canplay", onReady, { once: true });
1909
+ let consumed = false;
1910
+ const handler = () => {
1911
+ if (consumed) return;
1912
+ consumed = true;
1913
+ attemptPlay();
1914
+ };
1915
+ onReady = handler;
1916
+ video.addEventListener("loadeddata", handler, { once: true });
1917
+ video.addEventListener("canplay", handler, { once: true });
1900
1918
  }
1901
1919
  } else if (isActive && isManuallyPaused) {
1902
1920
  wasActiveRef.current = true;
@@ -1912,7 +1930,10 @@ function VideoSlotInner({
1912
1930
  }
1913
1931
  return () => {
1914
1932
  cancelled = true;
1915
- if (onReady) video.removeEventListener("canplay", onReady);
1933
+ if (onReady) {
1934
+ video.removeEventListener("loadeddata", onReady);
1935
+ video.removeEventListener("canplay", onReady);
1936
+ }
1916
1937
  };
1917
1938
  }, [isActive, isMuted, hasPlayedAhead, isManuallyPaused, onAutoplayBlocked]);
1918
1939
  react.useEffect(() => {
@@ -2052,7 +2073,7 @@ function VideoSlotInner({
2052
2073
  inset: 0,
2053
2074
  backgroundImage: `url(${item.poster})`,
2054
2075
  backgroundSize: "cover",
2055
- backgroundPosition: "center center",
2076
+ backgroundPosition: "center",
2056
2077
  opacity: showPosterOverlay ? 1 : 0,
2057
2078
  transition: "opacity 0.15s ease",
2058
2079
  pointerEvents: "none"
@@ -2499,88 +2520,6 @@ function parsePxTranslateY(el) {
2499
2520
  if (!match || !match[1]) return 0;
2500
2521
  return Number.parseFloat(match[1]);
2501
2522
  }
2502
- var DEFAULT_CLASSNAME = "grid grid-cols-2 gap-3";
2503
- var defaultGetKey = (item) => item.id;
2504
- var buttonResetStyle = {
2505
- all: "unset",
2506
- display: "block",
2507
- cursor: "pointer",
2508
- width: "100%",
2509
- boxSizing: "border-box"
2510
- };
2511
- function ReelsFeedThumbnail({
2512
- renderThumbnail,
2513
- onThumbnailClick,
2514
- renderLoading,
2515
- renderEmpty,
2516
- renderError,
2517
- className = DEFAULT_CLASSNAME,
2518
- wrap = true,
2519
- setFocusOnClick = true,
2520
- prefetchOnHover = false,
2521
- getKey = defaultGetKey
2522
- }) {
2523
- const { items, loading, error, refresh } = useFeed();
2524
- const { setFocusedIndexImmediate } = useResource();
2525
- const { adapters } = useSDK();
2526
- const prefetchedRef = react.useRef(/* @__PURE__ */ new Set());
2527
- const handleClick = react.useCallback(
2528
- (item, index) => {
2529
- if (setFocusOnClick) {
2530
- setFocusedIndexImmediate(index);
2531
- }
2532
- onThumbnailClick?.(item.id, item, index);
2533
- },
2534
- [onThumbnailClick, setFocusOnClick, setFocusedIndexImmediate]
2535
- );
2536
- const handlePointerEnter = react.useCallback(
2537
- (item) => {
2538
- if (!prefetchOnHover) return;
2539
- if (!isVideoItem(item)) return;
2540
- if (item.source.type !== "hls") return;
2541
- if (prefetchedRef.current.has(item.id)) return;
2542
- const loader = adapters.videoLoader;
2543
- if (!loader?.preloadMetadata) return;
2544
- if (loader.isPreloaded(item.id)) {
2545
- prefetchedRef.current.add(item.id);
2546
- return;
2547
- }
2548
- loader.preloadMetadata(item.source.url);
2549
- prefetchedRef.current.add(item.id);
2550
- },
2551
- [prefetchOnHover, adapters.videoLoader]
2552
- );
2553
- const children = react.useMemo(
2554
- () => items.map((item, index) => {
2555
- const key = getKey(item, index);
2556
- const onEnter = prefetchOnHover ? (_e) => handlePointerEnter(item) : void 0;
2557
- return /* @__PURE__ */ jsxRuntime.jsx(
2558
- "button",
2559
- {
2560
- type: "button",
2561
- style: buttonResetStyle,
2562
- "data-thumbnail-index": index,
2563
- "data-thumbnail-id": item.id,
2564
- onClick: () => handleClick(item, index),
2565
- onPointerEnter: onEnter,
2566
- children: renderThumbnail(item, index)
2567
- },
2568
- key
2569
- );
2570
- }),
2571
- [items, getKey, prefetchOnHover, handlePointerEnter, handleClick, renderThumbnail]
2572
- );
2573
- if (items.length === 0) {
2574
- if (loading && renderLoading) return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: renderLoading() });
2575
- if (error && renderError) {
2576
- return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: renderError({ message: error.message, retry: refresh }) });
2577
- }
2578
- if (!loading && !error && renderEmpty) return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: renderEmpty() });
2579
- return null;
2580
- }
2581
- if (!wrap) return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children });
2582
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className, children });
2583
- }
2584
2523
  function usePlayerSelector(selector) {
2585
2524
  const { playerEngine } = useSDK();
2586
2525
  const selectorRef = react.useRef(selector);
@@ -3001,12 +2940,16 @@ function transformVideoItem(raw) {
3001
2940
  const stats = transformStats(obj);
3002
2941
  const interaction = transformInteraction(obj);
3003
2942
  let poster;
3004
- let duration = 0;
3005
- const mediaArr = obj["media"];
3006
- if (Array.isArray(mediaArr) && mediaArr.length > 0) {
3007
- const first = mediaArr[0];
3008
- poster = toStr(first["poster"], "") || void 0;
3009
- duration = toNum(first["duration"], 0);
2943
+ const thumbnailObj = obj["thumbnail"];
2944
+ if (thumbnailObj && thumbnailObj["url"]) {
2945
+ poster = toStr(thumbnailObj["url"], "") || void 0;
2946
+ }
2947
+ if (!poster) {
2948
+ const mediaArr2 = obj["media"];
2949
+ if (Array.isArray(mediaArr2) && mediaArr2.length > 0) {
2950
+ const first = mediaArr2[0];
2951
+ poster = toStr(first["poster"], "") || void 0;
2952
+ }
3010
2953
  }
3011
2954
  if (!poster) {
3012
2955
  poster = toStr(
@@ -3014,11 +2957,11 @@ function transformVideoItem(raw) {
3014
2957
  void 0
3015
2958
  ) || void 0;
3016
2959
  }
3017
- if (!poster) {
3018
- const thumbnailObj = obj["thumbnail"];
3019
- if (thumbnailObj && thumbnailObj["url"]) {
3020
- poster = toStr(thumbnailObj["url"], "") || void 0;
3021
- }
2960
+ let duration = 0;
2961
+ const mediaArr = obj["media"];
2962
+ if (Array.isArray(mediaArr) && mediaArr.length > 0) {
2963
+ const first = mediaArr[0];
2964
+ duration = toNum(first["duration"], 0);
3022
2965
  }
3023
2966
  if (duration === 0) {
3024
2967
  duration = toNum(tryFields(obj, "duration", "duration_seconds", "length", "video_duration"), 0);
@@ -3213,7 +3156,6 @@ exports.OptimisticManager = OptimisticManager;
3213
3156
  exports.PlayerEngine = PlayerEngine;
3214
3157
  exports.PlayerStatus = PlayerStatus;
3215
3158
  exports.ReelsFeed = ReelsFeed;
3216
- exports.ReelsFeedThumbnail = ReelsFeedThumbnail;
3217
3159
  exports.ReelsProvider = ReelsProvider;
3218
3160
  exports.ResourceGovernor = ResourceGovernor;
3219
3161
  exports.VALID_TRANSITIONS = VALID_TRANSITIONS;
package/dist/index.d.cts CHANGED
@@ -875,41 +875,6 @@ interface VideoSlotProps {
875
875
  }
876
876
  declare function VideoSlot({ item, index, isActive, isPrefetch, isPreloaded, bufferTier, isMuted, onToggleMute, onAutoplayBlocked, showFps, isDragging, renderOverlay, renderActions, renderPauseAction, }: VideoSlotProps): react_jsx_runtime.JSX.Element;
877
877
 
878
- interface ReelsFeedThumbnailProps {
879
- /** Render function for a single item's visual card. */
880
- renderThumbnail: (videoData: ContentItem, index: number) => ReactNode;
881
- /** Click handler — receives the item id, full data, and index in the feed. */
882
- onThumbnailClick?: (id: string, videoData: ContentItem, index: number) => void;
883
- /** Rendered when the feed is loading and has zero items yet. */
884
- renderLoading?: () => ReactNode;
885
- /** Rendered when the feed has loaded but is empty. */
886
- renderEmpty?: () => ReactNode;
887
- /** Rendered when the feed fails with no items — receives a retry callback. */
888
- renderError?: (error: {
889
- message: string;
890
- retry: () => void;
891
- }) => ReactNode;
892
- /** Outer wrapper className. Defaults to `grid grid-cols-2 gap-3`. */
893
- className?: string;
894
- /** If the host wants to render its own wrapper, pass `false`. */
895
- wrap?: boolean;
896
- /**
897
- * Update ResourceGovernor's focused index on click so the drawer/feed opens
898
- * already pointing at the tapped item.
899
- * @default true
900
- */
901
- setFocusOnClick?: boolean;
902
- /**
903
- * Prefetch HLS metadata for this slot on `pointerenter`. Off by default;
904
- * only useful on hover-capable devices.
905
- * @default false
906
- */
907
- prefetchOnHover?: boolean;
908
- /** Key override — useful when host renders multiple lists from the same feed. */
909
- getKey?: (item: ContentItem, index: number) => string;
910
- }
911
- declare function ReelsFeedThumbnail({ renderThumbnail, onThumbnailClick, renderLoading, renderEmpty, renderError, className, wrap, setFocusOnClick, prefetchOnHover, getKey, }: ReelsFeedThumbnailProps): react_jsx_runtime.JSX.Element | null;
912
-
913
878
  declare function DefaultOverlay({ item }: {
914
879
  item: ContentItem;
915
880
  }): react_jsx_runtime.JSX.Element;
@@ -1209,4 +1174,4 @@ declare class HttpError extends Error {
1209
1174
  constructor(status: number, message: string, body?: string | undefined);
1210
1175
  }
1211
1176
 
1212
- 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, DefaultPauseAction, 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 PauseSlotActions, type PlayerConfig, PlayerEngine, type PlayerError, type PlayerEvent, type PlayerEventListener, type PlayerState, PlayerStatus, type PointerGestureConfig, type PreloadResult, type PreloadStatus, ReelsFeed, type ReelsFeedProps, ReelsFeedThumbnail, type ReelsFeedThumbnailProps, 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 };
1177
+ 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, DefaultPauseAction, 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 PauseSlotActions, 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
@@ -875,41 +875,6 @@ interface VideoSlotProps {
875
875
  }
876
876
  declare function VideoSlot({ item, index, isActive, isPrefetch, isPreloaded, bufferTier, isMuted, onToggleMute, onAutoplayBlocked, showFps, isDragging, renderOverlay, renderActions, renderPauseAction, }: VideoSlotProps): react_jsx_runtime.JSX.Element;
877
877
 
878
- interface ReelsFeedThumbnailProps {
879
- /** Render function for a single item's visual card. */
880
- renderThumbnail: (videoData: ContentItem, index: number) => ReactNode;
881
- /** Click handler — receives the item id, full data, and index in the feed. */
882
- onThumbnailClick?: (id: string, videoData: ContentItem, index: number) => void;
883
- /** Rendered when the feed is loading and has zero items yet. */
884
- renderLoading?: () => ReactNode;
885
- /** Rendered when the feed has loaded but is empty. */
886
- renderEmpty?: () => ReactNode;
887
- /** Rendered when the feed fails with no items — receives a retry callback. */
888
- renderError?: (error: {
889
- message: string;
890
- retry: () => void;
891
- }) => ReactNode;
892
- /** Outer wrapper className. Defaults to `grid grid-cols-2 gap-3`. */
893
- className?: string;
894
- /** If the host wants to render its own wrapper, pass `false`. */
895
- wrap?: boolean;
896
- /**
897
- * Update ResourceGovernor's focused index on click so the drawer/feed opens
898
- * already pointing at the tapped item.
899
- * @default true
900
- */
901
- setFocusOnClick?: boolean;
902
- /**
903
- * Prefetch HLS metadata for this slot on `pointerenter`. Off by default;
904
- * only useful on hover-capable devices.
905
- * @default false
906
- */
907
- prefetchOnHover?: boolean;
908
- /** Key override — useful when host renders multiple lists from the same feed. */
909
- getKey?: (item: ContentItem, index: number) => string;
910
- }
911
- declare function ReelsFeedThumbnail({ renderThumbnail, onThumbnailClick, renderLoading, renderEmpty, renderError, className, wrap, setFocusOnClick, prefetchOnHover, getKey, }: ReelsFeedThumbnailProps): react_jsx_runtime.JSX.Element | null;
912
-
913
878
  declare function DefaultOverlay({ item }: {
914
879
  item: ContentItem;
915
880
  }): react_jsx_runtime.JSX.Element;
@@ -1209,4 +1174,4 @@ declare class HttpError extends Error {
1209
1174
  constructor(status: number, message: string, body?: string | undefined);
1210
1175
  }
1211
1176
 
1212
- 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, DefaultPauseAction, 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 PauseSlotActions, type PlayerConfig, PlayerEngine, type PlayerError, type PlayerEvent, type PlayerEventListener, type PlayerState, PlayerStatus, type PointerGestureConfig, type PreloadResult, type PreloadStatus, ReelsFeed, type ReelsFeedProps, ReelsFeedThumbnail, type ReelsFeedThumbnailProps, 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 };
1177
+ 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, DefaultPauseAction, 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 PauseSlotActions, 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
@@ -1441,7 +1441,7 @@ function useHls(options) {
1441
1441
  setIsReady(false);
1442
1442
  canPlayFiredRef.current = false;
1443
1443
  currentSrcRef.current = src;
1444
- const handleCanPlay2 = () => {
1444
+ const handleCanPlay = () => {
1445
1445
  canPlayFiredRef.current = true;
1446
1446
  setIsReady(true);
1447
1447
  };
@@ -1451,10 +1451,10 @@ function useHls(options) {
1451
1451
  setIsReady(true);
1452
1452
  }
1453
1453
  };
1454
- video.addEventListener("canplay", handleCanPlay2, { once: true });
1454
+ video.addEventListener("canplay", handleCanPlay, { once: true });
1455
1455
  video.addEventListener("loadeddata", handleLoadedData, { once: true });
1456
1456
  return () => {
1457
- video.removeEventListener("canplay", handleCanPlay2);
1457
+ video.removeEventListener("canplay", handleCanPlay);
1458
1458
  video.removeEventListener("loadeddata", handleLoadedData);
1459
1459
  };
1460
1460
  }
@@ -1464,13 +1464,21 @@ function useHls(options) {
1464
1464
  }
1465
1465
  if (hlsRef.current && currentSrcRef.current === src) {
1466
1466
  if (!canPlayFiredRef.current) {
1467
- const handleCanPlay2 = () => {
1467
+ if (video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) {
1468
+ canPlayFiredRef.current = true;
1469
+ setIsReady(true);
1470
+ return void 0;
1471
+ }
1472
+ const handleReady2 = () => {
1473
+ if (canPlayFiredRef.current) return;
1468
1474
  canPlayFiredRef.current = true;
1469
1475
  setIsReady(true);
1470
1476
  };
1471
- video.addEventListener("canplay", handleCanPlay2, { once: true });
1477
+ video.addEventListener("loadeddata", handleReady2, { once: true });
1478
+ video.addEventListener("canplay", handleReady2, { once: true });
1472
1479
  return () => {
1473
- video.removeEventListener("canplay", handleCanPlay2);
1480
+ video.removeEventListener("loadeddata", handleReady2);
1481
+ video.removeEventListener("canplay", handleReady2);
1474
1482
  };
1475
1483
  }
1476
1484
  return void 0;
@@ -1507,15 +1515,18 @@ function useHls(options) {
1507
1515
  const mapped = mapHlsError(data);
1508
1516
  onErrorRef.current?.(mapped.code, mapped.message);
1509
1517
  });
1510
- const handleCanPlay = () => {
1518
+ const handleReady = () => {
1519
+ if (canPlayFiredRef.current) return;
1511
1520
  canPlayFiredRef.current = true;
1512
1521
  setIsReady(true);
1513
1522
  };
1514
- video.addEventListener("canplay", handleCanPlay, { once: true });
1523
+ video.addEventListener("loadeddata", handleReady, { once: true });
1524
+ video.addEventListener("canplay", handleReady, { once: true });
1515
1525
  hls.attachMedia(video);
1516
1526
  hls.loadSource(src);
1517
1527
  return () => {
1518
- video.removeEventListener("canplay", handleCanPlay);
1528
+ video.removeEventListener("loadeddata", handleReady);
1529
+ video.removeEventListener("canplay", handleReady);
1519
1530
  if (hlsRef.current === hls) {
1520
1531
  hls.destroy();
1521
1532
  hlsRef.current = null;
@@ -1886,11 +1897,18 @@ function VideoSlotInner({
1886
1897
  };
1887
1898
  if (isActive && !isManuallyPaused) {
1888
1899
  wasActiveRef.current = true;
1889
- if (video.readyState >= HTMLMediaElement.HAVE_FUTURE_DATA) {
1900
+ if (video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) {
1890
1901
  attemptPlay();
1891
1902
  } else {
1892
- onReady = attemptPlay;
1893
- video.addEventListener("canplay", onReady, { once: true });
1903
+ let consumed = false;
1904
+ const handler = () => {
1905
+ if (consumed) return;
1906
+ consumed = true;
1907
+ attemptPlay();
1908
+ };
1909
+ onReady = handler;
1910
+ video.addEventListener("loadeddata", handler, { once: true });
1911
+ video.addEventListener("canplay", handler, { once: true });
1894
1912
  }
1895
1913
  } else if (isActive && isManuallyPaused) {
1896
1914
  wasActiveRef.current = true;
@@ -1906,7 +1924,10 @@ function VideoSlotInner({
1906
1924
  }
1907
1925
  return () => {
1908
1926
  cancelled = true;
1909
- if (onReady) video.removeEventListener("canplay", onReady);
1927
+ if (onReady) {
1928
+ video.removeEventListener("loadeddata", onReady);
1929
+ video.removeEventListener("canplay", onReady);
1930
+ }
1910
1931
  };
1911
1932
  }, [isActive, isMuted, hasPlayedAhead, isManuallyPaused, onAutoplayBlocked]);
1912
1933
  useEffect(() => {
@@ -2046,7 +2067,7 @@ function VideoSlotInner({
2046
2067
  inset: 0,
2047
2068
  backgroundImage: `url(${item.poster})`,
2048
2069
  backgroundSize: "cover",
2049
- backgroundPosition: "center center",
2070
+ backgroundPosition: "center",
2050
2071
  opacity: showPosterOverlay ? 1 : 0,
2051
2072
  transition: "opacity 0.15s ease",
2052
2073
  pointerEvents: "none"
@@ -2493,88 +2514,6 @@ function parsePxTranslateY(el) {
2493
2514
  if (!match || !match[1]) return 0;
2494
2515
  return Number.parseFloat(match[1]);
2495
2516
  }
2496
- var DEFAULT_CLASSNAME = "grid grid-cols-2 gap-3";
2497
- var defaultGetKey = (item) => item.id;
2498
- var buttonResetStyle = {
2499
- all: "unset",
2500
- display: "block",
2501
- cursor: "pointer",
2502
- width: "100%",
2503
- boxSizing: "border-box"
2504
- };
2505
- function ReelsFeedThumbnail({
2506
- renderThumbnail,
2507
- onThumbnailClick,
2508
- renderLoading,
2509
- renderEmpty,
2510
- renderError,
2511
- className = DEFAULT_CLASSNAME,
2512
- wrap = true,
2513
- setFocusOnClick = true,
2514
- prefetchOnHover = false,
2515
- getKey = defaultGetKey
2516
- }) {
2517
- const { items, loading, error, refresh } = useFeed();
2518
- const { setFocusedIndexImmediate } = useResource();
2519
- const { adapters } = useSDK();
2520
- const prefetchedRef = useRef(/* @__PURE__ */ new Set());
2521
- const handleClick = useCallback(
2522
- (item, index) => {
2523
- if (setFocusOnClick) {
2524
- setFocusedIndexImmediate(index);
2525
- }
2526
- onThumbnailClick?.(item.id, item, index);
2527
- },
2528
- [onThumbnailClick, setFocusOnClick, setFocusedIndexImmediate]
2529
- );
2530
- const handlePointerEnter = useCallback(
2531
- (item) => {
2532
- if (!prefetchOnHover) return;
2533
- if (!isVideoItem(item)) return;
2534
- if (item.source.type !== "hls") return;
2535
- if (prefetchedRef.current.has(item.id)) return;
2536
- const loader = adapters.videoLoader;
2537
- if (!loader?.preloadMetadata) return;
2538
- if (loader.isPreloaded(item.id)) {
2539
- prefetchedRef.current.add(item.id);
2540
- return;
2541
- }
2542
- loader.preloadMetadata(item.source.url);
2543
- prefetchedRef.current.add(item.id);
2544
- },
2545
- [prefetchOnHover, adapters.videoLoader]
2546
- );
2547
- const children = useMemo(
2548
- () => items.map((item, index) => {
2549
- const key = getKey(item, index);
2550
- const onEnter = prefetchOnHover ? (_e) => handlePointerEnter(item) : void 0;
2551
- return /* @__PURE__ */ jsx(
2552
- "button",
2553
- {
2554
- type: "button",
2555
- style: buttonResetStyle,
2556
- "data-thumbnail-index": index,
2557
- "data-thumbnail-id": item.id,
2558
- onClick: () => handleClick(item, index),
2559
- onPointerEnter: onEnter,
2560
- children: renderThumbnail(item, index)
2561
- },
2562
- key
2563
- );
2564
- }),
2565
- [items, getKey, prefetchOnHover, handlePointerEnter, handleClick, renderThumbnail]
2566
- );
2567
- if (items.length === 0) {
2568
- if (loading && renderLoading) return /* @__PURE__ */ jsx(Fragment, { children: renderLoading() });
2569
- if (error && renderError) {
2570
- return /* @__PURE__ */ jsx(Fragment, { children: renderError({ message: error.message, retry: refresh }) });
2571
- }
2572
- if (!loading && !error && renderEmpty) return /* @__PURE__ */ jsx(Fragment, { children: renderEmpty() });
2573
- return null;
2574
- }
2575
- if (!wrap) return /* @__PURE__ */ jsx(Fragment, { children });
2576
- return /* @__PURE__ */ jsx("div", { className, children });
2577
- }
2578
2517
  function usePlayerSelector(selector) {
2579
2518
  const { playerEngine } = useSDK();
2580
2519
  const selectorRef = useRef(selector);
@@ -2995,12 +2934,16 @@ function transformVideoItem(raw) {
2995
2934
  const stats = transformStats(obj);
2996
2935
  const interaction = transformInteraction(obj);
2997
2936
  let poster;
2998
- let duration = 0;
2999
- const mediaArr = obj["media"];
3000
- if (Array.isArray(mediaArr) && mediaArr.length > 0) {
3001
- const first = mediaArr[0];
3002
- poster = toStr(first["poster"], "") || void 0;
3003
- duration = toNum(first["duration"], 0);
2937
+ const thumbnailObj = obj["thumbnail"];
2938
+ if (thumbnailObj && thumbnailObj["url"]) {
2939
+ poster = toStr(thumbnailObj["url"], "") || void 0;
2940
+ }
2941
+ if (!poster) {
2942
+ const mediaArr2 = obj["media"];
2943
+ if (Array.isArray(mediaArr2) && mediaArr2.length > 0) {
2944
+ const first = mediaArr2[0];
2945
+ poster = toStr(first["poster"], "") || void 0;
2946
+ }
3004
2947
  }
3005
2948
  if (!poster) {
3006
2949
  poster = toStr(
@@ -3008,11 +2951,11 @@ function transformVideoItem(raw) {
3008
2951
  void 0
3009
2952
  ) || void 0;
3010
2953
  }
3011
- if (!poster) {
3012
- const thumbnailObj = obj["thumbnail"];
3013
- if (thumbnailObj && thumbnailObj["url"]) {
3014
- poster = toStr(thumbnailObj["url"], "") || void 0;
3015
- }
2954
+ let duration = 0;
2955
+ const mediaArr = obj["media"];
2956
+ if (Array.isArray(mediaArr) && mediaArr.length > 0) {
2957
+ const first = mediaArr[0];
2958
+ duration = toNum(first["duration"], 0);
3016
2959
  }
3017
2960
  if (duration === 0) {
3018
2961
  duration = toNum(tryFields(obj, "duration", "duration_seconds", "length", "video_duration"), 0);
@@ -3185,4 +3128,4 @@ var HttpError = class extends Error {
3185
3128
  }
3186
3129
  };
3187
3130
 
3188
- 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, ReelsFeedThumbnail, ReelsProvider, ResourceGovernor, VALID_TRANSITIONS, VideoSlot, canPause, canPlay, canSeek, isArticle, isValidTransition, isVideoItem, useFeed, useFeedSelector, useHls, usePlayer, usePlayerSelector, usePointerGesture, useResource, useResourceSelector, useSDK, useSnapAnimation };
3131
+ 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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xhub-reels/sdk",
3
- "version": "0.2.1",
3
+ "version": "0.2.5",
4
4
  "description": "High-performance Short Video / Reels SDK for React — optimized for Flutter WebView",
5
5
  "license": "MIT",
6
6
  "type": "module",