@xhub-reels/sdk 0.1.7

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 ADDED
@@ -0,0 +1,2891 @@
1
+ 'use strict';
2
+
3
+ var vanilla = require('zustand/vanilla');
4
+ var react = require('react');
5
+ var jsxRuntime = require('react/jsx-runtime');
6
+ var Hls = require('hls.js');
7
+
8
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
9
+
10
+ var Hls__default = /*#__PURE__*/_interopDefault(Hls);
11
+
12
+ // src/types/content.ts
13
+ function isVideoItem(item) {
14
+ return item.type === "video";
15
+ }
16
+ function isArticle(item) {
17
+ return item.type === "article";
18
+ }
19
+
20
+ // src/types/feed.ts
21
+ var DEFAULT_FEED_CONFIG = {
22
+ pageSize: 10,
23
+ maxRetries: 3,
24
+ retryDelay: 1e3,
25
+ maxCacheSize: 50,
26
+ staleTTL: 5 * 60 * 1e3
27
+ };
28
+
29
+ // src/types/player.ts
30
+ var PlayerStatus = /* @__PURE__ */ ((PlayerStatus2) => {
31
+ PlayerStatus2["IDLE"] = "idle";
32
+ PlayerStatus2["LOADING"] = "loading";
33
+ PlayerStatus2["PLAYING"] = "playing";
34
+ PlayerStatus2["PAUSED"] = "paused";
35
+ PlayerStatus2["BUFFERING"] = "buffering";
36
+ PlayerStatus2["ERROR"] = "error";
37
+ return PlayerStatus2;
38
+ })(PlayerStatus || {});
39
+ var DEFAULT_PLAYER_CONFIG = {
40
+ defaultVolume: 1,
41
+ defaultMuted: true,
42
+ circuitBreakerThreshold: 3,
43
+ circuitBreakerResetMs: 1e4
44
+ };
45
+
46
+ // src/domain/state-machine.ts
47
+ var VALID_TRANSITIONS = {
48
+ ["idle" /* IDLE */]: ["loading" /* LOADING */],
49
+ ["loading" /* LOADING */]: ["playing" /* PLAYING */, "error" /* ERROR */, "idle" /* IDLE */],
50
+ ["playing" /* PLAYING */]: [
51
+ "paused" /* PAUSED */,
52
+ "buffering" /* BUFFERING */,
53
+ "error" /* ERROR */,
54
+ "idle" /* IDLE */
55
+ ],
56
+ ["paused" /* PAUSED */]: ["playing" /* PLAYING */, "idle" /* IDLE */, "loading" /* LOADING */],
57
+ ["buffering" /* BUFFERING */]: ["playing" /* PLAYING */, "error" /* ERROR */, "idle" /* IDLE */],
58
+ ["error" /* ERROR */]: ["idle" /* IDLE */, "loading" /* LOADING */]
59
+ };
60
+ function isValidTransition(from, to) {
61
+ if (from === to) return true;
62
+ return VALID_TRANSITIONS[from].includes(to);
63
+ }
64
+ function canPlay(status) {
65
+ return status === "paused" /* PAUSED */ || status === "loading" /* LOADING */;
66
+ }
67
+ function canPause(status) {
68
+ return status === "playing" /* PLAYING */ || status === "buffering" /* BUFFERING */;
69
+ }
70
+ function canSeek(status) {
71
+ return status === "playing" /* PLAYING */ || status === "paused" /* PAUSED */ || status === "buffering" /* BUFFERING */;
72
+ }
73
+
74
+ // src/domain/PlayerEngine.ts
75
+ function createInitialCircuit() {
76
+ return {
77
+ state: "closed" /* CLOSED */,
78
+ consecutiveErrors: 0,
79
+ openedAt: null,
80
+ lastError: null
81
+ };
82
+ }
83
+ function createInitialState(muted, volume) {
84
+ return {
85
+ status: "idle" /* IDLE */,
86
+ currentVideo: null,
87
+ currentVideoId: null,
88
+ currentTime: 0,
89
+ duration: 0,
90
+ buffered: 0,
91
+ volume,
92
+ muted,
93
+ playbackRate: 1,
94
+ loopCount: 0,
95
+ watchTime: 0,
96
+ error: null,
97
+ ended: false,
98
+ pendingRestoreTime: null,
99
+ pendingRestoreVideoId: null
100
+ };
101
+ }
102
+ var PlayerEngine = class {
103
+ constructor(config = {}, analytics, logger) {
104
+ this.listeners = /* @__PURE__ */ new Set();
105
+ this.circuit = createInitialCircuit();
106
+ this.circuitResetTimer = null;
107
+ this.watchTimeInterval = null;
108
+ this.lastStatus = "idle" /* IDLE */;
109
+ this.config = { ...DEFAULT_PLAYER_CONFIG, ...config };
110
+ this.analytics = analytics;
111
+ this.logger = logger;
112
+ this.store = vanilla.createStore(
113
+ () => createInitialState(this.config.defaultMuted, this.config.defaultVolume)
114
+ );
115
+ this.store.subscribe((state) => {
116
+ if (state.status !== this.lastStatus) {
117
+ this.emit({
118
+ type: "statusChange",
119
+ status: state.status,
120
+ previousStatus: this.lastStatus
121
+ });
122
+ this.lastStatus = state.status;
123
+ }
124
+ });
125
+ }
126
+ // ═══════════════════════════════════════════
127
+ // PUBLIC API — Playback Control
128
+ // ═══════════════════════════════════════════
129
+ /**
130
+ * Load a video and prepare for playback.
131
+ * Returns false if rejected by circuit breaker or invalid state.
132
+ */
133
+ load(video) {
134
+ if (!this.checkCircuit()) {
135
+ this.logger?.warn("[PlayerEngine] Load rejected \u2014 circuit OPEN");
136
+ this.emit({ type: "loadRejected", reason: "circuit_open" });
137
+ if (this.circuit.lastError) {
138
+ this.store.setState({ error: this.circuit.lastError });
139
+ this.transition("error" /* ERROR */);
140
+ }
141
+ return false;
142
+ }
143
+ const { status, currentVideo } = this.store.getState();
144
+ if (currentVideo && currentVideo.id !== video.id) {
145
+ this.trackLeave(this.store.getState());
146
+ }
147
+ if (status !== "idle" /* IDLE */ && status !== "error" /* ERROR */ && status !== "paused" /* PAUSED */) {
148
+ if (!this.transition("idle" /* IDLE */)) return false;
149
+ }
150
+ if (!this.transition("loading" /* LOADING */)) return false;
151
+ this.store.setState({
152
+ currentVideo: video,
153
+ currentVideoId: video.id,
154
+ currentTime: 0,
155
+ duration: video.duration,
156
+ loopCount: 0,
157
+ watchTime: 0,
158
+ error: null,
159
+ ended: false,
160
+ // Persist playback speed across videos
161
+ playbackRate: this.store.getState().playbackRate
162
+ });
163
+ this.emit({ type: "videoChange", video });
164
+ this.logger?.debug(`[PlayerEngine] Loaded: ${video.id}`);
165
+ return true;
166
+ }
167
+ play() {
168
+ const { status } = this.store.getState();
169
+ if (!canPlay(status)) {
170
+ this.logger?.warn(`[PlayerEngine] Cannot play from ${status}`);
171
+ return false;
172
+ }
173
+ this.startWatchTime();
174
+ return this.transition("playing" /* PLAYING */);
175
+ }
176
+ pause() {
177
+ const { status } = this.store.getState();
178
+ if (!canPause(status)) return false;
179
+ this.stopWatchTime();
180
+ return this.transition("paused" /* PAUSED */);
181
+ }
182
+ togglePlay() {
183
+ const { status } = this.store.getState();
184
+ if (status === "playing" /* PLAYING */ || status === "buffering" /* BUFFERING */) {
185
+ return this.pause();
186
+ }
187
+ return this.play();
188
+ }
189
+ seek(time) {
190
+ const { status, duration } = this.store.getState();
191
+ if (!canSeek(status)) return false;
192
+ const clamped = Math.max(0, Math.min(time, duration));
193
+ this.store.setState({ currentTime: clamped });
194
+ this.analytics?.trackPlaybackEvent(
195
+ this.store.getState().currentVideoId ?? "",
196
+ "seek",
197
+ clamped
198
+ );
199
+ return true;
200
+ }
201
+ // ═══════════════════════════════════════════
202
+ // PUBLIC API — Volume
203
+ // ═══════════════════════════════════════════
204
+ setMuted(muted) {
205
+ this.store.setState({ muted });
206
+ }
207
+ toggleMute() {
208
+ this.store.setState((s) => ({ muted: !s.muted }));
209
+ }
210
+ setVolume(volume) {
211
+ this.store.setState({ volume: Math.max(0, Math.min(1, volume)) });
212
+ }
213
+ setPlaybackRate(rate) {
214
+ this.store.setState({ playbackRate: rate });
215
+ }
216
+ // ═══════════════════════════════════════════
217
+ // PUBLIC API — Video Element Event Handlers
218
+ // ═══════════════════════════════════════════
219
+ /** Called when <video> fires `canplay` */
220
+ onCanPlay() {
221
+ const { status } = this.store.getState();
222
+ if (status === "loading" /* LOADING */ || status === "buffering" /* BUFFERING */) {
223
+ this.circuit.consecutiveErrors = 0;
224
+ this.play();
225
+ }
226
+ }
227
+ /** Called when <video> fires `waiting` */
228
+ onWaiting() {
229
+ const { status } = this.store.getState();
230
+ if (status !== "playing" /* PLAYING */) return false;
231
+ this.stopWatchTime();
232
+ return this.transition("buffering" /* BUFFERING */);
233
+ }
234
+ /** Called when <video> fires `playing` (after buffering) */
235
+ onPlaying() {
236
+ const { status } = this.store.getState();
237
+ if (status !== "buffering" /* BUFFERING */ && status !== "loading" /* LOADING */) return false;
238
+ this.startWatchTime();
239
+ return this.transition("playing" /* PLAYING */);
240
+ }
241
+ /** Called when <video> fires `timeupdate` */
242
+ onTimeUpdate(currentTime) {
243
+ this.store.setState({ currentTime });
244
+ }
245
+ /** Called when <video> fires `progress` (buffer update) */
246
+ onProgress(buffered) {
247
+ this.store.setState({ buffered });
248
+ }
249
+ /** Called when <video> fires `loadedmetadata` */
250
+ onLoadedMetadata(duration) {
251
+ this.store.setState({ duration });
252
+ }
253
+ /** Called when <video> fires `ended` */
254
+ onEnded() {
255
+ const state = this.store.getState();
256
+ this.stopWatchTime();
257
+ this.store.setState((s) => ({
258
+ ended: true,
259
+ loopCount: s.loopCount + 1
260
+ }));
261
+ this.analytics?.trackView(state.currentVideoId ?? "", state.watchTime);
262
+ this.emit({
263
+ type: "ended",
264
+ videoId: state.currentVideoId ?? "",
265
+ watchTime: state.watchTime,
266
+ loopCount: state.loopCount + 1
267
+ });
268
+ }
269
+ /** Called when <video> fires `error` */
270
+ onError(code, message) {
271
+ const error = {
272
+ code,
273
+ message,
274
+ recoverable: code !== "DECODE_ERROR" && code !== "NOT_SUPPORTED"
275
+ };
276
+ this.stopWatchTime();
277
+ this.recordCircuitError(error);
278
+ this.store.setState({ error });
279
+ this.transition("error" /* ERROR */);
280
+ this.emit({ type: "error", error });
281
+ this.analytics?.trackError(this.store.getState().currentVideoId ?? "", message);
282
+ }
283
+ // ═══════════════════════════════════════════
284
+ // PUBLIC API — Session Restore
285
+ // ═══════════════════════════════════════════
286
+ setPendingRestore(videoId, time) {
287
+ this.store.setState({
288
+ pendingRestoreVideoId: videoId,
289
+ pendingRestoreTime: time
290
+ });
291
+ }
292
+ consumePendingRestore(videoId) {
293
+ const { pendingRestoreVideoId, pendingRestoreTime } = this.store.getState();
294
+ if (pendingRestoreVideoId === videoId && pendingRestoreTime !== null) {
295
+ this.store.setState({ pendingRestoreTime: null, pendingRestoreVideoId: null });
296
+ return pendingRestoreTime;
297
+ }
298
+ return null;
299
+ }
300
+ // ═══════════════════════════════════════════
301
+ // PUBLIC API — Events
302
+ // ═══════════════════════════════════════════
303
+ on(listener) {
304
+ this.listeners.add(listener);
305
+ return () => this.listeners.delete(listener);
306
+ }
307
+ // ═══════════════════════════════════════════
308
+ // PUBLIC API — Lifecycle
309
+ // ═══════════════════════════════════════════
310
+ reset() {
311
+ this.stopWatchTime();
312
+ this.store.setState(
313
+ createInitialState(this.config.defaultMuted, this.config.defaultVolume)
314
+ );
315
+ this.lastStatus = "idle" /* IDLE */;
316
+ }
317
+ destroy() {
318
+ this.stopWatchTime();
319
+ if (this.circuitResetTimer) clearTimeout(this.circuitResetTimer);
320
+ this.listeners.clear();
321
+ }
322
+ // ═══════════════════════════════════════════
323
+ // PRIVATE — State Machine
324
+ // ═══════════════════════════════════════════
325
+ transition(to) {
326
+ const from = this.store.getState().status;
327
+ if (!isValidTransition(from, to)) {
328
+ this.logger?.warn(`[PlayerEngine] Invalid transition: ${from} \u2192 ${to}`);
329
+ return false;
330
+ }
331
+ this.store.setState({ status: to });
332
+ return true;
333
+ }
334
+ // ═══════════════════════════════════════════
335
+ // PRIVATE — Watch Time
336
+ // ═══════════════════════════════════════════
337
+ startWatchTime() {
338
+ if (this.watchTimeInterval) return;
339
+ this.watchTimeInterval = setInterval(() => {
340
+ this.store.setState((s) => ({ watchTime: s.watchTime + 1 }));
341
+ }, 1e3);
342
+ }
343
+ stopWatchTime() {
344
+ if (this.watchTimeInterval) {
345
+ clearInterval(this.watchTimeInterval);
346
+ this.watchTimeInterval = null;
347
+ }
348
+ }
349
+ // ═══════════════════════════════════════════
350
+ // PRIVATE — Circuit Breaker
351
+ // ═══════════════════════════════════════════
352
+ checkCircuit() {
353
+ if (this.circuit.state === "closed" /* CLOSED */) return true;
354
+ if (this.circuit.state === "half_open" /* HALF_OPEN */) return true;
355
+ const elapsed = Date.now() - (this.circuit.openedAt ?? 0);
356
+ if (elapsed >= this.config.circuitBreakerResetMs) {
357
+ this.circuit.state = "half_open" /* HALF_OPEN */;
358
+ this.logger?.info("[PlayerEngine] Circuit HALF_OPEN \u2014 trying recovery");
359
+ return true;
360
+ }
361
+ return false;
362
+ }
363
+ recordCircuitError(error) {
364
+ this.circuit.consecutiveErrors += 1;
365
+ this.circuit.lastError = error;
366
+ if (this.circuit.consecutiveErrors >= this.config.circuitBreakerThreshold) {
367
+ this.circuit.state = "open" /* OPEN */;
368
+ this.circuit.openedAt = Date.now();
369
+ this.logger?.warn(
370
+ `[PlayerEngine] Circuit OPEN after ${this.circuit.consecutiveErrors} errors`
371
+ );
372
+ if (this.circuitResetTimer) clearTimeout(this.circuitResetTimer);
373
+ this.circuitResetTimer = setTimeout(() => {
374
+ this.circuit.state = "half_open" /* HALF_OPEN */;
375
+ this.circuitResetTimer = null;
376
+ }, this.config.circuitBreakerResetMs);
377
+ }
378
+ }
379
+ // ═══════════════════════════════════════════
380
+ // PRIVATE — Analytics
381
+ // ═══════════════════════════════════════════
382
+ trackLeave(state) {
383
+ if (!this.analytics || !state.currentVideoId) return;
384
+ if (state.watchTime > 0) {
385
+ this.analytics.trackView(state.currentVideoId, state.watchTime);
386
+ }
387
+ }
388
+ // ═══════════════════════════════════════════
389
+ // PRIVATE — Events
390
+ // ═══════════════════════════════════════════
391
+ emit(event) {
392
+ for (const listener of this.listeners) {
393
+ try {
394
+ listener(event);
395
+ } catch {
396
+ }
397
+ }
398
+ }
399
+ };
400
+ function createInitialState2() {
401
+ return {
402
+ itemsById: /* @__PURE__ */ new Map(),
403
+ displayOrder: [],
404
+ loading: false,
405
+ loadingMore: false,
406
+ error: null,
407
+ cursor: null,
408
+ hasMore: true,
409
+ isStale: false,
410
+ lastFetchTime: null
411
+ };
412
+ }
413
+ var FeedManager = class {
414
+ constructor(dataSource, config = {}, logger) {
415
+ this.dataSource = dataSource;
416
+ /** Cancel in-flight requests */
417
+ this.abortController = null;
418
+ /** In-flight request deduplication: cursor → Promise */
419
+ this.inFlightRequests = /* @__PURE__ */ new Map();
420
+ /** LRU tracking: itemId → lastAccessTime */
421
+ this.accessOrder = /* @__PURE__ */ new Map();
422
+ /** Prefetch cache — instance-scoped (not static) */
423
+ this.prefetchCache = null;
424
+ this.config = { ...DEFAULT_FEED_CONFIG, ...config };
425
+ this.logger = logger;
426
+ this.store = vanilla.createStore(createInitialState2);
427
+ }
428
+ // ═══════════════════════════════════════════
429
+ // PUBLIC API — Data Source
430
+ // ═══════════════════════════════════════════
431
+ getDataSource() {
432
+ return this.dataSource;
433
+ }
434
+ setDataSource(dataSource, reset = true) {
435
+ this.dataSource = dataSource;
436
+ this.abortController?.abort();
437
+ this.abortController = null;
438
+ this.inFlightRequests.clear();
439
+ if (reset) {
440
+ this.store.setState(createInitialState2());
441
+ this.accessOrder.clear();
442
+ }
443
+ }
444
+ // ═══════════════════════════════════════════
445
+ // PUBLIC API — Prefetch
446
+ // ═══════════════════════════════════════════
447
+ async prefetch(ttlMs) {
448
+ if (this.prefetchCache) {
449
+ const ttl = ttlMs ?? this.config.staleTTL;
450
+ if (Date.now() - this.prefetchCache.timestamp < ttl) return;
451
+ }
452
+ try {
453
+ const page = await this.dataSource.fetchFeed(null);
454
+ this.prefetchCache = {
455
+ items: page.items,
456
+ nextCursor: page.nextCursor,
457
+ timestamp: Date.now()
458
+ };
459
+ } catch {
460
+ }
461
+ }
462
+ hasPrefetchCache() {
463
+ return this.prefetchCache !== null;
464
+ }
465
+ clearPrefetchCache() {
466
+ this.prefetchCache = null;
467
+ }
468
+ // ═══════════════════════════════════════════
469
+ // PUBLIC API — Loading
470
+ // ═══════════════════════════════════════════
471
+ async loadInitial() {
472
+ const state = this.store.getState();
473
+ if (state.itemsById.size > 0 && state.lastFetchTime) {
474
+ const isStale = Date.now() - state.lastFetchTime > this.config.staleTTL;
475
+ if (!isStale) return;
476
+ this.store.setState({ isStale: true });
477
+ }
478
+ if (this.prefetchCache) {
479
+ const { items, nextCursor } = this.prefetchCache;
480
+ this.prefetchCache = null;
481
+ this.applyItems(items, nextCursor, false);
482
+ this.store.setState({ loading: false, lastFetchTime: Date.now(), isStale: false });
483
+ return;
484
+ }
485
+ await this.fetchPage(null, false);
486
+ }
487
+ async loadMore() {
488
+ const { loadingMore, hasMore, cursor, loading } = this.store.getState();
489
+ if (loadingMore || loading || !hasMore) return;
490
+ await this.fetchPage(cursor, true);
491
+ }
492
+ async refresh() {
493
+ this.abortController?.abort();
494
+ this.abortController = null;
495
+ this.store.setState({
496
+ ...createInitialState2(),
497
+ // Preserve existing items while loading to avoid blank screen
498
+ itemsById: this.store.getState().itemsById,
499
+ displayOrder: this.store.getState().displayOrder,
500
+ isStale: true
501
+ });
502
+ await this.fetchPage(null, false);
503
+ }
504
+ // ═══════════════════════════════════════════
505
+ // PUBLIC API — Items
506
+ // ═══════════════════════════════════════════
507
+ getItems() {
508
+ const { itemsById, displayOrder } = this.store.getState();
509
+ return displayOrder.map((id) => itemsById.get(id)).filter((item) => item !== void 0);
510
+ }
511
+ getItemById(id) {
512
+ this.accessOrder.set(id, Date.now());
513
+ return this.store.getState().itemsById.get(id);
514
+ }
515
+ updateItem(id, patch) {
516
+ const { itemsById } = this.store.getState();
517
+ const item = itemsById.get(id);
518
+ if (!item) return;
519
+ const updated = new Map(itemsById);
520
+ updated.set(id, { ...item, ...patch });
521
+ this.store.setState({ itemsById: updated });
522
+ }
523
+ // ═══════════════════════════════════════════
524
+ // PUBLIC API — Lifecycle
525
+ // ═══════════════════════════════════════════
526
+ destroy() {
527
+ this.abortController?.abort();
528
+ this.inFlightRequests.clear();
529
+ this.accessOrder.clear();
530
+ this.prefetchCache = null;
531
+ }
532
+ // ═══════════════════════════════════════════
533
+ // PRIVATE — Fetch
534
+ // ═══════════════════════════════════════════
535
+ async fetchPage(cursor, isPagination) {
536
+ const cacheKey = cursor ?? "__initial__";
537
+ const inFlight = this.inFlightRequests.get(cacheKey);
538
+ if (inFlight) return inFlight;
539
+ const promise = this.doFetch(cursor, isPagination);
540
+ this.inFlightRequests.set(cacheKey, promise);
541
+ try {
542
+ await promise;
543
+ } finally {
544
+ this.inFlightRequests.delete(cacheKey);
545
+ }
546
+ }
547
+ async doFetch(cursor, isPagination) {
548
+ this.abortController?.abort();
549
+ const controller = new AbortController();
550
+ this.abortController = controller;
551
+ this.store.setState(
552
+ isPagination ? { loadingMore: true, error: null } : { loading: true, error: null }
553
+ );
554
+ let attempt = 0;
555
+ while (attempt <= this.config.maxRetries) {
556
+ if (controller.signal.aborted) return;
557
+ try {
558
+ const page = await this.dataSource.fetchFeed(cursor);
559
+ if (controller.signal.aborted) return;
560
+ this.applyItems(page.items, page.nextCursor, isPagination);
561
+ this.store.setState({
562
+ loading: false,
563
+ loadingMore: false,
564
+ hasMore: page.hasMore,
565
+ cursor: page.nextCursor,
566
+ lastFetchTime: Date.now(),
567
+ isStale: false,
568
+ error: null
569
+ });
570
+ return;
571
+ } catch (err) {
572
+ if (controller.signal.aborted) return;
573
+ attempt += 1;
574
+ if (attempt > this.config.maxRetries) {
575
+ const error = {
576
+ message: err instanceof Error ? err.message : "Unknown error",
577
+ code: "FETCH_FAILED",
578
+ retryable: true
579
+ };
580
+ this.store.setState({ loading: false, loadingMore: false, error });
581
+ this.logger?.error("[FeedManager] Fetch failed after retries", err);
582
+ return;
583
+ }
584
+ const delay = this.config.retryDelay * 2 ** (attempt - 1);
585
+ this.logger?.warn(`[FeedManager] Retrying in ${delay}ms (attempt ${attempt})`);
586
+ await sleep(delay);
587
+ }
588
+ }
589
+ }
590
+ // ═══════════════════════════════════════════
591
+ // PRIVATE — State Mutation
592
+ // ═══════════════════════════════════════════
593
+ applyItems(incoming, _nextCursor, append) {
594
+ const { itemsById, displayOrder } = this.store.getState();
595
+ const nextById = new Map(itemsById);
596
+ const existingIds = new Set(displayOrder);
597
+ const newIds = [];
598
+ for (const item of incoming) {
599
+ if (!existingIds.has(item.id)) {
600
+ newIds.push(item.id);
601
+ }
602
+ nextById.set(item.id, item);
603
+ this.accessOrder.set(item.id, Date.now());
604
+ }
605
+ const nextOrder = append ? [...displayOrder, ...newIds] : newIds;
606
+ if (nextById.size > this.config.maxCacheSize) {
607
+ this.evictLRU(nextById, nextOrder);
608
+ }
609
+ this.store.setState({ itemsById: nextById, displayOrder: nextOrder });
610
+ }
611
+ evictLRU(itemsById, displayOrder) {
612
+ const evictCount = itemsById.size - this.config.maxCacheSize;
613
+ if (evictCount <= 0) return;
614
+ const sorted = [...this.accessOrder.entries()].sort(([, a], [, b]) => a - b);
615
+ let evicted = 0;
616
+ for (const [id] of sorted) {
617
+ if (evicted >= evictCount) break;
618
+ itemsById.delete(id);
619
+ this.accessOrder.delete(id);
620
+ const idx = displayOrder.indexOf(id);
621
+ if (idx !== -1) displayOrder.splice(idx, 1);
622
+ evicted++;
623
+ }
624
+ this.logger?.debug(`[FeedManager] Evicted ${evicted} LRU items`);
625
+ }
626
+ };
627
+ function sleep(ms) {
628
+ return new Promise((resolve) => setTimeout(resolve, ms));
629
+ }
630
+ function createInitialState3() {
631
+ return {
632
+ pendingActions: /* @__PURE__ */ new Map(),
633
+ failedQueue: [],
634
+ hasPending: false,
635
+ isRetrying: false,
636
+ likeDeltas: /* @__PURE__ */ new Map(),
637
+ followState: /* @__PURE__ */ new Map()
638
+ };
639
+ }
640
+ var OptimisticManager = class {
641
+ constructor(interaction, logger) {
642
+ /** Debounce timers: contentId → timer */
643
+ this.likeDebounceTimers = /* @__PURE__ */ new Map();
644
+ /** Pending like direction: contentId → final intended state */
645
+ this.pendingLikeState = /* @__PURE__ */ new Map();
646
+ this.interaction = interaction;
647
+ this.logger = logger;
648
+ this.store = vanilla.createStore(createInitialState3);
649
+ }
650
+ // ═══════════════════════════════════════════
651
+ // PUBLIC API — Like (debounced toggle)
652
+ // ═══════════════════════════════════════════
653
+ /**
654
+ * Debounced like toggle — prevents rapid API spam on double-tap.
655
+ * UI updates instantly; API call fires after 600ms debounce.
656
+ */
657
+ toggleLike(contentId, currentIsLiked) {
658
+ const pendingState = this.pendingLikeState.get(contentId) ?? currentIsLiked;
659
+ const nextLiked = !pendingState;
660
+ this.pendingLikeState.set(contentId, nextLiked);
661
+ this.store.setState((s) => {
662
+ const nextDeltas = new Map(s.likeDeltas);
663
+ nextDeltas.get(contentId) ?? 0;
664
+ const delta = (nextLiked ? 1 : 0) - (currentIsLiked ? 1 : 0);
665
+ nextDeltas.set(contentId, delta);
666
+ return { likeDeltas: nextDeltas };
667
+ });
668
+ const existing = this.likeDebounceTimers.get(contentId);
669
+ if (existing) clearTimeout(existing);
670
+ const timer = setTimeout(async () => {
671
+ this.likeDebounceTimers.delete(contentId);
672
+ const finalState = this.pendingLikeState.get(contentId) ?? currentIsLiked;
673
+ this.pendingLikeState.delete(contentId);
674
+ try {
675
+ if (finalState) {
676
+ await this.interaction.like?.(contentId);
677
+ } else {
678
+ await this.interaction.unlike?.(contentId);
679
+ }
680
+ this.store.setState((s) => {
681
+ const nextDeltas = new Map(s.likeDeltas);
682
+ nextDeltas.delete(contentId);
683
+ return { likeDeltas: nextDeltas };
684
+ });
685
+ } catch (err) {
686
+ this.store.setState((s) => {
687
+ const nextDeltas = new Map(s.likeDeltas);
688
+ nextDeltas.delete(contentId);
689
+ return { likeDeltas: nextDeltas };
690
+ });
691
+ this.logger?.error("[OptimisticManager] toggleLike failed \u2014 rolled back", err);
692
+ }
693
+ }, 600);
694
+ this.likeDebounceTimers.set(contentId, timer);
695
+ }
696
+ // ═══════════════════════════════════════════
697
+ // PUBLIC API — Follow
698
+ // ═══════════════════════════════════════════
699
+ async toggleFollow(authorId, currentIsFollowing) {
700
+ const nextFollowing = !currentIsFollowing;
701
+ this.store.setState((s) => {
702
+ const next = new Map(s.followState);
703
+ next.set(authorId, nextFollowing);
704
+ return { followState: next };
705
+ });
706
+ try {
707
+ if (nextFollowing) {
708
+ await this.interaction.follow?.(authorId);
709
+ } else {
710
+ await this.interaction.unfollow?.(authorId);
711
+ }
712
+ return true;
713
+ } catch (err) {
714
+ this.store.setState((s) => {
715
+ const next = new Map(s.followState);
716
+ next.delete(authorId);
717
+ return { followState: next };
718
+ });
719
+ this.logger?.error("[OptimisticManager] toggleFollow failed \u2014 rolled back", err);
720
+ return false;
721
+ }
722
+ }
723
+ // ═══════════════════════════════════════════
724
+ // PUBLIC API — Helpers
725
+ // ═══════════════════════════════════════════
726
+ getLikeDelta(contentId) {
727
+ return this.store.getState().likeDeltas.get(contentId) ?? 0;
728
+ }
729
+ getFollowState(authorId) {
730
+ return this.store.getState().followState.get(authorId);
731
+ }
732
+ // ═══════════════════════════════════════════
733
+ // PUBLIC API — Lifecycle
734
+ // ═══════════════════════════════════════════
735
+ destroy() {
736
+ for (const timer of this.likeDebounceTimers.values()) {
737
+ clearTimeout(timer);
738
+ }
739
+ this.likeDebounceTimers.clear();
740
+ this.pendingLikeState.clear();
741
+ }
742
+ };
743
+ var DEFAULT_RESOURCE_CONFIG = {
744
+ maxAllocations: 11,
745
+ bufferWindow: 3,
746
+ warmWindow: 4,
747
+ // 0ms debounce — setFocusedIndexImmediate is already used post-snap.
748
+ focusDebounceMs: 0,
749
+ preloadLookAhead: 3
750
+ };
751
+ var ResourceGovernor = class {
752
+ constructor(config = {}, videoLoader, network, logger) {
753
+ this.focusDebounceTimer = null;
754
+ this.config = { ...DEFAULT_RESOURCE_CONFIG, ...config };
755
+ this.videoLoader = videoLoader;
756
+ this.network = network;
757
+ this.logger = logger;
758
+ this.store = vanilla.createStore(() => ({
759
+ activeAllocations: /* @__PURE__ */ new Set(),
760
+ warmAllocations: /* @__PURE__ */ new Set(),
761
+ preloadQueue: [],
762
+ focusedIndex: 0,
763
+ totalItems: 0,
764
+ networkType: network?.getNetworkType() ?? "unknown",
765
+ isActive: false,
766
+ prefetchIndex: null
767
+ }));
768
+ }
769
+ // ═══════════════════════════════════════════
770
+ // PUBLIC API — Lifecycle
771
+ // ═══════════════════════════════════════════
772
+ async activate() {
773
+ if (this.store.getState().isActive) return;
774
+ if (this.network) {
775
+ this.networkUnsubscribe = this.network.onNetworkChange((type) => {
776
+ this.store.setState({ networkType: type });
777
+ });
778
+ }
779
+ this.store.setState({ isActive: true });
780
+ this.recalculate();
781
+ this.logger?.debug("[ResourceGovernor] Activated");
782
+ }
783
+ deactivate() {
784
+ this.networkUnsubscribe?.();
785
+ if (this.focusDebounceTimer) clearTimeout(this.focusDebounceTimer);
786
+ this.store.setState({ isActive: false });
787
+ }
788
+ destroy() {
789
+ this.deactivate();
790
+ this.videoLoader?.clearAll();
791
+ }
792
+ // ═══════════════════════════════════════════
793
+ // PUBLIC API — Feed State
794
+ // ═══════════════════════════════════════════
795
+ setTotalItems(count) {
796
+ this.store.setState({ totalItems: count });
797
+ this.recalculate();
798
+ }
799
+ /**
800
+ * Debounced focus update — prevents rapid re-allocations during fast swipe
801
+ */
802
+ setFocusedIndex(index) {
803
+ if (this.focusDebounceTimer) clearTimeout(this.focusDebounceTimer);
804
+ this.focusDebounceTimer = setTimeout(() => {
805
+ this.focusDebounceTimer = null;
806
+ this.setFocusedIndexImmediate(index);
807
+ }, this.config.focusDebounceMs);
808
+ }
809
+ setFocusedIndexImmediate(index) {
810
+ this.store.setState({ focusedIndex: index, prefetchIndex: null });
811
+ this.recalculate();
812
+ }
813
+ /**
814
+ * Signal that the given index should have its video src eagerly loaded.
815
+ * Called by the gesture layer at ~50% drag. Pass null to clear.
816
+ */
817
+ setPrefetchIndex(index) {
818
+ this.store.setState({ prefetchIndex: index });
819
+ }
820
+ // ═══════════════════════════════════════════
821
+ // PUBLIC API — Allocation Queries
822
+ // ═══════════════════════════════════════════
823
+ isAllocated(index) {
824
+ return this.store.getState().activeAllocations.has(index);
825
+ }
826
+ isWarmAllocated(index) {
827
+ return this.store.getState().warmAllocations.has(index);
828
+ }
829
+ shouldRenderVideo(index) {
830
+ const state = this.store.getState();
831
+ return state.activeAllocations.has(index) || state.warmAllocations.has(index);
832
+ }
833
+ isPreloading(index) {
834
+ return this.store.getState().preloadQueue.includes(index);
835
+ }
836
+ getActiveAllocations() {
837
+ return [...this.store.getState().activeAllocations];
838
+ }
839
+ getWarmAllocations() {
840
+ return [...this.store.getState().warmAllocations];
841
+ }
842
+ // ═══════════════════════════════════════════
843
+ // PRIVATE — Allocation Logic (3-Tier)
844
+ // ═══════════════════════════════════════════
845
+ recalculate() {
846
+ const { focusedIndex, totalItems, isActive } = this.store.getState();
847
+ if (!isActive || totalItems === 0) return;
848
+ const { maxAllocations, bufferWindow, warmWindow, preloadLookAhead } = this.config;
849
+ const hotDesired = [];
850
+ hotDesired.push(focusedIndex);
851
+ for (let delta = 1; delta <= bufferWindow; delta++) {
852
+ const ahead = focusedIndex + delta;
853
+ const behind = focusedIndex - delta;
854
+ if (ahead < totalItems) hotDesired.push(ahead);
855
+ if (behind >= 0) hotDesired.push(behind);
856
+ }
857
+ const warmDesired = [];
858
+ const hotEnd = Math.min(totalItems - 1, focusedIndex + bufferWindow);
859
+ const hotStart = Math.max(0, focusedIndex - bufferWindow);
860
+ const forwardWarmCount = Math.max(0, warmWindow - 1);
861
+ for (let i = 1; i <= forwardWarmCount; i++) {
862
+ const idx = hotEnd + i;
863
+ if (idx < totalItems) warmDesired.push(idx);
864
+ }
865
+ const backwardIdx = hotStart - 1;
866
+ if (backwardIdx >= 0) warmDesired.push(backwardIdx);
867
+ const totalDesired = hotDesired.length + warmDesired.length;
868
+ let finalHot = hotDesired;
869
+ let finalWarm = warmDesired;
870
+ if (totalDesired > maxAllocations) {
871
+ const warmBudget = Math.max(0, maxAllocations - hotDesired.length);
872
+ finalWarm = warmDesired.slice(0, warmBudget);
873
+ if (hotDesired.length > maxAllocations) {
874
+ finalHot = hotDesired.slice(0, maxAllocations);
875
+ finalWarm = [];
876
+ }
877
+ }
878
+ const newActiveAllocations = new Set(finalHot);
879
+ const newWarmAllocations = new Set(finalWarm);
880
+ const warmEnd = finalWarm.length > 0 ? Math.max(...finalWarm.filter((i) => i > focusedIndex), hotEnd) : hotEnd;
881
+ const preloadStart = warmEnd + 1;
882
+ const preloadEnd = Math.min(totalItems - 1, preloadStart + preloadLookAhead - 1);
883
+ const preloadQueue = [];
884
+ for (let i = preloadStart; i <= preloadEnd; i++) {
885
+ preloadQueue.push(i);
886
+ }
887
+ this.store.setState({
888
+ activeAllocations: newActiveAllocations,
889
+ warmAllocations: newWarmAllocations,
890
+ preloadQueue
891
+ });
892
+ this.logger?.debug(
893
+ `[ResourceGovernor] Hot: [${finalHot}], Warm: [${finalWarm}], Preload: [${preloadQueue}]`
894
+ );
895
+ }
896
+ };
897
+ function usePointerGesture(config = {}) {
898
+ const {
899
+ axis = "y",
900
+ velocityThreshold = 0.3,
901
+ distanceThreshold = 80,
902
+ disabled = false,
903
+ onDragOffset,
904
+ onDragThreshold,
905
+ containerSize,
906
+ dragThresholdRatio = 0.5,
907
+ onSnap,
908
+ onBounceBack
909
+ } = config;
910
+ const isDraggingRef = react.useRef(false);
911
+ const dragOffsetRef = react.useRef(0);
912
+ const startPointRef = react.useRef(0);
913
+ const startTimeRef = react.useRef(0);
914
+ const lastPointRef = react.useRef(0);
915
+ const lastTimeRef = react.useRef(0);
916
+ const velocityRef = react.useRef(0);
917
+ const rafIdRef = react.useRef(null);
918
+ const isLockedRef = react.useRef(false);
919
+ const startCrossAxisRef = react.useRef(0);
920
+ const thresholdFiredRef = react.useRef(false);
921
+ const onDragOffsetRef = react.useRef(onDragOffset);
922
+ const onDragThresholdRef = react.useRef(onDragThreshold);
923
+ const onSnapRef = react.useRef(onSnap);
924
+ const onBounceBackRef = react.useRef(onBounceBack);
925
+ const disabledRef = react.useRef(disabled);
926
+ const containerSizeRef = react.useRef(containerSize);
927
+ const dragThresholdRatioRef = react.useRef(dragThresholdRatio);
928
+ react.useEffect(() => {
929
+ onDragOffsetRef.current = onDragOffset;
930
+ onDragThresholdRef.current = onDragThreshold;
931
+ onSnapRef.current = onSnap;
932
+ onBounceBackRef.current = onBounceBack;
933
+ disabledRef.current = disabled;
934
+ containerSizeRef.current = containerSize;
935
+ dragThresholdRatioRef.current = dragThresholdRatio;
936
+ });
937
+ const scheduleFrame = react.useCallback(
938
+ (offset) => {
939
+ if (rafIdRef.current !== null) {
940
+ cancelAnimationFrame(rafIdRef.current);
941
+ }
942
+ rafIdRef.current = requestAnimationFrame(() => {
943
+ rafIdRef.current = null;
944
+ onDragOffsetRef.current?.(offset);
945
+ });
946
+ },
947
+ []
948
+ );
949
+ const cancelPendingFrame = react.useCallback(() => {
950
+ if (rafIdRef.current !== null) {
951
+ cancelAnimationFrame(rafIdRef.current);
952
+ rafIdRef.current = null;
953
+ }
954
+ }, []);
955
+ const handlePointerMove = react.useCallback(
956
+ (e) => {
957
+ if (!isDraggingRef.current || isLockedRef.current) return;
958
+ const mainPos = axis === "y" ? e.clientY : e.clientX;
959
+ const crossPos = axis === "y" ? e.clientX : e.clientY;
960
+ const now = performance.now();
961
+ const mainDelta = mainPos - startPointRef.current;
962
+ const crossDelta = Math.abs(crossPos - startCrossAxisRef.current);
963
+ if (Math.abs(mainDelta) < 20 && crossDelta > Math.abs(mainDelta) * 1.5) {
964
+ isLockedRef.current = true;
965
+ cancelPendingFrame();
966
+ onDragOffsetRef.current?.(0);
967
+ return;
968
+ }
969
+ const dt = now - lastTimeRef.current;
970
+ if (dt > 0) {
971
+ const instantVelocity = (mainPos - lastPointRef.current) / dt;
972
+ velocityRef.current = velocityRef.current * 0.7 + instantVelocity * 0.3;
973
+ }
974
+ lastPointRef.current = mainPos;
975
+ lastTimeRef.current = now;
976
+ const offset = mainPos - startPointRef.current;
977
+ dragOffsetRef.current = offset;
978
+ if (!thresholdFiredRef.current && onDragThresholdRef.current) {
979
+ const size = containerSizeRef.current ?? (axis === "y" ? window.innerHeight : window.innerWidth);
980
+ const threshold = size * dragThresholdRatioRef.current;
981
+ if (Math.abs(offset) >= threshold) {
982
+ thresholdFiredRef.current = true;
983
+ const direction = offset < 0 ? "forward" : "backward";
984
+ onDragThresholdRef.current(direction);
985
+ }
986
+ }
987
+ scheduleFrame(offset);
988
+ },
989
+ [axis, scheduleFrame, cancelPendingFrame]
990
+ );
991
+ const handlePointerUp = react.useCallback(
992
+ (_e) => {
993
+ if (!isDraggingRef.current) return;
994
+ cancelPendingFrame();
995
+ isDraggingRef.current = false;
996
+ window.removeEventListener("pointermove", handlePointerMove);
997
+ window.removeEventListener("pointerup", handlePointerUp);
998
+ window.removeEventListener("pointercancel", handlePointerUp);
999
+ const offset = dragOffsetRef.current;
1000
+ const velocity = velocityRef.current;
1001
+ const shouldSnap = Math.abs(velocity) > velocityThreshold || Math.abs(offset) > distanceThreshold;
1002
+ if (shouldSnap) {
1003
+ const direction = offset < 0 || velocity < 0 ? "forward" : "backward";
1004
+ onSnapRef.current?.(direction);
1005
+ } else {
1006
+ onBounceBackRef.current?.();
1007
+ }
1008
+ dragOffsetRef.current = 0;
1009
+ velocityRef.current = 0;
1010
+ },
1011
+ [handlePointerMove, velocityThreshold, distanceThreshold, cancelPendingFrame]
1012
+ );
1013
+ const onPointerDown = react.useCallback(
1014
+ (e) => {
1015
+ if (disabledRef.current) return;
1016
+ if (!e.isPrimary) return;
1017
+ if (e.button !== 0) return;
1018
+ const mainPos = axis === "y" ? e.clientY : e.clientX;
1019
+ const crossPos = axis === "y" ? e.clientX : e.clientY;
1020
+ isDraggingRef.current = true;
1021
+ isLockedRef.current = false;
1022
+ thresholdFiredRef.current = false;
1023
+ startPointRef.current = mainPos;
1024
+ startCrossAxisRef.current = crossPos;
1025
+ startTimeRef.current = performance.now();
1026
+ lastPointRef.current = mainPos;
1027
+ lastTimeRef.current = performance.now();
1028
+ velocityRef.current = 0;
1029
+ dragOffsetRef.current = 0;
1030
+ e.currentTarget.setPointerCapture(e.pointerId);
1031
+ window.addEventListener("pointermove", handlePointerMove, { passive: true });
1032
+ window.addEventListener("pointerup", handlePointerUp);
1033
+ window.addEventListener("pointercancel", handlePointerUp);
1034
+ },
1035
+ [axis, handlePointerMove, handlePointerUp]
1036
+ );
1037
+ react.useEffect(() => {
1038
+ return () => {
1039
+ cancelPendingFrame();
1040
+ window.removeEventListener("pointermove", handlePointerMove);
1041
+ window.removeEventListener("pointerup", handlePointerUp);
1042
+ window.removeEventListener("pointercancel", handlePointerUp);
1043
+ };
1044
+ }, [cancelPendingFrame, handlePointerMove, handlePointerUp]);
1045
+ return {
1046
+ bind: { onPointerDown },
1047
+ isDraggingRef,
1048
+ dragOffsetRef
1049
+ };
1050
+ }
1051
+ function useSnapAnimation(config = {}) {
1052
+ const { duration = 280, easing = "cubic-bezier(0.25, 0.46, 0.45, 0.94)" } = config;
1053
+ const activeAnimations = react.useRef([]);
1054
+ const cancelAnimation = react.useCallback(() => {
1055
+ for (const anim of activeAnimations.current) {
1056
+ anim.cancel();
1057
+ }
1058
+ activeAnimations.current = [];
1059
+ }, []);
1060
+ const runAnimation = react.useCallback(
1061
+ (targets, animDuration) => {
1062
+ cancelAnimation();
1063
+ const animations = [];
1064
+ for (const { element, fromY, toY } of targets) {
1065
+ element.style.transition = "";
1066
+ const anim = element.animate(
1067
+ [
1068
+ { transform: `translateY(${fromY}px)` },
1069
+ { transform: `translateY(${toY}px)` }
1070
+ ],
1071
+ {
1072
+ duration: animDuration,
1073
+ easing,
1074
+ fill: "forwards"
1075
+ }
1076
+ );
1077
+ anim.addEventListener("finish", () => {
1078
+ element.style.transform = `translateY(${toY}px)`;
1079
+ anim.cancel();
1080
+ });
1081
+ animations.push(anim);
1082
+ }
1083
+ activeAnimations.current = animations;
1084
+ },
1085
+ [duration, easing, cancelAnimation]
1086
+ );
1087
+ const animateSnap = react.useCallback(
1088
+ (targets) => {
1089
+ runAnimation(targets, duration);
1090
+ },
1091
+ [runAnimation, duration]
1092
+ );
1093
+ const animateBounceBack = react.useCallback(
1094
+ (targets) => {
1095
+ runAnimation(targets, Math.round(duration * 1.2));
1096
+ },
1097
+ [runAnimation, duration]
1098
+ );
1099
+ react.useEffect(() => {
1100
+ return () => {
1101
+ cancelAnimation();
1102
+ };
1103
+ }, [cancelAnimation]);
1104
+ return { animateSnap, animateBounceBack, cancelAnimation };
1105
+ }
1106
+ var SDKContext = react.createContext(null);
1107
+ function ReelsProvider({ children, adapters, debug = false }) {
1108
+ const logger = adapters.logger;
1109
+ const sdkRef = react.useRef(null);
1110
+ const value = react.useMemo(() => {
1111
+ if (sdkRef.current) {
1112
+ sdkRef.current.feedManager.destroy();
1113
+ sdkRef.current.playerEngine.destroy();
1114
+ sdkRef.current.resourceGovernor.destroy();
1115
+ sdkRef.current.optimisticManager.destroy();
1116
+ }
1117
+ const feedManager = new FeedManager(adapters.dataSource, {}, logger);
1118
+ const playerEngine = new PlayerEngine(
1119
+ {},
1120
+ adapters.analytics,
1121
+ logger
1122
+ );
1123
+ const resourceGovernor = new ResourceGovernor(
1124
+ {},
1125
+ adapters.videoLoader,
1126
+ adapters.network,
1127
+ logger
1128
+ );
1129
+ const optimisticManager = new OptimisticManager(
1130
+ adapters.interaction ?? {},
1131
+ logger
1132
+ );
1133
+ const instance = {
1134
+ feedManager,
1135
+ playerEngine,
1136
+ resourceGovernor,
1137
+ optimisticManager,
1138
+ adapters
1139
+ };
1140
+ sdkRef.current = instance;
1141
+ return instance;
1142
+ }, [adapters.dataSource]);
1143
+ react.useEffect(() => {
1144
+ value.resourceGovernor.activate();
1145
+ return () => {
1146
+ value.resourceGovernor.deactivate();
1147
+ };
1148
+ }, [value.resourceGovernor]);
1149
+ react.useEffect(() => {
1150
+ if (debug) {
1151
+ logger?.debug("[ReelsProvider] Mounted in debug mode");
1152
+ }
1153
+ }, [debug, logger]);
1154
+ react.useEffect(() => {
1155
+ return () => {
1156
+ sdkRef.current?.feedManager.destroy();
1157
+ sdkRef.current?.playerEngine.destroy();
1158
+ sdkRef.current?.resourceGovernor.destroy();
1159
+ sdkRef.current?.optimisticManager.destroy();
1160
+ };
1161
+ }, []);
1162
+ return /* @__PURE__ */ jsxRuntime.jsx(SDKContext.Provider, { value, children });
1163
+ }
1164
+ function useSDK() {
1165
+ const ctx = react.useContext(SDKContext);
1166
+ if (!ctx) {
1167
+ throw new Error("[useSDK] Must be used inside <ReelsProvider>");
1168
+ }
1169
+ return ctx;
1170
+ }
1171
+ function useFeedSelector(selector) {
1172
+ const { feedManager } = useSDK();
1173
+ const selectorRef = react.useRef(selector);
1174
+ selectorRef.current = selector;
1175
+ const lastSnapshot = react.useRef(void 0);
1176
+ const lastState = react.useRef(void 0);
1177
+ const getSnapshot = react.useCallback(() => {
1178
+ const state = feedManager.store.getState();
1179
+ if (state !== lastState.current) {
1180
+ lastState.current = state;
1181
+ lastSnapshot.current = selectorRef.current(state);
1182
+ }
1183
+ return lastSnapshot.current;
1184
+ }, [feedManager]);
1185
+ return react.useSyncExternalStore(feedManager.store.subscribe, getSnapshot, getSnapshot);
1186
+ }
1187
+ function useFeed() {
1188
+ const { feedManager } = useSDK();
1189
+ const selectDisplayOrder = react.useCallback((s) => s.displayOrder, []);
1190
+ const selectLoading = react.useCallback((s) => s.loading, []);
1191
+ const selectLoadingMore = react.useCallback((s) => s.loadingMore, []);
1192
+ const selectHasMore = react.useCallback((s) => s.hasMore, []);
1193
+ const selectError = react.useCallback((s) => s.error, []);
1194
+ const selectIsStale = react.useCallback((s) => s.isStale, []);
1195
+ const selectItemsById = react.useCallback((s) => s.itemsById, []);
1196
+ const displayOrder = useFeedSelector(selectDisplayOrder);
1197
+ const itemsById = useFeedSelector(selectItemsById);
1198
+ const loading = useFeedSelector(selectLoading);
1199
+ const loadingMore = useFeedSelector(selectLoadingMore);
1200
+ const hasMore = useFeedSelector(selectHasMore);
1201
+ const error = useFeedSelector(selectError);
1202
+ const isStale = useFeedSelector(selectIsStale);
1203
+ const items = react.useMemo(
1204
+ () => displayOrder.map((id) => itemsById.get(id)).filter((item) => item !== void 0),
1205
+ [displayOrder, itemsById]
1206
+ );
1207
+ const loadInitial = react.useCallback(() => feedManager.loadInitial(), [feedManager]);
1208
+ const loadMore = react.useCallback(() => feedManager.loadMore(), [feedManager]);
1209
+ const refresh = react.useCallback(() => feedManager.refresh(), [feedManager]);
1210
+ return {
1211
+ items,
1212
+ loading,
1213
+ loadingMore,
1214
+ hasMore,
1215
+ error,
1216
+ isStale,
1217
+ loadInitial,
1218
+ loadMore,
1219
+ refresh
1220
+ };
1221
+ }
1222
+ function useResourceSelector(selector) {
1223
+ const { resourceGovernor } = useSDK();
1224
+ const selectorRef = react.useRef(selector);
1225
+ selectorRef.current = selector;
1226
+ const lastSnapshot = react.useRef(void 0);
1227
+ const lastState = react.useRef(void 0);
1228
+ const getSnapshot = react.useCallback(() => {
1229
+ const state = resourceGovernor.store.getState();
1230
+ if (state !== lastState.current) {
1231
+ lastState.current = state;
1232
+ lastSnapshot.current = selectorRef.current(state);
1233
+ }
1234
+ return lastSnapshot.current;
1235
+ }, [resourceGovernor]);
1236
+ return react.useSyncExternalStore(resourceGovernor.store.subscribe, getSnapshot, getSnapshot);
1237
+ }
1238
+ function useResource() {
1239
+ const { resourceGovernor } = useSDK();
1240
+ const activeAllocations = useResourceSelector((s) => s.activeAllocations);
1241
+ const warmAllocations = useResourceSelector((s) => s.warmAllocations);
1242
+ const focusedIndex = useResourceSelector((s) => s.focusedIndex);
1243
+ const totalItems = useResourceSelector((s) => s.totalItems);
1244
+ const networkType = useResourceSelector((s) => s.networkType);
1245
+ const isActive = useResourceSelector((s) => s.isActive);
1246
+ const prefetchIndex = useResourceSelector((s) => s.prefetchIndex);
1247
+ const activeIndices = react.useMemo(() => [...activeAllocations], [activeAllocations]);
1248
+ const warmIndices = react.useMemo(() => [...warmAllocations], [warmAllocations]);
1249
+ const setFocusedIndex = react.useCallback(
1250
+ (i) => resourceGovernor.setFocusedIndex(i),
1251
+ [resourceGovernor]
1252
+ );
1253
+ const setFocusedIndexImmediate = react.useCallback(
1254
+ (i) => resourceGovernor.setFocusedIndexImmediate(i),
1255
+ [resourceGovernor]
1256
+ );
1257
+ const setTotalItems = react.useCallback(
1258
+ (n) => resourceGovernor.setTotalItems(n),
1259
+ [resourceGovernor]
1260
+ );
1261
+ const shouldRenderVideo = react.useCallback(
1262
+ (i) => resourceGovernor.shouldRenderVideo(i),
1263
+ [resourceGovernor]
1264
+ );
1265
+ const isAllocated = react.useCallback(
1266
+ (i) => resourceGovernor.isAllocated(i),
1267
+ [resourceGovernor]
1268
+ );
1269
+ const isWarmAllocated = react.useCallback(
1270
+ (i) => resourceGovernor.isWarmAllocated(i),
1271
+ [resourceGovernor]
1272
+ );
1273
+ const setPrefetchIndex = react.useCallback(
1274
+ (i) => resourceGovernor.setPrefetchIndex(i),
1275
+ [resourceGovernor]
1276
+ );
1277
+ return {
1278
+ activeIndices,
1279
+ warmIndices,
1280
+ focusedIndex,
1281
+ totalItems,
1282
+ networkType,
1283
+ isActive,
1284
+ prefetchIndex,
1285
+ setFocusedIndex,
1286
+ setFocusedIndexImmediate,
1287
+ setTotalItems,
1288
+ shouldRenderVideo,
1289
+ isAllocated,
1290
+ isWarmAllocated,
1291
+ setPrefetchIndex
1292
+ };
1293
+ }
1294
+ var ACTIVE_HLS_DEFAULTS = {
1295
+ maxBufferLength: 10,
1296
+ maxMaxBufferLength: 15,
1297
+ capLevelToPlayerSize: true,
1298
+ startLevel: 0,
1299
+ abrEwmaDefaultEstimate: 5e5,
1300
+ lowLatencyMode: false,
1301
+ backBufferLength: 5,
1302
+ enableWorker: true
1303
+ };
1304
+ var HOT_HLS_DEFAULTS = {
1305
+ maxBufferLength: 2,
1306
+ maxMaxBufferLength: 3,
1307
+ capLevelToPlayerSize: true,
1308
+ startLevel: 0,
1309
+ abrEwmaDefaultEstimate: 5e5,
1310
+ lowLatencyMode: false,
1311
+ backBufferLength: 0,
1312
+ enableWorker: true
1313
+ };
1314
+ var WARM_HLS_DEFAULTS = {
1315
+ maxBufferLength: 0.5,
1316
+ maxMaxBufferLength: 1,
1317
+ capLevelToPlayerSize: true,
1318
+ startLevel: 0,
1319
+ abrEwmaDefaultEstimate: 5e5,
1320
+ lowLatencyMode: false,
1321
+ backBufferLength: 0,
1322
+ enableWorker: true
1323
+ };
1324
+ function getHlsConfigForTier(tier) {
1325
+ switch (tier) {
1326
+ case "hot":
1327
+ return HOT_HLS_DEFAULTS;
1328
+ case "warm":
1329
+ return WARM_HLS_DEFAULTS;
1330
+ case "active":
1331
+ default:
1332
+ return ACTIVE_HLS_DEFAULTS;
1333
+ }
1334
+ }
1335
+ function supportsNativeHls() {
1336
+ if (typeof document === "undefined") return false;
1337
+ const video = document.createElement("video");
1338
+ return video.canPlayType("application/vnd.apple.mpegurl") !== "";
1339
+ }
1340
+ function mapHlsError(data) {
1341
+ switch (data.type) {
1342
+ case Hls__default.default.ErrorTypes.NETWORK_ERROR:
1343
+ return {
1344
+ code: "NETWORK_ERROR",
1345
+ message: `HLS network error: ${data.details} \u2014 ${data.error?.message ?? "unknown"}`
1346
+ };
1347
+ case Hls__default.default.ErrorTypes.MEDIA_ERROR:
1348
+ if (data.details === Hls__default.default.ErrorDetails.FRAG_PARSING_ERROR || data.details === Hls__default.default.ErrorDetails.BUFFER_APPEND_ERROR) {
1349
+ return {
1350
+ code: "DECODE_ERROR",
1351
+ message: `HLS decode error: ${data.details}`
1352
+ };
1353
+ }
1354
+ return {
1355
+ code: "MEDIA_ERROR",
1356
+ message: `HLS media error: ${data.details}`
1357
+ };
1358
+ default:
1359
+ return {
1360
+ code: "UNKNOWN",
1361
+ message: `HLS error: ${data.type} / ${data.details}`
1362
+ };
1363
+ }
1364
+ }
1365
+ function useHls(options) {
1366
+ const { src, videoRef, isActive, isPrefetch, bufferTier = "active", hlsConfig, onError } = options;
1367
+ const isHlsSupported = typeof window !== "undefined" && Hls__default.default.isSupported();
1368
+ const isNative = supportsNativeHls();
1369
+ const isHlsJs = isHlsSupported && !isNative;
1370
+ const [isReady, setIsReady] = react.useState(false);
1371
+ const hlsRef = react.useRef(null);
1372
+ const onErrorRef = react.useRef(onError);
1373
+ const mediaRecoveryAttemptedRef = react.useRef(false);
1374
+ const currentTierRef = react.useRef(bufferTier);
1375
+ const canPlayFiredRef = react.useRef(false);
1376
+ onErrorRef.current = onError;
1377
+ const destroy = react.useCallback(() => {
1378
+ if (hlsRef.current) {
1379
+ hlsRef.current.destroy();
1380
+ hlsRef.current = null;
1381
+ }
1382
+ canPlayFiredRef.current = false;
1383
+ }, []);
1384
+ const currentSrcRef = react.useRef(void 0);
1385
+ react.useEffect(() => {
1386
+ const video = videoRef.current;
1387
+ if (!video || !src) {
1388
+ destroy();
1389
+ setIsReady(false);
1390
+ canPlayFiredRef.current = false;
1391
+ currentSrcRef.current = void 0;
1392
+ return;
1393
+ }
1394
+ if (!isActive && !isPrefetch) {
1395
+ destroy();
1396
+ setIsReady(false);
1397
+ canPlayFiredRef.current = false;
1398
+ currentSrcRef.current = void 0;
1399
+ return;
1400
+ }
1401
+ if (isNative) {
1402
+ if (video.src !== src) {
1403
+ video.src = src;
1404
+ }
1405
+ if (video.readyState >= HTMLMediaElement.HAVE_FUTURE_DATA) {
1406
+ setIsReady(true);
1407
+ currentSrcRef.current = src;
1408
+ return;
1409
+ }
1410
+ setIsReady(false);
1411
+ currentSrcRef.current = src;
1412
+ const handleCanPlay2 = () => setIsReady(true);
1413
+ video.addEventListener("canplay", handleCanPlay2, { once: true });
1414
+ return () => {
1415
+ video.removeEventListener("canplay", handleCanPlay2);
1416
+ };
1417
+ }
1418
+ if (!isHlsSupported) {
1419
+ onErrorRef.current?.("UNKNOWN", "HLS playback not supported in this browser");
1420
+ return;
1421
+ }
1422
+ if (hlsRef.current && currentSrcRef.current === src) {
1423
+ if (!canPlayFiredRef.current) {
1424
+ const handleCanPlay2 = () => {
1425
+ canPlayFiredRef.current = true;
1426
+ setIsReady(true);
1427
+ };
1428
+ video.addEventListener("canplay", handleCanPlay2, { once: true });
1429
+ return () => {
1430
+ video.removeEventListener("canplay", handleCanPlay2);
1431
+ };
1432
+ }
1433
+ return void 0;
1434
+ }
1435
+ if (hlsRef.current) {
1436
+ hlsRef.current.destroy();
1437
+ hlsRef.current = null;
1438
+ }
1439
+ setIsReady(false);
1440
+ canPlayFiredRef.current = false;
1441
+ mediaRecoveryAttemptedRef.current = false;
1442
+ currentSrcRef.current = src;
1443
+ const initialTier = bufferTier;
1444
+ currentTierRef.current = initialTier;
1445
+ const tierDefaults = getHlsConfigForTier(initialTier);
1446
+ const config = { ...tierDefaults };
1447
+ if (hlsConfig) {
1448
+ const keys = Object.keys(hlsConfig);
1449
+ for (const key of keys) {
1450
+ config[key] = hlsConfig[key];
1451
+ }
1452
+ }
1453
+ const hls = new Hls__default.default(config);
1454
+ hlsRef.current = hls;
1455
+ hls.on(Hls__default.default.Events.ERROR, (_event, data) => {
1456
+ if (!data.fatal) {
1457
+ return;
1458
+ }
1459
+ if (data.type === Hls__default.default.ErrorTypes.MEDIA_ERROR && !mediaRecoveryAttemptedRef.current) {
1460
+ mediaRecoveryAttemptedRef.current = true;
1461
+ hls.recoverMediaError();
1462
+ return;
1463
+ }
1464
+ const mapped = mapHlsError(data);
1465
+ onErrorRef.current?.(mapped.code, mapped.message);
1466
+ });
1467
+ const handleCanPlay = () => {
1468
+ canPlayFiredRef.current = true;
1469
+ setIsReady(true);
1470
+ };
1471
+ video.addEventListener("canplay", handleCanPlay, { once: true });
1472
+ hls.attachMedia(video);
1473
+ hls.loadSource(src);
1474
+ return () => {
1475
+ video.removeEventListener("canplay", handleCanPlay);
1476
+ if (hlsRef.current === hls) {
1477
+ hls.destroy();
1478
+ hlsRef.current = null;
1479
+ canPlayFiredRef.current = false;
1480
+ currentSrcRef.current = void 0;
1481
+ }
1482
+ };
1483
+ }, [src, isActive, isPrefetch]);
1484
+ react.useEffect(() => {
1485
+ const hls = hlsRef.current;
1486
+ if (!hls) {
1487
+ currentTierRef.current = bufferTier;
1488
+ return;
1489
+ }
1490
+ const prevTier = currentTierRef.current;
1491
+ if (prevTier === bufferTier) return;
1492
+ currentTierRef.current = bufferTier;
1493
+ const newConfig = getHlsConfigForTier(bufferTier);
1494
+ const hlsAnyConfig = hls.config;
1495
+ const configKeys = Object.keys(newConfig);
1496
+ for (const key of configKeys) {
1497
+ hlsAnyConfig[key] = newConfig[key];
1498
+ }
1499
+ }, [bufferTier]);
1500
+ return {
1501
+ isHlsJs,
1502
+ isReady,
1503
+ destroy
1504
+ };
1505
+ }
1506
+ function DefaultOverlay({ item }) {
1507
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1508
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { fontWeight: 700, fontSize: 15, marginBottom: 4 }, children: [
1509
+ "@",
1510
+ item.author.name
1511
+ ] }),
1512
+ item.title && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: 13, color: "#ddd", lineHeight: 1.4 }, children: item.title })
1513
+ ] });
1514
+ }
1515
+ function DefaultActions({ item, actions }) {
1516
+ const likes = item.stats.likes + actions.likeDelta;
1517
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1518
+ /* @__PURE__ */ jsxRuntime.jsx(ActionBtn, { icon: "\u2764\uFE0F", count: formatCount(likes) }),
1519
+ /* @__PURE__ */ jsxRuntime.jsx(ActionBtn, { icon: "\u{1F4AC}", count: formatCount(item.stats.comments) }),
1520
+ /* @__PURE__ */ jsxRuntime.jsx(ActionBtn, { icon: "\u2197\uFE0F", count: "Share" })
1521
+ ] });
1522
+ }
1523
+ function ActionBtn({ icon, count }) {
1524
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { textAlign: "center", cursor: "pointer" }, children: [
1525
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: 28 }, children: icon }),
1526
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: 11, color: "#fff", marginTop: 2 }, children: count })
1527
+ ] });
1528
+ }
1529
+ function formatCount(n) {
1530
+ if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
1531
+ if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
1532
+ return String(n);
1533
+ }
1534
+ function DefaultSkeleton() {
1535
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1536
+ "div",
1537
+ {
1538
+ style: {
1539
+ width: "100%",
1540
+ height: "100dvh",
1541
+ background: "#111",
1542
+ position: "relative",
1543
+ overflow: "hidden"
1544
+ },
1545
+ children: [
1546
+ /* @__PURE__ */ jsxRuntime.jsx(
1547
+ "div",
1548
+ {
1549
+ style: {
1550
+ position: "absolute",
1551
+ inset: 0,
1552
+ background: "linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.04) 50%, transparent 100%)",
1553
+ animation: "reels-sdk-shimmer 1.5s ease-in-out infinite"
1554
+ }
1555
+ }
1556
+ ),
1557
+ /* @__PURE__ */ jsxRuntime.jsxs(
1558
+ "div",
1559
+ {
1560
+ style: {
1561
+ position: "absolute",
1562
+ bottom: 100,
1563
+ left: 16,
1564
+ display: "flex",
1565
+ flexDirection: "column",
1566
+ gap: 8
1567
+ },
1568
+ children: [
1569
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: skeletonBar(120, 14) }),
1570
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: skeletonBar(200, 12) }),
1571
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: skeletonBar(160, 12) })
1572
+ ]
1573
+ }
1574
+ ),
1575
+ /* @__PURE__ */ jsxRuntime.jsxs(
1576
+ "div",
1577
+ {
1578
+ style: {
1579
+ position: "absolute",
1580
+ bottom: 100,
1581
+ right: 16,
1582
+ display: "flex",
1583
+ flexDirection: "column",
1584
+ gap: 20,
1585
+ alignItems: "center"
1586
+ },
1587
+ children: [
1588
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: skeletonCircle(40) }),
1589
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: skeletonCircle(40) }),
1590
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: skeletonCircle(40) })
1591
+ ]
1592
+ }
1593
+ ),
1594
+ /* @__PURE__ */ jsxRuntime.jsx("style", { children: `
1595
+ @keyframes reels-sdk-shimmer {
1596
+ 0% { transform: translateX(-100%); }
1597
+ 100% { transform: translateX(100%); }
1598
+ }
1599
+ ` })
1600
+ ]
1601
+ }
1602
+ );
1603
+ }
1604
+ function skeletonBar(width, height) {
1605
+ return {
1606
+ width,
1607
+ height,
1608
+ borderRadius: height / 2,
1609
+ background: "rgba(255,255,255,0.1)"
1610
+ };
1611
+ }
1612
+ function skeletonCircle(size) {
1613
+ return {
1614
+ width: size,
1615
+ height: size,
1616
+ borderRadius: "50%",
1617
+ background: "rgba(255,255,255,0.1)"
1618
+ };
1619
+ }
1620
+ function VideoSlot({
1621
+ item,
1622
+ index,
1623
+ isActive,
1624
+ isPrefetch,
1625
+ isPreloaded,
1626
+ bufferTier,
1627
+ isMuted,
1628
+ onToggleMute,
1629
+ showFps = false,
1630
+ renderOverlay,
1631
+ renderActions
1632
+ }) {
1633
+ const { optimisticManager, adapters } = useSDK();
1634
+ if (!isVideoItem(item)) {
1635
+ return /* @__PURE__ */ jsxRuntime.jsx(
1636
+ "div",
1637
+ {
1638
+ style: {
1639
+ position: "relative",
1640
+ width: "100%",
1641
+ height: "100%",
1642
+ background: "#111",
1643
+ display: "flex",
1644
+ alignItems: "center",
1645
+ justifyContent: "center",
1646
+ color: "#fff",
1647
+ fontSize: 14
1648
+ },
1649
+ children: /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Non-video content" })
1650
+ }
1651
+ );
1652
+ }
1653
+ return /* @__PURE__ */ jsxRuntime.jsx(
1654
+ VideoSlotInner,
1655
+ {
1656
+ item,
1657
+ index,
1658
+ isActive,
1659
+ isPrefetch,
1660
+ isPreloaded,
1661
+ bufferTier,
1662
+ isMuted,
1663
+ onToggleMute,
1664
+ showFps,
1665
+ renderOverlay,
1666
+ renderActions,
1667
+ optimisticManager,
1668
+ adapters
1669
+ }
1670
+ );
1671
+ }
1672
+ function VideoSlotInner({
1673
+ item,
1674
+ index,
1675
+ isActive,
1676
+ isPrefetch,
1677
+ isPreloaded,
1678
+ bufferTier,
1679
+ isMuted,
1680
+ onToggleMute,
1681
+ showFps,
1682
+ renderOverlay,
1683
+ renderActions,
1684
+ optimisticManager,
1685
+ adapters
1686
+ }) {
1687
+ const videoRef = react.useRef(null);
1688
+ const shouldLoadSrc = isActive || isPrefetch || isPreloaded;
1689
+ const src = item.source.url;
1690
+ const sourceType = item.source.type;
1691
+ const isHlsSource = sourceType === "hls";
1692
+ const hlsSrc = isHlsSource && shouldLoadSrc ? src : void 0;
1693
+ const mp4Src = !isHlsSource && shouldLoadSrc ? src : void 0;
1694
+ const { isReady: hlsReady } = useHls({
1695
+ src: hlsSrc,
1696
+ videoRef,
1697
+ isActive,
1698
+ // Pass true for isPrefetch when the slot is preloaded (hot/warm tier)
1699
+ // so useHls creates the HLS instance and starts buffering
1700
+ isPrefetch: isPrefetch || isPreloaded,
1701
+ bufferTier,
1702
+ onError: (code, message) => {
1703
+ console.error(`[VideoSlot] HLS error: ${code} \u2014 ${message}`);
1704
+ }
1705
+ });
1706
+ const [mp4Ready, setMp4Ready] = react.useState(false);
1707
+ react.useEffect(() => {
1708
+ if (isHlsSource) return;
1709
+ const video = videoRef.current;
1710
+ if (!video || !mp4Src) {
1711
+ setMp4Ready(false);
1712
+ return;
1713
+ }
1714
+ if (video.readyState >= HTMLMediaElement.HAVE_FUTURE_DATA) {
1715
+ setMp4Ready(true);
1716
+ return;
1717
+ }
1718
+ setMp4Ready(false);
1719
+ const onCanPlay = () => setMp4Ready(true);
1720
+ video.addEventListener("canplay", onCanPlay, { once: true });
1721
+ return () => video.removeEventListener("canplay", onCanPlay);
1722
+ }, [mp4Src, isHlsSource]);
1723
+ react.useEffect(() => {
1724
+ if (isHlsSource) return;
1725
+ const video = videoRef.current;
1726
+ if (!video || !mp4Src) return;
1727
+ if (!isActive && (isPrefetch || isPreloaded)) {
1728
+ video.load();
1729
+ }
1730
+ }, [mp4Src, isActive, isPrefetch, isPreloaded, isHlsSource]);
1731
+ const isReady = isHlsSource ? hlsReady : mp4Ready;
1732
+ const [hasPlayedAhead, setHasPlayedAhead] = react.useState(false);
1733
+ react.useEffect(() => {
1734
+ const video = videoRef.current;
1735
+ if (!video) return;
1736
+ if (isActive || !isReady) return;
1737
+ if (hasPlayedAhead) return;
1738
+ const prevMuted = video.muted;
1739
+ video.muted = true;
1740
+ let cancelled = false;
1741
+ const doPlayAhead = async () => {
1742
+ try {
1743
+ await video.play();
1744
+ if (cancelled) return;
1745
+ const pauseAfterDecode = () => {
1746
+ if (cancelled) return;
1747
+ video.pause();
1748
+ video.currentTime = 0;
1749
+ video.muted = prevMuted;
1750
+ setHasPlayedAhead(true);
1751
+ };
1752
+ setTimeout(pauseAfterDecode, 50);
1753
+ } catch {
1754
+ }
1755
+ };
1756
+ doPlayAhead();
1757
+ return () => {
1758
+ cancelled = true;
1759
+ };
1760
+ }, [isActive, isReady, hasPlayedAhead]);
1761
+ react.useEffect(() => {
1762
+ setHasPlayedAhead(false);
1763
+ }, [src]);
1764
+ const wasActiveRef = react.useRef(false);
1765
+ react.useEffect(() => {
1766
+ const video = videoRef.current;
1767
+ if (!video) return;
1768
+ let onReady = null;
1769
+ if (isActive) {
1770
+ wasActiveRef.current = true;
1771
+ video.muted = isMuted;
1772
+ if (video.readyState >= HTMLMediaElement.HAVE_FUTURE_DATA) {
1773
+ video.play().catch(() => {
1774
+ });
1775
+ } else {
1776
+ onReady = () => {
1777
+ video.play().catch(() => {
1778
+ });
1779
+ };
1780
+ video.addEventListener("canplay", onReady, { once: true });
1781
+ }
1782
+ } else if (wasActiveRef.current) {
1783
+ video.pause();
1784
+ video.currentTime = 0;
1785
+ wasActiveRef.current = false;
1786
+ setHasPlayedAhead(false);
1787
+ } else if (!hasPlayedAhead) {
1788
+ video.pause();
1789
+ }
1790
+ return () => {
1791
+ if (onReady) video.removeEventListener("canplay", onReady);
1792
+ };
1793
+ }, [isActive, isMuted, hasPlayedAhead]);
1794
+ react.useEffect(() => {
1795
+ const video = videoRef.current;
1796
+ if (!video) return;
1797
+ video.muted = isMuted;
1798
+ }, [isMuted]);
1799
+ const showPosterOverlay = !isReady && !hasPlayedAhead;
1800
+ const [showMuteIndicator, setShowMuteIndicator] = react.useState(false);
1801
+ const muteIndicatorTimer = react.useRef(null);
1802
+ const handleTap = react.useCallback(() => {
1803
+ onToggleMute();
1804
+ setShowMuteIndicator(true);
1805
+ if (muteIndicatorTimer.current) clearTimeout(muteIndicatorTimer.current);
1806
+ muteIndicatorTimer.current = setTimeout(() => setShowMuteIndicator(false), 1200);
1807
+ }, [onToggleMute]);
1808
+ react.useEffect(() => {
1809
+ return () => {
1810
+ if (muteIndicatorTimer.current) clearTimeout(muteIndicatorTimer.current);
1811
+ };
1812
+ }, []);
1813
+ const likeDelta = react.useSyncExternalStore(
1814
+ optimisticManager.store.subscribe,
1815
+ () => optimisticManager.getLikeDelta(item.id),
1816
+ () => optimisticManager.getLikeDelta(item.id)
1817
+ );
1818
+ const followState = react.useSyncExternalStore(
1819
+ optimisticManager.store.subscribe,
1820
+ () => optimisticManager.getFollowState(item.author.id),
1821
+ () => optimisticManager.getFollowState(item.author.id)
1822
+ );
1823
+ const actions = react.useMemo(() => ({
1824
+ toggleLike: () => optimisticManager.toggleLike(item.id, item.interaction.isLiked),
1825
+ likeDelta,
1826
+ toggleFollow: () => optimisticManager.toggleFollow(item.author.id, item.interaction.isFollowing),
1827
+ followState,
1828
+ share: () => adapters.interaction?.share?.(item.id),
1829
+ isMuted,
1830
+ toggleMute: onToggleMute,
1831
+ isActive,
1832
+ index
1833
+ }), [item, likeDelta, followState, isMuted, isActive, index, optimisticManager, adapters, onToggleMute]);
1834
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1835
+ "div",
1836
+ {
1837
+ style: {
1838
+ position: "relative",
1839
+ width: "100%",
1840
+ height: "100%",
1841
+ background: "#111",
1842
+ overflow: "hidden"
1843
+ },
1844
+ onClick: handleTap,
1845
+ children: [
1846
+ /* @__PURE__ */ jsxRuntime.jsx(
1847
+ "video",
1848
+ {
1849
+ ref: videoRef,
1850
+ src: mp4Src,
1851
+ loop: true,
1852
+ muted: isMuted,
1853
+ playsInline: true,
1854
+ preload: shouldLoadSrc ? "auto" : "none",
1855
+ style: {
1856
+ width: "100%",
1857
+ height: "100%",
1858
+ objectFit: "cover",
1859
+ // Hide video until ready to avoid black frame flash
1860
+ opacity: showPosterOverlay ? 0 : 1,
1861
+ transition: "opacity 0.15s ease"
1862
+ }
1863
+ }
1864
+ ),
1865
+ item.poster && /* @__PURE__ */ jsxRuntime.jsx(
1866
+ "div",
1867
+ {
1868
+ style: {
1869
+ position: "absolute",
1870
+ inset: 0,
1871
+ backgroundImage: `url(${item.poster})`,
1872
+ backgroundSize: "cover",
1873
+ backgroundPosition: "center",
1874
+ opacity: showPosterOverlay ? 1 : 0,
1875
+ transition: "opacity 0.15s ease",
1876
+ pointerEvents: "none"
1877
+ }
1878
+ }
1879
+ ),
1880
+ showMuteIndicator && /* @__PURE__ */ jsxRuntime.jsx(
1881
+ "div",
1882
+ {
1883
+ style: {
1884
+ position: "absolute",
1885
+ top: "50%",
1886
+ left: "50%",
1887
+ transform: "translate(-50%, -50%)",
1888
+ background: "rgba(0,0,0,0.6)",
1889
+ borderRadius: "50%",
1890
+ width: 64,
1891
+ height: 64,
1892
+ display: "flex",
1893
+ alignItems: "center",
1894
+ justifyContent: "center",
1895
+ fontSize: 28,
1896
+ pointerEvents: "none",
1897
+ animation: "fadeInOut 1.2s ease forwards"
1898
+ },
1899
+ children: isMuted ? "\u{1F507}" : "\u{1F50A}"
1900
+ }
1901
+ ),
1902
+ /* @__PURE__ */ jsxRuntime.jsx(
1903
+ "div",
1904
+ {
1905
+ style: {
1906
+ position: "absolute",
1907
+ bottom: 80,
1908
+ left: 16,
1909
+ right: 80,
1910
+ pointerEvents: "none",
1911
+ color: "#fff"
1912
+ },
1913
+ children: renderOverlay ? renderOverlay(item, actions) : /* @__PURE__ */ jsxRuntime.jsx(DefaultOverlay, { item })
1914
+ }
1915
+ ),
1916
+ /* @__PURE__ */ jsxRuntime.jsx(
1917
+ "div",
1918
+ {
1919
+ style: {
1920
+ position: "absolute",
1921
+ bottom: 80,
1922
+ right: 16,
1923
+ display: "flex",
1924
+ flexDirection: "column",
1925
+ gap: 20,
1926
+ alignItems: "center"
1927
+ },
1928
+ children: renderActions ? renderActions(item, actions) : /* @__PURE__ */ jsxRuntime.jsx(DefaultActions, { item, actions })
1929
+ }
1930
+ ),
1931
+ showFps && /* @__PURE__ */ jsxRuntime.jsx(FpsCounter, {})
1932
+ ]
1933
+ }
1934
+ );
1935
+ }
1936
+ function FpsCounter() {
1937
+ const [fps, setFps] = react.useState(0);
1938
+ const frameCountRef = react.useRef(0);
1939
+ const lastTimeRef = react.useRef(performance.now());
1940
+ react.useEffect(() => {
1941
+ let rafId;
1942
+ const tick = () => {
1943
+ frameCountRef.current++;
1944
+ const now = performance.now();
1945
+ if (now - lastTimeRef.current >= 1e3) {
1946
+ setFps(frameCountRef.current);
1947
+ frameCountRef.current = 0;
1948
+ lastTimeRef.current = now;
1949
+ }
1950
+ rafId = requestAnimationFrame(tick);
1951
+ };
1952
+ rafId = requestAnimationFrame(tick);
1953
+ return () => cancelAnimationFrame(rafId);
1954
+ }, []);
1955
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1956
+ "div",
1957
+ {
1958
+ style: {
1959
+ position: "absolute",
1960
+ top: 12,
1961
+ right: 12,
1962
+ background: fps >= 55 ? "#00c853" : fps >= 30 ? "#ffd600" : "#d50000",
1963
+ color: "#000",
1964
+ fontSize: 11,
1965
+ fontWeight: 700,
1966
+ padding: "2px 6px",
1967
+ borderRadius: 4,
1968
+ fontFamily: "monospace"
1969
+ },
1970
+ children: [
1971
+ fps,
1972
+ "fps"
1973
+ ]
1974
+ }
1975
+ );
1976
+ }
1977
+ var centerStyle = {
1978
+ height: "100dvh",
1979
+ display: "flex",
1980
+ alignItems: "center",
1981
+ justifyContent: "center",
1982
+ background: "#000",
1983
+ color: "#fff"
1984
+ };
1985
+ function ReelsFeed({
1986
+ renderOverlay,
1987
+ renderActions,
1988
+ renderLoading,
1989
+ renderEmpty,
1990
+ renderError: _renderError,
1991
+ showFps = false,
1992
+ loadMoreThreshold = 5,
1993
+ onSlotChange,
1994
+ gestureConfig,
1995
+ snapConfig
1996
+ }) {
1997
+ const { items, loading, loadInitial, loadMore, hasMore } = useFeed();
1998
+ const {
1999
+ focusedIndex,
2000
+ prefetchIndex,
2001
+ setFocusedIndexImmediate,
2002
+ setTotalItems,
2003
+ shouldRenderVideo,
2004
+ isWarmAllocated,
2005
+ setPrefetchIndex
2006
+ } = useResource();
2007
+ const [isMuted, setIsMuted] = react.useState(true);
2008
+ const containerRef = react.useRef(null);
2009
+ const slotCacheRef = react.useRef(/* @__PURE__ */ new Map());
2010
+ const activeIndexRef = react.useRef(0);
2011
+ activeIndexRef.current = focusedIndex;
2012
+ const [isSnapping, setIsSnapping] = react.useState(false);
2013
+ const { animateSnap, animateBounceBack, cancelAnimation } = useSnapAnimation({
2014
+ duration: snapConfig?.duration ?? 260,
2015
+ easing: snapConfig?.easing ?? "cubic-bezier(0.25, 0.46, 0.45, 0.94)"
2016
+ });
2017
+ react.useEffect(() => {
2018
+ loadInitial();
2019
+ }, [loadInitial]);
2020
+ react.useEffect(() => {
2021
+ setTotalItems(items.length);
2022
+ }, [items.length, setTotalItems]);
2023
+ react.useEffect(() => {
2024
+ if (items.length - focusedIndex <= loadMoreThreshold && hasMore && !loading) {
2025
+ loadMore();
2026
+ }
2027
+ }, [focusedIndex, items.length, hasMore, loading, loadMore, loadMoreThreshold]);
2028
+ react.useEffect(() => {
2029
+ const container = containerRef.current;
2030
+ if (!container) return;
2031
+ const rebuild = () => {
2032
+ slotCacheRef.current.clear();
2033
+ const slots = container.querySelectorAll("[data-slot-index]");
2034
+ for (const slot of slots) {
2035
+ const idx = Number(slot.dataset.slotIndex);
2036
+ slotCacheRef.current.set(idx, slot);
2037
+ }
2038
+ };
2039
+ rebuild();
2040
+ const observer = new MutationObserver(rebuild);
2041
+ observer.observe(container, { childList: true, subtree: true });
2042
+ return () => observer.disconnect();
2043
+ }, [items.length]);
2044
+ const containerHeight = react.useRef(
2045
+ typeof window !== "undefined" ? window.innerHeight : 800
2046
+ );
2047
+ react.useEffect(() => {
2048
+ const container = containerRef.current;
2049
+ if (!container) return;
2050
+ containerHeight.current = container.getBoundingClientRect().height || window.innerHeight;
2051
+ const ro = new ResizeObserver(([entry]) => {
2052
+ if (entry) {
2053
+ containerHeight.current = entry.contentRect.height;
2054
+ applyPositions(activeIndexRef.current, 0, false);
2055
+ }
2056
+ });
2057
+ ro.observe(container);
2058
+ return () => ro.disconnect();
2059
+ }, []);
2060
+ const applyPositions = react.useCallback((activeIdx, dragOffset = 0, _withTransition = false) => {
2061
+ const h = containerHeight.current;
2062
+ for (const [idx, slot] of slotCacheRef.current) {
2063
+ const targetY = (idx - activeIdx) * h + dragOffset;
2064
+ slot.style.transition = "none";
2065
+ slot.style.transform = `translateY(${targetY}px)`;
2066
+ }
2067
+ }, []);
2068
+ react.useEffect(() => {
2069
+ applyPositions(focusedIndex, 0, false);
2070
+ }, [focusedIndex, applyPositions]);
2071
+ const handleDragThreshold = react.useCallback(
2072
+ (direction) => {
2073
+ const current = activeIndexRef.current;
2074
+ const nextIdx = direction === "forward" ? Math.min(current + 1, items.length - 1) : Math.max(current - 1, 0);
2075
+ if (nextIdx !== current) {
2076
+ setPrefetchIndex(nextIdx);
2077
+ }
2078
+ },
2079
+ [items.length, setPrefetchIndex]
2080
+ );
2081
+ const handleSnap = react.useCallback(
2082
+ (direction) => {
2083
+ const current = activeIndexRef.current;
2084
+ const next = direction === "forward" ? Math.min(current + 1, items.length - 1) : Math.max(current - 1, 0);
2085
+ if (next === current) {
2086
+ const targets2 = [];
2087
+ for (const [idx, el] of slotCacheRef.current) {
2088
+ targets2.push({
2089
+ element: el,
2090
+ fromY: parsePxTranslateY(el),
2091
+ toY: (idx - current) * containerHeight.current
2092
+ });
2093
+ }
2094
+ animateBounceBack(targets2);
2095
+ setPrefetchIndex(null);
2096
+ return;
2097
+ }
2098
+ cancelAnimation();
2099
+ setIsSnapping(true);
2100
+ setPrefetchIndex(null);
2101
+ setFocusedIndexImmediate(next);
2102
+ const nextItem = items[next];
2103
+ if (nextItem) {
2104
+ onSlotChange?.(next, nextItem, current);
2105
+ }
2106
+ const h = containerHeight.current;
2107
+ const targets = [];
2108
+ for (const [idx, el] of slotCacheRef.current) {
2109
+ targets.push({
2110
+ element: el,
2111
+ fromY: parsePxTranslateY(el),
2112
+ toY: (idx - next) * h
2113
+ });
2114
+ }
2115
+ animateSnap(targets);
2116
+ setTimeout(() => setIsSnapping(false), 300);
2117
+ },
2118
+ [items, animateSnap, animateBounceBack, cancelAnimation, setFocusedIndexImmediate, setPrefetchIndex, onSlotChange]
2119
+ );
2120
+ const handleBounceBack = react.useCallback(() => {
2121
+ const current = activeIndexRef.current;
2122
+ const h = containerHeight.current;
2123
+ const targets = [];
2124
+ for (const [idx, el] of slotCacheRef.current) {
2125
+ targets.push({
2126
+ element: el,
2127
+ fromY: parsePxTranslateY(el),
2128
+ toY: (idx - current) * h
2129
+ });
2130
+ }
2131
+ animateBounceBack(targets);
2132
+ setPrefetchIndex(null);
2133
+ }, [animateBounceBack, setPrefetchIndex]);
2134
+ const { bind } = usePointerGesture({
2135
+ axis: "y",
2136
+ velocityThreshold: gestureConfig?.velocityThreshold ?? 0.3,
2137
+ distanceThreshold: gestureConfig?.distanceThreshold ?? 80,
2138
+ disabled: isSnapping,
2139
+ containerSize: containerHeight.current,
2140
+ dragThresholdRatio: gestureConfig?.dragThresholdRatio ?? 0.5,
2141
+ onDragOffset: (offset) => {
2142
+ applyPositions(activeIndexRef.current, offset, false);
2143
+ },
2144
+ onDragThreshold: handleDragThreshold,
2145
+ onSnap: handleSnap,
2146
+ onBounceBack: handleBounceBack
2147
+ });
2148
+ const getInitialTransformPx = react.useCallback(
2149
+ (index) => {
2150
+ const h = containerHeight.current;
2151
+ return `translateY(${(index - focusedIndex) * h}px)`;
2152
+ },
2153
+ // eslint-disable-next-line react-hooks/exhaustive-deps
2154
+ []
2155
+ // Only for initial render; imperative system takes over after mount
2156
+ );
2157
+ const handleToggleMute = react.useCallback(() => {
2158
+ setIsMuted((prev) => !prev);
2159
+ }, []);
2160
+ if (loading && items.length === 0) {
2161
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { ...centerStyle, flexDirection: "column", gap: 0 }, children: renderLoading ? renderLoading() : /* @__PURE__ */ jsxRuntime.jsx(DefaultSkeleton, {}) });
2162
+ }
2163
+ if (!loading && items.length === 0) {
2164
+ return renderEmpty ? renderEmpty() : /* @__PURE__ */ jsxRuntime.jsx("div", { style: centerStyle, children: "No videos found" });
2165
+ }
2166
+ return /* @__PURE__ */ jsxRuntime.jsxs(
2167
+ "div",
2168
+ {
2169
+ ref: containerRef,
2170
+ ...bind,
2171
+ style: {
2172
+ position: "relative",
2173
+ width: "100%",
2174
+ height: "100dvh",
2175
+ overflow: "hidden",
2176
+ background: "#000",
2177
+ touchAction: "none",
2178
+ userSelect: "none"
2179
+ },
2180
+ children: [
2181
+ /* @__PURE__ */ jsxRuntime.jsx("style", { children: `
2182
+ @keyframes reels-sdk-fadeInOut {
2183
+ 0% { opacity: 0; transform: translate(-50%, -50%) scale(0.8); }
2184
+ 15% { opacity: 1; transform: translate(-50%, -50%) scale(1); }
2185
+ 70% { opacity: 1; transform: translate(-50%, -50%) scale(1); }
2186
+ 100% { opacity: 0; transform: translate(-50%, -50%) scale(0.8); }
2187
+ }
2188
+ @keyframes reels-sdk-spin {
2189
+ to { transform: rotate(360deg); }
2190
+ }
2191
+ ` }),
2192
+ items.map((item, index) => {
2193
+ const isActive = index === focusedIndex;
2194
+ const isPrefetch = index === prefetchIndex;
2195
+ const isWarm = isWarmAllocated(index);
2196
+ const isVisible = shouldRenderVideo(index) || isPrefetch;
2197
+ const bufferTier = isActive ? "active" : isWarm ? "warm" : "hot";
2198
+ return /* @__PURE__ */ jsxRuntime.jsx(
2199
+ "div",
2200
+ {
2201
+ "data-slot-index": index,
2202
+ style: {
2203
+ position: "absolute",
2204
+ inset: 0,
2205
+ willChange: "transform",
2206
+ transform: getInitialTransformPx(index)
2207
+ },
2208
+ children: /* @__PURE__ */ jsxRuntime.jsx(
2209
+ VideoSlot,
2210
+ {
2211
+ item,
2212
+ index,
2213
+ isActive,
2214
+ isPrefetch,
2215
+ isPreloaded: !isActive && !isPrefetch && isVisible,
2216
+ bufferTier,
2217
+ isMuted,
2218
+ onToggleMute: handleToggleMute,
2219
+ showFps: showFps && isActive,
2220
+ renderOverlay,
2221
+ renderActions
2222
+ }
2223
+ )
2224
+ },
2225
+ item.id
2226
+ );
2227
+ })
2228
+ ]
2229
+ }
2230
+ );
2231
+ }
2232
+ function parsePxTranslateY(el) {
2233
+ const transform = el.style.transform;
2234
+ if (!transform) return 0;
2235
+ const match = transform.match(/translateY\((-?[\d.]+)/);
2236
+ if (!match || !match[1]) return 0;
2237
+ return Number.parseFloat(match[1]);
2238
+ }
2239
+ function usePlayerSelector(selector) {
2240
+ const { playerEngine } = useSDK();
2241
+ const selectorRef = react.useRef(selector);
2242
+ selectorRef.current = selector;
2243
+ const lastSnapshot = react.useRef(void 0);
2244
+ const lastState = react.useRef(void 0);
2245
+ const getSnapshot = react.useCallback(() => {
2246
+ const state = playerEngine.store.getState();
2247
+ if (state !== lastState.current) {
2248
+ lastState.current = state;
2249
+ lastSnapshot.current = selectorRef.current(state);
2250
+ }
2251
+ return lastSnapshot.current;
2252
+ }, [playerEngine]);
2253
+ return react.useSyncExternalStore(playerEngine.store.subscribe, getSnapshot, getSnapshot);
2254
+ }
2255
+ function usePlayer() {
2256
+ const { playerEngine } = useSDK();
2257
+ const status = usePlayerSelector((s) => s.status);
2258
+ const currentVideo = usePlayerSelector((s) => s.currentVideo);
2259
+ const currentTime = usePlayerSelector((s) => s.currentTime);
2260
+ const duration = usePlayerSelector((s) => s.duration);
2261
+ const buffered = usePlayerSelector((s) => s.buffered);
2262
+ const muted = usePlayerSelector((s) => s.muted);
2263
+ const volume = usePlayerSelector((s) => s.volume);
2264
+ const playbackRate = usePlayerSelector((s) => s.playbackRate);
2265
+ const error = usePlayerSelector((s) => s.error);
2266
+ const loopCount = usePlayerSelector((s) => s.loopCount);
2267
+ const watchTime = usePlayerSelector((s) => s.watchTime);
2268
+ const isPlaying = status === "playing" /* PLAYING */;
2269
+ const isPaused = status === "paused" /* PAUSED */;
2270
+ const isBuffering = status === "buffering" /* BUFFERING */;
2271
+ const isLoading = status === "loading" /* LOADING */;
2272
+ const hasError = status === "error" /* ERROR */;
2273
+ const progress = duration > 0 ? currentTime / duration * 100 : 0;
2274
+ const bufferProgress = duration > 0 ? buffered / duration * 100 : 0;
2275
+ const play = react.useCallback(() => playerEngine.play(), [playerEngine]);
2276
+ const pause = react.useCallback(() => playerEngine.pause(), [playerEngine]);
2277
+ const togglePlay = react.useCallback(() => playerEngine.togglePlay(), [playerEngine]);
2278
+ const seek = react.useCallback((t) => playerEngine.seek(t), [playerEngine]);
2279
+ const setMuted = react.useCallback((m) => playerEngine.setMuted(m), [playerEngine]);
2280
+ const toggleMute = react.useCallback(() => playerEngine.toggleMute(), [playerEngine]);
2281
+ const setVolume = react.useCallback((v) => playerEngine.setVolume(v), [playerEngine]);
2282
+ const setPlaybackRate = react.useCallback((r) => playerEngine.setPlaybackRate(r), [playerEngine]);
2283
+ const handlers = {
2284
+ onCanPlay: () => playerEngine.onCanPlay(),
2285
+ onWaiting: () => playerEngine.onWaiting(),
2286
+ onPlaying: () => playerEngine.onPlaying(),
2287
+ onEnded: () => playerEngine.onEnded(),
2288
+ onTimeUpdate: (e) => playerEngine.onTimeUpdate(e.currentTarget.currentTime),
2289
+ onProgress: (e) => {
2290
+ const video = e.currentTarget;
2291
+ if (video.buffered.length > 0) {
2292
+ playerEngine.onProgress(video.buffered.end(video.buffered.length - 1));
2293
+ }
2294
+ },
2295
+ onLoadedMetadata: (e) => playerEngine.onLoadedMetadata(e.currentTarget.duration),
2296
+ onError: () => playerEngine.onError("MEDIA_ERROR", "Video playback error")
2297
+ };
2298
+ return {
2299
+ // State
2300
+ status,
2301
+ currentVideo,
2302
+ currentTime,
2303
+ duration,
2304
+ buffered,
2305
+ muted,
2306
+ volume,
2307
+ playbackRate,
2308
+ error,
2309
+ loopCount,
2310
+ watchTime,
2311
+ // Computed
2312
+ isPlaying,
2313
+ isPaused,
2314
+ isBuffering,
2315
+ isLoading,
2316
+ hasError,
2317
+ progress,
2318
+ bufferProgress,
2319
+ // Controls
2320
+ play,
2321
+ pause,
2322
+ togglePlay,
2323
+ seek,
2324
+ setMuted,
2325
+ toggleMute,
2326
+ setVolume,
2327
+ setPlaybackRate,
2328
+ // Element handlers
2329
+ handlers
2330
+ };
2331
+ }
2332
+
2333
+ // src/adapters/mock/index.ts
2334
+ var MockLogger = class {
2335
+ constructor(prefix = "[MockLogger]") {
2336
+ this.prefix = prefix;
2337
+ }
2338
+ debug(msg, ...args) {
2339
+ console.debug(this.prefix, msg, ...args);
2340
+ }
2341
+ info(msg, ...args) {
2342
+ console.info(this.prefix, msg, ...args);
2343
+ }
2344
+ warn(msg, ...args) {
2345
+ console.warn(this.prefix, msg, ...args);
2346
+ }
2347
+ error(msg, ...args) {
2348
+ console.error(this.prefix, msg, ...args);
2349
+ }
2350
+ };
2351
+ var MockAnalytics = class {
2352
+ constructor() {
2353
+ this.events = [];
2354
+ }
2355
+ log(event, data) {
2356
+ this.events.push({ event, data, ts: Date.now() });
2357
+ }
2358
+ trackView(videoId, duration) {
2359
+ this.log("view", { videoId, duration });
2360
+ }
2361
+ trackLike(videoId, isLiked) {
2362
+ this.log("like", { videoId, isLiked });
2363
+ }
2364
+ trackShare(videoId) {
2365
+ this.log("share", { videoId });
2366
+ }
2367
+ trackComment(videoId) {
2368
+ this.log("comment", { videoId });
2369
+ }
2370
+ trackError(videoId, error) {
2371
+ this.log("error", { videoId, error });
2372
+ }
2373
+ trackPlaybackEvent(videoId, event, position) {
2374
+ this.log("playback", { videoId, event, position });
2375
+ }
2376
+ };
2377
+ var MockInteraction = class {
2378
+ constructor(delayMs = 300) {
2379
+ this.calls = [];
2380
+ this.delay = delayMs;
2381
+ }
2382
+ async simulate(action) {
2383
+ await new Promise((r) => setTimeout(r, this.delay));
2384
+ this.calls.push(action);
2385
+ }
2386
+ async like(id) {
2387
+ await this.simulate(`like:${id}`);
2388
+ }
2389
+ async unlike(id) {
2390
+ await this.simulate(`unlike:${id}`);
2391
+ }
2392
+ async follow(id) {
2393
+ await this.simulate(`follow:${id}`);
2394
+ }
2395
+ async unfollow(id) {
2396
+ await this.simulate(`unfollow:${id}`);
2397
+ }
2398
+ async bookmark(id) {
2399
+ await this.simulate(`bookmark:${id}`);
2400
+ }
2401
+ async unbookmark(id) {
2402
+ await this.simulate(`unbookmark:${id}`);
2403
+ }
2404
+ async share(id) {
2405
+ await this.simulate(`share:${id}`);
2406
+ }
2407
+ };
2408
+ var MockSessionStorage = class {
2409
+ constructor() {
2410
+ this.store = /* @__PURE__ */ new Map();
2411
+ }
2412
+ get(key) {
2413
+ return this.store.get(key) ?? null;
2414
+ }
2415
+ set(key, value) {
2416
+ this.store.set(key, value);
2417
+ }
2418
+ remove(key) {
2419
+ this.store.delete(key);
2420
+ }
2421
+ clear() {
2422
+ this.store.clear();
2423
+ }
2424
+ };
2425
+ var MockNetworkAdapter = class {
2426
+ constructor() {
2427
+ this.type = "wifi";
2428
+ this.callbacks = [];
2429
+ }
2430
+ getNetworkType() {
2431
+ return this.type;
2432
+ }
2433
+ isOnline() {
2434
+ return this.type !== "offline";
2435
+ }
2436
+ onNetworkChange(cb) {
2437
+ this.callbacks.push(cb);
2438
+ return () => {
2439
+ this.callbacks = this.callbacks.filter((c) => c !== cb);
2440
+ };
2441
+ }
2442
+ /** Test helper: simulate network change */
2443
+ simulateChange(type) {
2444
+ this.type = type;
2445
+ this.callbacks.forEach((cb) => cb(type));
2446
+ }
2447
+ };
2448
+ var MockVideoLoader = class {
2449
+ constructor(delayMs = 100) {
2450
+ this.preloaded = /* @__PURE__ */ new Set();
2451
+ this.loading = /* @__PURE__ */ new Set();
2452
+ this.delayMs = delayMs;
2453
+ }
2454
+ async preload(videoId, _url, signal) {
2455
+ this.loading.add(videoId);
2456
+ await new Promise((resolve, reject) => {
2457
+ const t = setTimeout(resolve, this.delayMs);
2458
+ signal?.addEventListener("abort", () => {
2459
+ clearTimeout(t);
2460
+ reject(new Error("aborted"));
2461
+ });
2462
+ });
2463
+ this.loading.delete(videoId);
2464
+ this.preloaded.add(videoId);
2465
+ return { videoId, status: "loaded", loadedBytes: 5e5 };
2466
+ }
2467
+ cancel(videoId) {
2468
+ this.loading.delete(videoId);
2469
+ }
2470
+ isPreloaded(videoId) {
2471
+ return this.preloaded.has(videoId);
2472
+ }
2473
+ getPreloadStatus(videoId) {
2474
+ if (this.preloaded.has(videoId)) return "loaded";
2475
+ if (this.loading.has(videoId)) return "loading";
2476
+ return "idle";
2477
+ }
2478
+ clearAll() {
2479
+ this.preloaded.clear();
2480
+ this.loading.clear();
2481
+ }
2482
+ };
2483
+ var MockDataSource = class {
2484
+ constructor(options = {}) {
2485
+ this.totalItems = options.totalItems ?? 20;
2486
+ this.pageSize = options.pageSize ?? 5;
2487
+ this.delayMs = options.delayMs ?? 500;
2488
+ }
2489
+ async fetchFeed(cursor) {
2490
+ await new Promise((r) => setTimeout(r, this.delayMs));
2491
+ const page = cursor ? Number.parseInt(cursor, 10) : 0;
2492
+ const start = page * this.pageSize;
2493
+ const end = Math.min(start + this.pageSize, this.totalItems);
2494
+ const items = Array.from({ length: end - start }, (_, i) => {
2495
+ const idx = start + i;
2496
+ return {
2497
+ id: `video-${idx}`,
2498
+ type: "video",
2499
+ source: {
2500
+ url: `https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4`,
2501
+ type: "mp4"
2502
+ },
2503
+ poster: `https://picsum.photos/seed/${idx}/400/700`,
2504
+ duration: 30 + idx % 60,
2505
+ title: `Video ${idx}`,
2506
+ description: `Description for video ${idx} #shorts #viral`,
2507
+ author: {
2508
+ id: `author-${idx % 5}`,
2509
+ name: `Creator ${idx % 5}`,
2510
+ avatar: `https://picsum.photos/seed/author-${idx % 5}/100/100`,
2511
+ isVerified: idx % 3 === 0
2512
+ },
2513
+ stats: {
2514
+ likes: 1e3 + idx * 137,
2515
+ comments: 50 + idx * 7,
2516
+ shares: 20 + idx * 3,
2517
+ views: 1e4 + idx * 1500
2518
+ },
2519
+ interaction: {
2520
+ isLiked: false,
2521
+ isBookmarked: false,
2522
+ isFollowing: false
2523
+ }
2524
+ };
2525
+ });
2526
+ const hasMore = end < this.totalItems;
2527
+ const nextCursor = hasMore ? String(page + 1) : null;
2528
+ return { items, nextCursor, hasMore };
2529
+ }
2530
+ };
2531
+ var MockCommentAdapter = class {
2532
+ async fetchComments(contentId, cursor) {
2533
+ await new Promise((r) => setTimeout(r, 300));
2534
+ const page = cursor ? Number.parseInt(cursor, 10) : 0;
2535
+ const items = Array.from({ length: 10 }, (_, i) => ({
2536
+ id: `comment-${contentId}-${page * 10 + i}`,
2537
+ contentId,
2538
+ authorId: `user-${i}`,
2539
+ authorName: `User ${i}`,
2540
+ text: `Comment ${page * 10 + i} on ${contentId}`,
2541
+ createdAt: Date.now() - i * 6e4,
2542
+ likeCount: i * 3,
2543
+ isLiked: false
2544
+ }));
2545
+ return { items, nextCursor: page < 2 ? String(page + 1) : null, hasMore: page < 2, total: 30 };
2546
+ }
2547
+ async postComment(contentId, text) {
2548
+ return {
2549
+ id: `comment-new-${Date.now()}`,
2550
+ contentId,
2551
+ authorId: "me",
2552
+ authorName: "Me",
2553
+ text,
2554
+ createdAt: Date.now(),
2555
+ likeCount: 0,
2556
+ isLiked: false
2557
+ };
2558
+ }
2559
+ async deleteComment(_id) {
2560
+ }
2561
+ async likeComment(_id) {
2562
+ }
2563
+ async unlikeComment(_id) {
2564
+ }
2565
+ };
2566
+
2567
+ // src/adapters/browser/HttpDataSource.ts
2568
+ function getNestedValue(obj, path) {
2569
+ if (!obj || typeof obj !== "object") return void 0;
2570
+ const keys = path.split(".");
2571
+ let current = obj;
2572
+ for (const key of keys) {
2573
+ if (current === null || current === void 0) return void 0;
2574
+ if (typeof current !== "object") return void 0;
2575
+ current = current[key];
2576
+ }
2577
+ return current;
2578
+ }
2579
+ function tryFields(obj, ...paths) {
2580
+ for (const path of paths) {
2581
+ const value = getNestedValue(obj, path);
2582
+ if (value !== void 0 && value !== null) return value;
2583
+ }
2584
+ return void 0;
2585
+ }
2586
+ function toStr(value, fallback = "") {
2587
+ if (value === null || value === void 0) return fallback;
2588
+ return String(value);
2589
+ }
2590
+ function toNum(value, fallback = 0) {
2591
+ if (value === null || value === void 0) return fallback;
2592
+ const n = Number(value);
2593
+ return Number.isNaN(n) ? fallback : n;
2594
+ }
2595
+ function toBool(value, fallback = false) {
2596
+ if (value === null || value === void 0) return fallback;
2597
+ if (typeof value === "boolean") return value;
2598
+ if (typeof value === "string") return value === "true" || value === "1";
2599
+ return Boolean(value);
2600
+ }
2601
+ function transformSource(obj) {
2602
+ const mediaArr = obj["media"];
2603
+ if (Array.isArray(mediaArr) && mediaArr.length > 0) {
2604
+ const first = mediaArr[0];
2605
+ const url2 = toStr(first["url"], "");
2606
+ const type2 = url2.includes(".m3u8") ? "hls" : "mp4";
2607
+ return { url: url2, type: type2 };
2608
+ }
2609
+ const url = toStr(
2610
+ tryFields(obj, "video_url", "url", "source_url", "playback_url", "stream_url"),
2611
+ ""
2612
+ );
2613
+ const type = url.includes(".m3u8") ? "hls" : "mp4";
2614
+ return { url, type };
2615
+ }
2616
+ function transformAuthor(obj) {
2617
+ const authorObj = tryFields(obj, "owner", "user", "author", "creator") ?? obj;
2618
+ const firstName = toStr(tryFields(authorObj, "first_name"), "");
2619
+ const lastName = toStr(tryFields(authorObj, "last_name"), "");
2620
+ const fullName = [firstName, lastName].filter(Boolean).join(" ") || toStr(
2621
+ tryFields(authorObj, "display_name", "name", "username", "nickname"),
2622
+ "Unknown"
2623
+ );
2624
+ return {
2625
+ id: toStr(tryFields(authorObj, "id", "user_id", "author_id"), ""),
2626
+ name: fullName || "Unknown",
2627
+ avatar: toStr(
2628
+ tryFields(authorObj, "avatar", "avatar_url", "profile_picture", "photo"),
2629
+ void 0
2630
+ ) || void 0,
2631
+ isVerified: toBool(tryFields(authorObj, "is_verified", "verified"), false)
2632
+ };
2633
+ }
2634
+ function transformStats(obj) {
2635
+ return {
2636
+ views: toNum(tryFields(obj, "views", "view_count", "play_count"), 0),
2637
+ likes: toNum(tryFields(obj, "likes", "like_count", "digg_count"), 0),
2638
+ comments: toNum(tryFields(obj, "total_comments", "comment_count", "comments"), 0),
2639
+ shares: toNum(tryFields(obj, "share_count", "shares"), 0),
2640
+ bookmarks: toNum(tryFields(obj, "bookmark_count", "bookmarks", "saves"), 0)
2641
+ };
2642
+ }
2643
+ function transformInteraction(obj) {
2644
+ return {
2645
+ isLiked: toBool(tryFields(obj, "liked", "is_liked", "user_liked", "has_liked", "isLiked"), false),
2646
+ isBookmarked: toBool(tryFields(obj, "is_bookmarked", "bookmarked", "saved", "isBookmarked"), false),
2647
+ isFollowing: toBool(tryFields(obj, "is_following", "following", "user_following", "isFollowing"), false)
2648
+ };
2649
+ }
2650
+ function transformVideoItem(raw) {
2651
+ const obj = raw;
2652
+ const source = transformSource(obj);
2653
+ const author = transformAuthor(obj);
2654
+ const stats = transformStats(obj);
2655
+ const interaction = transformInteraction(obj);
2656
+ let poster;
2657
+ const thumbnailObj = obj["thumbnail"];
2658
+ if (thumbnailObj && thumbnailObj["url"]) {
2659
+ poster = toStr(thumbnailObj["url"], "") || void 0;
2660
+ }
2661
+ if (!poster) {
2662
+ const mediaArr2 = obj["media"];
2663
+ if (Array.isArray(mediaArr2) && mediaArr2.length > 0) {
2664
+ const first = mediaArr2[0];
2665
+ poster = toStr(first["poster"], "") || void 0;
2666
+ }
2667
+ }
2668
+ if (!poster) {
2669
+ poster = toStr(
2670
+ tryFields(obj, "thumbnail_url", "cover", "cover_url"),
2671
+ void 0
2672
+ ) || void 0;
2673
+ }
2674
+ let duration = 0;
2675
+ const mediaArr = obj["media"];
2676
+ if (Array.isArray(mediaArr) && mediaArr.length > 0) {
2677
+ const first = mediaArr[0];
2678
+ duration = toNum(first["duration"], 0);
2679
+ }
2680
+ if (duration === 0) {
2681
+ duration = toNum(tryFields(obj, "duration", "duration_seconds", "length", "video_duration"), 0);
2682
+ }
2683
+ return {
2684
+ type: "video",
2685
+ id: toStr(tryFields(obj, "id", "video_id", "_id"), ""),
2686
+ source,
2687
+ poster,
2688
+ duration,
2689
+ title: toStr(tryFields(obj, "title", "caption", "text"), void 0) || void 0,
2690
+ description: toStr(tryFields(obj, "description", "caption", "text", "content"), void 0) || void 0,
2691
+ author,
2692
+ stats,
2693
+ interaction
2694
+ };
2695
+ }
2696
+ function transformArticleImage(raw) {
2697
+ if (typeof raw === "string") return { url: raw };
2698
+ const obj = raw;
2699
+ return {
2700
+ url: toStr(tryFields(obj, "url", "image_url", "src"), "")
2701
+ };
2702
+ }
2703
+ function transformArticle(raw) {
2704
+ const obj = raw;
2705
+ const author = transformAuthor(obj);
2706
+ const stats = transformStats(obj);
2707
+ const interaction = transformInteraction(obj);
2708
+ let rawImages = tryFields(obj, "images", "media", "photos", "gallery");
2709
+ if (!Array.isArray(rawImages) || rawImages.length === 0) {
2710
+ const single = toStr(tryFields(obj, "image", "image_url", "url", "thumbnail", "cover"), "");
2711
+ rawImages = single ? [single] : [];
2712
+ }
2713
+ const images = rawImages.map(transformArticleImage);
2714
+ return {
2715
+ type: "article",
2716
+ id: toStr(tryFields(obj, "id", "article_id", "post_id", "_id"), ""),
2717
+ images,
2718
+ title: toStr(tryFields(obj, "title", "subject"), void 0) || void 0,
2719
+ description: toStr(tryFields(obj, "description", "caption", "text", "content"), void 0) || void 0,
2720
+ author,
2721
+ stats,
2722
+ interaction
2723
+ };
2724
+ }
2725
+ function transformContentItem(raw) {
2726
+ const obj = raw;
2727
+ const type = toStr(tryFields(obj, "type", "content_type", "item_type"), "reel");
2728
+ if (type === "article" || type === "image") {
2729
+ return transformArticle(obj);
2730
+ }
2731
+ return transformVideoItem(obj);
2732
+ }
2733
+ function unwrapFeedResponse(response) {
2734
+ if (!response || typeof response !== "object") {
2735
+ return { items: [], nextCursor: null, hasMore: false };
2736
+ }
2737
+ const obj = response;
2738
+ const dataObj = obj["data"];
2739
+ if (dataObj && typeof dataObj === "object" && !Array.isArray(dataObj)) {
2740
+ const data = dataObj;
2741
+ const reels = data["reels"];
2742
+ if (Array.isArray(reels)) {
2743
+ const nextCursor2 = toStr(tryFields(data, "next_cursor", "nextCursor", "cursor"), "") || null;
2744
+ const hasMore2 = toBool(tryFields(data, "has_next", "has_more", "hasMore", "hasNext"), nextCursor2 !== null);
2745
+ return { items: reels, nextCursor: nextCursor2, hasMore: hasMore2 };
2746
+ }
2747
+ const fallbackItems = tryFields(data, "items", "videos", "results", "content", "feeds");
2748
+ if (Array.isArray(fallbackItems)) {
2749
+ const nextCursor2 = toStr(tryFields(data, "next_cursor", "nextCursor", "cursor"), "") || null;
2750
+ const hasMore2 = toBool(tryFields(data, "has_next", "has_more", "hasMore", "hasNext"), nextCursor2 !== null);
2751
+ return { items: fallbackItems, nextCursor: nextCursor2, hasMore: hasMore2 };
2752
+ }
2753
+ }
2754
+ const rawItems = Array.isArray(obj) ? obj : tryFields(obj, "items", "videos", "results", "content", "feeds");
2755
+ const items = Array.isArray(rawItems) ? rawItems : [];
2756
+ const nextCursor = toStr(
2757
+ tryFields(obj, "next_cursor", "nextCursor", "cursor", "next_page_token"),
2758
+ ""
2759
+ ) || null;
2760
+ const hasMore = nextCursor !== null ? true : toBool(tryFields(obj, "has_more", "hasMore", "has_next", "hasNext"), items.length > 0);
2761
+ return { items, nextCursor, hasMore };
2762
+ }
2763
+ var HttpDataSource = class {
2764
+ constructor(config) {
2765
+ this.baseUrl = config.baseUrl.replace(/\/$/, "");
2766
+ this.apiKey = config.apiKey;
2767
+ this.getAccessToken = config.getAccessToken ?? (() => null);
2768
+ this.feedPath = config.feedPath ?? "/reels";
2769
+ this.cursorParam = config.cursorParam ?? "cursor";
2770
+ this.limitParam = config.limitParam ?? "limit";
2771
+ this.pageSize = config.pageSize ?? 10;
2772
+ this.extraHeaders = config.headers ?? {};
2773
+ this.timeoutMs = config.timeoutMs ?? 1e4;
2774
+ this.logger = config.logger;
2775
+ }
2776
+ async fetchFeed(cursor) {
2777
+ try {
2778
+ const params = new URLSearchParams({
2779
+ [this.limitParam]: String(this.pageSize)
2780
+ });
2781
+ if (this.apiKey) {
2782
+ params.set("api_key", this.apiKey);
2783
+ }
2784
+ if (cursor) {
2785
+ params.set(this.cursorParam, cursor);
2786
+ }
2787
+ const url = `${this.baseUrl}${this.feedPath}?${params.toString()}`;
2788
+ const response = await this.fetch(url);
2789
+ const { items: rawItems, nextCursor, hasMore } = unwrapFeedResponse(response);
2790
+ const items = rawItems.map((raw) => {
2791
+ try {
2792
+ return transformContentItem(raw);
2793
+ } catch (err) {
2794
+ this.logger?.warn("[HttpDataSource] Failed to transform item, skipping", String(err));
2795
+ return transformVideoItem(raw);
2796
+ }
2797
+ });
2798
+ this.logger?.debug(`[HttpDataSource] fetchFeed \u2014 ${items.length} items, hasMore=${hasMore}`);
2799
+ return { items, nextCursor, hasMore };
2800
+ } catch (err) {
2801
+ this.logger?.error("[HttpDataSource] fetchFeed failed", String(err));
2802
+ throw err;
2803
+ }
2804
+ }
2805
+ // ─── Private helpers ───
2806
+ async fetch(url) {
2807
+ const controller = new AbortController();
2808
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
2809
+ try {
2810
+ const headers = await this.buildHeaders();
2811
+ const response = await fetch(url, {
2812
+ method: "GET",
2813
+ headers,
2814
+ signal: controller.signal
2815
+ });
2816
+ clearTimeout(timer);
2817
+ if (!response.ok) {
2818
+ const body = await response.text().catch(() => "");
2819
+ throw new HttpError(response.status, `HTTP ${response.status}: ${response.statusText}`, body);
2820
+ }
2821
+ return response.json();
2822
+ } catch (err) {
2823
+ clearTimeout(timer);
2824
+ throw err;
2825
+ }
2826
+ }
2827
+ async buildHeaders() {
2828
+ const headers = {
2829
+ "Content-Type": "application/json",
2830
+ Accept: "application/json",
2831
+ ...this.extraHeaders
2832
+ };
2833
+ if (!this.apiKey) {
2834
+ const token = await this.getAccessToken();
2835
+ if (token) {
2836
+ headers["Authorization"] = `Bearer ${token}`;
2837
+ }
2838
+ }
2839
+ return headers;
2840
+ }
2841
+ };
2842
+ var HttpError = class extends Error {
2843
+ constructor(status, message, body) {
2844
+ super(message);
2845
+ this.status = status;
2846
+ this.body = body;
2847
+ this.name = "HttpError";
2848
+ }
2849
+ };
2850
+
2851
+ exports.DEFAULT_FEED_CONFIG = DEFAULT_FEED_CONFIG;
2852
+ exports.DEFAULT_PLAYER_CONFIG = DEFAULT_PLAYER_CONFIG;
2853
+ exports.DEFAULT_RESOURCE_CONFIG = DEFAULT_RESOURCE_CONFIG;
2854
+ exports.DefaultActions = DefaultActions;
2855
+ exports.DefaultOverlay = DefaultOverlay;
2856
+ exports.DefaultSkeleton = DefaultSkeleton;
2857
+ exports.FeedManager = FeedManager;
2858
+ exports.HttpDataSource = HttpDataSource;
2859
+ exports.HttpError = HttpError;
2860
+ exports.MockAnalytics = MockAnalytics;
2861
+ exports.MockCommentAdapter = MockCommentAdapter;
2862
+ exports.MockDataSource = MockDataSource;
2863
+ exports.MockInteraction = MockInteraction;
2864
+ exports.MockLogger = MockLogger;
2865
+ exports.MockNetworkAdapter = MockNetworkAdapter;
2866
+ exports.MockSessionStorage = MockSessionStorage;
2867
+ exports.MockVideoLoader = MockVideoLoader;
2868
+ exports.OptimisticManager = OptimisticManager;
2869
+ exports.PlayerEngine = PlayerEngine;
2870
+ exports.PlayerStatus = PlayerStatus;
2871
+ exports.ReelsFeed = ReelsFeed;
2872
+ exports.ReelsProvider = ReelsProvider;
2873
+ exports.ResourceGovernor = ResourceGovernor;
2874
+ exports.VALID_TRANSITIONS = VALID_TRANSITIONS;
2875
+ exports.VideoSlot = VideoSlot;
2876
+ exports.canPause = canPause;
2877
+ exports.canPlay = canPlay;
2878
+ exports.canSeek = canSeek;
2879
+ exports.isArticle = isArticle;
2880
+ exports.isValidTransition = isValidTransition;
2881
+ exports.isVideoItem = isVideoItem;
2882
+ exports.useFeed = useFeed;
2883
+ exports.useFeedSelector = useFeedSelector;
2884
+ exports.useHls = useHls;
2885
+ exports.usePlayer = usePlayer;
2886
+ exports.usePlayerSelector = usePlayerSelector;
2887
+ exports.usePointerGesture = usePointerGesture;
2888
+ exports.useResource = useResource;
2889
+ exports.useResourceSelector = useResourceSelector;
2890
+ exports.useSDK = useSDK;
2891
+ exports.useSnapAnimation = useSnapAnimation;