@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.
@@ -0,0 +1,1075 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+ import { StoreApi } from 'zustand/vanilla';
4
+ import * as react_jsx_runtime from 'react/jsx-runtime';
5
+ import { HlsConfig } from 'hls.js';
6
+
7
+ /**
8
+ * Content types — VideoItem, Article, ContentItem
9
+ */
10
+ interface VideoSource {
11
+ /** MP4 direct URL or HLS .m3u8 URL */
12
+ url: string;
13
+ type: 'mp4' | 'hls';
14
+ qualities?: VideoQuality[];
15
+ }
16
+ interface VideoQuality {
17
+ label: string;
18
+ bitrate: number;
19
+ width: number;
20
+ height: number;
21
+ url: string;
22
+ }
23
+ interface Author {
24
+ id: string;
25
+ name: string;
26
+ avatar?: string;
27
+ description?: string;
28
+ isVerified?: boolean;
29
+ }
30
+ interface ContentStats {
31
+ likes: number;
32
+ comments: number;
33
+ shares: number;
34
+ bookmarks?: number;
35
+ views: number;
36
+ }
37
+ interface InteractionState {
38
+ isLiked: boolean;
39
+ isBookmarked: boolean;
40
+ isFollowing: boolean;
41
+ }
42
+ interface VideoItem {
43
+ id: string;
44
+ type: 'video';
45
+ source: VideoSource;
46
+ poster?: string;
47
+ duration: number;
48
+ title?: string;
49
+ description?: string;
50
+ author: Author;
51
+ stats: ContentStats;
52
+ interaction: InteractionState;
53
+ /** HLS-specific: start position in seconds for session restore */
54
+ startPosition?: number;
55
+ /** Extra metadata for host app */
56
+ meta?: Record<string, unknown>;
57
+ }
58
+ interface ArticleImage {
59
+ url: string;
60
+ width?: number;
61
+ height?: number;
62
+ alt?: string;
63
+ }
64
+ interface Article {
65
+ id: string;
66
+ type: 'article';
67
+ images: ArticleImage[];
68
+ title?: string;
69
+ description?: string;
70
+ author: Author;
71
+ stats: ContentStats;
72
+ interaction: InteractionState;
73
+ meta?: Record<string, unknown>;
74
+ }
75
+ type ContentItem = VideoItem | Article;
76
+ declare function isVideoItem(item: ContentItem): item is VideoItem;
77
+ declare function isArticle(item: ContentItem): item is Article;
78
+
79
+ /**
80
+ * Feed adapter interfaces and state types
81
+ */
82
+
83
+ interface FeedPage {
84
+ items: ContentItem[];
85
+ nextCursor: string | null;
86
+ hasMore: boolean;
87
+ }
88
+ interface IDataSource {
89
+ fetchFeed(cursor?: string | null): Promise<FeedPage>;
90
+ }
91
+ interface FeedState {
92
+ /** Normalized item map for O(1) lookup */
93
+ itemsById: Map<string, ContentItem>;
94
+ /** Ordered item IDs */
95
+ displayOrder: string[];
96
+ /** Initial load in progress */
97
+ loading: boolean;
98
+ /** Pagination load in progress */
99
+ loadingMore: boolean;
100
+ /** Current error */
101
+ error: FeedError | null;
102
+ /** Pagination cursor */
103
+ cursor: string | null;
104
+ /** Whether more items are available */
105
+ hasMore: boolean;
106
+ /** Whether current data is stale (SWR) */
107
+ isStale: boolean;
108
+ /** Last successful fetch timestamp */
109
+ lastFetchTime: number | null;
110
+ }
111
+ interface FeedError {
112
+ message: string;
113
+ code: string;
114
+ retryable: boolean;
115
+ }
116
+ interface FeedConfig {
117
+ /** Items per page (default: 10) */
118
+ pageSize?: number;
119
+ /** Max retries on error (default: 3) */
120
+ maxRetries?: number;
121
+ /** Base retry delay ms (default: 1000) */
122
+ retryDelay?: number;
123
+ /** Max cached items before LRU eviction (default: 50) */
124
+ maxCacheSize?: number;
125
+ /** Stale-while-revalidate TTL ms (default: 5min) */
126
+ staleTTL?: number;
127
+ }
128
+ declare const DEFAULT_FEED_CONFIG: Required<FeedConfig>;
129
+
130
+ /**
131
+ * Player state types
132
+ */
133
+
134
+ declare enum PlayerStatus {
135
+ IDLE = "idle",
136
+ LOADING = "loading",
137
+ PLAYING = "playing",
138
+ PAUSED = "paused",
139
+ BUFFERING = "buffering",
140
+ ERROR = "error"
141
+ }
142
+ interface PlayerError {
143
+ message: string;
144
+ code: 'MEDIA_ERROR' | 'NETWORK_ERROR' | 'DECODE_ERROR' | 'NOT_SUPPORTED' | 'UNKNOWN';
145
+ recoverable: boolean;
146
+ originalError?: Error;
147
+ }
148
+ interface PlayerState {
149
+ status: PlayerStatus;
150
+ currentVideo: VideoItem | null;
151
+ currentVideoId: string | null;
152
+ currentTime: number;
153
+ duration: number;
154
+ /** Buffered seconds */
155
+ buffered: number;
156
+ volume: number;
157
+ muted: boolean;
158
+ playbackRate: number;
159
+ loopCount: number;
160
+ watchTime: number;
161
+ error: PlayerError | null;
162
+ ended: boolean;
163
+ /** Session restore: seek to this position when matching videoId loads */
164
+ pendingRestoreTime: number | null;
165
+ pendingRestoreVideoId: string | null;
166
+ }
167
+ interface PlayerConfig {
168
+ defaultVolume?: number;
169
+ defaultMuted?: boolean;
170
+ /** Max consecutive errors before circuit opens (default: 3) */
171
+ circuitBreakerThreshold?: number;
172
+ /** Time ms to wait before circuit resets to HALF_OPEN (default: 10000) */
173
+ circuitBreakerResetMs?: number;
174
+ }
175
+ declare const DEFAULT_PLAYER_CONFIG: Required<PlayerConfig>;
176
+ type PlayerEvent = {
177
+ type: 'statusChange';
178
+ status: PlayerStatus;
179
+ previousStatus: PlayerStatus;
180
+ } | {
181
+ type: 'videoChange';
182
+ video: VideoItem;
183
+ } | {
184
+ type: 'loadRejected';
185
+ reason: 'circuit_open' | 'invalid_transition';
186
+ } | {
187
+ type: 'error';
188
+ error: PlayerError;
189
+ } | {
190
+ type: 'ended';
191
+ videoId: string;
192
+ watchTime: number;
193
+ loopCount: number;
194
+ };
195
+ type PlayerEventListener = (event: PlayerEvent) => void;
196
+
197
+ /**
198
+ * Adapter interfaces (Ports) — implemented by host app or use built-in mocks
199
+ */
200
+ type LogLevel = 'debug' | 'info' | 'warn' | 'error';
201
+ interface ILogger {
202
+ debug(message: string, ...args: unknown[]): void;
203
+ info(message: string, ...args: unknown[]): void;
204
+ warn(message: string, ...args: unknown[]): void;
205
+ error(message: string, ...args: unknown[]): void;
206
+ }
207
+ interface IAnalytics {
208
+ trackView(videoId: string, duration: number): void;
209
+ trackLike(videoId: string, isLiked: boolean): void;
210
+ trackShare(videoId: string): void;
211
+ trackComment(videoId: string): void;
212
+ trackError(videoId: string, error: string): void;
213
+ trackPlaybackEvent(videoId: string, event: 'play' | 'pause' | 'seek' | 'end', position?: number): void;
214
+ }
215
+ interface IInteraction {
216
+ like(contentId: string): Promise<void>;
217
+ unlike(contentId: string): Promise<void>;
218
+ follow(authorId: string): Promise<void>;
219
+ unfollow(authorId: string): Promise<void>;
220
+ bookmark(contentId: string): Promise<void>;
221
+ unbookmark(contentId: string): Promise<void>;
222
+ share(contentId: string): Promise<void>;
223
+ }
224
+ interface ISessionStorage {
225
+ get<T>(key: string): T | null;
226
+ set<T>(key: string, value: T): void;
227
+ remove(key: string): void;
228
+ clear(): void;
229
+ }
230
+ type NetworkType = 'wifi' | '4g' | '3g' | '2g' | 'slow-2g' | 'offline' | 'unknown';
231
+ interface INetworkAdapter {
232
+ getNetworkType(): NetworkType;
233
+ isOnline(): boolean;
234
+ onNetworkChange(callback: (type: NetworkType) => void): () => void;
235
+ }
236
+ type PreloadStatus = 'idle' | 'loading' | 'loaded' | 'error';
237
+ interface PreloadResult {
238
+ videoId: string;
239
+ status: PreloadStatus;
240
+ loadedBytes?: number;
241
+ error?: Error;
242
+ }
243
+ interface IVideoLoader {
244
+ preload(videoId: string, url: string, signal?: AbortSignal): Promise<PreloadResult>;
245
+ cancel(videoId: string): void;
246
+ isPreloaded(videoId: string): boolean;
247
+ getPreloadStatus(videoId: string): PreloadStatus;
248
+ clearAll(): void;
249
+ }
250
+ interface CommentItem {
251
+ id: string;
252
+ contentId: string;
253
+ authorId: string;
254
+ authorName: string;
255
+ authorAvatar?: string;
256
+ text: string;
257
+ createdAt: number;
258
+ likeCount: number;
259
+ isLiked: boolean;
260
+ }
261
+ interface CommentPage {
262
+ items: CommentItem[];
263
+ nextCursor: string | null;
264
+ hasMore: boolean;
265
+ total: number;
266
+ }
267
+ interface ICommentAdapter {
268
+ fetchComments(contentId: string, cursor?: string | null): Promise<CommentPage>;
269
+ postComment(contentId: string, text: string): Promise<CommentItem>;
270
+ deleteComment(commentId: string): Promise<void>;
271
+ likeComment(commentId: string): Promise<void>;
272
+ unlikeComment(commentId: string): Promise<void>;
273
+ }
274
+ interface SDKAdapters {
275
+ dataSource: IDataSource;
276
+ interaction?: Partial<IInteraction>;
277
+ storage?: ISessionStorage;
278
+ analytics?: IAnalytics;
279
+ logger?: ILogger;
280
+ network?: INetworkAdapter;
281
+ videoLoader?: IVideoLoader;
282
+ comment?: ICommentAdapter;
283
+ }
284
+
285
+ /**
286
+ * ReelsFeed component types — Render Props architecture
287
+ */
288
+
289
+ /** Actions provided to render prop callbacks for each video slot */
290
+ interface SlotActions {
291
+ /** Optimistic like toggle via OptimisticManager */
292
+ toggleLike: () => void;
293
+ /** Current like delta from optimistic updates */
294
+ likeDelta: number;
295
+ /** Toggle follow via OptimisticManager */
296
+ toggleFollow: () => Promise<boolean>;
297
+ /** Optimistic follow state: undefined = server state, boolean = optimistic */
298
+ followState: boolean | undefined;
299
+ /** Share — calls IInteraction.share() if provided */
300
+ share: () => void;
301
+ /** Current global mute state */
302
+ isMuted: boolean;
303
+ /** Toggle global mute */
304
+ toggleMute: () => void;
305
+ /** Whether this slot is the active playing slot */
306
+ isActive: boolean;
307
+ /** Slot index in feed */
308
+ index: number;
309
+ }
310
+ /** Props for the ReelsFeed component */
311
+ interface ReelsFeedProps {
312
+ /**
313
+ * Custom overlay rendered on each video slot.
314
+ * Receives the content item + actions. Return ReactNode.
315
+ * Positioned absolute bottom-left by SDK.
316
+ * If not provided, SDK uses DefaultOverlay showing author + title.
317
+ */
318
+ renderOverlay?: (item: ContentItem, actions: SlotActions) => ReactNode;
319
+ /**
320
+ * Custom action bar rendered on each video slot.
321
+ * Receives the content item + actions. Return ReactNode.
322
+ * Positioned absolute bottom-right by SDK.
323
+ * If not provided, SDK uses DefaultActions showing like/comment/share emojis.
324
+ */
325
+ renderActions?: (item: ContentItem, actions: SlotActions) => ReactNode;
326
+ /**
327
+ * Custom loading skeleton shown during initial feed load.
328
+ * If not provided, SDK uses DefaultSkeleton with shimmer animation.
329
+ */
330
+ renderLoading?: () => ReactNode;
331
+ /**
332
+ * Custom empty state shown when feed has zero items after load.
333
+ * If not provided, SDK shows a simple "No videos found" message.
334
+ */
335
+ renderEmpty?: () => ReactNode;
336
+ /**
337
+ * Custom error state shown when feed fails to load.
338
+ * If not provided, SDK shows the error message with a retry button.
339
+ */
340
+ renderError?: (error: {
341
+ message: string;
342
+ retry: () => void;
343
+ }) => ReactNode;
344
+ /** Show FPS counter overlay — dev only (default: false) */
345
+ showFps?: boolean;
346
+ /** Number of items from end to trigger loadMore (default: 5) */
347
+ loadMoreThreshold?: number;
348
+ /**
349
+ * Called when the active slot changes (user swipes to a new video).
350
+ * Fires AFTER snap animation triggers and new focusedIndex is set.
351
+ * Use for analytics tracking, preloading comments, updating URL, etc.
352
+ *
353
+ * @param index - New focused index
354
+ * @param item - The ContentItem at the new index
355
+ * @param prevIndex - Previous focused index
356
+ */
357
+ onSlotChange?: (index: number, item: ContentItem, prevIndex: number) => void;
358
+ /** Gesture config overrides */
359
+ gestureConfig?: {
360
+ velocityThreshold?: number;
361
+ distanceThreshold?: number;
362
+ dragThresholdRatio?: number;
363
+ };
364
+ /** Snap animation config overrides */
365
+ snapConfig?: {
366
+ duration?: number;
367
+ easing?: string;
368
+ };
369
+ }
370
+
371
+ /**
372
+ * PlayerEngine — Video player state machine with Circuit Breaker
373
+ *
374
+ * Features:
375
+ * - Guarded state transitions (only valid transitions allowed)
376
+ * - Circuit Breaker: auto-opens after N consecutive errors (WebView crash protection)
377
+ * - Watch time tracking
378
+ * - Analytics integration via adapter (optional)
379
+ * - Event system for external listeners
380
+ * - Framework-agnostic: uses zustand/vanilla (no React dependency)
381
+ */
382
+
383
+ declare class PlayerEngine {
384
+ readonly store: StoreApi<PlayerState>;
385
+ private readonly config;
386
+ private readonly analytics?;
387
+ private readonly logger?;
388
+ private readonly listeners;
389
+ private circuit;
390
+ private circuitResetTimer;
391
+ private watchTimeInterval;
392
+ private lastStatus;
393
+ constructor(config?: PlayerConfig, analytics?: IAnalytics, logger?: ILogger);
394
+ /**
395
+ * Load a video and prepare for playback.
396
+ * Returns false if rejected by circuit breaker or invalid state.
397
+ */
398
+ load(video: VideoItem): boolean;
399
+ play(): boolean;
400
+ pause(): boolean;
401
+ togglePlay(): boolean;
402
+ seek(time: number): boolean;
403
+ setMuted(muted: boolean): void;
404
+ toggleMute(): void;
405
+ setVolume(volume: number): void;
406
+ setPlaybackRate(rate: number): void;
407
+ /** Called when <video> fires `canplay` */
408
+ onCanPlay(): void;
409
+ /** Called when <video> fires `waiting` */
410
+ onWaiting(): boolean;
411
+ /** Called when <video> fires `playing` (after buffering) */
412
+ onPlaying(): boolean;
413
+ /** Called when <video> fires `timeupdate` */
414
+ onTimeUpdate(currentTime: number): void;
415
+ /** Called when <video> fires `progress` (buffer update) */
416
+ onProgress(buffered: number): void;
417
+ /** Called when <video> fires `loadedmetadata` */
418
+ onLoadedMetadata(duration: number): void;
419
+ /** Called when <video> fires `ended` */
420
+ onEnded(): void;
421
+ /** Called when <video> fires `error` */
422
+ onError(code: PlayerError['code'], message: string): void;
423
+ setPendingRestore(videoId: string, time: number): void;
424
+ consumePendingRestore(videoId: string): number | null;
425
+ on(listener: PlayerEventListener): () => void;
426
+ reset(): void;
427
+ destroy(): void;
428
+ private transition;
429
+ private startWatchTime;
430
+ private stopWatchTime;
431
+ private checkCircuit;
432
+ private recordCircuitError;
433
+ private trackLeave;
434
+ private emit;
435
+ }
436
+
437
+ declare class FeedManager {
438
+ private dataSource;
439
+ readonly store: StoreApi<FeedState>;
440
+ private readonly config;
441
+ private readonly logger?;
442
+ /** Cancel in-flight requests */
443
+ private abortController;
444
+ /** In-flight request deduplication: cursor → Promise */
445
+ private inFlightRequests;
446
+ /** LRU tracking: itemId → lastAccessTime */
447
+ private accessOrder;
448
+ /** Prefetch cache — instance-scoped (not static) */
449
+ private prefetchCache;
450
+ constructor(dataSource: IDataSource, config?: FeedConfig, logger?: ILogger);
451
+ getDataSource(): IDataSource;
452
+ setDataSource(dataSource: IDataSource, reset?: boolean): void;
453
+ prefetch(ttlMs?: number): Promise<void>;
454
+ hasPrefetchCache(): boolean;
455
+ clearPrefetchCache(): void;
456
+ loadInitial(): Promise<void>;
457
+ loadMore(): Promise<void>;
458
+ refresh(): Promise<void>;
459
+ getItems(): ContentItem[];
460
+ getItemById(id: string): ContentItem | undefined;
461
+ updateItem(id: string, patch: Partial<ContentItem>): void;
462
+ destroy(): void;
463
+ private fetchPage;
464
+ private doFetch;
465
+ private applyItems;
466
+ private evictLRU;
467
+ }
468
+
469
+ /**
470
+ * OptimisticManager — Optimistic UI updates with rollback
471
+ *
472
+ * Handles like/follow/bookmark with:
473
+ * - Immediate local state update (optimistic)
474
+ * - Background API call
475
+ * - Rollback on failure
476
+ * - Debounced toggleLike (prevent rapid double-tap API spam)
477
+ */
478
+
479
+ type ActionType = 'like' | 'unlike' | 'follow' | 'unfollow' | 'bookmark' | 'unbookmark';
480
+ interface PendingAction {
481
+ id: string;
482
+ type: ActionType;
483
+ contentId: string;
484
+ enqueuedAt: number;
485
+ }
486
+ interface OptimisticState {
487
+ /** Map of actionId → PendingAction */
488
+ pendingActions: Map<string, PendingAction>;
489
+ /** IDs of failed actions waiting for retry */
490
+ failedQueue: string[];
491
+ hasPending: boolean;
492
+ isRetrying: boolean;
493
+ /** Optimistic like counts: contentId → delta */
494
+ likeDeltas: Map<string, number>;
495
+ /** Optimistic follow state: authorId → isFollowing */
496
+ followState: Map<string, boolean>;
497
+ }
498
+ declare class OptimisticManager {
499
+ readonly store: StoreApi<OptimisticState>;
500
+ private readonly interaction;
501
+ private readonly logger?;
502
+ /** Debounce timers: contentId → timer */
503
+ private likeDebounceTimers;
504
+ /** Pending like direction: contentId → final intended state */
505
+ private pendingLikeState;
506
+ constructor(interaction: Partial<IInteraction>, logger?: ILogger);
507
+ /**
508
+ * Debounced like toggle — prevents rapid API spam on double-tap.
509
+ * UI updates instantly; API call fires after 600ms debounce.
510
+ */
511
+ toggleLike(contentId: string, currentIsLiked: boolean): void;
512
+ toggleFollow(authorId: string, currentIsFollowing: boolean): Promise<boolean>;
513
+ getLikeDelta(contentId: string): number;
514
+ getFollowState(authorId: string): boolean | undefined;
515
+ destroy(): void;
516
+ }
517
+
518
+ /**
519
+ * ResourceGovernor — Video DOM allocation manager (3-Tier Pre-buffering)
520
+ *
521
+ * Tier 1 (Active): 1 slot — playing, 10s HLS buffer
522
+ * Tier 2 (Hot): ±bufferWindow slots — DOM + HLS + 2s pre-buffered → instant swap
523
+ * Tier 3 (Warm): +warmWindow beyond hot — DOM + HLS manifest + 1 segment (~0.5s) → ~300ms show
524
+ * Tier 4 (Cold): preloadLookAhead beyond warm — metadata/manifest prefetch only (no DOM)
525
+ *
526
+ * Total DOM nodes = 1 + 2×bufferWindow + warmWindow (forward) + 1 (backward)
527
+ * Default: 1 + 6 + 4 = 11 DOM nodes, ~112 MB memory (fits comfortably in 1 GB budget)
528
+ */
529
+
530
+ interface ResourceState {
531
+ /** Set of indices with active video DOM allocations (Tier 1 + 2 — hot) */
532
+ activeAllocations: Set<number>;
533
+ /** Set of indices with warm video DOM allocations (Tier 3 — manifest + 1 segment) */
534
+ warmAllocations: Set<number>;
535
+ /** Indices queued for preloading (Tier 4 — no DOM) */
536
+ preloadQueue: number[];
537
+ /** Currently focused index */
538
+ focusedIndex: number;
539
+ /** Total items in feed */
540
+ totalItems: number;
541
+ /** Current network type */
542
+ networkType: NetworkType;
543
+ /** Whether governor is active */
544
+ isActive: boolean;
545
+ /**
546
+ * Index that should be eagerly prefetched (src loaded) before the swipe commits.
547
+ * Set by the gesture layer at ~50% drag; cleared after snap completes.
548
+ * null = no active prefetch.
549
+ */
550
+ prefetchIndex: number | null;
551
+ }
552
+ interface ResourceConfig {
553
+ /** Max concurrent video DOM nodes — Tier 1+2+3 combined (default: 11) */
554
+ maxAllocations?: number;
555
+ /** Buffer window above/below focused index for hot tier (default: 3) */
556
+ bufferWindow?: number;
557
+ /** Additional warm slots beyond hot window — forward-biased (default: 4) */
558
+ warmWindow?: number;
559
+ /** Debounce ms for focusedIndex changes (default: 0) */
560
+ focusDebounceMs?: number;
561
+ /** How many items ahead of the warm window to queue for cold preload (default: 3) */
562
+ preloadLookAhead?: number;
563
+ }
564
+ declare const DEFAULT_RESOURCE_CONFIG: Required<ResourceConfig>;
565
+ declare class ResourceGovernor {
566
+ readonly store: StoreApi<ResourceState>;
567
+ private readonly config;
568
+ private readonly videoLoader?;
569
+ private readonly network?;
570
+ private readonly logger?;
571
+ private focusDebounceTimer;
572
+ private networkUnsubscribe?;
573
+ constructor(config?: ResourceConfig, videoLoader?: IVideoLoader, network?: INetworkAdapter, logger?: ILogger);
574
+ activate(): Promise<void>;
575
+ deactivate(): void;
576
+ destroy(): void;
577
+ setTotalItems(count: number): void;
578
+ /**
579
+ * Debounced focus update — prevents rapid re-allocations during fast swipe
580
+ */
581
+ setFocusedIndex(index: number): void;
582
+ setFocusedIndexImmediate(index: number): void;
583
+ /**
584
+ * Signal that the given index should have its video src eagerly loaded.
585
+ * Called by the gesture layer at ~50% drag. Pass null to clear.
586
+ */
587
+ setPrefetchIndex(index: number | null): void;
588
+ isAllocated(index: number): boolean;
589
+ isWarmAllocated(index: number): boolean;
590
+ shouldRenderVideo(index: number): boolean;
591
+ isPreloading(index: number): boolean;
592
+ getActiveAllocations(): number[];
593
+ getWarmAllocations(): number[];
594
+ private recalculate;
595
+ }
596
+
597
+ /**
598
+ * Player State Machine — Guarded transitions
599
+ *
600
+ * State Flow:
601
+ * IDLE → LOADING → PLAYING ↔ PAUSED
602
+ * ↓ ↓
603
+ * ERROR ← BUFFERING
604
+ * ↓
605
+ * IDLE (retry)
606
+ */
607
+
608
+ declare const VALID_TRANSITIONS: Record<PlayerStatus, PlayerStatus[]>;
609
+ declare function isValidTransition(from: PlayerStatus, to: PlayerStatus): boolean;
610
+ declare function canPlay(status: PlayerStatus): boolean;
611
+ declare function canPause(status: PlayerStatus): boolean;
612
+ declare function canSeek(status: PlayerStatus): boolean;
613
+
614
+ /**
615
+ * usePointerGesture — Low-level pointer event handler for swipe gestures
616
+ *
617
+ * Architecture fix vs xhub-short:
618
+ * - Uses native Pointer Events (not React synthetic events)
619
+ * - ZERO React state updates during drag (only on snap)
620
+ * - No setTimeout polling for sync
621
+ * - MutationObserver-based slot cache (not querySelectorAll per frame)
622
+ * - cancelAnimationFrame dedup built-in
623
+ *
624
+ * @returns bind — spread on the container element
625
+ */
626
+ interface PointerGestureConfig {
627
+ /** Axis to track (default: 'y') */
628
+ axis?: 'x' | 'y';
629
+ /** Minimum velocity px/ms to trigger snap (default: 0.3) */
630
+ velocityThreshold?: number;
631
+ /** Minimum distance px to trigger snap (default: 80) */
632
+ distanceThreshold?: number;
633
+ /** Whether gestures are disabled (default: false) */
634
+ disabled?: boolean;
635
+ /**
636
+ * Called every animation frame during drag with current offset.
637
+ * Use for direct DOM manipulation — do NOT call setState here.
638
+ */
639
+ onDragOffset?: (offset: number) => void;
640
+ /**
641
+ * Called once when drag passes the given threshold ratio (0–1) of containerSize.
642
+ * direction indicates which way the user is swiping.
643
+ * Use for prefetching — do NOT call setState here.
644
+ */
645
+ onDragThreshold?: (direction: 'forward' | 'backward') => void;
646
+ /**
647
+ * Container size in px used to calculate threshold ratio (default: window.innerHeight).
648
+ * Typically set to the viewport height for vertical feeds.
649
+ */
650
+ containerSize?: number;
651
+ /**
652
+ * Ratio of containerSize at which onDragThreshold fires (default: 0.5 = 50%).
653
+ */
654
+ dragThresholdRatio?: number;
655
+ /** Called when gesture ends and snap is triggered */
656
+ onSnap?: (direction: 'forward' | 'backward') => void;
657
+ /** Called when gesture ends without snap (bounce back) */
658
+ onBounceBack?: () => void;
659
+ }
660
+ interface PointerGestureBind {
661
+ onPointerDown: (e: React.PointerEvent) => void;
662
+ }
663
+ declare function usePointerGesture(config?: PointerGestureConfig): {
664
+ bind: PointerGestureBind;
665
+ isDraggingRef: React.RefObject<boolean>;
666
+ dragOffsetRef: React.RefObject<number>;
667
+ };
668
+
669
+ /**
670
+ * useSnapAnimation — Web Animations API snap
671
+ *
672
+ * Fixes xhub-short issue where CSS transition was interrupted by React renders.
673
+ * Web Animations API is imperative — runs independently of React lifecycle.
674
+ *
675
+ * Features:
676
+ * - Can be cancelled at any time (e.g., user starts new swipe during animation)
677
+ * - Predictable timing regardless of React render schedule
678
+ * - Automatic cleanup on unmount
679
+ */
680
+ interface SnapAnimationConfig {
681
+ /** Duration in ms (default: 280) */
682
+ duration?: number;
683
+ /** CSS easing (default: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)') */
684
+ easing?: string;
685
+ }
686
+ interface SnapTarget {
687
+ element: HTMLElement;
688
+ fromY: number;
689
+ toY: number;
690
+ }
691
+ declare function useSnapAnimation(config?: SnapAnimationConfig): {
692
+ /** Animate one or more slot elements from their current position to target */
693
+ animateSnap: (targets: SnapTarget[]) => void;
694
+ /** Animate bounce-back (offset → 0) */
695
+ animateBounceBack: (targets: SnapTarget[]) => void;
696
+ /** Cancel any in-progress animation */
697
+ cancelAnimation: () => void;
698
+ };
699
+
700
+ interface SDKContextValue {
701
+ feedManager: FeedManager;
702
+ playerEngine: PlayerEngine;
703
+ resourceGovernor: ResourceGovernor;
704
+ optimisticManager: OptimisticManager;
705
+ adapters: Required<Omit<SDKAdapters, 'dataSource'>> & Pick<SDKAdapters, 'dataSource'>;
706
+ }
707
+ interface ReelsProviderProps {
708
+ children: ReactNode;
709
+ adapters: SDKAdapters;
710
+ /** Enable verbose logging (default: false) */
711
+ debug?: boolean;
712
+ }
713
+ declare function ReelsProvider({ children, adapters, debug }: ReelsProviderProps): react_jsx_runtime.JSX.Element;
714
+ declare function useSDK(): SDKContextValue;
715
+
716
+ declare function ReelsFeed({ renderOverlay, renderActions, renderLoading, renderEmpty, renderError: _renderError, showFps, loadMoreThreshold, onSlotChange, gestureConfig, snapConfig, }: ReelsFeedProps): string | number | bigint | boolean | Iterable<react.ReactNode> | Promise<string | number | bigint | boolean | react.ReactPortal | react.ReactElement<unknown, string | react.JSXElementConstructor<any>> | Iterable<react.ReactNode> | null | undefined> | react_jsx_runtime.JSX.Element | null | undefined;
717
+
718
+ /**
719
+ * useHls — React hook for hls.js lifecycle management (3-Tier buffer support)
720
+ *
721
+ * Handles:
722
+ * - hls.js initialization with optimized buffer config for video feeds
723
+ * - 3-tier buffer strategy: active (10s), hot (2s), warm (0.5s manifest+1seg)
724
+ * - Safari fallback (native HLS — no hls.js needed)
725
+ * - Automatic cleanup on unmount / src change (hls.destroy())
726
+ * - Fatal error mapping to PlayerEngine error codes
727
+ * - Media error recovery (one retry via recoverMediaError)
728
+ * - Quality level selection (ABR or manual)
729
+ *
730
+ * KEY FIX: When bufferTier changes (e.g. hot→active on swipe), the existing
731
+ * HLS instance is reconfigured in-place instead of being destroyed/recreated.
732
+ * This preserves already-buffered video data, eliminating the
733
+ * poster → black → play flash on swipe.
734
+ *
735
+ * Contract:
736
+ * - This hook does NOT call setState during playback frames
737
+ * - isReady state update happens once per src load (on canplay)
738
+ * - Cleanup is guaranteed via useEffect return
739
+ */
740
+
741
+ /** Buffer tier determines how aggressively this slot pre-buffers video data. */
742
+ type BufferTier = 'active' | 'hot' | 'warm';
743
+ interface UseHlsOptions {
744
+ /** The .m3u8 URL to load. Pass undefined to unload. */
745
+ src: string | undefined;
746
+ /** Ref to the <video> element */
747
+ videoRef: React.RefObject<HTMLVideoElement | null>;
748
+ /** Whether this slot is the active (playing) one */
749
+ isActive: boolean;
750
+ /** Whether this slot should prefetch (load source but not play) */
751
+ isPrefetch: boolean;
752
+ /**
753
+ * Buffer tier for this slot (default: 'active').
754
+ * - 'active': Full 10s buffer (Tier 1 — currently playing)
755
+ * - 'hot': 2s buffer (Tier 2 — instant swap on swipe)
756
+ * - 'warm': 0.5s buffer (Tier 3 — manifest + 1 segment for fast show)
757
+ */
758
+ bufferTier?: BufferTier;
759
+ /** HLS config overrides (merged with tier-specific defaults) */
760
+ hlsConfig?: Partial<HlsConfig>;
761
+ /** Called when hls.js encounters a fatal error. Maps to PlayerEngine error codes. */
762
+ onError?: (code: 'NETWORK_ERROR' | 'MEDIA_ERROR' | 'DECODE_ERROR' | 'UNKNOWN', message: string) => void;
763
+ }
764
+ interface UseHlsReturn {
765
+ /** Whether hls.js is being used (false = native HLS on Safari) */
766
+ isHlsJs: boolean;
767
+ /** Whether the video has buffered enough data to play without black flash */
768
+ isReady: boolean;
769
+ /** Destroy the HLS instance manually (also called automatically on unmount) */
770
+ destroy: () => void;
771
+ }
772
+ declare function useHls(options: UseHlsOptions): UseHlsReturn;
773
+
774
+ interface VideoSlotProps {
775
+ item: ContentItem;
776
+ index: number;
777
+ isActive: boolean;
778
+ isPrefetch: boolean;
779
+ isPreloaded: boolean;
780
+ bufferTier: BufferTier;
781
+ isMuted: boolean;
782
+ onToggleMute: () => void;
783
+ showFps?: boolean;
784
+ renderOverlay?: (item: ContentItem, actions: SlotActions) => ReactNode;
785
+ renderActions?: (item: ContentItem, actions: SlotActions) => ReactNode;
786
+ }
787
+ declare function VideoSlot({ item, index, isActive, isPrefetch, isPreloaded, bufferTier, isMuted, onToggleMute, showFps, renderOverlay, renderActions, }: VideoSlotProps): react_jsx_runtime.JSX.Element;
788
+
789
+ declare function DefaultOverlay({ item }: {
790
+ item: ContentItem;
791
+ }): react_jsx_runtime.JSX.Element;
792
+
793
+ declare function DefaultActions({ item, actions }: {
794
+ item: ContentItem;
795
+ actions: SlotActions;
796
+ }): react_jsx_runtime.JSX.Element;
797
+
798
+ /**
799
+ * DefaultSkeleton — Default loading skeleton with shimmer animation
800
+ *
801
+ * Used when host app does not provide renderLoading prop.
802
+ */
803
+ declare function DefaultSkeleton(): react_jsx_runtime.JSX.Element;
804
+
805
+ declare function useFeedSelector<T>(selector: (state: FeedState) => T): T;
806
+ declare function useFeed(): {
807
+ items: NonNullable<ContentItem | undefined>[];
808
+ loading: boolean;
809
+ loadingMore: boolean;
810
+ hasMore: boolean;
811
+ error: FeedError | null;
812
+ isStale: boolean;
813
+ loadInitial: () => Promise<void>;
814
+ loadMore: () => Promise<void>;
815
+ refresh: () => Promise<void>;
816
+ };
817
+
818
+ declare function usePlayerSelector<T>(selector: (state: PlayerState) => T): T;
819
+ declare function usePlayer(): {
820
+ status: PlayerStatus;
821
+ currentVideo: VideoItem | null;
822
+ currentTime: number;
823
+ duration: number;
824
+ buffered: number;
825
+ muted: boolean;
826
+ volume: number;
827
+ playbackRate: number;
828
+ error: PlayerError | null;
829
+ loopCount: number;
830
+ watchTime: number;
831
+ isPlaying: boolean;
832
+ isPaused: boolean;
833
+ isBuffering: boolean;
834
+ isLoading: boolean;
835
+ hasError: boolean;
836
+ progress: number;
837
+ bufferProgress: number;
838
+ play: () => boolean;
839
+ pause: () => boolean;
840
+ togglePlay: () => boolean;
841
+ seek: (t: number) => boolean;
842
+ setMuted: (m: boolean) => void;
843
+ toggleMute: () => void;
844
+ setVolume: (v: number) => void;
845
+ setPlaybackRate: (r: number) => void;
846
+ handlers: {
847
+ onCanPlay: () => void;
848
+ onWaiting: () => boolean;
849
+ onPlaying: () => boolean;
850
+ onEnded: () => void;
851
+ onTimeUpdate: (e: React.SyntheticEvent<HTMLVideoElement>) => void;
852
+ onProgress: (e: React.SyntheticEvent<HTMLVideoElement>) => void;
853
+ onLoadedMetadata: (e: React.SyntheticEvent<HTMLVideoElement>) => void;
854
+ onError: () => void;
855
+ };
856
+ };
857
+
858
+ declare function useResourceSelector<T>(selector: (state: ResourceState) => T): T;
859
+ declare function useResource(): {
860
+ activeIndices: number[];
861
+ warmIndices: number[];
862
+ focusedIndex: number;
863
+ totalItems: number;
864
+ networkType: NetworkType;
865
+ isActive: boolean;
866
+ prefetchIndex: number | null;
867
+ setFocusedIndex: (i: number) => void;
868
+ setFocusedIndexImmediate: (i: number) => void;
869
+ setTotalItems: (n: number) => void;
870
+ shouldRenderVideo: (i: number) => boolean;
871
+ isAllocated: (i: number) => boolean;
872
+ isWarmAllocated: (i: number) => boolean;
873
+ setPrefetchIndex: (i: number | null) => void;
874
+ };
875
+
876
+ /**
877
+ * Mock adapters for development and testing
878
+ */
879
+
880
+ declare class MockLogger implements ILogger {
881
+ private readonly prefix;
882
+ constructor(prefix?: string);
883
+ debug(msg: string, ...args: unknown[]): void;
884
+ info(msg: string, ...args: unknown[]): void;
885
+ warn(msg: string, ...args: unknown[]): void;
886
+ error(msg: string, ...args: unknown[]): void;
887
+ }
888
+ declare class MockAnalytics implements IAnalytics {
889
+ readonly events: unknown[];
890
+ private log;
891
+ trackView(videoId: string, duration: number): void;
892
+ trackLike(videoId: string, isLiked: boolean): void;
893
+ trackShare(videoId: string): void;
894
+ trackComment(videoId: string): void;
895
+ trackError(videoId: string, error: string): void;
896
+ trackPlaybackEvent(videoId: string, event: string, position?: number): void;
897
+ }
898
+ declare class MockInteraction implements IInteraction {
899
+ readonly calls: string[];
900
+ private delay;
901
+ constructor(delayMs?: number);
902
+ private simulate;
903
+ like(id: string): Promise<void>;
904
+ unlike(id: string): Promise<void>;
905
+ follow(id: string): Promise<void>;
906
+ unfollow(id: string): Promise<void>;
907
+ bookmark(id: string): Promise<void>;
908
+ unbookmark(id: string): Promise<void>;
909
+ share(id: string): Promise<void>;
910
+ }
911
+ declare class MockSessionStorage implements ISessionStorage {
912
+ private store;
913
+ get<T>(key: string): T | null;
914
+ set<T>(key: string, value: T): void;
915
+ remove(key: string): void;
916
+ clear(): void;
917
+ }
918
+ declare class MockNetworkAdapter implements INetworkAdapter {
919
+ private type;
920
+ private callbacks;
921
+ getNetworkType(): NetworkType;
922
+ isOnline(): boolean;
923
+ onNetworkChange(cb: (t: NetworkType) => void): () => void;
924
+ /** Test helper: simulate network change */
925
+ simulateChange(type: NetworkType): void;
926
+ }
927
+ declare class MockVideoLoader implements IVideoLoader {
928
+ private preloaded;
929
+ private loading;
930
+ private delayMs;
931
+ constructor(delayMs?: number);
932
+ preload(videoId: string, _url: string, signal?: AbortSignal): Promise<PreloadResult>;
933
+ cancel(videoId: string): void;
934
+ isPreloaded(videoId: string): boolean;
935
+ getPreloadStatus(videoId: string): "idle" | "loading" | "loaded";
936
+ clearAll(): void;
937
+ }
938
+ declare class MockDataSource implements IDataSource {
939
+ private totalItems;
940
+ private pageSize;
941
+ private delayMs;
942
+ constructor(options?: {
943
+ totalItems?: number;
944
+ pageSize?: number;
945
+ delayMs?: number;
946
+ });
947
+ fetchFeed(cursor?: string | null): Promise<{
948
+ items: {
949
+ id: string;
950
+ type: "video";
951
+ source: {
952
+ url: string;
953
+ type: "mp4";
954
+ };
955
+ poster: string;
956
+ duration: number;
957
+ title: string;
958
+ description: string;
959
+ author: {
960
+ id: string;
961
+ name: string;
962
+ avatar: string;
963
+ isVerified: boolean;
964
+ };
965
+ stats: {
966
+ likes: number;
967
+ comments: number;
968
+ shares: number;
969
+ views: number;
970
+ };
971
+ interaction: {
972
+ isLiked: boolean;
973
+ isBookmarked: boolean;
974
+ isFollowing: boolean;
975
+ };
976
+ }[];
977
+ nextCursor: string | null;
978
+ hasMore: boolean;
979
+ }>;
980
+ }
981
+ declare class MockCommentAdapter implements ICommentAdapter {
982
+ fetchComments(contentId: string, cursor?: string | null): Promise<{
983
+ items: {
984
+ id: string;
985
+ contentId: string;
986
+ authorId: string;
987
+ authorName: string;
988
+ text: string;
989
+ createdAt: number;
990
+ likeCount: number;
991
+ isLiked: boolean;
992
+ }[];
993
+ nextCursor: string | null;
994
+ hasMore: boolean;
995
+ total: number;
996
+ }>;
997
+ postComment(contentId: string, text: string): Promise<{
998
+ id: string;
999
+ contentId: string;
1000
+ authorId: string;
1001
+ authorName: string;
1002
+ text: string;
1003
+ createdAt: number;
1004
+ likeCount: number;
1005
+ isLiked: boolean;
1006
+ }>;
1007
+ deleteComment(_id: string): Promise<void>;
1008
+ likeComment(_id: string): Promise<void>;
1009
+ unlikeComment(_id: string): Promise<void>;
1010
+ }
1011
+
1012
+ /**
1013
+ * HttpDataSource — IDataSource implementation for REST APIs
1014
+ *
1015
+ * Maps API responses (snake_case) → SDK ContentItem (camelCase).
1016
+ * Targets: GET /api/v1/reels?api_key=<key>&limit=<n>&cursor=<cursor>
1017
+ *
1018
+ * Response shape:
1019
+ * { code, data: { reels[], total, number_of_items, has_next, next_cursor }, success }
1020
+ *
1021
+ * Usage:
1022
+ * const ds = new HttpDataSource({
1023
+ * baseUrl: 'https://gw-stg-messages.blocktrend.xyz/api/v1',
1024
+ * apiKey: 'yaah',
1025
+ * });
1026
+ */
1027
+
1028
+ interface HttpDataSourceConfig {
1029
+ /** Base URL of the API, e.g. https://gw-stg-messages.blocktrend.xyz/api/v1 */
1030
+ baseUrl: string;
1031
+ /**
1032
+ * API key sent as `?api_key=<value>` query param.
1033
+ * Takes precedence over `getAccessToken` for this backend.
1034
+ */
1035
+ apiKey?: string;
1036
+ /** Returns Bearer token (sync or async). Return null/undefined to skip. */
1037
+ getAccessToken?: () => string | null | undefined | Promise<string | null | undefined>;
1038
+ /** Feed list endpoint path (default: '/reels') */
1039
+ feedPath?: string;
1040
+ /** Query param name for cursor (default: 'cursor') */
1041
+ cursorParam?: string;
1042
+ /** Query param name for page size (default: 'limit') */
1043
+ limitParam?: string;
1044
+ /** Number of items per page (default: 10) */
1045
+ pageSize?: number;
1046
+ /** Extra static headers to include in every request */
1047
+ headers?: Record<string, string>;
1048
+ /** Request timeout in ms (default: 10000) */
1049
+ timeoutMs?: number;
1050
+ /** Logger adapter */
1051
+ logger?: ILogger;
1052
+ }
1053
+ declare class HttpDataSource implements IDataSource {
1054
+ private readonly baseUrl;
1055
+ private readonly apiKey;
1056
+ private readonly getAccessToken;
1057
+ private readonly feedPath;
1058
+ private readonly cursorParam;
1059
+ private readonly limitParam;
1060
+ private readonly pageSize;
1061
+ private readonly extraHeaders;
1062
+ private readonly timeoutMs;
1063
+ private readonly logger?;
1064
+ constructor(config: HttpDataSourceConfig);
1065
+ fetchFeed(cursor?: string | null): Promise<FeedPage>;
1066
+ private fetch;
1067
+ private buildHeaders;
1068
+ }
1069
+ declare class HttpError extends Error {
1070
+ readonly status: number;
1071
+ readonly body?: string | undefined;
1072
+ constructor(status: number, message: string, body?: string | undefined);
1073
+ }
1074
+
1075
+ export { type Article, type ArticleImage, type Author, type BufferTier, type CommentItem, type CommentPage, type ContentItem, type ContentStats, DEFAULT_FEED_CONFIG, DEFAULT_PLAYER_CONFIG, DEFAULT_RESOURCE_CONFIG, DefaultActions, DefaultOverlay, DefaultSkeleton, type FeedConfig, type FeedError, FeedManager, type FeedPage, type FeedState, HttpDataSource, type HttpDataSourceConfig, HttpError, type IAnalytics, type ICommentAdapter, type IDataSource, type IInteraction, type ILogger, type INetworkAdapter, type ISessionStorage, type IVideoLoader, type InteractionState, type LogLevel, MockAnalytics, MockCommentAdapter, MockDataSource, MockInteraction, MockLogger, MockNetworkAdapter, MockSessionStorage, MockVideoLoader, type NetworkType, OptimisticManager, type PlayerConfig, PlayerEngine, type PlayerError, type PlayerEvent, type PlayerEventListener, type PlayerState, PlayerStatus, type PointerGestureConfig, type PreloadResult, type PreloadStatus, ReelsFeed, type ReelsFeedProps, ReelsProvider, type ReelsProviderProps, type ResourceConfig, ResourceGovernor, type ResourceState, type SDKAdapters, type SDKContextValue, type SlotActions, type SnapTarget, type UseHlsOptions, type UseHlsReturn, VALID_TRANSITIONS, type VideoItem, type VideoQuality, VideoSlot, type VideoSource, canPause, canPlay, canSeek, isArticle, isValidTransition, isVideoItem, useFeed, useFeedSelector, useHls, usePlayer, usePlayerSelector, usePointerGesture, useResource, useResourceSelector, useSDK, useSnapAnimation };