@xhub-reels/sdk 0.2.16 → 0.2.18
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 +111 -43
- package/dist/index.d.cts +60 -2
- package/dist/index.d.ts +60 -2
- package/dist/index.js +111 -44
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1267,7 +1267,7 @@ function usePointerGesture(config = {}) {
|
|
|
1267
1267
|
};
|
|
1268
1268
|
}
|
|
1269
1269
|
function useSnapAnimation(config = {}) {
|
|
1270
|
-
const { duration = 280, easing = "
|
|
1270
|
+
const { duration = 280, easing = "ease-out" } = config;
|
|
1271
1271
|
const activeAnimations = react.useRef([]);
|
|
1272
1272
|
const cancelAnimation = react.useCallback(() => {
|
|
1273
1273
|
for (const anim of activeAnimations.current) {
|
|
@@ -2112,42 +2112,6 @@ function VideoSlotInner({
|
|
|
2112
2112
|
}
|
|
2113
2113
|
}, [mp4Src, isActive, isPrefetch, isPreloaded, isHlsSource]);
|
|
2114
2114
|
const isReady = isHlsSource ? hlsReady : mp4Ready;
|
|
2115
|
-
const [capturedPoster, setCapturedPoster] = react.useState(null);
|
|
2116
|
-
react.useEffect(() => {
|
|
2117
|
-
const video = videoRef.current;
|
|
2118
|
-
if (!video || !shouldLoadSrc) return;
|
|
2119
|
-
let cancelled = false;
|
|
2120
|
-
const captureFrame = () => {
|
|
2121
|
-
if (cancelled) return;
|
|
2122
|
-
if (video.readyState < HTMLMediaElement.HAVE_CURRENT_DATA) return;
|
|
2123
|
-
if (video.videoWidth === 0 || video.videoHeight === 0) return;
|
|
2124
|
-
try {
|
|
2125
|
-
const canvas = document.createElement("canvas");
|
|
2126
|
-
canvas.width = video.videoWidth;
|
|
2127
|
-
canvas.height = video.videoHeight;
|
|
2128
|
-
const ctx = canvas.getContext("2d");
|
|
2129
|
-
if (!ctx) return;
|
|
2130
|
-
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
|
2131
|
-
const dataUrl = canvas.toDataURL("image/webp", 0.85);
|
|
2132
|
-
if (!cancelled) {
|
|
2133
|
-
setCapturedPoster(dataUrl);
|
|
2134
|
-
}
|
|
2135
|
-
} catch {
|
|
2136
|
-
}
|
|
2137
|
-
};
|
|
2138
|
-
if (video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA && video.videoWidth > 0) {
|
|
2139
|
-
captureFrame();
|
|
2140
|
-
} else {
|
|
2141
|
-
video.addEventListener("loadeddata", captureFrame, { once: true });
|
|
2142
|
-
}
|
|
2143
|
-
return () => {
|
|
2144
|
-
cancelled = true;
|
|
2145
|
-
video.removeEventListener("loadeddata", captureFrame);
|
|
2146
|
-
};
|
|
2147
|
-
}, [src, shouldLoadSrc]);
|
|
2148
|
-
react.useEffect(() => {
|
|
2149
|
-
setCapturedPoster(null);
|
|
2150
|
-
}, [src]);
|
|
2151
2115
|
const [isVideoPlaying, setIsVideoPlaying] = react.useState(false);
|
|
2152
2116
|
react.useEffect(() => {
|
|
2153
2117
|
const video = videoRef.current;
|
|
@@ -2408,13 +2372,13 @@ function VideoSlotInner({
|
|
|
2408
2372
|
}
|
|
2409
2373
|
}
|
|
2410
2374
|
),
|
|
2411
|
-
|
|
2375
|
+
item.poster && !isPreDecoded && /* @__PURE__ */ jsxRuntime.jsx(
|
|
2412
2376
|
"div",
|
|
2413
2377
|
{
|
|
2414
2378
|
style: {
|
|
2415
2379
|
position: "absolute",
|
|
2416
2380
|
inset: 0,
|
|
2417
|
-
backgroundImage: `url(${
|
|
2381
|
+
backgroundImage: `url(${item.poster})`,
|
|
2418
2382
|
backgroundSize: "cover",
|
|
2419
2383
|
backgroundPosition: "center",
|
|
2420
2384
|
opacity: showPosterOverlay ? 1 : 0,
|
|
@@ -2584,7 +2548,7 @@ function ReelsFeed({
|
|
|
2584
2548
|
activeIndexRef.current = focusedIndex;
|
|
2585
2549
|
const { animateSnap, animateBounceBack, cancelAnimation } = useSnapAnimation({
|
|
2586
2550
|
duration: snapConfig?.duration ?? 260,
|
|
2587
|
-
easing: snapConfig?.easing ?? "
|
|
2551
|
+
easing: snapConfig?.easing ?? "ease-out"
|
|
2588
2552
|
});
|
|
2589
2553
|
react.useEffect(() => {
|
|
2590
2554
|
loadInitial();
|
|
@@ -3070,7 +3034,8 @@ function ReelsModal({
|
|
|
3070
3034
|
if (prewarmedRef.current === openIndex) return;
|
|
3071
3035
|
prewarmedRef.current = openIndex;
|
|
3072
3036
|
setFocusedIndexImmediate(openIndex);
|
|
3073
|
-
const
|
|
3037
|
+
const loader = adapters.videoLoader;
|
|
3038
|
+
const preload = loader?.preloadMetadata?.bind(loader);
|
|
3074
3039
|
if (!preload) return;
|
|
3075
3040
|
const targets = /* @__PURE__ */ new Set();
|
|
3076
3041
|
targets.add(openIndex);
|
|
@@ -3163,12 +3128,12 @@ function ReelsModal({
|
|
|
3163
3128
|
const panel = panelRef.current;
|
|
3164
3129
|
if (!panel) return;
|
|
3165
3130
|
const first = panel.querySelector(FOCUSABLE);
|
|
3166
|
-
(first ?? panel).focus();
|
|
3131
|
+
(first ?? panel).focus({ preventScroll: true });
|
|
3167
3132
|
});
|
|
3168
3133
|
return () => cancelAnimationFrame(id);
|
|
3169
3134
|
}
|
|
3170
3135
|
if (phase === "closed") {
|
|
3171
|
-
previouslyFocusedRef.current?.focus?.();
|
|
3136
|
+
previouslyFocusedRef.current?.focus?.({ preventScroll: true });
|
|
3172
3137
|
previouslyFocusedRef.current = null;
|
|
3173
3138
|
}
|
|
3174
3139
|
return void 0;
|
|
@@ -3926,6 +3891,109 @@ var HttpError = class extends Error {
|
|
|
3926
3891
|
}
|
|
3927
3892
|
};
|
|
3928
3893
|
|
|
3894
|
+
// src/adapters/browser/BrowserVideoLoader.ts
|
|
3895
|
+
var BrowserVideoLoader = class {
|
|
3896
|
+
constructor(config = {}) {
|
|
3897
|
+
this.statusById = /* @__PURE__ */ new Map();
|
|
3898
|
+
this.controllers = /* @__PURE__ */ new Map();
|
|
3899
|
+
/** URLs already warmed via preloadMetadata — dedupes fire-and-forget calls. */
|
|
3900
|
+
this.warmedUrls = /* @__PURE__ */ new Set();
|
|
3901
|
+
this.timeoutMs = config.timeoutMs ?? 8e3;
|
|
3902
|
+
this.mp4PrefetchBytes = config.mp4PrefetchBytes ?? 512 * 1024;
|
|
3903
|
+
this.headers = config.headers ?? {};
|
|
3904
|
+
this.logger = config.logger;
|
|
3905
|
+
}
|
|
3906
|
+
async preload(videoId, url, signal) {
|
|
3907
|
+
if (typeof fetch === "undefined") {
|
|
3908
|
+
return { videoId, status: "idle" };
|
|
3909
|
+
}
|
|
3910
|
+
if (this.statusById.get(videoId) === "loaded") {
|
|
3911
|
+
return { videoId, status: "loaded" };
|
|
3912
|
+
}
|
|
3913
|
+
this.cancel(videoId);
|
|
3914
|
+
const controller = new AbortController();
|
|
3915
|
+
this.controllers.set(videoId, controller);
|
|
3916
|
+
this.statusById.set(videoId, "loading");
|
|
3917
|
+
const onAbort = () => controller.abort();
|
|
3918
|
+
signal?.addEventListener("abort", onAbort);
|
|
3919
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
3920
|
+
const isHls = url.includes(".m3u8");
|
|
3921
|
+
const requestHeaders = { ...this.headers };
|
|
3922
|
+
if (!isHls && this.mp4PrefetchBytes > 0) {
|
|
3923
|
+
requestHeaders["Range"] = `bytes=0-${this.mp4PrefetchBytes - 1}`;
|
|
3924
|
+
}
|
|
3925
|
+
try {
|
|
3926
|
+
const res = await fetch(url, {
|
|
3927
|
+
method: "GET",
|
|
3928
|
+
signal: controller.signal,
|
|
3929
|
+
headers: requestHeaders
|
|
3930
|
+
});
|
|
3931
|
+
if (!res.ok && res.status !== 206) {
|
|
3932
|
+
throw new Error(`HTTP ${res.status}`);
|
|
3933
|
+
}
|
|
3934
|
+
const buf = await res.arrayBuffer();
|
|
3935
|
+
this.statusById.set(videoId, "loaded");
|
|
3936
|
+
this.warmedUrls.add(url);
|
|
3937
|
+
return { videoId, status: "loaded", loadedBytes: buf.byteLength };
|
|
3938
|
+
} catch (err) {
|
|
3939
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
3940
|
+
if (error.name === "AbortError") {
|
|
3941
|
+
this.statusById.set(videoId, "idle");
|
|
3942
|
+
return { videoId, status: "idle", error };
|
|
3943
|
+
}
|
|
3944
|
+
this.statusById.set(videoId, "error");
|
|
3945
|
+
this.logger?.warn(`[BrowserVideoLoader] preload failed for ${videoId}`, error.message);
|
|
3946
|
+
return { videoId, status: "error", error };
|
|
3947
|
+
} finally {
|
|
3948
|
+
clearTimeout(timer);
|
|
3949
|
+
signal?.removeEventListener("abort", onAbort);
|
|
3950
|
+
this.controllers.delete(videoId);
|
|
3951
|
+
}
|
|
3952
|
+
}
|
|
3953
|
+
cancel(videoId) {
|
|
3954
|
+
const controller = this.controllers.get(videoId);
|
|
3955
|
+
if (controller) {
|
|
3956
|
+
controller.abort();
|
|
3957
|
+
this.controllers.delete(videoId);
|
|
3958
|
+
}
|
|
3959
|
+
if (this.statusById.get(videoId) === "loading") {
|
|
3960
|
+
this.statusById.set(videoId, "idle");
|
|
3961
|
+
}
|
|
3962
|
+
}
|
|
3963
|
+
isPreloaded(videoId) {
|
|
3964
|
+
return this.statusById.get(videoId) === "loaded";
|
|
3965
|
+
}
|
|
3966
|
+
getPreloadStatus(videoId) {
|
|
3967
|
+
return this.statusById.get(videoId) ?? "idle";
|
|
3968
|
+
}
|
|
3969
|
+
clearAll() {
|
|
3970
|
+
for (const controller of this.controllers.values()) {
|
|
3971
|
+
controller.abort();
|
|
3972
|
+
}
|
|
3973
|
+
this.controllers.clear();
|
|
3974
|
+
this.statusById.clear();
|
|
3975
|
+
this.warmedUrls.clear();
|
|
3976
|
+
}
|
|
3977
|
+
/**
|
|
3978
|
+
* Tier-4 cold prefetch. Fire-and-forget GET that warms the HTTP cache.
|
|
3979
|
+
* No DOM, no segment download, never throws. Deduped per-URL.
|
|
3980
|
+
*/
|
|
3981
|
+
preloadMetadata(url) {
|
|
3982
|
+
if (typeof fetch === "undefined") return;
|
|
3983
|
+
if (!url || this.warmedUrls.has(url)) return;
|
|
3984
|
+
this.warmedUrls.add(url);
|
|
3985
|
+
const isHls = url.includes(".m3u8");
|
|
3986
|
+
const headers = { ...this.headers };
|
|
3987
|
+
if (!isHls) {
|
|
3988
|
+
headers["Range"] = "bytes=0-0";
|
|
3989
|
+
}
|
|
3990
|
+
void fetch(url, { method: "GET", headers }).catch(() => {
|
|
3991
|
+
this.warmedUrls.delete(url);
|
|
3992
|
+
});
|
|
3993
|
+
}
|
|
3994
|
+
};
|
|
3995
|
+
|
|
3996
|
+
exports.BrowserVideoLoader = BrowserVideoLoader;
|
|
3929
3997
|
exports.DEFAULT_FEED_CONFIG = DEFAULT_FEED_CONFIG;
|
|
3930
3998
|
exports.DEFAULT_NAVIGATION_CONFIG = DEFAULT_NAVIGATION_CONFIG;
|
|
3931
3999
|
exports.DEFAULT_PLAYER_CONFIG = DEFAULT_PLAYER_CONFIG;
|
package/dist/index.d.cts
CHANGED
|
@@ -858,7 +858,7 @@ declare function usePointerGesture(config?: PointerGestureConfig): {
|
|
|
858
858
|
interface SnapAnimationConfig {
|
|
859
859
|
/** Duration in ms (default: 280) */
|
|
860
860
|
duration?: number;
|
|
861
|
-
/** CSS easing (default: '
|
|
861
|
+
/** CSS easing (default: 'ease-out') */
|
|
862
862
|
easing?: string;
|
|
863
863
|
}
|
|
864
864
|
interface SnapTarget {
|
|
@@ -1423,4 +1423,62 @@ declare class HttpError extends Error {
|
|
|
1423
1423
|
constructor(status: number, message: string, body?: string | undefined);
|
|
1424
1424
|
}
|
|
1425
1425
|
|
|
1426
|
-
|
|
1426
|
+
/**
|
|
1427
|
+
* BrowserVideoLoader — IVideoLoader implementation for browsers / WebViews.
|
|
1428
|
+
*
|
|
1429
|
+
* Warms the HTTP browser cache so hls.js / <video> can start from cache instead
|
|
1430
|
+
* of hitting the network cold on mount. This is what closes the "poster stuck
|
|
1431
|
+
* ~3s" stall: both the modal-owned prewarm path (ReelsModal `opening` phase) and
|
|
1432
|
+
* the ResourceGovernor Tier-4 cold prefetch call `videoLoader?.preloadMetadata`.
|
|
1433
|
+
* That method is OPTIONAL on IVideoLoader, so if no loader implements it the
|
|
1434
|
+
* entire prewarm path is silently a no-op. This adapter implements it.
|
|
1435
|
+
*
|
|
1436
|
+
* Strategy:
|
|
1437
|
+
* - `preloadMetadata(url)` — fire-and-forget GET of the HLS manifest (or mp4
|
|
1438
|
+
* head via `Range: bytes=0-0`) into the HTTP cache. No DOM, no segment
|
|
1439
|
+
* download, never throws. Deduped per-URL so repeated taps are cheap.
|
|
1440
|
+
* - `preload(videoId, url, signal?)` — awaitable warm with state tracking and
|
|
1441
|
+
* cancellation; resolves to a PreloadResult. For progressive mp4 it fetches
|
|
1442
|
+
* only the first `mp4PrefetchBytes` via a Range request.
|
|
1443
|
+
*
|
|
1444
|
+
* SSR-safe: when `fetch` is unavailable (server render) every method degrades to
|
|
1445
|
+
* a no-op rather than throwing.
|
|
1446
|
+
*/
|
|
1447
|
+
|
|
1448
|
+
interface BrowserVideoLoaderConfig {
|
|
1449
|
+
/** Abort an in-flight {@link BrowserVideoLoader.preload} after this many ms (default: 8000). */
|
|
1450
|
+
timeoutMs?: number;
|
|
1451
|
+
/**
|
|
1452
|
+
* For progressive mp4 sources, fetch only the first N bytes via a Range
|
|
1453
|
+
* request to warm the cache without downloading the whole file.
|
|
1454
|
+
* Set to 0 to fetch the full file. Default: 524288 (512 KB).
|
|
1455
|
+
*/
|
|
1456
|
+
mp4PrefetchBytes?: number;
|
|
1457
|
+
/** Static headers attached to every warm request (e.g. auth). */
|
|
1458
|
+
headers?: Record<string, string>;
|
|
1459
|
+
/** Optional logger for warm failures. */
|
|
1460
|
+
logger?: ILogger;
|
|
1461
|
+
}
|
|
1462
|
+
declare class BrowserVideoLoader implements IVideoLoader {
|
|
1463
|
+
private readonly timeoutMs;
|
|
1464
|
+
private readonly mp4PrefetchBytes;
|
|
1465
|
+
private readonly headers;
|
|
1466
|
+
private readonly logger?;
|
|
1467
|
+
private readonly statusById;
|
|
1468
|
+
private readonly controllers;
|
|
1469
|
+
/** URLs already warmed via preloadMetadata — dedupes fire-and-forget calls. */
|
|
1470
|
+
private readonly warmedUrls;
|
|
1471
|
+
constructor(config?: BrowserVideoLoaderConfig);
|
|
1472
|
+
preload(videoId: string, url: string, signal?: AbortSignal): Promise<PreloadResult>;
|
|
1473
|
+
cancel(videoId: string): void;
|
|
1474
|
+
isPreloaded(videoId: string): boolean;
|
|
1475
|
+
getPreloadStatus(videoId: string): PreloadStatus;
|
|
1476
|
+
clearAll(): void;
|
|
1477
|
+
/**
|
|
1478
|
+
* Tier-4 cold prefetch. Fire-and-forget GET that warms the HTTP cache.
|
|
1479
|
+
* No DOM, no segment download, never throws. Deduped per-URL.
|
|
1480
|
+
*/
|
|
1481
|
+
preloadMetadata(url: string): void;
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
export { type Article, type ArticleImage, type Author, BrowserVideoLoader, type BrowserVideoLoaderConfig, type BufferTier, type CommentItem, type CommentPage, type ContentItem, type ContentStats, DEFAULT_FEED_CONFIG, DEFAULT_NAVIGATION_CONFIG, DEFAULT_PLAYER_CONFIG, DEFAULT_RESOURCE_CONFIG, DefaultActions, DefaultOverlay, DefaultPauseAction, DefaultSkeleton, type FeedConfig, type FeedError, FeedManager, type FeedPage, type FeedState, HttpDataSource, type HttpDataSourceConfig, HttpError, type IAnalytics, type ICommentAdapter, type IDataSource, type IInteraction, type ILogger, type INetworkAdapter, type ISessionStorage, type IVideoLoader, type InteractionState, type LogLevel, MockAnalytics, MockCommentAdapter, MockDataSource, MockInteraction, MockLogger, MockNetworkAdapter, MockSessionStorage, MockVideoLoader, type NavigationConfig, NavigationManager, type NavigationPhase, type NavigationState, type NetworkType, OptimisticManager, type PauseSlotActions, type PlayerConfig, PlayerEngine, type PlayerError, type PlayerEvent, type PlayerEventListener, type PlayerState, PlayerStatus, type PointerGestureConfig, type PreloadResult, type PreloadStatus, ReelsFeed, type ReelsFeedProps, ReelsFeedThumbnail, type ReelsFeedThumbnailProps, ReelsModal, type ReelsModalAnimationConfig, type ReelsModalProps, type ReelsModalRenderState, ReelsProvider, type ReelsProviderProps, type ResourceConfig, ResourceGovernor, type ResourceState, type SDKAdapters, type SDKContextValue, type SlotActions, type SnapTarget, type UseHlsOptions, type UseHlsReturn, VALID_TRANSITIONS, type VideoItem, type VideoQuality, VideoSlot, type VideoSource, canPause, canPlay, canSeek, isArticle, isValidNavTransition, isValidTransition, isVideoItem, useFeed, useFeedSelector, useHls, useNavigation, useNavigationSelector, usePlayer, usePlayerSelector, usePointerGesture, useResource, useResourceSelector, useSDK, useSnapAnimation };
|
package/dist/index.d.ts
CHANGED
|
@@ -858,7 +858,7 @@ declare function usePointerGesture(config?: PointerGestureConfig): {
|
|
|
858
858
|
interface SnapAnimationConfig {
|
|
859
859
|
/** Duration in ms (default: 280) */
|
|
860
860
|
duration?: number;
|
|
861
|
-
/** CSS easing (default: '
|
|
861
|
+
/** CSS easing (default: 'ease-out') */
|
|
862
862
|
easing?: string;
|
|
863
863
|
}
|
|
864
864
|
interface SnapTarget {
|
|
@@ -1423,4 +1423,62 @@ declare class HttpError extends Error {
|
|
|
1423
1423
|
constructor(status: number, message: string, body?: string | undefined);
|
|
1424
1424
|
}
|
|
1425
1425
|
|
|
1426
|
-
|
|
1426
|
+
/**
|
|
1427
|
+
* BrowserVideoLoader — IVideoLoader implementation for browsers / WebViews.
|
|
1428
|
+
*
|
|
1429
|
+
* Warms the HTTP browser cache so hls.js / <video> can start from cache instead
|
|
1430
|
+
* of hitting the network cold on mount. This is what closes the "poster stuck
|
|
1431
|
+
* ~3s" stall: both the modal-owned prewarm path (ReelsModal `opening` phase) and
|
|
1432
|
+
* the ResourceGovernor Tier-4 cold prefetch call `videoLoader?.preloadMetadata`.
|
|
1433
|
+
* That method is OPTIONAL on IVideoLoader, so if no loader implements it the
|
|
1434
|
+
* entire prewarm path is silently a no-op. This adapter implements it.
|
|
1435
|
+
*
|
|
1436
|
+
* Strategy:
|
|
1437
|
+
* - `preloadMetadata(url)` — fire-and-forget GET of the HLS manifest (or mp4
|
|
1438
|
+
* head via `Range: bytes=0-0`) into the HTTP cache. No DOM, no segment
|
|
1439
|
+
* download, never throws. Deduped per-URL so repeated taps are cheap.
|
|
1440
|
+
* - `preload(videoId, url, signal?)` — awaitable warm with state tracking and
|
|
1441
|
+
* cancellation; resolves to a PreloadResult. For progressive mp4 it fetches
|
|
1442
|
+
* only the first `mp4PrefetchBytes` via a Range request.
|
|
1443
|
+
*
|
|
1444
|
+
* SSR-safe: when `fetch` is unavailable (server render) every method degrades to
|
|
1445
|
+
* a no-op rather than throwing.
|
|
1446
|
+
*/
|
|
1447
|
+
|
|
1448
|
+
interface BrowserVideoLoaderConfig {
|
|
1449
|
+
/** Abort an in-flight {@link BrowserVideoLoader.preload} after this many ms (default: 8000). */
|
|
1450
|
+
timeoutMs?: number;
|
|
1451
|
+
/**
|
|
1452
|
+
* For progressive mp4 sources, fetch only the first N bytes via a Range
|
|
1453
|
+
* request to warm the cache without downloading the whole file.
|
|
1454
|
+
* Set to 0 to fetch the full file. Default: 524288 (512 KB).
|
|
1455
|
+
*/
|
|
1456
|
+
mp4PrefetchBytes?: number;
|
|
1457
|
+
/** Static headers attached to every warm request (e.g. auth). */
|
|
1458
|
+
headers?: Record<string, string>;
|
|
1459
|
+
/** Optional logger for warm failures. */
|
|
1460
|
+
logger?: ILogger;
|
|
1461
|
+
}
|
|
1462
|
+
declare class BrowserVideoLoader implements IVideoLoader {
|
|
1463
|
+
private readonly timeoutMs;
|
|
1464
|
+
private readonly mp4PrefetchBytes;
|
|
1465
|
+
private readonly headers;
|
|
1466
|
+
private readonly logger?;
|
|
1467
|
+
private readonly statusById;
|
|
1468
|
+
private readonly controllers;
|
|
1469
|
+
/** URLs already warmed via preloadMetadata — dedupes fire-and-forget calls. */
|
|
1470
|
+
private readonly warmedUrls;
|
|
1471
|
+
constructor(config?: BrowserVideoLoaderConfig);
|
|
1472
|
+
preload(videoId: string, url: string, signal?: AbortSignal): Promise<PreloadResult>;
|
|
1473
|
+
cancel(videoId: string): void;
|
|
1474
|
+
isPreloaded(videoId: string): boolean;
|
|
1475
|
+
getPreloadStatus(videoId: string): PreloadStatus;
|
|
1476
|
+
clearAll(): void;
|
|
1477
|
+
/**
|
|
1478
|
+
* Tier-4 cold prefetch. Fire-and-forget GET that warms the HTTP cache.
|
|
1479
|
+
* No DOM, no segment download, never throws. Deduped per-URL.
|
|
1480
|
+
*/
|
|
1481
|
+
preloadMetadata(url: string): void;
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
export { type Article, type ArticleImage, type Author, BrowserVideoLoader, type BrowserVideoLoaderConfig, type BufferTier, type CommentItem, type CommentPage, type ContentItem, type ContentStats, DEFAULT_FEED_CONFIG, DEFAULT_NAVIGATION_CONFIG, DEFAULT_PLAYER_CONFIG, DEFAULT_RESOURCE_CONFIG, DefaultActions, DefaultOverlay, DefaultPauseAction, DefaultSkeleton, type FeedConfig, type FeedError, FeedManager, type FeedPage, type FeedState, HttpDataSource, type HttpDataSourceConfig, HttpError, type IAnalytics, type ICommentAdapter, type IDataSource, type IInteraction, type ILogger, type INetworkAdapter, type ISessionStorage, type IVideoLoader, type InteractionState, type LogLevel, MockAnalytics, MockCommentAdapter, MockDataSource, MockInteraction, MockLogger, MockNetworkAdapter, MockSessionStorage, MockVideoLoader, type NavigationConfig, NavigationManager, type NavigationPhase, type NavigationState, type NetworkType, OptimisticManager, type PauseSlotActions, type PlayerConfig, PlayerEngine, type PlayerError, type PlayerEvent, type PlayerEventListener, type PlayerState, PlayerStatus, type PointerGestureConfig, type PreloadResult, type PreloadStatus, ReelsFeed, type ReelsFeedProps, ReelsFeedThumbnail, type ReelsFeedThumbnailProps, ReelsModal, type ReelsModalAnimationConfig, type ReelsModalProps, type ReelsModalRenderState, ReelsProvider, type ReelsProviderProps, type ResourceConfig, ResourceGovernor, type ResourceState, type SDKAdapters, type SDKContextValue, type SlotActions, type SnapTarget, type UseHlsOptions, type UseHlsReturn, VALID_TRANSITIONS, type VideoItem, type VideoQuality, VideoSlot, type VideoSource, canPause, canPlay, canSeek, isArticle, isValidNavTransition, isValidTransition, isVideoItem, useFeed, useFeedSelector, useHls, useNavigation, useNavigationSelector, usePlayer, usePlayerSelector, usePointerGesture, useResource, useResourceSelector, useSDK, useSnapAnimation };
|
package/dist/index.js
CHANGED
|
@@ -1261,7 +1261,7 @@ function usePointerGesture(config = {}) {
|
|
|
1261
1261
|
};
|
|
1262
1262
|
}
|
|
1263
1263
|
function useSnapAnimation(config = {}) {
|
|
1264
|
-
const { duration = 280, easing = "
|
|
1264
|
+
const { duration = 280, easing = "ease-out" } = config;
|
|
1265
1265
|
const activeAnimations = useRef([]);
|
|
1266
1266
|
const cancelAnimation = useCallback(() => {
|
|
1267
1267
|
for (const anim of activeAnimations.current) {
|
|
@@ -2106,42 +2106,6 @@ function VideoSlotInner({
|
|
|
2106
2106
|
}
|
|
2107
2107
|
}, [mp4Src, isActive, isPrefetch, isPreloaded, isHlsSource]);
|
|
2108
2108
|
const isReady = isHlsSource ? hlsReady : mp4Ready;
|
|
2109
|
-
const [capturedPoster, setCapturedPoster] = useState(null);
|
|
2110
|
-
useEffect(() => {
|
|
2111
|
-
const video = videoRef.current;
|
|
2112
|
-
if (!video || !shouldLoadSrc) return;
|
|
2113
|
-
let cancelled = false;
|
|
2114
|
-
const captureFrame = () => {
|
|
2115
|
-
if (cancelled) return;
|
|
2116
|
-
if (video.readyState < HTMLMediaElement.HAVE_CURRENT_DATA) return;
|
|
2117
|
-
if (video.videoWidth === 0 || video.videoHeight === 0) return;
|
|
2118
|
-
try {
|
|
2119
|
-
const canvas = document.createElement("canvas");
|
|
2120
|
-
canvas.width = video.videoWidth;
|
|
2121
|
-
canvas.height = video.videoHeight;
|
|
2122
|
-
const ctx = canvas.getContext("2d");
|
|
2123
|
-
if (!ctx) return;
|
|
2124
|
-
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
|
2125
|
-
const dataUrl = canvas.toDataURL("image/webp", 0.85);
|
|
2126
|
-
if (!cancelled) {
|
|
2127
|
-
setCapturedPoster(dataUrl);
|
|
2128
|
-
}
|
|
2129
|
-
} catch {
|
|
2130
|
-
}
|
|
2131
|
-
};
|
|
2132
|
-
if (video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA && video.videoWidth > 0) {
|
|
2133
|
-
captureFrame();
|
|
2134
|
-
} else {
|
|
2135
|
-
video.addEventListener("loadeddata", captureFrame, { once: true });
|
|
2136
|
-
}
|
|
2137
|
-
return () => {
|
|
2138
|
-
cancelled = true;
|
|
2139
|
-
video.removeEventListener("loadeddata", captureFrame);
|
|
2140
|
-
};
|
|
2141
|
-
}, [src, shouldLoadSrc]);
|
|
2142
|
-
useEffect(() => {
|
|
2143
|
-
setCapturedPoster(null);
|
|
2144
|
-
}, [src]);
|
|
2145
2109
|
const [isVideoPlaying, setIsVideoPlaying] = useState(false);
|
|
2146
2110
|
useEffect(() => {
|
|
2147
2111
|
const video = videoRef.current;
|
|
@@ -2402,13 +2366,13 @@ function VideoSlotInner({
|
|
|
2402
2366
|
}
|
|
2403
2367
|
}
|
|
2404
2368
|
),
|
|
2405
|
-
|
|
2369
|
+
item.poster && !isPreDecoded && /* @__PURE__ */ jsx(
|
|
2406
2370
|
"div",
|
|
2407
2371
|
{
|
|
2408
2372
|
style: {
|
|
2409
2373
|
position: "absolute",
|
|
2410
2374
|
inset: 0,
|
|
2411
|
-
backgroundImage: `url(${
|
|
2375
|
+
backgroundImage: `url(${item.poster})`,
|
|
2412
2376
|
backgroundSize: "cover",
|
|
2413
2377
|
backgroundPosition: "center",
|
|
2414
2378
|
opacity: showPosterOverlay ? 1 : 0,
|
|
@@ -2578,7 +2542,7 @@ function ReelsFeed({
|
|
|
2578
2542
|
activeIndexRef.current = focusedIndex;
|
|
2579
2543
|
const { animateSnap, animateBounceBack, cancelAnimation } = useSnapAnimation({
|
|
2580
2544
|
duration: snapConfig?.duration ?? 260,
|
|
2581
|
-
easing: snapConfig?.easing ?? "
|
|
2545
|
+
easing: snapConfig?.easing ?? "ease-out"
|
|
2582
2546
|
});
|
|
2583
2547
|
useEffect(() => {
|
|
2584
2548
|
loadInitial();
|
|
@@ -3064,7 +3028,8 @@ function ReelsModal({
|
|
|
3064
3028
|
if (prewarmedRef.current === openIndex) return;
|
|
3065
3029
|
prewarmedRef.current = openIndex;
|
|
3066
3030
|
setFocusedIndexImmediate(openIndex);
|
|
3067
|
-
const
|
|
3031
|
+
const loader = adapters.videoLoader;
|
|
3032
|
+
const preload = loader?.preloadMetadata?.bind(loader);
|
|
3068
3033
|
if (!preload) return;
|
|
3069
3034
|
const targets = /* @__PURE__ */ new Set();
|
|
3070
3035
|
targets.add(openIndex);
|
|
@@ -3157,12 +3122,12 @@ function ReelsModal({
|
|
|
3157
3122
|
const panel = panelRef.current;
|
|
3158
3123
|
if (!panel) return;
|
|
3159
3124
|
const first = panel.querySelector(FOCUSABLE);
|
|
3160
|
-
(first ?? panel).focus();
|
|
3125
|
+
(first ?? panel).focus({ preventScroll: true });
|
|
3161
3126
|
});
|
|
3162
3127
|
return () => cancelAnimationFrame(id);
|
|
3163
3128
|
}
|
|
3164
3129
|
if (phase === "closed") {
|
|
3165
|
-
previouslyFocusedRef.current?.focus?.();
|
|
3130
|
+
previouslyFocusedRef.current?.focus?.({ preventScroll: true });
|
|
3166
3131
|
previouslyFocusedRef.current = null;
|
|
3167
3132
|
}
|
|
3168
3133
|
return void 0;
|
|
@@ -3920,4 +3885,106 @@ var HttpError = class extends Error {
|
|
|
3920
3885
|
}
|
|
3921
3886
|
};
|
|
3922
3887
|
|
|
3923
|
-
|
|
3888
|
+
// src/adapters/browser/BrowserVideoLoader.ts
|
|
3889
|
+
var BrowserVideoLoader = class {
|
|
3890
|
+
constructor(config = {}) {
|
|
3891
|
+
this.statusById = /* @__PURE__ */ new Map();
|
|
3892
|
+
this.controllers = /* @__PURE__ */ new Map();
|
|
3893
|
+
/** URLs already warmed via preloadMetadata — dedupes fire-and-forget calls. */
|
|
3894
|
+
this.warmedUrls = /* @__PURE__ */ new Set();
|
|
3895
|
+
this.timeoutMs = config.timeoutMs ?? 8e3;
|
|
3896
|
+
this.mp4PrefetchBytes = config.mp4PrefetchBytes ?? 512 * 1024;
|
|
3897
|
+
this.headers = config.headers ?? {};
|
|
3898
|
+
this.logger = config.logger;
|
|
3899
|
+
}
|
|
3900
|
+
async preload(videoId, url, signal) {
|
|
3901
|
+
if (typeof fetch === "undefined") {
|
|
3902
|
+
return { videoId, status: "idle" };
|
|
3903
|
+
}
|
|
3904
|
+
if (this.statusById.get(videoId) === "loaded") {
|
|
3905
|
+
return { videoId, status: "loaded" };
|
|
3906
|
+
}
|
|
3907
|
+
this.cancel(videoId);
|
|
3908
|
+
const controller = new AbortController();
|
|
3909
|
+
this.controllers.set(videoId, controller);
|
|
3910
|
+
this.statusById.set(videoId, "loading");
|
|
3911
|
+
const onAbort = () => controller.abort();
|
|
3912
|
+
signal?.addEventListener("abort", onAbort);
|
|
3913
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
3914
|
+
const isHls = url.includes(".m3u8");
|
|
3915
|
+
const requestHeaders = { ...this.headers };
|
|
3916
|
+
if (!isHls && this.mp4PrefetchBytes > 0) {
|
|
3917
|
+
requestHeaders["Range"] = `bytes=0-${this.mp4PrefetchBytes - 1}`;
|
|
3918
|
+
}
|
|
3919
|
+
try {
|
|
3920
|
+
const res = await fetch(url, {
|
|
3921
|
+
method: "GET",
|
|
3922
|
+
signal: controller.signal,
|
|
3923
|
+
headers: requestHeaders
|
|
3924
|
+
});
|
|
3925
|
+
if (!res.ok && res.status !== 206) {
|
|
3926
|
+
throw new Error(`HTTP ${res.status}`);
|
|
3927
|
+
}
|
|
3928
|
+
const buf = await res.arrayBuffer();
|
|
3929
|
+
this.statusById.set(videoId, "loaded");
|
|
3930
|
+
this.warmedUrls.add(url);
|
|
3931
|
+
return { videoId, status: "loaded", loadedBytes: buf.byteLength };
|
|
3932
|
+
} catch (err) {
|
|
3933
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
3934
|
+
if (error.name === "AbortError") {
|
|
3935
|
+
this.statusById.set(videoId, "idle");
|
|
3936
|
+
return { videoId, status: "idle", error };
|
|
3937
|
+
}
|
|
3938
|
+
this.statusById.set(videoId, "error");
|
|
3939
|
+
this.logger?.warn(`[BrowserVideoLoader] preload failed for ${videoId}`, error.message);
|
|
3940
|
+
return { videoId, status: "error", error };
|
|
3941
|
+
} finally {
|
|
3942
|
+
clearTimeout(timer);
|
|
3943
|
+
signal?.removeEventListener("abort", onAbort);
|
|
3944
|
+
this.controllers.delete(videoId);
|
|
3945
|
+
}
|
|
3946
|
+
}
|
|
3947
|
+
cancel(videoId) {
|
|
3948
|
+
const controller = this.controllers.get(videoId);
|
|
3949
|
+
if (controller) {
|
|
3950
|
+
controller.abort();
|
|
3951
|
+
this.controllers.delete(videoId);
|
|
3952
|
+
}
|
|
3953
|
+
if (this.statusById.get(videoId) === "loading") {
|
|
3954
|
+
this.statusById.set(videoId, "idle");
|
|
3955
|
+
}
|
|
3956
|
+
}
|
|
3957
|
+
isPreloaded(videoId) {
|
|
3958
|
+
return this.statusById.get(videoId) === "loaded";
|
|
3959
|
+
}
|
|
3960
|
+
getPreloadStatus(videoId) {
|
|
3961
|
+
return this.statusById.get(videoId) ?? "idle";
|
|
3962
|
+
}
|
|
3963
|
+
clearAll() {
|
|
3964
|
+
for (const controller of this.controllers.values()) {
|
|
3965
|
+
controller.abort();
|
|
3966
|
+
}
|
|
3967
|
+
this.controllers.clear();
|
|
3968
|
+
this.statusById.clear();
|
|
3969
|
+
this.warmedUrls.clear();
|
|
3970
|
+
}
|
|
3971
|
+
/**
|
|
3972
|
+
* Tier-4 cold prefetch. Fire-and-forget GET that warms the HTTP cache.
|
|
3973
|
+
* No DOM, no segment download, never throws. Deduped per-URL.
|
|
3974
|
+
*/
|
|
3975
|
+
preloadMetadata(url) {
|
|
3976
|
+
if (typeof fetch === "undefined") return;
|
|
3977
|
+
if (!url || this.warmedUrls.has(url)) return;
|
|
3978
|
+
this.warmedUrls.add(url);
|
|
3979
|
+
const isHls = url.includes(".m3u8");
|
|
3980
|
+
const headers = { ...this.headers };
|
|
3981
|
+
if (!isHls) {
|
|
3982
|
+
headers["Range"] = "bytes=0-0";
|
|
3983
|
+
}
|
|
3984
|
+
void fetch(url, { method: "GET", headers }).catch(() => {
|
|
3985
|
+
this.warmedUrls.delete(url);
|
|
3986
|
+
});
|
|
3987
|
+
}
|
|
3988
|
+
};
|
|
3989
|
+
|
|
3990
|
+
export { BrowserVideoLoader, DEFAULT_FEED_CONFIG, DEFAULT_NAVIGATION_CONFIG, DEFAULT_PLAYER_CONFIG, DEFAULT_RESOURCE_CONFIG, DefaultActions, DefaultOverlay, DefaultPauseAction, DefaultSkeleton, FeedManager, HttpDataSource, HttpError, MockAnalytics, MockCommentAdapter, MockDataSource, MockInteraction, MockLogger, MockNetworkAdapter, MockSessionStorage, MockVideoLoader, NavigationManager, OptimisticManager, PlayerEngine, PlayerStatus, ReelsFeed, ReelsFeedThumbnail, ReelsModal, ReelsProvider, ResourceGovernor, VALID_TRANSITIONS, VideoSlot, canPause, canPlay, canSeek, isArticle, isValidNavTransition, isValidTransition, isVideoItem, useFeed, useFeedSelector, useHls, useNavigation, useNavigationSelector, usePlayer, usePlayerSelector, usePointerGesture, useResource, useResourceSelector, useSDK, useSnapAnimation };
|