@xhub-reels/sdk 0.1.17 → 0.1.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +64 -12
- package/dist/index.d.cts +17 -2
- package/dist/index.d.ts +17 -2
- package/dist/index.js +64 -12
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -741,9 +741,12 @@ var OptimisticManager = class {
|
|
|
741
741
|
}
|
|
742
742
|
};
|
|
743
743
|
var DEFAULT_RESOURCE_CONFIG = {
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
744
|
+
// bufferWindow=2 → ±2 hot slots + 1 active = 5 HLS instances max.
|
|
745
|
+
// Reduced from 3 to eliminate media pipeline thread contention on iOS/Android WebView
|
|
746
|
+
// that caused jank after scrolling 4-5 videos (7 concurrent HLS instances was too many).
|
|
747
|
+
maxAllocations: 8,
|
|
748
|
+
bufferWindow: 2,
|
|
749
|
+
warmWindow: 3,
|
|
747
750
|
// 0ms debounce — setFocusedIndexImmediate is already used post-snap.
|
|
748
751
|
focusDebounceMs: 0,
|
|
749
752
|
preloadLookAhead: 3
|
|
@@ -1393,7 +1396,7 @@ function mapHlsError(data) {
|
|
|
1393
1396
|
}
|
|
1394
1397
|
}
|
|
1395
1398
|
function useHls(options) {
|
|
1396
|
-
const { src, videoRef, isActive, isPrefetch, bufferTier = "active", hlsConfig, onError } = options;
|
|
1399
|
+
const { src, videoRef, isActive, isPrefetch, bufferTier = "active", hlsConfig, onError, isDragging = false } = options;
|
|
1397
1400
|
const isHlsSupported = typeof window !== "undefined" && Hls__default.default.isSupported();
|
|
1398
1401
|
const isNative = supportsNativeHls();
|
|
1399
1402
|
const isHlsJs = isHlsSupported && !isNative;
|
|
@@ -1549,6 +1552,21 @@ function useHls(options) {
|
|
|
1549
1552
|
}
|
|
1550
1553
|
}
|
|
1551
1554
|
}, [bufferTier]);
|
|
1555
|
+
react.useEffect(() => {
|
|
1556
|
+
const hls = hlsRef.current;
|
|
1557
|
+
if (!hls || isActive) return;
|
|
1558
|
+
if (isDragging) {
|
|
1559
|
+
try {
|
|
1560
|
+
hls.stopLoad();
|
|
1561
|
+
} catch {
|
|
1562
|
+
}
|
|
1563
|
+
} else {
|
|
1564
|
+
try {
|
|
1565
|
+
hls.startLoad(-1);
|
|
1566
|
+
} catch {
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
}, [isDragging, isActive]);
|
|
1552
1570
|
return {
|
|
1553
1571
|
isHlsJs,
|
|
1554
1572
|
isReady,
|
|
@@ -1705,7 +1723,8 @@ function DefaultPauseAction() {
|
|
|
1705
1723
|
}
|
|
1706
1724
|
);
|
|
1707
1725
|
}
|
|
1708
|
-
var PLAY_AHEAD_MAX_CONCURRENT =
|
|
1726
|
+
var PLAY_AHEAD_MAX_CONCURRENT = 3;
|
|
1727
|
+
var PLAY_AHEAD_STAGGER_MS = 50;
|
|
1709
1728
|
var _playAheadActive = 0;
|
|
1710
1729
|
var _playAheadQueue = [];
|
|
1711
1730
|
function acquirePlayAhead() {
|
|
@@ -1713,7 +1732,12 @@ function acquirePlayAhead() {
|
|
|
1713
1732
|
_playAheadActive++;
|
|
1714
1733
|
return Promise.resolve();
|
|
1715
1734
|
}
|
|
1716
|
-
return new Promise((resolve) =>
|
|
1735
|
+
return new Promise((resolve) => {
|
|
1736
|
+
const queuePosition = _playAheadQueue.length;
|
|
1737
|
+
_playAheadQueue.push(() => {
|
|
1738
|
+
setTimeout(resolve, PLAY_AHEAD_STAGGER_MS * queuePosition);
|
|
1739
|
+
});
|
|
1740
|
+
});
|
|
1717
1741
|
}
|
|
1718
1742
|
function releasePlayAhead() {
|
|
1719
1743
|
_playAheadActive = Math.max(0, _playAheadActive - 1);
|
|
@@ -1734,6 +1758,7 @@ function VideoSlot({
|
|
|
1734
1758
|
onToggleMute,
|
|
1735
1759
|
onAutoplayBlocked,
|
|
1736
1760
|
showFps = false,
|
|
1761
|
+
isDragging = false,
|
|
1737
1762
|
renderOverlay,
|
|
1738
1763
|
renderActions,
|
|
1739
1764
|
renderPauseAction
|
|
@@ -1771,6 +1796,7 @@ function VideoSlot({
|
|
|
1771
1796
|
onToggleMute,
|
|
1772
1797
|
onAutoplayBlocked,
|
|
1773
1798
|
showFps,
|
|
1799
|
+
isDragging,
|
|
1774
1800
|
renderOverlay,
|
|
1775
1801
|
renderActions,
|
|
1776
1802
|
renderPauseAction,
|
|
@@ -1790,6 +1816,7 @@ function VideoSlotInner({
|
|
|
1790
1816
|
onToggleMute,
|
|
1791
1817
|
onAutoplayBlocked,
|
|
1792
1818
|
showFps,
|
|
1819
|
+
isDragging,
|
|
1793
1820
|
renderOverlay,
|
|
1794
1821
|
renderActions,
|
|
1795
1822
|
renderPauseAction,
|
|
@@ -1811,6 +1838,7 @@ function VideoSlotInner({
|
|
|
1811
1838
|
// so useHls creates the HLS instance and starts buffering
|
|
1812
1839
|
isPrefetch: isPrefetch || isPreloaded,
|
|
1813
1840
|
bufferTier,
|
|
1841
|
+
isDragging,
|
|
1814
1842
|
onError: (code, message) => {
|
|
1815
1843
|
console.error(`[VideoSlot] HLS error: ${code} \u2014 ${message}`);
|
|
1816
1844
|
}
|
|
@@ -1947,6 +1975,7 @@ function VideoSlotInner({
|
|
|
1947
1975
|
video.muted = isActive ? isMuted : true;
|
|
1948
1976
|
}, [isMuted, isActive]);
|
|
1949
1977
|
const showPosterOverlay = !isReady && !hasPlayedAhead;
|
|
1978
|
+
const isPreDecoded = hasPlayedAhead;
|
|
1950
1979
|
const [showMuteIndicator, setShowMuteIndicator] = react.useState(false);
|
|
1951
1980
|
const muteIndicatorTimer = react.useRef(null);
|
|
1952
1981
|
const handleToggleMute = react.useCallback(() => {
|
|
@@ -2062,13 +2091,14 @@ function VideoSlotInner({
|
|
|
2062
2091
|
width: "100%",
|
|
2063
2092
|
height: "100%",
|
|
2064
2093
|
objectFit: "cover",
|
|
2065
|
-
// Hide video until ready to avoid black frame flash
|
|
2094
|
+
// Hide video until ready to avoid black frame flash.
|
|
2095
|
+
// When pre-decoded, skip transition — first frame is already on canvas.
|
|
2066
2096
|
opacity: showPosterOverlay ? 0 : 1,
|
|
2067
|
-
transition: "opacity 0.15s ease"
|
|
2097
|
+
transition: isPreDecoded ? "none" : "opacity 0.15s ease"
|
|
2068
2098
|
}
|
|
2069
2099
|
}
|
|
2070
2100
|
),
|
|
2071
|
-
item.poster && /* @__PURE__ */ jsxRuntime.jsx(
|
|
2101
|
+
item.poster && !isPreDecoded && /* @__PURE__ */ jsxRuntime.jsx(
|
|
2072
2102
|
"div",
|
|
2073
2103
|
{
|
|
2074
2104
|
style: {
|
|
@@ -2198,6 +2228,7 @@ function FpsCounter() {
|
|
|
2198
2228
|
);
|
|
2199
2229
|
}
|
|
2200
2230
|
var RENDER_WINDOW_RADIUS = 3;
|
|
2231
|
+
var PLACEHOLDER_EXTRA = 2;
|
|
2201
2232
|
var centerStyle = {
|
|
2202
2233
|
height: "100dvh",
|
|
2203
2234
|
display: "flex",
|
|
@@ -2275,9 +2306,20 @@ function ReelsFeed({
|
|
|
2275
2306
|
}
|
|
2276
2307
|
};
|
|
2277
2308
|
rebuild();
|
|
2278
|
-
|
|
2309
|
+
let rebuildTimer = null;
|
|
2310
|
+
const debouncedRebuild = () => {
|
|
2311
|
+
if (rebuildTimer !== null) return;
|
|
2312
|
+
rebuildTimer = setTimeout(() => {
|
|
2313
|
+
rebuildTimer = null;
|
|
2314
|
+
rebuild();
|
|
2315
|
+
}, 16);
|
|
2316
|
+
};
|
|
2317
|
+
const observer = new MutationObserver(debouncedRebuild);
|
|
2279
2318
|
observer.observe(container, { childList: true, subtree: true });
|
|
2280
|
-
return () =>
|
|
2319
|
+
return () => {
|
|
2320
|
+
observer.disconnect();
|
|
2321
|
+
if (rebuildTimer !== null) clearTimeout(rebuildTimer);
|
|
2322
|
+
};
|
|
2281
2323
|
}, [items.length, focusedIndex]);
|
|
2282
2324
|
const containerHeight = react.useRef(
|
|
2283
2325
|
typeof window !== "undefined" ? window.innerHeight : 800
|
|
@@ -2403,6 +2445,15 @@ function ReelsFeed({
|
|
|
2403
2445
|
const handleToggleMute = react.useCallback(() => {
|
|
2404
2446
|
setIsMuted((prev) => !prev);
|
|
2405
2447
|
}, []);
|
|
2448
|
+
const windowedItems = react.useMemo(() => {
|
|
2449
|
+
const start = Math.max(0, focusedIndex - RENDER_WINDOW_RADIUS - PLACEHOLDER_EXTRA);
|
|
2450
|
+
const end = Math.min(items.length - 1, focusedIndex + RENDER_WINDOW_RADIUS + PLACEHOLDER_EXTRA);
|
|
2451
|
+
const result = [];
|
|
2452
|
+
for (let i = start; i <= end; i++) {
|
|
2453
|
+
result.push({ item: items[i], index: i });
|
|
2454
|
+
}
|
|
2455
|
+
return result;
|
|
2456
|
+
}, [items, focusedIndex]);
|
|
2406
2457
|
if (loading && items.length === 0) {
|
|
2407
2458
|
return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { ...centerStyle, flexDirection: "column", gap: 0 }, children: renderLoading ? renderLoading() : /* @__PURE__ */ jsxRuntime.jsx(DefaultSkeleton, {}) });
|
|
2408
2459
|
}
|
|
@@ -2439,7 +2490,7 @@ function ReelsFeed({
|
|
|
2439
2490
|
to { transform: rotate(360deg); }
|
|
2440
2491
|
}
|
|
2441
2492
|
` }),
|
|
2442
|
-
|
|
2493
|
+
windowedItems.map(({ item, index }) => {
|
|
2443
2494
|
const distFromFocus = Math.abs(index - focusedIndex);
|
|
2444
2495
|
const wrapperStyle = {
|
|
2445
2496
|
position: "absolute",
|
|
@@ -2474,6 +2525,7 @@ function ReelsFeed({
|
|
|
2474
2525
|
onToggleMute: handleToggleMute,
|
|
2475
2526
|
onAutoplayBlocked,
|
|
2476
2527
|
showFps: showFps && isActive,
|
|
2528
|
+
isDragging: isDragMuted,
|
|
2477
2529
|
renderOverlay,
|
|
2478
2530
|
renderActions,
|
|
2479
2531
|
renderPauseAction
|
package/dist/index.d.cts
CHANGED
|
@@ -577,7 +577,10 @@ declare class OptimisticManager {
|
|
|
577
577
|
* Tier 4 (Cold): preloadLookAhead beyond warm — metadata/manifest prefetch only (no DOM)
|
|
578
578
|
*
|
|
579
579
|
* Total DOM nodes = 1 + 2×bufferWindow + warmWindow (forward) + 1 (backward)
|
|
580
|
-
* Default: 1 +
|
|
580
|
+
* Default: 1 + 4 + 3 = 8 DOM nodes, ~80 MB memory (fits comfortably in 1 GB budget)
|
|
581
|
+
*
|
|
582
|
+
* bufferWindow reduced from 3→2 to cut concurrent HLS instances from 7→5,
|
|
583
|
+
* eliminating media pipeline thread contention on iOS/Android WebView.
|
|
581
584
|
*/
|
|
582
585
|
|
|
583
586
|
interface ResourceState {
|
|
@@ -831,6 +834,13 @@ interface UseHlsOptions {
|
|
|
831
834
|
hlsConfig?: Partial<HlsConfig>;
|
|
832
835
|
/** Called when hls.js encounters a fatal error. Maps to PlayerEngine error codes. */
|
|
833
836
|
onError?: (code: 'NETWORK_ERROR' | 'MEDIA_ERROR' | 'DECODE_ERROR' | 'UNKNOWN', message: string) => void;
|
|
837
|
+
/**
|
|
838
|
+
* Whether the user is currently dragging/swiping.
|
|
839
|
+
* When true, non-active slots pause HLS network fetching (hls.stopLoad) to
|
|
840
|
+
* free bandwidth for the active slot and reduce main-thread contention.
|
|
841
|
+
* Active slot is never paused regardless of this flag.
|
|
842
|
+
*/
|
|
843
|
+
isDragging?: boolean;
|
|
834
844
|
}
|
|
835
845
|
interface UseHlsReturn {
|
|
836
846
|
/** Whether hls.js is being used (false = native HLS on Safari) */
|
|
@@ -854,11 +864,16 @@ interface VideoSlotProps {
|
|
|
854
864
|
/** Called when unmuted autoplay fails and SDK falls back to muted playback */
|
|
855
865
|
onAutoplayBlocked?: () => void;
|
|
856
866
|
showFps?: boolean;
|
|
867
|
+
/**
|
|
868
|
+
* Whether the user is currently dragging. Passed to useHls to pause
|
|
869
|
+
* non-active HLS fetching during gesture, freeing bandwidth + main thread.
|
|
870
|
+
*/
|
|
871
|
+
isDragging?: boolean;
|
|
857
872
|
renderOverlay?: (item: ContentItem, actions: SlotActions) => react.ReactNode;
|
|
858
873
|
renderActions?: (item: ContentItem, actions: SlotActions) => react.ReactNode;
|
|
859
874
|
renderPauseAction?: (item: ContentItem, actions: PauseSlotActions) => react.ReactNode;
|
|
860
875
|
}
|
|
861
|
-
declare function VideoSlot({ item, index, isActive, isPrefetch, isPreloaded, bufferTier, isMuted, onToggleMute, onAutoplayBlocked, showFps, renderOverlay, renderActions, renderPauseAction, }: VideoSlotProps): react_jsx_runtime.JSX.Element;
|
|
876
|
+
declare function VideoSlot({ item, index, isActive, isPrefetch, isPreloaded, bufferTier, isMuted, onToggleMute, onAutoplayBlocked, showFps, isDragging, renderOverlay, renderActions, renderPauseAction, }: VideoSlotProps): react_jsx_runtime.JSX.Element;
|
|
862
877
|
|
|
863
878
|
declare function DefaultOverlay({ item }: {
|
|
864
879
|
item: ContentItem;
|
package/dist/index.d.ts
CHANGED
|
@@ -577,7 +577,10 @@ declare class OptimisticManager {
|
|
|
577
577
|
* Tier 4 (Cold): preloadLookAhead beyond warm — metadata/manifest prefetch only (no DOM)
|
|
578
578
|
*
|
|
579
579
|
* Total DOM nodes = 1 + 2×bufferWindow + warmWindow (forward) + 1 (backward)
|
|
580
|
-
* Default: 1 +
|
|
580
|
+
* Default: 1 + 4 + 3 = 8 DOM nodes, ~80 MB memory (fits comfortably in 1 GB budget)
|
|
581
|
+
*
|
|
582
|
+
* bufferWindow reduced from 3→2 to cut concurrent HLS instances from 7→5,
|
|
583
|
+
* eliminating media pipeline thread contention on iOS/Android WebView.
|
|
581
584
|
*/
|
|
582
585
|
|
|
583
586
|
interface ResourceState {
|
|
@@ -831,6 +834,13 @@ interface UseHlsOptions {
|
|
|
831
834
|
hlsConfig?: Partial<HlsConfig>;
|
|
832
835
|
/** Called when hls.js encounters a fatal error. Maps to PlayerEngine error codes. */
|
|
833
836
|
onError?: (code: 'NETWORK_ERROR' | 'MEDIA_ERROR' | 'DECODE_ERROR' | 'UNKNOWN', message: string) => void;
|
|
837
|
+
/**
|
|
838
|
+
* Whether the user is currently dragging/swiping.
|
|
839
|
+
* When true, non-active slots pause HLS network fetching (hls.stopLoad) to
|
|
840
|
+
* free bandwidth for the active slot and reduce main-thread contention.
|
|
841
|
+
* Active slot is never paused regardless of this flag.
|
|
842
|
+
*/
|
|
843
|
+
isDragging?: boolean;
|
|
834
844
|
}
|
|
835
845
|
interface UseHlsReturn {
|
|
836
846
|
/** Whether hls.js is being used (false = native HLS on Safari) */
|
|
@@ -854,11 +864,16 @@ interface VideoSlotProps {
|
|
|
854
864
|
/** Called when unmuted autoplay fails and SDK falls back to muted playback */
|
|
855
865
|
onAutoplayBlocked?: () => void;
|
|
856
866
|
showFps?: boolean;
|
|
867
|
+
/**
|
|
868
|
+
* Whether the user is currently dragging. Passed to useHls to pause
|
|
869
|
+
* non-active HLS fetching during gesture, freeing bandwidth + main thread.
|
|
870
|
+
*/
|
|
871
|
+
isDragging?: boolean;
|
|
857
872
|
renderOverlay?: (item: ContentItem, actions: SlotActions) => react.ReactNode;
|
|
858
873
|
renderActions?: (item: ContentItem, actions: SlotActions) => react.ReactNode;
|
|
859
874
|
renderPauseAction?: (item: ContentItem, actions: PauseSlotActions) => react.ReactNode;
|
|
860
875
|
}
|
|
861
|
-
declare function VideoSlot({ item, index, isActive, isPrefetch, isPreloaded, bufferTier, isMuted, onToggleMute, onAutoplayBlocked, showFps, renderOverlay, renderActions, renderPauseAction, }: VideoSlotProps): react_jsx_runtime.JSX.Element;
|
|
876
|
+
declare function VideoSlot({ item, index, isActive, isPrefetch, isPreloaded, bufferTier, isMuted, onToggleMute, onAutoplayBlocked, showFps, isDragging, renderOverlay, renderActions, renderPauseAction, }: VideoSlotProps): react_jsx_runtime.JSX.Element;
|
|
862
877
|
|
|
863
878
|
declare function DefaultOverlay({ item }: {
|
|
864
879
|
item: ContentItem;
|
package/dist/index.js
CHANGED
|
@@ -735,9 +735,12 @@ var OptimisticManager = class {
|
|
|
735
735
|
}
|
|
736
736
|
};
|
|
737
737
|
var DEFAULT_RESOURCE_CONFIG = {
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
738
|
+
// bufferWindow=2 → ±2 hot slots + 1 active = 5 HLS instances max.
|
|
739
|
+
// Reduced from 3 to eliminate media pipeline thread contention on iOS/Android WebView
|
|
740
|
+
// that caused jank after scrolling 4-5 videos (7 concurrent HLS instances was too many).
|
|
741
|
+
maxAllocations: 8,
|
|
742
|
+
bufferWindow: 2,
|
|
743
|
+
warmWindow: 3,
|
|
741
744
|
// 0ms debounce — setFocusedIndexImmediate is already used post-snap.
|
|
742
745
|
focusDebounceMs: 0,
|
|
743
746
|
preloadLookAhead: 3
|
|
@@ -1387,7 +1390,7 @@ function mapHlsError(data) {
|
|
|
1387
1390
|
}
|
|
1388
1391
|
}
|
|
1389
1392
|
function useHls(options) {
|
|
1390
|
-
const { src, videoRef, isActive, isPrefetch, bufferTier = "active", hlsConfig, onError } = options;
|
|
1393
|
+
const { src, videoRef, isActive, isPrefetch, bufferTier = "active", hlsConfig, onError, isDragging = false } = options;
|
|
1391
1394
|
const isHlsSupported = typeof window !== "undefined" && Hls.isSupported();
|
|
1392
1395
|
const isNative = supportsNativeHls();
|
|
1393
1396
|
const isHlsJs = isHlsSupported && !isNative;
|
|
@@ -1543,6 +1546,21 @@ function useHls(options) {
|
|
|
1543
1546
|
}
|
|
1544
1547
|
}
|
|
1545
1548
|
}, [bufferTier]);
|
|
1549
|
+
useEffect(() => {
|
|
1550
|
+
const hls = hlsRef.current;
|
|
1551
|
+
if (!hls || isActive) return;
|
|
1552
|
+
if (isDragging) {
|
|
1553
|
+
try {
|
|
1554
|
+
hls.stopLoad();
|
|
1555
|
+
} catch {
|
|
1556
|
+
}
|
|
1557
|
+
} else {
|
|
1558
|
+
try {
|
|
1559
|
+
hls.startLoad(-1);
|
|
1560
|
+
} catch {
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
}, [isDragging, isActive]);
|
|
1546
1564
|
return {
|
|
1547
1565
|
isHlsJs,
|
|
1548
1566
|
isReady,
|
|
@@ -1699,7 +1717,8 @@ function DefaultPauseAction() {
|
|
|
1699
1717
|
}
|
|
1700
1718
|
);
|
|
1701
1719
|
}
|
|
1702
|
-
var PLAY_AHEAD_MAX_CONCURRENT =
|
|
1720
|
+
var PLAY_AHEAD_MAX_CONCURRENT = 3;
|
|
1721
|
+
var PLAY_AHEAD_STAGGER_MS = 50;
|
|
1703
1722
|
var _playAheadActive = 0;
|
|
1704
1723
|
var _playAheadQueue = [];
|
|
1705
1724
|
function acquirePlayAhead() {
|
|
@@ -1707,7 +1726,12 @@ function acquirePlayAhead() {
|
|
|
1707
1726
|
_playAheadActive++;
|
|
1708
1727
|
return Promise.resolve();
|
|
1709
1728
|
}
|
|
1710
|
-
return new Promise((resolve) =>
|
|
1729
|
+
return new Promise((resolve) => {
|
|
1730
|
+
const queuePosition = _playAheadQueue.length;
|
|
1731
|
+
_playAheadQueue.push(() => {
|
|
1732
|
+
setTimeout(resolve, PLAY_AHEAD_STAGGER_MS * queuePosition);
|
|
1733
|
+
});
|
|
1734
|
+
});
|
|
1711
1735
|
}
|
|
1712
1736
|
function releasePlayAhead() {
|
|
1713
1737
|
_playAheadActive = Math.max(0, _playAheadActive - 1);
|
|
@@ -1728,6 +1752,7 @@ function VideoSlot({
|
|
|
1728
1752
|
onToggleMute,
|
|
1729
1753
|
onAutoplayBlocked,
|
|
1730
1754
|
showFps = false,
|
|
1755
|
+
isDragging = false,
|
|
1731
1756
|
renderOverlay,
|
|
1732
1757
|
renderActions,
|
|
1733
1758
|
renderPauseAction
|
|
@@ -1765,6 +1790,7 @@ function VideoSlot({
|
|
|
1765
1790
|
onToggleMute,
|
|
1766
1791
|
onAutoplayBlocked,
|
|
1767
1792
|
showFps,
|
|
1793
|
+
isDragging,
|
|
1768
1794
|
renderOverlay,
|
|
1769
1795
|
renderActions,
|
|
1770
1796
|
renderPauseAction,
|
|
@@ -1784,6 +1810,7 @@ function VideoSlotInner({
|
|
|
1784
1810
|
onToggleMute,
|
|
1785
1811
|
onAutoplayBlocked,
|
|
1786
1812
|
showFps,
|
|
1813
|
+
isDragging,
|
|
1787
1814
|
renderOverlay,
|
|
1788
1815
|
renderActions,
|
|
1789
1816
|
renderPauseAction,
|
|
@@ -1805,6 +1832,7 @@ function VideoSlotInner({
|
|
|
1805
1832
|
// so useHls creates the HLS instance and starts buffering
|
|
1806
1833
|
isPrefetch: isPrefetch || isPreloaded,
|
|
1807
1834
|
bufferTier,
|
|
1835
|
+
isDragging,
|
|
1808
1836
|
onError: (code, message) => {
|
|
1809
1837
|
console.error(`[VideoSlot] HLS error: ${code} \u2014 ${message}`);
|
|
1810
1838
|
}
|
|
@@ -1941,6 +1969,7 @@ function VideoSlotInner({
|
|
|
1941
1969
|
video.muted = isActive ? isMuted : true;
|
|
1942
1970
|
}, [isMuted, isActive]);
|
|
1943
1971
|
const showPosterOverlay = !isReady && !hasPlayedAhead;
|
|
1972
|
+
const isPreDecoded = hasPlayedAhead;
|
|
1944
1973
|
const [showMuteIndicator, setShowMuteIndicator] = useState(false);
|
|
1945
1974
|
const muteIndicatorTimer = useRef(null);
|
|
1946
1975
|
const handleToggleMute = useCallback(() => {
|
|
@@ -2056,13 +2085,14 @@ function VideoSlotInner({
|
|
|
2056
2085
|
width: "100%",
|
|
2057
2086
|
height: "100%",
|
|
2058
2087
|
objectFit: "cover",
|
|
2059
|
-
// Hide video until ready to avoid black frame flash
|
|
2088
|
+
// Hide video until ready to avoid black frame flash.
|
|
2089
|
+
// When pre-decoded, skip transition — first frame is already on canvas.
|
|
2060
2090
|
opacity: showPosterOverlay ? 0 : 1,
|
|
2061
|
-
transition: "opacity 0.15s ease"
|
|
2091
|
+
transition: isPreDecoded ? "none" : "opacity 0.15s ease"
|
|
2062
2092
|
}
|
|
2063
2093
|
}
|
|
2064
2094
|
),
|
|
2065
|
-
item.poster && /* @__PURE__ */ jsx(
|
|
2095
|
+
item.poster && !isPreDecoded && /* @__PURE__ */ jsx(
|
|
2066
2096
|
"div",
|
|
2067
2097
|
{
|
|
2068
2098
|
style: {
|
|
@@ -2192,6 +2222,7 @@ function FpsCounter() {
|
|
|
2192
2222
|
);
|
|
2193
2223
|
}
|
|
2194
2224
|
var RENDER_WINDOW_RADIUS = 3;
|
|
2225
|
+
var PLACEHOLDER_EXTRA = 2;
|
|
2195
2226
|
var centerStyle = {
|
|
2196
2227
|
height: "100dvh",
|
|
2197
2228
|
display: "flex",
|
|
@@ -2269,9 +2300,20 @@ function ReelsFeed({
|
|
|
2269
2300
|
}
|
|
2270
2301
|
};
|
|
2271
2302
|
rebuild();
|
|
2272
|
-
|
|
2303
|
+
let rebuildTimer = null;
|
|
2304
|
+
const debouncedRebuild = () => {
|
|
2305
|
+
if (rebuildTimer !== null) return;
|
|
2306
|
+
rebuildTimer = setTimeout(() => {
|
|
2307
|
+
rebuildTimer = null;
|
|
2308
|
+
rebuild();
|
|
2309
|
+
}, 16);
|
|
2310
|
+
};
|
|
2311
|
+
const observer = new MutationObserver(debouncedRebuild);
|
|
2273
2312
|
observer.observe(container, { childList: true, subtree: true });
|
|
2274
|
-
return () =>
|
|
2313
|
+
return () => {
|
|
2314
|
+
observer.disconnect();
|
|
2315
|
+
if (rebuildTimer !== null) clearTimeout(rebuildTimer);
|
|
2316
|
+
};
|
|
2275
2317
|
}, [items.length, focusedIndex]);
|
|
2276
2318
|
const containerHeight = useRef(
|
|
2277
2319
|
typeof window !== "undefined" ? window.innerHeight : 800
|
|
@@ -2397,6 +2439,15 @@ function ReelsFeed({
|
|
|
2397
2439
|
const handleToggleMute = useCallback(() => {
|
|
2398
2440
|
setIsMuted((prev) => !prev);
|
|
2399
2441
|
}, []);
|
|
2442
|
+
const windowedItems = useMemo(() => {
|
|
2443
|
+
const start = Math.max(0, focusedIndex - RENDER_WINDOW_RADIUS - PLACEHOLDER_EXTRA);
|
|
2444
|
+
const end = Math.min(items.length - 1, focusedIndex + RENDER_WINDOW_RADIUS + PLACEHOLDER_EXTRA);
|
|
2445
|
+
const result = [];
|
|
2446
|
+
for (let i = start; i <= end; i++) {
|
|
2447
|
+
result.push({ item: items[i], index: i });
|
|
2448
|
+
}
|
|
2449
|
+
return result;
|
|
2450
|
+
}, [items, focusedIndex]);
|
|
2400
2451
|
if (loading && items.length === 0) {
|
|
2401
2452
|
return /* @__PURE__ */ jsx("div", { style: { ...centerStyle, flexDirection: "column", gap: 0 }, children: renderLoading ? renderLoading() : /* @__PURE__ */ jsx(DefaultSkeleton, {}) });
|
|
2402
2453
|
}
|
|
@@ -2433,7 +2484,7 @@ function ReelsFeed({
|
|
|
2433
2484
|
to { transform: rotate(360deg); }
|
|
2434
2485
|
}
|
|
2435
2486
|
` }),
|
|
2436
|
-
|
|
2487
|
+
windowedItems.map(({ item, index }) => {
|
|
2437
2488
|
const distFromFocus = Math.abs(index - focusedIndex);
|
|
2438
2489
|
const wrapperStyle = {
|
|
2439
2490
|
position: "absolute",
|
|
@@ -2468,6 +2519,7 @@ function ReelsFeed({
|
|
|
2468
2519
|
onToggleMute: handleToggleMute,
|
|
2469
2520
|
onAutoplayBlocked,
|
|
2470
2521
|
showFps: showFps && isActive,
|
|
2522
|
+
isDragging: isDragMuted,
|
|
2471
2523
|
renderOverlay,
|
|
2472
2524
|
renderActions,
|
|
2473
2525
|
renderPauseAction
|