@xhub-reels/sdk 0.2.17 → 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 +107 -39
- package/dist/index.d.cts +59 -1
- package/dist/index.d.ts +59 -1
- package/dist/index.js +107 -40
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -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,
|
|
@@ -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);
|
|
@@ -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
|
@@ -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
|
@@ -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
|
@@ -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,
|
|
@@ -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);
|
|
@@ -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 };
|