@xhub-reels/sdk 0.2.1 → 0.2.6
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 +66 -42
- package/dist/index.cjs +92 -74
- package/dist/index.d.cts +27 -35
- package/dist/index.d.ts +27 -35
- package/dist/index.js +92 -74
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -86,6 +86,72 @@ function MyFeed() {
|
|
|
86
86
|
}
|
|
87
87
|
```
|
|
88
88
|
|
|
89
|
+
## Thumbnail grid
|
|
90
|
+
|
|
91
|
+
`<ReelsFeedThumbnail>` is a headless grid component for the pre-drawer browsing UX
|
|
92
|
+
(e.g. a community feed that opens the player drawer on tap). It must be mounted
|
|
93
|
+
inside `<ReelsProvider>` and reads items from the same shared feed. The host
|
|
94
|
+
provides card visuals via `renderThumbnail`.
|
|
95
|
+
|
|
96
|
+
```tsx
|
|
97
|
+
import { ReelsProvider, ReelsFeedThumbnail } from 'xhub-reels-sdk';
|
|
98
|
+
|
|
99
|
+
function CommunityPage() {
|
|
100
|
+
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<ReelsProvider adapters={{ dataSource, interaction }}>
|
|
104
|
+
<ReelsFeedThumbnail
|
|
105
|
+
renderThumbnail={(item) => (
|
|
106
|
+
<article className="aspect-[3/2] rounded-lg overflow-hidden">
|
|
107
|
+
<img src={item.poster} alt="" />
|
|
108
|
+
<p>@{item.author.name}</p>
|
|
109
|
+
</article>
|
|
110
|
+
)}
|
|
111
|
+
onThumbnailClick={(id) => {
|
|
112
|
+
setDrawerOpen(true);
|
|
113
|
+
window.history.replaceState(null, '', `#reel_uuid=${id}`);
|
|
114
|
+
}}
|
|
115
|
+
/>
|
|
116
|
+
{drawerOpen && <ReelsDrawer onClose={() => setDrawerOpen(false)} />}
|
|
117
|
+
</ReelsProvider>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
By default, clicking a card calls `setFocusedIndexImmediate(index)` on the
|
|
123
|
+
`ResourceGovernor` so the player opens instantly without a scroll-to-index. To
|
|
124
|
+
disable this glue (e.g. if the host manages focus separately), pass
|
|
125
|
+
`setFocusOnClick={false}`.
|
|
126
|
+
|
|
127
|
+
### Props
|
|
128
|
+
|
|
129
|
+
| Prop | Type | Default | Description |
|
|
130
|
+
|---|---|---|---|
|
|
131
|
+
| `renderThumbnail` | `(item, index) => ReactNode` | required | Card visual for a single item |
|
|
132
|
+
| `onThumbnailClick` | `(id, item, index) => void` | — | Click handler |
|
|
133
|
+
| `renderLoading` | `() => ReactNode` | — | Shown while loading with no items |
|
|
134
|
+
| `renderEmpty` | `() => ReactNode` | — | Shown when feed is empty |
|
|
135
|
+
| `renderError` | `({ message, retry }) => ReactNode` | — | Shown on error with no items |
|
|
136
|
+
| `className` | `string` | `'grid grid-cols-2 gap-3'` | Outer wrapper className |
|
|
137
|
+
| `wrap` | `boolean` | `true` | Set `false` to render without an outer `<div>` |
|
|
138
|
+
| `setFocusOnClick` | `boolean` | `true` | Pre-focus the slot before `onThumbnailClick` fires |
|
|
139
|
+
| `prefetchOnHover` | `boolean` | `false` | Opt-in HLS metadata prefetch on `pointerenter` |
|
|
140
|
+
| `getKey` | `(item, index) => string` | `item.id` | Key override for duplicate lists |
|
|
141
|
+
|
|
142
|
+
### Hover prefetch
|
|
143
|
+
|
|
144
|
+
For hover-capable devices, opt into manifest prefetch so opening the player
|
|
145
|
+
feels instant:
|
|
146
|
+
|
|
147
|
+
```tsx
|
|
148
|
+
<ReelsFeedThumbnail prefetchOnHover renderThumbnail={...} />
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
The SDK uses `adapters.videoLoader?.preloadMetadata?.(url)` and dedupes per item
|
|
152
|
+
so a card only triggers one prefetch regardless of how many times it's hovered.
|
|
153
|
+
Off by default to avoid surprising bandwidth on touch devices.
|
|
154
|
+
|
|
89
155
|
## Gesture Engine
|
|
90
156
|
|
|
91
157
|
```tsx
|
|
@@ -111,48 +177,6 @@ function SwipeableFeed() {
|
|
|
111
177
|
}
|
|
112
178
|
```
|
|
113
179
|
|
|
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
180
|
## Architecture
|
|
157
181
|
|
|
158
182
|
```
|
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,87 +2520,80 @@ 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
2523
|
function ReelsFeedThumbnail({
|
|
2512
2524
|
renderThumbnail,
|
|
2513
2525
|
onThumbnailClick,
|
|
2514
2526
|
renderLoading,
|
|
2515
2527
|
renderEmpty,
|
|
2516
2528
|
renderError,
|
|
2517
|
-
className =
|
|
2529
|
+
className = "grid grid-cols-2 gap-3",
|
|
2518
2530
|
wrap = true,
|
|
2519
2531
|
setFocusOnClick = true,
|
|
2520
2532
|
prefetchOnHover = false,
|
|
2521
|
-
getKey
|
|
2533
|
+
getKey
|
|
2522
2534
|
}) {
|
|
2523
2535
|
const { items, loading, error, refresh } = useFeed();
|
|
2524
2536
|
const { setFocusedIndexImmediate } = useResource();
|
|
2525
2537
|
const { adapters } = useSDK();
|
|
2526
2538
|
const prefetchedRef = react.useRef(/* @__PURE__ */ new Set());
|
|
2527
2539
|
const handleClick = react.useCallback(
|
|
2528
|
-
(item, index) => {
|
|
2540
|
+
(id, item, index) => {
|
|
2529
2541
|
if (setFocusOnClick) {
|
|
2530
2542
|
setFocusedIndexImmediate(index);
|
|
2531
2543
|
}
|
|
2532
|
-
onThumbnailClick?.(
|
|
2544
|
+
onThumbnailClick?.(id, item, index);
|
|
2533
2545
|
},
|
|
2534
|
-
[
|
|
2546
|
+
[setFocusOnClick, setFocusedIndexImmediate, onThumbnailClick]
|
|
2535
2547
|
);
|
|
2536
2548
|
const handlePointerEnter = react.useCallback(
|
|
2537
2549
|
(item) => {
|
|
2538
2550
|
if (!prefetchOnHover) return;
|
|
2539
2551
|
if (!isVideoItem(item)) return;
|
|
2540
2552
|
if (item.source.type !== "hls") return;
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
prefetchedRef.current.add(item.id);
|
|
2546
|
-
return;
|
|
2547
|
-
}
|
|
2548
|
-
loader.preloadMetadata(item.source.url);
|
|
2549
|
-
prefetchedRef.current.add(item.id);
|
|
2553
|
+
const url = item.source.url;
|
|
2554
|
+
if (prefetchedRef.current.has(url)) return;
|
|
2555
|
+
prefetchedRef.current.add(url);
|
|
2556
|
+
adapters.videoLoader?.preloadMetadata?.(url);
|
|
2550
2557
|
},
|
|
2551
2558
|
[prefetchOnHover, adapters.videoLoader]
|
|
2552
2559
|
);
|
|
2553
|
-
|
|
2554
|
-
()
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
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) {
|
|
2560
|
+
if (loading && items.length === 0) {
|
|
2561
|
+
if (renderLoading) return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: renderLoading() });
|
|
2562
|
+
return null;
|
|
2563
|
+
}
|
|
2564
|
+
if (error && items.length === 0) {
|
|
2565
|
+
if (renderError) {
|
|
2576
2566
|
return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: renderError({ message: error.message, retry: refresh }) });
|
|
2577
2567
|
}
|
|
2578
|
-
if (!loading && !error && renderEmpty) return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: renderEmpty() });
|
|
2579
2568
|
return null;
|
|
2580
2569
|
}
|
|
2581
|
-
if (!
|
|
2582
|
-
|
|
2570
|
+
if (!loading && items.length === 0) {
|
|
2571
|
+
if (renderEmpty) return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: renderEmpty() });
|
|
2572
|
+
return null;
|
|
2573
|
+
}
|
|
2574
|
+
const content = items.map((item, index) => {
|
|
2575
|
+
const key = getKey ? getKey(item, index) : item.id;
|
|
2576
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2577
|
+
"button",
|
|
2578
|
+
{
|
|
2579
|
+
type: "button",
|
|
2580
|
+
onClick: () => handleClick(item.id, item, index),
|
|
2581
|
+
onPointerEnter: () => handlePointerEnter(item),
|
|
2582
|
+
style: {
|
|
2583
|
+
all: "unset",
|
|
2584
|
+
cursor: "pointer",
|
|
2585
|
+
display: "block",
|
|
2586
|
+
width: "100%"
|
|
2587
|
+
},
|
|
2588
|
+
children: renderThumbnail(item, index)
|
|
2589
|
+
},
|
|
2590
|
+
key
|
|
2591
|
+
);
|
|
2592
|
+
});
|
|
2593
|
+
if (!wrap) {
|
|
2594
|
+
return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: content });
|
|
2595
|
+
}
|
|
2596
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className, children: content });
|
|
2583
2597
|
}
|
|
2584
2598
|
function usePlayerSelector(selector) {
|
|
2585
2599
|
const { playerEngine } = useSDK();
|
|
@@ -3001,12 +3015,16 @@ function transformVideoItem(raw) {
|
|
|
3001
3015
|
const stats = transformStats(obj);
|
|
3002
3016
|
const interaction = transformInteraction(obj);
|
|
3003
3017
|
let poster;
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3018
|
+
const thumbnailObj = obj["thumbnail"];
|
|
3019
|
+
if (thumbnailObj && thumbnailObj["url"]) {
|
|
3020
|
+
poster = toStr(thumbnailObj["url"], "") || void 0;
|
|
3021
|
+
}
|
|
3022
|
+
if (!poster) {
|
|
3023
|
+
const mediaArr2 = obj["media"];
|
|
3024
|
+
if (Array.isArray(mediaArr2) && mediaArr2.length > 0) {
|
|
3025
|
+
const first = mediaArr2[0];
|
|
3026
|
+
poster = toStr(first["poster"], "") || void 0;
|
|
3027
|
+
}
|
|
3010
3028
|
}
|
|
3011
3029
|
if (!poster) {
|
|
3012
3030
|
poster = toStr(
|
|
@@ -3014,11 +3032,11 @@ function transformVideoItem(raw) {
|
|
|
3014
3032
|
void 0
|
|
3015
3033
|
) || void 0;
|
|
3016
3034
|
}
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3035
|
+
let duration = 0;
|
|
3036
|
+
const mediaArr = obj["media"];
|
|
3037
|
+
if (Array.isArray(mediaArr) && mediaArr.length > 0) {
|
|
3038
|
+
const first = mediaArr[0];
|
|
3039
|
+
duration = toNum(first["duration"], 0);
|
|
3022
3040
|
}
|
|
3023
3041
|
if (duration === 0) {
|
|
3024
3042
|
duration = toNum(tryFields(obj, "duration", "duration_seconds", "length", "video_duration"), 0);
|
package/dist/index.d.cts
CHANGED
|
@@ -789,6 +789,33 @@ declare function useSDK(): SDKContextValue;
|
|
|
789
789
|
|
|
790
790
|
declare function ReelsFeed({ renderOverlay, renderActions, renderPauseAction, renderLoading, renderEmpty, renderError: _renderError, showFps, loadMoreThreshold, onSlotChange, gestureConfig, snapConfig, initialMuted, onAutoplayBlocked, }: 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;
|
|
791
791
|
|
|
792
|
+
interface ReelsFeedThumbnailProps {
|
|
793
|
+
/** Render function for a single item's visual card. */
|
|
794
|
+
renderThumbnail: (videoData: ContentItem, index: number) => ReactNode;
|
|
795
|
+
/** Click handler — receives the item id, full data, and index in the feed. */
|
|
796
|
+
onThumbnailClick?: (id: string, videoData: ContentItem, index: number) => void;
|
|
797
|
+
/** Rendered when the feed is loading and has zero items yet. */
|
|
798
|
+
renderLoading?: () => ReactNode;
|
|
799
|
+
/** Rendered when the feed has loaded but is empty. */
|
|
800
|
+
renderEmpty?: () => ReactNode;
|
|
801
|
+
/** Rendered when the feed fails with no items — receives a retry callback. */
|
|
802
|
+
renderError?: (error: {
|
|
803
|
+
message: string;
|
|
804
|
+
retry: () => void;
|
|
805
|
+
}) => ReactNode;
|
|
806
|
+
/** Outer wrapper className. Defaults to `grid grid-cols-2 gap-3`. */
|
|
807
|
+
className?: string;
|
|
808
|
+
/** If the host wants to render its own wrapper, pass `false`. Default: true. */
|
|
809
|
+
wrap?: boolean;
|
|
810
|
+
/** Enable click → update focused index on ReelsProvider before host reacts. Default: true. */
|
|
811
|
+
setFocusOnClick?: boolean;
|
|
812
|
+
/** Prefetch metadata for this slot on pointer enter. Off by default. */
|
|
813
|
+
prefetchOnHover?: boolean;
|
|
814
|
+
/** Key override — useful when host renders multiple lists from the same feed. */
|
|
815
|
+
getKey?: (item: ContentItem, index: number) => string;
|
|
816
|
+
}
|
|
817
|
+
declare function ReelsFeedThumbnail({ renderThumbnail, onThumbnailClick, renderLoading, renderEmpty, renderError, className, wrap, setFocusOnClick, prefetchOnHover, getKey, }: ReelsFeedThumbnailProps): react_jsx_runtime.JSX.Element | null;
|
|
818
|
+
|
|
792
819
|
/**
|
|
793
820
|
* useHls — React hook for hls.js lifecycle management (3-Tier buffer support)
|
|
794
821
|
*
|
|
@@ -875,41 +902,6 @@ interface VideoSlotProps {
|
|
|
875
902
|
}
|
|
876
903
|
declare function VideoSlot({ item, index, isActive, isPrefetch, isPreloaded, bufferTier, isMuted, onToggleMute, onAutoplayBlocked, showFps, isDragging, renderOverlay, renderActions, renderPauseAction, }: VideoSlotProps): react_jsx_runtime.JSX.Element;
|
|
877
904
|
|
|
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
905
|
declare function DefaultOverlay({ item }: {
|
|
914
906
|
item: ContentItem;
|
|
915
907
|
}): react_jsx_runtime.JSX.Element;
|
package/dist/index.d.ts
CHANGED
|
@@ -789,6 +789,33 @@ declare function useSDK(): SDKContextValue;
|
|
|
789
789
|
|
|
790
790
|
declare function ReelsFeed({ renderOverlay, renderActions, renderPauseAction, renderLoading, renderEmpty, renderError: _renderError, showFps, loadMoreThreshold, onSlotChange, gestureConfig, snapConfig, initialMuted, onAutoplayBlocked, }: 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;
|
|
791
791
|
|
|
792
|
+
interface ReelsFeedThumbnailProps {
|
|
793
|
+
/** Render function for a single item's visual card. */
|
|
794
|
+
renderThumbnail: (videoData: ContentItem, index: number) => ReactNode;
|
|
795
|
+
/** Click handler — receives the item id, full data, and index in the feed. */
|
|
796
|
+
onThumbnailClick?: (id: string, videoData: ContentItem, index: number) => void;
|
|
797
|
+
/** Rendered when the feed is loading and has zero items yet. */
|
|
798
|
+
renderLoading?: () => ReactNode;
|
|
799
|
+
/** Rendered when the feed has loaded but is empty. */
|
|
800
|
+
renderEmpty?: () => ReactNode;
|
|
801
|
+
/** Rendered when the feed fails with no items — receives a retry callback. */
|
|
802
|
+
renderError?: (error: {
|
|
803
|
+
message: string;
|
|
804
|
+
retry: () => void;
|
|
805
|
+
}) => ReactNode;
|
|
806
|
+
/** Outer wrapper className. Defaults to `grid grid-cols-2 gap-3`. */
|
|
807
|
+
className?: string;
|
|
808
|
+
/** If the host wants to render its own wrapper, pass `false`. Default: true. */
|
|
809
|
+
wrap?: boolean;
|
|
810
|
+
/** Enable click → update focused index on ReelsProvider before host reacts. Default: true. */
|
|
811
|
+
setFocusOnClick?: boolean;
|
|
812
|
+
/** Prefetch metadata for this slot on pointer enter. Off by default. */
|
|
813
|
+
prefetchOnHover?: boolean;
|
|
814
|
+
/** Key override — useful when host renders multiple lists from the same feed. */
|
|
815
|
+
getKey?: (item: ContentItem, index: number) => string;
|
|
816
|
+
}
|
|
817
|
+
declare function ReelsFeedThumbnail({ renderThumbnail, onThumbnailClick, renderLoading, renderEmpty, renderError, className, wrap, setFocusOnClick, prefetchOnHover, getKey, }: ReelsFeedThumbnailProps): react_jsx_runtime.JSX.Element | null;
|
|
818
|
+
|
|
792
819
|
/**
|
|
793
820
|
* useHls — React hook for hls.js lifecycle management (3-Tier buffer support)
|
|
794
821
|
*
|
|
@@ -875,41 +902,6 @@ interface VideoSlotProps {
|
|
|
875
902
|
}
|
|
876
903
|
declare function VideoSlot({ item, index, isActive, isPrefetch, isPreloaded, bufferTier, isMuted, onToggleMute, onAutoplayBlocked, showFps, isDragging, renderOverlay, renderActions, renderPauseAction, }: VideoSlotProps): react_jsx_runtime.JSX.Element;
|
|
877
904
|
|
|
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
905
|
declare function DefaultOverlay({ item }: {
|
|
914
906
|
item: ContentItem;
|
|
915
907
|
}): react_jsx_runtime.JSX.Element;
|
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,87 +2514,80 @@ 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
2517
|
function ReelsFeedThumbnail({
|
|
2506
2518
|
renderThumbnail,
|
|
2507
2519
|
onThumbnailClick,
|
|
2508
2520
|
renderLoading,
|
|
2509
2521
|
renderEmpty,
|
|
2510
2522
|
renderError,
|
|
2511
|
-
className =
|
|
2523
|
+
className = "grid grid-cols-2 gap-3",
|
|
2512
2524
|
wrap = true,
|
|
2513
2525
|
setFocusOnClick = true,
|
|
2514
2526
|
prefetchOnHover = false,
|
|
2515
|
-
getKey
|
|
2527
|
+
getKey
|
|
2516
2528
|
}) {
|
|
2517
2529
|
const { items, loading, error, refresh } = useFeed();
|
|
2518
2530
|
const { setFocusedIndexImmediate } = useResource();
|
|
2519
2531
|
const { adapters } = useSDK();
|
|
2520
2532
|
const prefetchedRef = useRef(/* @__PURE__ */ new Set());
|
|
2521
2533
|
const handleClick = useCallback(
|
|
2522
|
-
(item, index) => {
|
|
2534
|
+
(id, item, index) => {
|
|
2523
2535
|
if (setFocusOnClick) {
|
|
2524
2536
|
setFocusedIndexImmediate(index);
|
|
2525
2537
|
}
|
|
2526
|
-
onThumbnailClick?.(
|
|
2538
|
+
onThumbnailClick?.(id, item, index);
|
|
2527
2539
|
},
|
|
2528
|
-
[
|
|
2540
|
+
[setFocusOnClick, setFocusedIndexImmediate, onThumbnailClick]
|
|
2529
2541
|
);
|
|
2530
2542
|
const handlePointerEnter = useCallback(
|
|
2531
2543
|
(item) => {
|
|
2532
2544
|
if (!prefetchOnHover) return;
|
|
2533
2545
|
if (!isVideoItem(item)) return;
|
|
2534
2546
|
if (item.source.type !== "hls") return;
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
prefetchedRef.current.add(item.id);
|
|
2540
|
-
return;
|
|
2541
|
-
}
|
|
2542
|
-
loader.preloadMetadata(item.source.url);
|
|
2543
|
-
prefetchedRef.current.add(item.id);
|
|
2547
|
+
const url = item.source.url;
|
|
2548
|
+
if (prefetchedRef.current.has(url)) return;
|
|
2549
|
+
prefetchedRef.current.add(url);
|
|
2550
|
+
adapters.videoLoader?.preloadMetadata?.(url);
|
|
2544
2551
|
},
|
|
2545
2552
|
[prefetchOnHover, adapters.videoLoader]
|
|
2546
2553
|
);
|
|
2547
|
-
|
|
2548
|
-
()
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
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) {
|
|
2554
|
+
if (loading && items.length === 0) {
|
|
2555
|
+
if (renderLoading) return /* @__PURE__ */ jsx(Fragment, { children: renderLoading() });
|
|
2556
|
+
return null;
|
|
2557
|
+
}
|
|
2558
|
+
if (error && items.length === 0) {
|
|
2559
|
+
if (renderError) {
|
|
2570
2560
|
return /* @__PURE__ */ jsx(Fragment, { children: renderError({ message: error.message, retry: refresh }) });
|
|
2571
2561
|
}
|
|
2572
|
-
if (!loading && !error && renderEmpty) return /* @__PURE__ */ jsx(Fragment, { children: renderEmpty() });
|
|
2573
2562
|
return null;
|
|
2574
2563
|
}
|
|
2575
|
-
if (!
|
|
2576
|
-
|
|
2564
|
+
if (!loading && items.length === 0) {
|
|
2565
|
+
if (renderEmpty) return /* @__PURE__ */ jsx(Fragment, { children: renderEmpty() });
|
|
2566
|
+
return null;
|
|
2567
|
+
}
|
|
2568
|
+
const content = items.map((item, index) => {
|
|
2569
|
+
const key = getKey ? getKey(item, index) : item.id;
|
|
2570
|
+
return /* @__PURE__ */ jsx(
|
|
2571
|
+
"button",
|
|
2572
|
+
{
|
|
2573
|
+
type: "button",
|
|
2574
|
+
onClick: () => handleClick(item.id, item, index),
|
|
2575
|
+
onPointerEnter: () => handlePointerEnter(item),
|
|
2576
|
+
style: {
|
|
2577
|
+
all: "unset",
|
|
2578
|
+
cursor: "pointer",
|
|
2579
|
+
display: "block",
|
|
2580
|
+
width: "100%"
|
|
2581
|
+
},
|
|
2582
|
+
children: renderThumbnail(item, index)
|
|
2583
|
+
},
|
|
2584
|
+
key
|
|
2585
|
+
);
|
|
2586
|
+
});
|
|
2587
|
+
if (!wrap) {
|
|
2588
|
+
return /* @__PURE__ */ jsx(Fragment, { children: content });
|
|
2589
|
+
}
|
|
2590
|
+
return /* @__PURE__ */ jsx("div", { className, children: content });
|
|
2577
2591
|
}
|
|
2578
2592
|
function usePlayerSelector(selector) {
|
|
2579
2593
|
const { playerEngine } = useSDK();
|
|
@@ -2995,12 +3009,16 @@ function transformVideoItem(raw) {
|
|
|
2995
3009
|
const stats = transformStats(obj);
|
|
2996
3010
|
const interaction = transformInteraction(obj);
|
|
2997
3011
|
let poster;
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3012
|
+
const thumbnailObj = obj["thumbnail"];
|
|
3013
|
+
if (thumbnailObj && thumbnailObj["url"]) {
|
|
3014
|
+
poster = toStr(thumbnailObj["url"], "") || void 0;
|
|
3015
|
+
}
|
|
3016
|
+
if (!poster) {
|
|
3017
|
+
const mediaArr2 = obj["media"];
|
|
3018
|
+
if (Array.isArray(mediaArr2) && mediaArr2.length > 0) {
|
|
3019
|
+
const first = mediaArr2[0];
|
|
3020
|
+
poster = toStr(first["poster"], "") || void 0;
|
|
3021
|
+
}
|
|
3004
3022
|
}
|
|
3005
3023
|
if (!poster) {
|
|
3006
3024
|
poster = toStr(
|
|
@@ -3008,11 +3026,11 @@ function transformVideoItem(raw) {
|
|
|
3008
3026
|
void 0
|
|
3009
3027
|
) || void 0;
|
|
3010
3028
|
}
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3029
|
+
let duration = 0;
|
|
3030
|
+
const mediaArr = obj["media"];
|
|
3031
|
+
if (Array.isArray(mediaArr) && mediaArr.length > 0) {
|
|
3032
|
+
const first = mediaArr[0];
|
|
3033
|
+
duration = toNum(first["duration"], 0);
|
|
3016
3034
|
}
|
|
3017
3035
|
if (duration === 0) {
|
|
3018
3036
|
duration = toNum(tryFields(obj, "duration", "duration_seconds", "length", "video_duration"), 0);
|