@xhub-reels/sdk 0.1.18 → 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 CHANGED
@@ -741,9 +741,12 @@ var OptimisticManager = class {
741
741
  }
742
742
  };
743
743
  var DEFAULT_RESOURCE_CONFIG = {
744
- maxAllocations: 11,
745
- bufferWindow: 3,
746
- warmWindow: 4,
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 = 2;
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) => _playAheadQueue.push(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
  }
@@ -2200,6 +2228,7 @@ function FpsCounter() {
2200
2228
  );
2201
2229
  }
2202
2230
  var RENDER_WINDOW_RADIUS = 3;
2231
+ var PLACEHOLDER_EXTRA = 2;
2203
2232
  var centerStyle = {
2204
2233
  height: "100dvh",
2205
2234
  display: "flex",
@@ -2277,9 +2306,20 @@ function ReelsFeed({
2277
2306
  }
2278
2307
  };
2279
2308
  rebuild();
2280
- const observer = new MutationObserver(rebuild);
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);
2281
2318
  observer.observe(container, { childList: true, subtree: true });
2282
- return () => observer.disconnect();
2319
+ return () => {
2320
+ observer.disconnect();
2321
+ if (rebuildTimer !== null) clearTimeout(rebuildTimer);
2322
+ };
2283
2323
  }, [items.length, focusedIndex]);
2284
2324
  const containerHeight = react.useRef(
2285
2325
  typeof window !== "undefined" ? window.innerHeight : 800
@@ -2405,6 +2445,15 @@ function ReelsFeed({
2405
2445
  const handleToggleMute = react.useCallback(() => {
2406
2446
  setIsMuted((prev) => !prev);
2407
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]);
2408
2457
  if (loading && items.length === 0) {
2409
2458
  return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { ...centerStyle, flexDirection: "column", gap: 0 }, children: renderLoading ? renderLoading() : /* @__PURE__ */ jsxRuntime.jsx(DefaultSkeleton, {}) });
2410
2459
  }
@@ -2441,7 +2490,7 @@ function ReelsFeed({
2441
2490
  to { transform: rotate(360deg); }
2442
2491
  }
2443
2492
  ` }),
2444
- items.map((item, index) => {
2493
+ windowedItems.map(({ item, index }) => {
2445
2494
  const distFromFocus = Math.abs(index - focusedIndex);
2446
2495
  const wrapperStyle = {
2447
2496
  position: "absolute",
@@ -2476,6 +2525,7 @@ function ReelsFeed({
2476
2525
  onToggleMute: handleToggleMute,
2477
2526
  onAutoplayBlocked,
2478
2527
  showFps: showFps && isActive,
2528
+ isDragging: isDragMuted,
2479
2529
  renderOverlay,
2480
2530
  renderActions,
2481
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 + 6 + 4 = 11 DOM nodes, ~112 MB memory (fits comfortably in 1 GB budget)
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 + 6 + 4 = 11 DOM nodes, ~112 MB memory (fits comfortably in 1 GB budget)
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
- maxAllocations: 11,
739
- bufferWindow: 3,
740
- warmWindow: 4,
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 = 2;
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) => _playAheadQueue.push(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
  }
@@ -2194,6 +2222,7 @@ function FpsCounter() {
2194
2222
  );
2195
2223
  }
2196
2224
  var RENDER_WINDOW_RADIUS = 3;
2225
+ var PLACEHOLDER_EXTRA = 2;
2197
2226
  var centerStyle = {
2198
2227
  height: "100dvh",
2199
2228
  display: "flex",
@@ -2271,9 +2300,20 @@ function ReelsFeed({
2271
2300
  }
2272
2301
  };
2273
2302
  rebuild();
2274
- const observer = new MutationObserver(rebuild);
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);
2275
2312
  observer.observe(container, { childList: true, subtree: true });
2276
- return () => observer.disconnect();
2313
+ return () => {
2314
+ observer.disconnect();
2315
+ if (rebuildTimer !== null) clearTimeout(rebuildTimer);
2316
+ };
2277
2317
  }, [items.length, focusedIndex]);
2278
2318
  const containerHeight = useRef(
2279
2319
  typeof window !== "undefined" ? window.innerHeight : 800
@@ -2399,6 +2439,15 @@ function ReelsFeed({
2399
2439
  const handleToggleMute = useCallback(() => {
2400
2440
  setIsMuted((prev) => !prev);
2401
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]);
2402
2451
  if (loading && items.length === 0) {
2403
2452
  return /* @__PURE__ */ jsx("div", { style: { ...centerStyle, flexDirection: "column", gap: 0 }, children: renderLoading ? renderLoading() : /* @__PURE__ */ jsx(DefaultSkeleton, {}) });
2404
2453
  }
@@ -2435,7 +2484,7 @@ function ReelsFeed({
2435
2484
  to { transform: rotate(360deg); }
2436
2485
  }
2437
2486
  ` }),
2438
- items.map((item, index) => {
2487
+ windowedItems.map(({ item, index }) => {
2439
2488
  const distFromFocus = Math.abs(index - focusedIndex);
2440
2489
  const wrapperStyle = {
2441
2490
  position: "absolute",
@@ -2470,6 +2519,7 @@ function ReelsFeed({
2470
2519
  onToggleMute: handleToggleMute,
2471
2520
  onAutoplayBlocked,
2472
2521
  showFps: showFps && isActive,
2522
+ isDragging: isDragMuted,
2473
2523
  renderOverlay,
2474
2524
  renderActions,
2475
2525
  renderPauseAction
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xhub-reels/sdk",
3
- "version": "0.1.18",
3
+ "version": "0.1.19",
4
4
  "description": "High-performance Short Video / Reels SDK for React — optimized for Flutter WebView",
5
5
  "license": "MIT",
6
6
  "type": "module",