react-helios 2.0.1 → 2.1.1

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # react-helios
2
2
 
3
- Production-grade React video player with HLS streaming, adaptive quality selection, live stream support, subtitle tracks, thumbnail preview, Picture-in-Picture, and full keyboard control.
3
+ Production-grade React video player with HLS streaming, adaptive quality selection, live stream support, subtitle tracks, VTT sprite sheet thumbnail preview, Picture-in-Picture, and full keyboard control.
4
4
 
5
5
  ## Installation
6
6
 
@@ -47,6 +47,42 @@ Pass any `.m3u8` URL — HLS.js is initialised automatically:
47
47
 
48
48
  On Safari the browser's native HLS engine is used. A **LIVE** badge and **GO LIVE** button appear automatically for live streams.
49
49
 
50
+ ## Thumbnail Preview
51
+
52
+ Hover over the progress bar to see a time tooltip. For rich sprite-sheet thumbnails, pass a `thumbnailVtt` URL pointing to a [WebVTT thumbnail file](https://developer.bitmovin.com/playback/docs/webvtt-based-thumbnails).
53
+
54
+ ```tsx
55
+ <VideoPlayer
56
+ src="https://example.com/video.mp4"
57
+ thumbnailVtt="https://example.com/thumbs/storyboard.vtt"
58
+ />
59
+ ```
60
+
61
+ ### VTT format
62
+
63
+ Each cue in the `.vtt` file maps a time range to a rectangular region inside a sprite image using the `#xywh=x,y,w,h` fragment:
64
+
65
+ ```
66
+ WEBVTT
67
+
68
+ 00:00:00.000 --> 00:00:05.000
69
+ https://example.com/thumbs/sprite.jpg#xywh=0,0,160,90
70
+
71
+ 00:00:05.000 --> 00:00:10.000
72
+ https://example.com/thumbs/sprite.jpg#xywh=160,0,160,90
73
+
74
+ 00:00:10.000 --> 00:00:15.000
75
+ https://example.com/thumbs/sprite.jpg#xywh=320,0,160,90
76
+ ```
77
+
78
+ The player fetches the VTT file once, parses all cues, and uses CSS `background-position` to display the correct sprite cell during hover — **no additional network requests per hover**.
79
+
80
+ To disable the preview entirely:
81
+
82
+ ```tsx
83
+ <VideoPlayer src="..." enablePreview={false} />
84
+ ```
85
+
50
86
  ## Props
51
87
 
52
88
  | Prop | Type | Default | Description |
@@ -60,7 +96,8 @@ On Safari the browser's native HLS engine is used. A **LIVE** badge and **GO LIV
60
96
  | `preload` | `"none" \| "metadata" \| "auto"` | `"metadata"` | Native `preload` attribute |
61
97
  | `playbackRates` | `PlaybackRate[]` | `[0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2]` | Available speed options |
62
98
  | `enableHLS` | `boolean` | `true` | Enable HLS.js for `.m3u8` sources |
63
- | `enablePreview` | `boolean` | `true` | Show thumbnail preview on progress bar hover (disabled automatically for HLS) |
99
+ | `enablePreview` | `boolean` | `true` | Show thumbnail / time tooltip on progress bar hover |
100
+ | `thumbnailVtt` | `string` | — | URL to a WebVTT sprite sheet file for rich thumbnail preview |
64
101
  | `hlsConfig` | `Partial<HlsConfig>` | — | Override any [hls.js configuration](https://github.com/video-dev/hls.js/blob/master/docs/API.md#fine-tuning) option |
65
102
  | `subtitles` | `SubtitleTrack[]` | — | Subtitle / caption tracks |
66
103
  | `crossOrigin` | `"anonymous" \| "use-credentials"` | — | CORS attribute for the video element |
@@ -165,7 +202,11 @@ import type {
165
202
  BufferedRange,
166
203
  VideoError,
167
204
  VideoErrorCode,
205
+ ThumbnailCue,
168
206
  } from "react-helios";
207
+
208
+ // VTT utilities (useful for server-side pre-parsing or custom UIs)
209
+ import { parseThumbnailVtt, findThumbnailCue } from "react-helios";
169
210
  ```
170
211
 
171
212
  ### `PlayerState`
@@ -207,14 +248,37 @@ interface VideoError {
207
248
  }
208
249
  ```
209
250
 
251
+ ### `ThumbnailCue`
252
+
253
+ ```ts
254
+ interface ThumbnailCue {
255
+ start: number; // seconds
256
+ end: number; // seconds
257
+ url: string; // absolute URL to the sprite image
258
+ x: number; // pixel offset within sprite
259
+ y: number;
260
+ w: number; // cell width in pixels
261
+ h: number; // cell height in pixels
262
+ }
263
+ ```
264
+
265
+ ## Performance
266
+
267
+ The player is architected to produce **zero React re-renders during playback**:
268
+
269
+ - `timeupdate` and `progress` events are handled by direct DOM mutation (refs), not React state.
270
+ - `ProgressBar` and `TimeDisplay` self-subscribe to the video element — the parent tree never re-renders on seek or time change.
271
+ - VTT sprite thumbnails are looked up via binary search (O(log n)) and rendered via CSS `background-position` — no hidden `<video>` element, no canvas, no network requests per hover.
272
+ - Buffered ranges are the only state that triggers a re-render (fires every few seconds during buffering, not 60× per second).
273
+
210
274
  ## Project Structure
211
275
 
212
276
  ```
213
- react-video-player/
277
+ react-helios/
214
278
  ├── src/ # Library source
215
279
  │ ├── components/ # VideoPlayer, Controls, control elements
216
280
  │ ├── hooks/ # useVideoPlayer (state + HLS init)
217
- │ ├── lib/ # Types, HLS utilities, format helpers
281
+ │ ├── lib/ # Types, HLS utilities, VTT parser, format helpers
218
282
  │ └── styles/ # CSS
219
283
  ├── examples/
220
284
  │ └── nextjs-demo/ # Standalone Next.js demo app
package/dist/index.d.mts CHANGED
@@ -11,12 +11,12 @@ interface VideoError {
11
11
  code: VideoErrorCode;
12
12
  message: string;
13
13
  }
14
+ /** Display name e.g. "1080p", "720p", "Auto" */
14
15
  interface HLSQualityLevel {
15
16
  id: number;
16
17
  height: number;
17
18
  width: number;
18
19
  bitrate: number;
19
- /** Display name e.g. "1080p", "720p", "Auto" */
20
20
  name: string;
21
21
  }
22
22
  interface SubtitleTrack {
@@ -38,11 +38,9 @@ interface PlayerState {
38
38
  error: VideoError | null;
39
39
  isFullscreen: boolean;
40
40
  isPictureInPicture: boolean;
41
- /** True when the stream is a live HLS stream (Infinity duration) */
41
+ isTheaterMode: boolean;
42
42
  isLive: boolean;
43
- /** Available HLS quality levels; empty for non-HLS sources */
44
43
  qualityLevels: HLSQualityLevel[];
45
- /** Currently active quality level id; -1 = ABR auto */
46
44
  currentQualityLevel: number;
47
45
  }
48
46
  type PlaybackRate = 0.25 | 0.5 | 0.75 | 1 | 1.25 | 1.5 | 1.75 | 2;
@@ -51,15 +49,13 @@ interface VideoPlayerRef {
51
49
  pause: () => void;
52
50
  seek: (time: number) => void;
53
51
  setVolume: (volume: number) => void;
54
- /** Toggle mute while remembering the pre-mute volume */
55
52
  toggleMute: () => void;
56
53
  setPlaybackRate: (rate: PlaybackRate) => void;
57
- /** Set HLS quality level; pass -1 for automatic ABR */
58
54
  setQualityLevel: (level: number) => void;
59
- /** Jump to the live edge of an HLS live stream */
60
55
  seekToLive: () => void;
61
56
  toggleFullscreen: () => Promise<void>;
62
57
  togglePictureInPicture: () => Promise<void>;
58
+ toggleTheaterMode: () => void;
63
59
  getState: () => PlayerState;
64
60
  getVideoElement: () => HTMLVideoElement | null;
65
61
  }
@@ -75,11 +71,26 @@ interface VideoPlayerProps {
75
71
  className?: string;
76
72
  enableHLS?: boolean;
77
73
  enablePreview?: boolean;
78
- /** Additional hls.js configuration options */
74
+ /**
75
+ * URL to a WebVTT thumbnail track for sprite-sheet preview on the progress bar.
76
+ *
77
+ * The VTT file should map time ranges to sprite-sheet coordinates using the
78
+ * standard `#xywh=x,y,w,h` fragment format:
79
+ *
80
+ * ```
81
+ * WEBVTT
82
+ *
83
+ * 00:00:00.000 --> 00:00:05.000
84
+ * https://cdn.example.com/thumbs/storyboard0.jpg#xywh=0,0,160,90
85
+ * ```
86
+ *
87
+ * When provided, hovering the progress bar shows a thumbnail instead of
88
+ * requiring a second video decode. If omitted, only the timestamp tooltip
89
+ * is shown.
90
+ */
91
+ thumbnailVtt?: string;
79
92
  hlsConfig?: Partial<HlsConfig>;
80
- /** Subtitle / caption tracks */
81
93
  subtitles?: SubtitleTrack[];
82
- /** crossorigin attribute for CORS-enabled preview thumbnails */
83
94
  crossOrigin?: "anonymous" | "use-credentials";
84
95
  onPlay?: () => void;
85
96
  onPause?: () => void;
@@ -88,46 +99,39 @@ interface VideoPlayerProps {
88
99
  onTimeUpdate?: (time: number) => void;
89
100
  onDurationChange?: (duration: number) => void;
90
101
  onBuffering?: (isBuffering: boolean) => void;
102
+ onTheaterModeChange?: (isTheater: boolean) => void;
91
103
  }
92
104
 
93
105
  declare const VideoPlayer: react__default.ForwardRefExoticComponent<VideoPlayerProps & react__default.RefAttributes<VideoPlayerRef>>;
94
106
 
95
107
  interface ControlsProps {
108
+ videoRef: react__default.RefObject<HTMLVideoElement | null>;
96
109
  playerRef: VideoPlayerRef;
97
- /** Ref to the outer player container; used to scope keyboard shortcuts to the focused player */
98
110
  playerContainerRef: react__default.RefObject<HTMLElement | null>;
99
111
  playbackRates: PlaybackRate[];
100
112
  enablePreview: boolean;
113
+ thumbnailVtt?: string;
101
114
  isPlaying: boolean;
102
- currentTime: number;
103
- duration: number;
104
115
  volume: number;
105
116
  isMuted: boolean;
106
117
  playbackRate: number;
107
118
  isFullscreen: boolean;
108
119
  isPictureInPicture: boolean;
120
+ isTheaterMode: boolean;
109
121
  isLive: boolean;
110
122
  qualityLevels: HLSQualityLevel[];
111
123
  currentQualityLevel: number;
112
- bufferedRanges: BufferedRange[];
113
124
  }
114
- /**
115
- * Controls is intentionally NOT wrapped in React.memo here – it receives
116
- * currentTime which changes every tick, so memo wouldn't help at this level.
117
- * Instead, all its CHILDREN are memoized so they skip renders when their
118
- * specific props haven't changed.
119
- */
120
125
  declare const Controls: react__default.FC<ControlsProps>;
121
126
 
122
127
  interface TimeDisplayProps {
123
- currentTime: number;
124
- duration: number;
128
+ videoRef: React.RefObject<HTMLVideoElement | null>;
125
129
  isLive?: boolean;
126
130
  }
127
131
  /**
128
- * TimeDisplay re-renders every timeupdate tick (currentTime changes).
129
- * Wrapped in memo anyway so that if its specific props haven't changed
130
- * (e.g. when only volume changes) it skips the render.
132
+ * TimeDisplay subscribes directly to the video element's timeupdate and
133
+ * durationchange events, updating the DOM via refs. It never re-renders
134
+ * during playback only when isLive changes (once per source change).
131
135
  */
132
136
  declare const TimeDisplay: react.NamedExoticComponent<TimeDisplayProps>;
133
137
 
@@ -139,22 +143,13 @@ interface SettingsMenuProps {
139
143
  currentQualityLevel?: number;
140
144
  onQualityChange?: (level: number) => void;
141
145
  }
142
- /**
143
- * SettingsMenu wrapped in React.memo.
144
- * playbackRate and qualityLevel rarely change → this component skips
145
- * almost all re-renders during normal playback.
146
- *
147
- * sortedLevels is memoized so the .sort() only runs when qualityLevels
148
- * actually changes (usually once after manifest is parsed).
149
- */
150
146
  declare const SettingsMenu: react.NamedExoticComponent<SettingsMenuProps>;
151
147
 
152
148
  interface ProgressBarProps {
149
+ videoRef: react__default.RefObject<HTMLVideoElement | null>;
153
150
  playerRef: VideoPlayerRef;
154
- currentTime: number;
155
- duration: number;
156
- bufferedRanges: BufferedRange[];
157
151
  enablePreview?: boolean;
152
+ thumbnailVtt?: string;
158
153
  }
159
154
  declare const ProgressBar: react__default.FC<ProgressBarProps>;
160
155
 
@@ -164,11 +159,6 @@ interface VolumeControlProps {
164
159
  onVolumeChange: (volume: number) => void;
165
160
  onToggleMute: () => void;
166
161
  }
167
- /**
168
- * VolumeControl wrapped in React.memo.
169
- * volume and isMuted don't change during timeupdate → 0 re-renders during
170
- * normal playback when the user isn't touching volume controls.
171
- */
172
162
  declare const VolumeControl: react.NamedExoticComponent<VolumeControlProps>;
173
163
 
174
164
  interface PlayButtonProps {
@@ -185,22 +175,22 @@ interface PiPButtonProps {
185
175
  onClick: () => void;
186
176
  isPiP?: boolean;
187
177
  }
188
- /**
189
- * All button components are wrapped in React.memo.
190
- * They receive stable callback refs (useCallback in Controls), so they
191
- * only re-render when their specific props (isFullscreen, isPiP, etc.)
192
- * actually change – not on every timeupdate tick.
193
- */
194
178
  declare const PlayButton: react.NamedExoticComponent<PlayButtonProps>;
195
179
  declare const PauseButton: react.NamedExoticComponent<PauseButtonProps>;
196
180
  declare const FullscreenButton: react.NamedExoticComponent<FullscreenButtonProps>;
197
181
  declare const PiPButton: react.NamedExoticComponent<PiPButtonProps>;
182
+ interface TheaterButtonProps {
183
+ onClick: () => void;
184
+ isTheater?: boolean;
185
+ }
186
+ declare const TheaterButton: react.NamedExoticComponent<TheaterButtonProps>;
198
187
 
199
188
  declare const ControlElements: {
200
189
  PlayButton: react.NamedExoticComponent<PlayButtonProps>;
201
190
  PauseButton: react.NamedExoticComponent<PauseButtonProps>;
202
191
  FullscreenButton: react.NamedExoticComponent<FullscreenButtonProps>;
203
192
  PiPButton: react.NamedExoticComponent<PiPButtonProps>;
193
+ TheaterButton: react.NamedExoticComponent<TheaterButtonProps>;
204
194
  VolumeControl: react.NamedExoticComponent<VolumeControlProps>;
205
195
  ProgressBar: react.FC<ProgressBarProps>;
206
196
  SettingsMenu: react.NamedExoticComponent<SettingsMenuProps>;
@@ -220,12 +210,14 @@ declare const index_ProgressBar: typeof ProgressBar;
220
210
  type index_ProgressBarProps = ProgressBarProps;
221
211
  declare const index_SettingsMenu: typeof SettingsMenu;
222
212
  type index_SettingsMenuProps = SettingsMenuProps;
213
+ declare const index_TheaterButton: typeof TheaterButton;
214
+ type index_TheaterButtonProps = TheaterButtonProps;
223
215
  declare const index_TimeDisplay: typeof TimeDisplay;
224
216
  type index_TimeDisplayProps = TimeDisplayProps;
225
217
  declare const index_VolumeControl: typeof VolumeControl;
226
218
  type index_VolumeControlProps = VolumeControlProps;
227
219
  declare namespace index {
228
- export { index_ControlElements as ControlElements, index_FullscreenButton as FullscreenButton, type index_FullscreenButtonProps as FullscreenButtonProps, index_PauseButton as PauseButton, type index_PauseButtonProps as PauseButtonProps, index_PiPButton as PiPButton, type index_PiPButtonProps as PiPButtonProps, index_PlayButton as PlayButton, type index_PlayButtonProps as PlayButtonProps, index_ProgressBar as ProgressBar, type index_ProgressBarProps as ProgressBarProps, index_SettingsMenu as SettingsMenu, type index_SettingsMenuProps as SettingsMenuProps, index_TimeDisplay as TimeDisplay, type index_TimeDisplayProps as TimeDisplayProps, index_VolumeControl as VolumeControl, type index_VolumeControlProps as VolumeControlProps };
220
+ export { index_ControlElements as ControlElements, index_FullscreenButton as FullscreenButton, type index_FullscreenButtonProps as FullscreenButtonProps, index_PauseButton as PauseButton, type index_PauseButtonProps as PauseButtonProps, index_PiPButton as PiPButton, type index_PiPButtonProps as PiPButtonProps, index_PlayButton as PlayButton, type index_PlayButtonProps as PlayButtonProps, index_ProgressBar as ProgressBar, type index_ProgressBarProps as ProgressBarProps, index_SettingsMenu as SettingsMenu, type index_SettingsMenuProps as SettingsMenuProps, index_TheaterButton as TheaterButton, type index_TheaterButtonProps as TheaterButtonProps, index_TimeDisplay as TimeDisplay, type index_TimeDisplayProps as TimeDisplayProps, index_VolumeControl as VolumeControl, type index_VolumeControlProps as VolumeControlProps };
229
221
  }
230
222
 
231
223
  /**
@@ -241,4 +233,35 @@ declare function isHLSUrl(url: string): boolean;
241
233
  */
242
234
  declare function getMimeType(url: string): string;
243
235
 
244
- export { type BufferedRange, index as ControlElements, Controls, type HLSQualityLevel, type PlaybackRate, type PlayerState, type SubtitleTrack, type VideoError, type VideoErrorCode, VideoPlayer, type VideoPlayerProps, type VideoPlayerRef, formatTime, getMimeType, isHLSUrl };
236
+ interface ThumbnailCue {
237
+ start: number;
238
+ end: number;
239
+ /** Absolute URL to the sprite image */
240
+ url: string;
241
+ /** Pixel offset from the left of the sprite sheet */
242
+ x: number;
243
+ /** Pixel offset from the top of the sprite sheet */
244
+ y: number;
245
+ /** Width of a single thumbnail cell */
246
+ w: number;
247
+ /** Height of a single thumbnail cell */
248
+ h: number;
249
+ }
250
+ /**
251
+ * Parse a WebVTT thumbnail track into an array of cues.
252
+ *
253
+ * Supports the standard sprite-sheet format:
254
+ * 00:00:00.000 --> 00:00:05.000
255
+ * https://cdn.example.com/thumbs/s0.jpg#xywh=0,0,160,90
256
+ *
257
+ * @param text Raw VTT file text
258
+ * @param baseUrl VTT file URL — used to resolve relative image paths
259
+ */
260
+ declare function parseThumbnailVtt(text: string, baseUrl?: string): ThumbnailCue[];
261
+ /**
262
+ * Binary-search for the cue that covers `time` (seconds).
263
+ * Returns null if no cue covers that timestamp.
264
+ */
265
+ declare function findThumbnailCue(cues: ThumbnailCue[], time: number): ThumbnailCue | null;
266
+
267
+ export { type BufferedRange, index as ControlElements, Controls, type HLSQualityLevel, type PlaybackRate, type PlayerState, type SubtitleTrack, type ThumbnailCue, type VideoError, type VideoErrorCode, VideoPlayer, type VideoPlayerProps, type VideoPlayerRef, findThumbnailCue, formatTime, getMimeType, isHLSUrl, parseThumbnailVtt };
package/dist/index.d.ts CHANGED
@@ -11,12 +11,12 @@ interface VideoError {
11
11
  code: VideoErrorCode;
12
12
  message: string;
13
13
  }
14
+ /** Display name e.g. "1080p", "720p", "Auto" */
14
15
  interface HLSQualityLevel {
15
16
  id: number;
16
17
  height: number;
17
18
  width: number;
18
19
  bitrate: number;
19
- /** Display name e.g. "1080p", "720p", "Auto" */
20
20
  name: string;
21
21
  }
22
22
  interface SubtitleTrack {
@@ -38,11 +38,9 @@ interface PlayerState {
38
38
  error: VideoError | null;
39
39
  isFullscreen: boolean;
40
40
  isPictureInPicture: boolean;
41
- /** True when the stream is a live HLS stream (Infinity duration) */
41
+ isTheaterMode: boolean;
42
42
  isLive: boolean;
43
- /** Available HLS quality levels; empty for non-HLS sources */
44
43
  qualityLevels: HLSQualityLevel[];
45
- /** Currently active quality level id; -1 = ABR auto */
46
44
  currentQualityLevel: number;
47
45
  }
48
46
  type PlaybackRate = 0.25 | 0.5 | 0.75 | 1 | 1.25 | 1.5 | 1.75 | 2;
@@ -51,15 +49,13 @@ interface VideoPlayerRef {
51
49
  pause: () => void;
52
50
  seek: (time: number) => void;
53
51
  setVolume: (volume: number) => void;
54
- /** Toggle mute while remembering the pre-mute volume */
55
52
  toggleMute: () => void;
56
53
  setPlaybackRate: (rate: PlaybackRate) => void;
57
- /** Set HLS quality level; pass -1 for automatic ABR */
58
54
  setQualityLevel: (level: number) => void;
59
- /** Jump to the live edge of an HLS live stream */
60
55
  seekToLive: () => void;
61
56
  toggleFullscreen: () => Promise<void>;
62
57
  togglePictureInPicture: () => Promise<void>;
58
+ toggleTheaterMode: () => void;
63
59
  getState: () => PlayerState;
64
60
  getVideoElement: () => HTMLVideoElement | null;
65
61
  }
@@ -75,11 +71,26 @@ interface VideoPlayerProps {
75
71
  className?: string;
76
72
  enableHLS?: boolean;
77
73
  enablePreview?: boolean;
78
- /** Additional hls.js configuration options */
74
+ /**
75
+ * URL to a WebVTT thumbnail track for sprite-sheet preview on the progress bar.
76
+ *
77
+ * The VTT file should map time ranges to sprite-sheet coordinates using the
78
+ * standard `#xywh=x,y,w,h` fragment format:
79
+ *
80
+ * ```
81
+ * WEBVTT
82
+ *
83
+ * 00:00:00.000 --> 00:00:05.000
84
+ * https://cdn.example.com/thumbs/storyboard0.jpg#xywh=0,0,160,90
85
+ * ```
86
+ *
87
+ * When provided, hovering the progress bar shows a thumbnail instead of
88
+ * requiring a second video decode. If omitted, only the timestamp tooltip
89
+ * is shown.
90
+ */
91
+ thumbnailVtt?: string;
79
92
  hlsConfig?: Partial<HlsConfig>;
80
- /** Subtitle / caption tracks */
81
93
  subtitles?: SubtitleTrack[];
82
- /** crossorigin attribute for CORS-enabled preview thumbnails */
83
94
  crossOrigin?: "anonymous" | "use-credentials";
84
95
  onPlay?: () => void;
85
96
  onPause?: () => void;
@@ -88,46 +99,39 @@ interface VideoPlayerProps {
88
99
  onTimeUpdate?: (time: number) => void;
89
100
  onDurationChange?: (duration: number) => void;
90
101
  onBuffering?: (isBuffering: boolean) => void;
102
+ onTheaterModeChange?: (isTheater: boolean) => void;
91
103
  }
92
104
 
93
105
  declare const VideoPlayer: react__default.ForwardRefExoticComponent<VideoPlayerProps & react__default.RefAttributes<VideoPlayerRef>>;
94
106
 
95
107
  interface ControlsProps {
108
+ videoRef: react__default.RefObject<HTMLVideoElement | null>;
96
109
  playerRef: VideoPlayerRef;
97
- /** Ref to the outer player container; used to scope keyboard shortcuts to the focused player */
98
110
  playerContainerRef: react__default.RefObject<HTMLElement | null>;
99
111
  playbackRates: PlaybackRate[];
100
112
  enablePreview: boolean;
113
+ thumbnailVtt?: string;
101
114
  isPlaying: boolean;
102
- currentTime: number;
103
- duration: number;
104
115
  volume: number;
105
116
  isMuted: boolean;
106
117
  playbackRate: number;
107
118
  isFullscreen: boolean;
108
119
  isPictureInPicture: boolean;
120
+ isTheaterMode: boolean;
109
121
  isLive: boolean;
110
122
  qualityLevels: HLSQualityLevel[];
111
123
  currentQualityLevel: number;
112
- bufferedRanges: BufferedRange[];
113
124
  }
114
- /**
115
- * Controls is intentionally NOT wrapped in React.memo here – it receives
116
- * currentTime which changes every tick, so memo wouldn't help at this level.
117
- * Instead, all its CHILDREN are memoized so they skip renders when their
118
- * specific props haven't changed.
119
- */
120
125
  declare const Controls: react__default.FC<ControlsProps>;
121
126
 
122
127
  interface TimeDisplayProps {
123
- currentTime: number;
124
- duration: number;
128
+ videoRef: React.RefObject<HTMLVideoElement | null>;
125
129
  isLive?: boolean;
126
130
  }
127
131
  /**
128
- * TimeDisplay re-renders every timeupdate tick (currentTime changes).
129
- * Wrapped in memo anyway so that if its specific props haven't changed
130
- * (e.g. when only volume changes) it skips the render.
132
+ * TimeDisplay subscribes directly to the video element's timeupdate and
133
+ * durationchange events, updating the DOM via refs. It never re-renders
134
+ * during playback only when isLive changes (once per source change).
131
135
  */
132
136
  declare const TimeDisplay: react.NamedExoticComponent<TimeDisplayProps>;
133
137
 
@@ -139,22 +143,13 @@ interface SettingsMenuProps {
139
143
  currentQualityLevel?: number;
140
144
  onQualityChange?: (level: number) => void;
141
145
  }
142
- /**
143
- * SettingsMenu wrapped in React.memo.
144
- * playbackRate and qualityLevel rarely change → this component skips
145
- * almost all re-renders during normal playback.
146
- *
147
- * sortedLevels is memoized so the .sort() only runs when qualityLevels
148
- * actually changes (usually once after manifest is parsed).
149
- */
150
146
  declare const SettingsMenu: react.NamedExoticComponent<SettingsMenuProps>;
151
147
 
152
148
  interface ProgressBarProps {
149
+ videoRef: react__default.RefObject<HTMLVideoElement | null>;
153
150
  playerRef: VideoPlayerRef;
154
- currentTime: number;
155
- duration: number;
156
- bufferedRanges: BufferedRange[];
157
151
  enablePreview?: boolean;
152
+ thumbnailVtt?: string;
158
153
  }
159
154
  declare const ProgressBar: react__default.FC<ProgressBarProps>;
160
155
 
@@ -164,11 +159,6 @@ interface VolumeControlProps {
164
159
  onVolumeChange: (volume: number) => void;
165
160
  onToggleMute: () => void;
166
161
  }
167
- /**
168
- * VolumeControl wrapped in React.memo.
169
- * volume and isMuted don't change during timeupdate → 0 re-renders during
170
- * normal playback when the user isn't touching volume controls.
171
- */
172
162
  declare const VolumeControl: react.NamedExoticComponent<VolumeControlProps>;
173
163
 
174
164
  interface PlayButtonProps {
@@ -185,22 +175,22 @@ interface PiPButtonProps {
185
175
  onClick: () => void;
186
176
  isPiP?: boolean;
187
177
  }
188
- /**
189
- * All button components are wrapped in React.memo.
190
- * They receive stable callback refs (useCallback in Controls), so they
191
- * only re-render when their specific props (isFullscreen, isPiP, etc.)
192
- * actually change – not on every timeupdate tick.
193
- */
194
178
  declare const PlayButton: react.NamedExoticComponent<PlayButtonProps>;
195
179
  declare const PauseButton: react.NamedExoticComponent<PauseButtonProps>;
196
180
  declare const FullscreenButton: react.NamedExoticComponent<FullscreenButtonProps>;
197
181
  declare const PiPButton: react.NamedExoticComponent<PiPButtonProps>;
182
+ interface TheaterButtonProps {
183
+ onClick: () => void;
184
+ isTheater?: boolean;
185
+ }
186
+ declare const TheaterButton: react.NamedExoticComponent<TheaterButtonProps>;
198
187
 
199
188
  declare const ControlElements: {
200
189
  PlayButton: react.NamedExoticComponent<PlayButtonProps>;
201
190
  PauseButton: react.NamedExoticComponent<PauseButtonProps>;
202
191
  FullscreenButton: react.NamedExoticComponent<FullscreenButtonProps>;
203
192
  PiPButton: react.NamedExoticComponent<PiPButtonProps>;
193
+ TheaterButton: react.NamedExoticComponent<TheaterButtonProps>;
204
194
  VolumeControl: react.NamedExoticComponent<VolumeControlProps>;
205
195
  ProgressBar: react.FC<ProgressBarProps>;
206
196
  SettingsMenu: react.NamedExoticComponent<SettingsMenuProps>;
@@ -220,12 +210,14 @@ declare const index_ProgressBar: typeof ProgressBar;
220
210
  type index_ProgressBarProps = ProgressBarProps;
221
211
  declare const index_SettingsMenu: typeof SettingsMenu;
222
212
  type index_SettingsMenuProps = SettingsMenuProps;
213
+ declare const index_TheaterButton: typeof TheaterButton;
214
+ type index_TheaterButtonProps = TheaterButtonProps;
223
215
  declare const index_TimeDisplay: typeof TimeDisplay;
224
216
  type index_TimeDisplayProps = TimeDisplayProps;
225
217
  declare const index_VolumeControl: typeof VolumeControl;
226
218
  type index_VolumeControlProps = VolumeControlProps;
227
219
  declare namespace index {
228
- export { index_ControlElements as ControlElements, index_FullscreenButton as FullscreenButton, type index_FullscreenButtonProps as FullscreenButtonProps, index_PauseButton as PauseButton, type index_PauseButtonProps as PauseButtonProps, index_PiPButton as PiPButton, type index_PiPButtonProps as PiPButtonProps, index_PlayButton as PlayButton, type index_PlayButtonProps as PlayButtonProps, index_ProgressBar as ProgressBar, type index_ProgressBarProps as ProgressBarProps, index_SettingsMenu as SettingsMenu, type index_SettingsMenuProps as SettingsMenuProps, index_TimeDisplay as TimeDisplay, type index_TimeDisplayProps as TimeDisplayProps, index_VolumeControl as VolumeControl, type index_VolumeControlProps as VolumeControlProps };
220
+ export { index_ControlElements as ControlElements, index_FullscreenButton as FullscreenButton, type index_FullscreenButtonProps as FullscreenButtonProps, index_PauseButton as PauseButton, type index_PauseButtonProps as PauseButtonProps, index_PiPButton as PiPButton, type index_PiPButtonProps as PiPButtonProps, index_PlayButton as PlayButton, type index_PlayButtonProps as PlayButtonProps, index_ProgressBar as ProgressBar, type index_ProgressBarProps as ProgressBarProps, index_SettingsMenu as SettingsMenu, type index_SettingsMenuProps as SettingsMenuProps, index_TheaterButton as TheaterButton, type index_TheaterButtonProps as TheaterButtonProps, index_TimeDisplay as TimeDisplay, type index_TimeDisplayProps as TimeDisplayProps, index_VolumeControl as VolumeControl, type index_VolumeControlProps as VolumeControlProps };
229
221
  }
230
222
 
231
223
  /**
@@ -241,4 +233,35 @@ declare function isHLSUrl(url: string): boolean;
241
233
  */
242
234
  declare function getMimeType(url: string): string;
243
235
 
244
- export { type BufferedRange, index as ControlElements, Controls, type HLSQualityLevel, type PlaybackRate, type PlayerState, type SubtitleTrack, type VideoError, type VideoErrorCode, VideoPlayer, type VideoPlayerProps, type VideoPlayerRef, formatTime, getMimeType, isHLSUrl };
236
+ interface ThumbnailCue {
237
+ start: number;
238
+ end: number;
239
+ /** Absolute URL to the sprite image */
240
+ url: string;
241
+ /** Pixel offset from the left of the sprite sheet */
242
+ x: number;
243
+ /** Pixel offset from the top of the sprite sheet */
244
+ y: number;
245
+ /** Width of a single thumbnail cell */
246
+ w: number;
247
+ /** Height of a single thumbnail cell */
248
+ h: number;
249
+ }
250
+ /**
251
+ * Parse a WebVTT thumbnail track into an array of cues.
252
+ *
253
+ * Supports the standard sprite-sheet format:
254
+ * 00:00:00.000 --> 00:00:05.000
255
+ * https://cdn.example.com/thumbs/s0.jpg#xywh=0,0,160,90
256
+ *
257
+ * @param text Raw VTT file text
258
+ * @param baseUrl VTT file URL — used to resolve relative image paths
259
+ */
260
+ declare function parseThumbnailVtt(text: string, baseUrl?: string): ThumbnailCue[];
261
+ /**
262
+ * Binary-search for the cue that covers `time` (seconds).
263
+ * Returns null if no cue covers that timestamp.
264
+ */
265
+ declare function findThumbnailCue(cues: ThumbnailCue[], time: number): ThumbnailCue | null;
266
+
267
+ export { type BufferedRange, index as ControlElements, Controls, type HLSQualityLevel, type PlaybackRate, type PlayerState, type SubtitleTrack, type ThumbnailCue, type VideoError, type VideoErrorCode, VideoPlayer, type VideoPlayerProps, type VideoPlayerRef, findThumbnailCue, formatTime, getMimeType, isHLSUrl, parseThumbnailVtt };