@xhub-reels/sdk 0.1.18 → 0.1.20
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 +73 -19
- package/dist/index.d.cts +17 -2
- package/dist/index.d.ts +17 -2
- package/dist/index.js +73 -19
- 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,
|
|
@@ -1706,6 +1724,7 @@ function DefaultPauseAction() {
|
|
|
1706
1724
|
);
|
|
1707
1725
|
}
|
|
1708
1726
|
var PLAY_AHEAD_MAX_CONCURRENT = 2;
|
|
1727
|
+
var PLAY_AHEAD_STAGGER_MS = 80;
|
|
1709
1728
|
var _playAheadActive = 0;
|
|
1710
1729
|
var _playAheadQueue = [];
|
|
1711
1730
|
function acquirePlayAhead() {
|
|
@@ -1713,7 +1732,11 @@ function acquirePlayAhead() {
|
|
|
1713
1732
|
_playAheadActive++;
|
|
1714
1733
|
return Promise.resolve();
|
|
1715
1734
|
}
|
|
1716
|
-
return new Promise((resolve) =>
|
|
1735
|
+
return new Promise((resolve) => {
|
|
1736
|
+
_playAheadQueue.push(() => {
|
|
1737
|
+
setTimeout(resolve, PLAY_AHEAD_STAGGER_MS);
|
|
1738
|
+
});
|
|
1739
|
+
});
|
|
1717
1740
|
}
|
|
1718
1741
|
function releasePlayAhead() {
|
|
1719
1742
|
_playAheadActive = Math.max(0, _playAheadActive - 1);
|
|
@@ -1734,6 +1757,7 @@ function VideoSlot({
|
|
|
1734
1757
|
onToggleMute,
|
|
1735
1758
|
onAutoplayBlocked,
|
|
1736
1759
|
showFps = false,
|
|
1760
|
+
isDragging = false,
|
|
1737
1761
|
renderOverlay,
|
|
1738
1762
|
renderActions,
|
|
1739
1763
|
renderPauseAction
|
|
@@ -1771,6 +1795,7 @@ function VideoSlot({
|
|
|
1771
1795
|
onToggleMute,
|
|
1772
1796
|
onAutoplayBlocked,
|
|
1773
1797
|
showFps,
|
|
1798
|
+
isDragging,
|
|
1774
1799
|
renderOverlay,
|
|
1775
1800
|
renderActions,
|
|
1776
1801
|
renderPauseAction,
|
|
@@ -1790,6 +1815,7 @@ function VideoSlotInner({
|
|
|
1790
1815
|
onToggleMute,
|
|
1791
1816
|
onAutoplayBlocked,
|
|
1792
1817
|
showFps,
|
|
1818
|
+
isDragging,
|
|
1793
1819
|
renderOverlay,
|
|
1794
1820
|
renderActions,
|
|
1795
1821
|
renderPauseAction,
|
|
@@ -1811,6 +1837,7 @@ function VideoSlotInner({
|
|
|
1811
1837
|
// so useHls creates the HLS instance and starts buffering
|
|
1812
1838
|
isPrefetch: isPrefetch || isPreloaded,
|
|
1813
1839
|
bufferTier,
|
|
1840
|
+
isDragging,
|
|
1814
1841
|
onError: (code, message) => {
|
|
1815
1842
|
console.error(`[VideoSlot] HLS error: ${code} \u2014 ${message}`);
|
|
1816
1843
|
}
|
|
@@ -2200,6 +2227,7 @@ function FpsCounter() {
|
|
|
2200
2227
|
);
|
|
2201
2228
|
}
|
|
2202
2229
|
var RENDER_WINDOW_RADIUS = 3;
|
|
2230
|
+
var PLACEHOLDER_EXTRA = 2;
|
|
2203
2231
|
var centerStyle = {
|
|
2204
2232
|
height: "100dvh",
|
|
2205
2233
|
display: "flex",
|
|
@@ -2241,7 +2269,6 @@ function ReelsFeed({
|
|
|
2241
2269
|
const slotCacheRef = react.useRef(/* @__PURE__ */ new Map());
|
|
2242
2270
|
const activeIndexRef = react.useRef(0);
|
|
2243
2271
|
activeIndexRef.current = focusedIndex;
|
|
2244
|
-
const [isSnapping, setIsSnapping] = react.useState(false);
|
|
2245
2272
|
const { animateSnap, animateBounceBack, cancelAnimation } = useSnapAnimation({
|
|
2246
2273
|
duration: snapConfig?.duration ?? 260,
|
|
2247
2274
|
easing: snapConfig?.easing ?? "cubic-bezier(0.25, 0.46, 0.45, 0.94)"
|
|
@@ -2277,9 +2304,20 @@ function ReelsFeed({
|
|
|
2277
2304
|
}
|
|
2278
2305
|
};
|
|
2279
2306
|
rebuild();
|
|
2280
|
-
|
|
2307
|
+
let rebuildTimer = null;
|
|
2308
|
+
const debouncedRebuild = () => {
|
|
2309
|
+
if (rebuildTimer !== null) return;
|
|
2310
|
+
rebuildTimer = setTimeout(() => {
|
|
2311
|
+
rebuildTimer = null;
|
|
2312
|
+
rebuild();
|
|
2313
|
+
}, 16);
|
|
2314
|
+
};
|
|
2315
|
+
const observer = new MutationObserver(debouncedRebuild);
|
|
2281
2316
|
observer.observe(container, { childList: true, subtree: true });
|
|
2282
|
-
return () =>
|
|
2317
|
+
return () => {
|
|
2318
|
+
observer.disconnect();
|
|
2319
|
+
if (rebuildTimer !== null) clearTimeout(rebuildTimer);
|
|
2320
|
+
};
|
|
2283
2321
|
}, [items.length, focusedIndex]);
|
|
2284
2322
|
const containerHeight = react.useRef(
|
|
2285
2323
|
typeof window !== "undefined" ? window.innerHeight : 800
|
|
@@ -2336,13 +2374,7 @@ function ReelsFeed({
|
|
|
2336
2374
|
return;
|
|
2337
2375
|
}
|
|
2338
2376
|
cancelAnimation();
|
|
2339
|
-
setIsSnapping(true);
|
|
2340
2377
|
setPrefetchIndex(null);
|
|
2341
|
-
setFocusedIndexImmediate(next);
|
|
2342
|
-
const nextItem = items[next];
|
|
2343
|
-
if (nextItem) {
|
|
2344
|
-
onSlotChange?.(next, nextItem, current);
|
|
2345
|
-
}
|
|
2346
2378
|
const h = containerHeight.current;
|
|
2347
2379
|
const targets = [];
|
|
2348
2380
|
for (const [idx, el] of slotCacheRef.current) {
|
|
@@ -2353,7 +2385,13 @@ function ReelsFeed({
|
|
|
2353
2385
|
});
|
|
2354
2386
|
}
|
|
2355
2387
|
animateSnap(targets);
|
|
2356
|
-
|
|
2388
|
+
requestAnimationFrame(() => {
|
|
2389
|
+
setFocusedIndexImmediate(next);
|
|
2390
|
+
const nextItem = items[next];
|
|
2391
|
+
if (nextItem) {
|
|
2392
|
+
onSlotChange?.(next, nextItem, current);
|
|
2393
|
+
}
|
|
2394
|
+
});
|
|
2357
2395
|
},
|
|
2358
2396
|
[items, animateSnap, animateBounceBack, cancelAnimation, setFocusedIndexImmediate, setPrefetchIndex, onSlotChange]
|
|
2359
2397
|
);
|
|
@@ -2375,13 +2413,13 @@ function ReelsFeed({
|
|
|
2375
2413
|
setIsDragMuted(true);
|
|
2376
2414
|
}, []);
|
|
2377
2415
|
const handleDragEnd = react.useCallback(() => {
|
|
2378
|
-
setTimeout(() => setIsDragMuted(false),
|
|
2416
|
+
setTimeout(() => setIsDragMuted(false), 300);
|
|
2379
2417
|
}, []);
|
|
2380
2418
|
const { bind } = usePointerGesture({
|
|
2381
2419
|
axis: "y",
|
|
2382
2420
|
velocityThreshold: gestureConfig?.velocityThreshold ?? 0.3,
|
|
2383
2421
|
distanceThreshold: gestureConfig?.distanceThreshold ?? 80,
|
|
2384
|
-
disabled:
|
|
2422
|
+
disabled: false,
|
|
2385
2423
|
containerSize: containerHeight.current,
|
|
2386
2424
|
dragThresholdRatio: gestureConfig?.dragThresholdRatio ?? 0.5,
|
|
2387
2425
|
onDragOffset: (offset) => {
|
|
@@ -2405,6 +2443,15 @@ function ReelsFeed({
|
|
|
2405
2443
|
const handleToggleMute = react.useCallback(() => {
|
|
2406
2444
|
setIsMuted((prev) => !prev);
|
|
2407
2445
|
}, []);
|
|
2446
|
+
const windowedItems = react.useMemo(() => {
|
|
2447
|
+
const start = Math.max(0, focusedIndex - RENDER_WINDOW_RADIUS - PLACEHOLDER_EXTRA);
|
|
2448
|
+
const end = Math.min(items.length - 1, focusedIndex + RENDER_WINDOW_RADIUS + PLACEHOLDER_EXTRA);
|
|
2449
|
+
const result = [];
|
|
2450
|
+
for (let i = start; i <= end; i++) {
|
|
2451
|
+
result.push({ item: items[i], index: i });
|
|
2452
|
+
}
|
|
2453
|
+
return result;
|
|
2454
|
+
}, [items, focusedIndex]);
|
|
2408
2455
|
if (loading && items.length === 0) {
|
|
2409
2456
|
return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { ...centerStyle, flexDirection: "column", gap: 0 }, children: renderLoading ? renderLoading() : /* @__PURE__ */ jsxRuntime.jsx(DefaultSkeleton, {}) });
|
|
2410
2457
|
}
|
|
@@ -2441,12 +2488,18 @@ function ReelsFeed({
|
|
|
2441
2488
|
to { transform: rotate(360deg); }
|
|
2442
2489
|
}
|
|
2443
2490
|
` }),
|
|
2444
|
-
|
|
2491
|
+
windowedItems.map(({ item, index }) => {
|
|
2445
2492
|
const distFromFocus = Math.abs(index - focusedIndex);
|
|
2446
2493
|
const wrapperStyle = {
|
|
2447
2494
|
position: "absolute",
|
|
2448
2495
|
inset: 0,
|
|
2449
|
-
|
|
2496
|
+
// Fix 4: 'strict' = layout + style + paint + size containment.
|
|
2497
|
+
// Tells browser this slot is fully self-contained — no layout
|
|
2498
|
+
// escapes to parent. Eliminates layout thrash during animation.
|
|
2499
|
+
contain: "strict",
|
|
2500
|
+
// Fix 4: willChange only on active ±1 (3 slots max).
|
|
2501
|
+
// Previously ±1 already, keep it. Avoids unnecessary GPU layers
|
|
2502
|
+
// on warm/cold slots that are never animated directly.
|
|
2450
2503
|
willChange: distFromFocus <= 1 ? "transform" : "auto",
|
|
2451
2504
|
transform: getInitialTransformPx(index)
|
|
2452
2505
|
};
|
|
@@ -2476,6 +2529,7 @@ function ReelsFeed({
|
|
|
2476
2529
|
onToggleMute: handleToggleMute,
|
|
2477
2530
|
onAutoplayBlocked,
|
|
2478
2531
|
showFps: showFps && isActive,
|
|
2532
|
+
isDragging: isDragMuted,
|
|
2479
2533
|
renderOverlay,
|
|
2480
2534
|
renderActions,
|
|
2481
2535
|
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,
|
|
@@ -1700,6 +1718,7 @@ function DefaultPauseAction() {
|
|
|
1700
1718
|
);
|
|
1701
1719
|
}
|
|
1702
1720
|
var PLAY_AHEAD_MAX_CONCURRENT = 2;
|
|
1721
|
+
var PLAY_AHEAD_STAGGER_MS = 80;
|
|
1703
1722
|
var _playAheadActive = 0;
|
|
1704
1723
|
var _playAheadQueue = [];
|
|
1705
1724
|
function acquirePlayAhead() {
|
|
@@ -1707,7 +1726,11 @@ function acquirePlayAhead() {
|
|
|
1707
1726
|
_playAheadActive++;
|
|
1708
1727
|
return Promise.resolve();
|
|
1709
1728
|
}
|
|
1710
|
-
return new Promise((resolve) =>
|
|
1729
|
+
return new Promise((resolve) => {
|
|
1730
|
+
_playAheadQueue.push(() => {
|
|
1731
|
+
setTimeout(resolve, PLAY_AHEAD_STAGGER_MS);
|
|
1732
|
+
});
|
|
1733
|
+
});
|
|
1711
1734
|
}
|
|
1712
1735
|
function releasePlayAhead() {
|
|
1713
1736
|
_playAheadActive = Math.max(0, _playAheadActive - 1);
|
|
@@ -1728,6 +1751,7 @@ function VideoSlot({
|
|
|
1728
1751
|
onToggleMute,
|
|
1729
1752
|
onAutoplayBlocked,
|
|
1730
1753
|
showFps = false,
|
|
1754
|
+
isDragging = false,
|
|
1731
1755
|
renderOverlay,
|
|
1732
1756
|
renderActions,
|
|
1733
1757
|
renderPauseAction
|
|
@@ -1765,6 +1789,7 @@ function VideoSlot({
|
|
|
1765
1789
|
onToggleMute,
|
|
1766
1790
|
onAutoplayBlocked,
|
|
1767
1791
|
showFps,
|
|
1792
|
+
isDragging,
|
|
1768
1793
|
renderOverlay,
|
|
1769
1794
|
renderActions,
|
|
1770
1795
|
renderPauseAction,
|
|
@@ -1784,6 +1809,7 @@ function VideoSlotInner({
|
|
|
1784
1809
|
onToggleMute,
|
|
1785
1810
|
onAutoplayBlocked,
|
|
1786
1811
|
showFps,
|
|
1812
|
+
isDragging,
|
|
1787
1813
|
renderOverlay,
|
|
1788
1814
|
renderActions,
|
|
1789
1815
|
renderPauseAction,
|
|
@@ -1805,6 +1831,7 @@ function VideoSlotInner({
|
|
|
1805
1831
|
// so useHls creates the HLS instance and starts buffering
|
|
1806
1832
|
isPrefetch: isPrefetch || isPreloaded,
|
|
1807
1833
|
bufferTier,
|
|
1834
|
+
isDragging,
|
|
1808
1835
|
onError: (code, message) => {
|
|
1809
1836
|
console.error(`[VideoSlot] HLS error: ${code} \u2014 ${message}`);
|
|
1810
1837
|
}
|
|
@@ -2194,6 +2221,7 @@ function FpsCounter() {
|
|
|
2194
2221
|
);
|
|
2195
2222
|
}
|
|
2196
2223
|
var RENDER_WINDOW_RADIUS = 3;
|
|
2224
|
+
var PLACEHOLDER_EXTRA = 2;
|
|
2197
2225
|
var centerStyle = {
|
|
2198
2226
|
height: "100dvh",
|
|
2199
2227
|
display: "flex",
|
|
@@ -2235,7 +2263,6 @@ function ReelsFeed({
|
|
|
2235
2263
|
const slotCacheRef = useRef(/* @__PURE__ */ new Map());
|
|
2236
2264
|
const activeIndexRef = useRef(0);
|
|
2237
2265
|
activeIndexRef.current = focusedIndex;
|
|
2238
|
-
const [isSnapping, setIsSnapping] = useState(false);
|
|
2239
2266
|
const { animateSnap, animateBounceBack, cancelAnimation } = useSnapAnimation({
|
|
2240
2267
|
duration: snapConfig?.duration ?? 260,
|
|
2241
2268
|
easing: snapConfig?.easing ?? "cubic-bezier(0.25, 0.46, 0.45, 0.94)"
|
|
@@ -2271,9 +2298,20 @@ function ReelsFeed({
|
|
|
2271
2298
|
}
|
|
2272
2299
|
};
|
|
2273
2300
|
rebuild();
|
|
2274
|
-
|
|
2301
|
+
let rebuildTimer = null;
|
|
2302
|
+
const debouncedRebuild = () => {
|
|
2303
|
+
if (rebuildTimer !== null) return;
|
|
2304
|
+
rebuildTimer = setTimeout(() => {
|
|
2305
|
+
rebuildTimer = null;
|
|
2306
|
+
rebuild();
|
|
2307
|
+
}, 16);
|
|
2308
|
+
};
|
|
2309
|
+
const observer = new MutationObserver(debouncedRebuild);
|
|
2275
2310
|
observer.observe(container, { childList: true, subtree: true });
|
|
2276
|
-
return () =>
|
|
2311
|
+
return () => {
|
|
2312
|
+
observer.disconnect();
|
|
2313
|
+
if (rebuildTimer !== null) clearTimeout(rebuildTimer);
|
|
2314
|
+
};
|
|
2277
2315
|
}, [items.length, focusedIndex]);
|
|
2278
2316
|
const containerHeight = useRef(
|
|
2279
2317
|
typeof window !== "undefined" ? window.innerHeight : 800
|
|
@@ -2330,13 +2368,7 @@ function ReelsFeed({
|
|
|
2330
2368
|
return;
|
|
2331
2369
|
}
|
|
2332
2370
|
cancelAnimation();
|
|
2333
|
-
setIsSnapping(true);
|
|
2334
2371
|
setPrefetchIndex(null);
|
|
2335
|
-
setFocusedIndexImmediate(next);
|
|
2336
|
-
const nextItem = items[next];
|
|
2337
|
-
if (nextItem) {
|
|
2338
|
-
onSlotChange?.(next, nextItem, current);
|
|
2339
|
-
}
|
|
2340
2372
|
const h = containerHeight.current;
|
|
2341
2373
|
const targets = [];
|
|
2342
2374
|
for (const [idx, el] of slotCacheRef.current) {
|
|
@@ -2347,7 +2379,13 @@ function ReelsFeed({
|
|
|
2347
2379
|
});
|
|
2348
2380
|
}
|
|
2349
2381
|
animateSnap(targets);
|
|
2350
|
-
|
|
2382
|
+
requestAnimationFrame(() => {
|
|
2383
|
+
setFocusedIndexImmediate(next);
|
|
2384
|
+
const nextItem = items[next];
|
|
2385
|
+
if (nextItem) {
|
|
2386
|
+
onSlotChange?.(next, nextItem, current);
|
|
2387
|
+
}
|
|
2388
|
+
});
|
|
2351
2389
|
},
|
|
2352
2390
|
[items, animateSnap, animateBounceBack, cancelAnimation, setFocusedIndexImmediate, setPrefetchIndex, onSlotChange]
|
|
2353
2391
|
);
|
|
@@ -2369,13 +2407,13 @@ function ReelsFeed({
|
|
|
2369
2407
|
setIsDragMuted(true);
|
|
2370
2408
|
}, []);
|
|
2371
2409
|
const handleDragEnd = useCallback(() => {
|
|
2372
|
-
setTimeout(() => setIsDragMuted(false),
|
|
2410
|
+
setTimeout(() => setIsDragMuted(false), 300);
|
|
2373
2411
|
}, []);
|
|
2374
2412
|
const { bind } = usePointerGesture({
|
|
2375
2413
|
axis: "y",
|
|
2376
2414
|
velocityThreshold: gestureConfig?.velocityThreshold ?? 0.3,
|
|
2377
2415
|
distanceThreshold: gestureConfig?.distanceThreshold ?? 80,
|
|
2378
|
-
disabled:
|
|
2416
|
+
disabled: false,
|
|
2379
2417
|
containerSize: containerHeight.current,
|
|
2380
2418
|
dragThresholdRatio: gestureConfig?.dragThresholdRatio ?? 0.5,
|
|
2381
2419
|
onDragOffset: (offset) => {
|
|
@@ -2399,6 +2437,15 @@ function ReelsFeed({
|
|
|
2399
2437
|
const handleToggleMute = useCallback(() => {
|
|
2400
2438
|
setIsMuted((prev) => !prev);
|
|
2401
2439
|
}, []);
|
|
2440
|
+
const windowedItems = useMemo(() => {
|
|
2441
|
+
const start = Math.max(0, focusedIndex - RENDER_WINDOW_RADIUS - PLACEHOLDER_EXTRA);
|
|
2442
|
+
const end = Math.min(items.length - 1, focusedIndex + RENDER_WINDOW_RADIUS + PLACEHOLDER_EXTRA);
|
|
2443
|
+
const result = [];
|
|
2444
|
+
for (let i = start; i <= end; i++) {
|
|
2445
|
+
result.push({ item: items[i], index: i });
|
|
2446
|
+
}
|
|
2447
|
+
return result;
|
|
2448
|
+
}, [items, focusedIndex]);
|
|
2402
2449
|
if (loading && items.length === 0) {
|
|
2403
2450
|
return /* @__PURE__ */ jsx("div", { style: { ...centerStyle, flexDirection: "column", gap: 0 }, children: renderLoading ? renderLoading() : /* @__PURE__ */ jsx(DefaultSkeleton, {}) });
|
|
2404
2451
|
}
|
|
@@ -2435,12 +2482,18 @@ function ReelsFeed({
|
|
|
2435
2482
|
to { transform: rotate(360deg); }
|
|
2436
2483
|
}
|
|
2437
2484
|
` }),
|
|
2438
|
-
|
|
2485
|
+
windowedItems.map(({ item, index }) => {
|
|
2439
2486
|
const distFromFocus = Math.abs(index - focusedIndex);
|
|
2440
2487
|
const wrapperStyle = {
|
|
2441
2488
|
position: "absolute",
|
|
2442
2489
|
inset: 0,
|
|
2443
|
-
|
|
2490
|
+
// Fix 4: 'strict' = layout + style + paint + size containment.
|
|
2491
|
+
// Tells browser this slot is fully self-contained — no layout
|
|
2492
|
+
// escapes to parent. Eliminates layout thrash during animation.
|
|
2493
|
+
contain: "strict",
|
|
2494
|
+
// Fix 4: willChange only on active ±1 (3 slots max).
|
|
2495
|
+
// Previously ±1 already, keep it. Avoids unnecessary GPU layers
|
|
2496
|
+
// on warm/cold slots that are never animated directly.
|
|
2444
2497
|
willChange: distFromFocus <= 1 ? "transform" : "auto",
|
|
2445
2498
|
transform: getInitialTransformPx(index)
|
|
2446
2499
|
};
|
|
@@ -2470,6 +2523,7 @@ function ReelsFeed({
|
|
|
2470
2523
|
onToggleMute: handleToggleMute,
|
|
2471
2524
|
onAutoplayBlocked,
|
|
2472
2525
|
showFps: showFps && isActive,
|
|
2526
|
+
isDragging: isDragMuted,
|
|
2473
2527
|
renderOverlay,
|
|
2474
2528
|
renderActions,
|
|
2475
2529
|
renderPauseAction
|