@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 +0 -42
- package/dist/index.cjs +50 -108
- package/dist/index.d.cts +1 -36
- package/dist/index.d.ts +1 -36
- package/dist/index.js +51 -108
- package/package.json +1 -1
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
|
|
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",
|
|
1460
|
+
video.addEventListener("canplay", handleCanPlay, { once: true });
|
|
1461
1461
|
video.addEventListener("loadeddata", handleLoadedData, { once: true });
|
|
1462
1462
|
return () => {
|
|
1463
|
-
video.removeEventListener("canplay",
|
|
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
|
-
|
|
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("
|
|
1483
|
+
video.addEventListener("loadeddata", handleReady2, { once: true });
|
|
1484
|
+
video.addEventListener("canplay", handleReady2, { once: true });
|
|
1478
1485
|
return () => {
|
|
1479
|
-
video.removeEventListener("
|
|
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
|
|
1524
|
+
const handleReady = () => {
|
|
1525
|
+
if (canPlayFiredRef.current) return;
|
|
1517
1526
|
canPlayFiredRef.current = true;
|
|
1518
1527
|
setIsReady(true);
|
|
1519
1528
|
};
|
|
1520
|
-
video.addEventListener("
|
|
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("
|
|
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.
|
|
1906
|
+
if (video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) {
|
|
1896
1907
|
attemptPlay();
|
|
1897
1908
|
} else {
|
|
1898
|
-
|
|
1899
|
-
|
|
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)
|
|
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
|
|
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
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
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
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
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,
|
|
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,
|
|
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
|
|
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",
|
|
1454
|
+
video.addEventListener("canplay", handleCanPlay, { once: true });
|
|
1455
1455
|
video.addEventListener("loadeddata", handleLoadedData, { once: true });
|
|
1456
1456
|
return () => {
|
|
1457
|
-
video.removeEventListener("canplay",
|
|
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
|
-
|
|
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("
|
|
1477
|
+
video.addEventListener("loadeddata", handleReady2, { once: true });
|
|
1478
|
+
video.addEventListener("canplay", handleReady2, { once: true });
|
|
1472
1479
|
return () => {
|
|
1473
|
-
video.removeEventListener("
|
|
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
|
|
1518
|
+
const handleReady = () => {
|
|
1519
|
+
if (canPlayFiredRef.current) return;
|
|
1511
1520
|
canPlayFiredRef.current = true;
|
|
1512
1521
|
setIsReady(true);
|
|
1513
1522
|
};
|
|
1514
|
-
video.addEventListener("
|
|
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("
|
|
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.
|
|
1900
|
+
if (video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) {
|
|
1890
1901
|
attemptPlay();
|
|
1891
1902
|
} else {
|
|
1892
|
-
|
|
1893
|
-
|
|
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)
|
|
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
|
|
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
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
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
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
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,
|
|
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 };
|