@livepeer-frameworks/player-react 0.1.0 → 0.1.2

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.
Files changed (187) hide show
  1. package/README.md +7 -9
  2. package/dist/cjs/_virtual/_rollupPluginBabelHelpers.js +359 -0
  3. package/dist/cjs/_virtual/_rollupPluginBabelHelpers.js.map +1 -0
  4. package/dist/cjs/assets/logomark.svg.js +8 -0
  5. package/dist/cjs/assets/logomark.svg.js.map +1 -0
  6. package/dist/cjs/components/DevModePanel.js +826 -0
  7. package/dist/cjs/components/DevModePanel.js.map +1 -0
  8. package/dist/cjs/components/DvdLogo.js +200 -0
  9. package/dist/cjs/components/DvdLogo.js.map +1 -0
  10. package/dist/cjs/components/Icons.js +439 -0
  11. package/dist/cjs/components/Icons.js.map +1 -0
  12. package/dist/cjs/components/IdleScreen.js +587 -0
  13. package/dist/cjs/components/IdleScreen.js.map +1 -0
  14. package/dist/cjs/components/LoadingScreen.js +523 -0
  15. package/dist/cjs/components/LoadingScreen.js.map +1 -0
  16. package/dist/cjs/components/Player.js +420 -0
  17. package/dist/cjs/components/Player.js.map +1 -0
  18. package/dist/cjs/components/PlayerControls.js +798 -0
  19. package/dist/cjs/components/PlayerControls.js.map +1 -0
  20. package/dist/cjs/components/PlayerErrorBoundary.js +80 -0
  21. package/dist/cjs/components/PlayerErrorBoundary.js.map +1 -0
  22. package/dist/cjs/components/SeekBar.js +253 -0
  23. package/dist/cjs/components/SeekBar.js.map +1 -0
  24. package/dist/cjs/components/SkipIndicator.js +92 -0
  25. package/dist/cjs/components/SkipIndicator.js.map +1 -0
  26. package/dist/cjs/components/SpeedIndicator.js +43 -0
  27. package/dist/cjs/components/SpeedIndicator.js.map +1 -0
  28. package/dist/cjs/components/StatsPanel.js +202 -0
  29. package/dist/cjs/components/StatsPanel.js.map +1 -0
  30. package/dist/cjs/components/StreamStateOverlay.js +229 -0
  31. package/dist/cjs/components/StreamStateOverlay.js.map +1 -0
  32. package/dist/cjs/components/ThumbnailOverlay.js +86 -0
  33. package/dist/cjs/components/ThumbnailOverlay.js.map +1 -0
  34. package/dist/cjs/components/TitleOverlay.js +32 -0
  35. package/dist/cjs/components/TitleOverlay.js.map +1 -0
  36. package/dist/cjs/context/PlayerContext.js +46 -0
  37. package/dist/cjs/context/PlayerContext.js.map +1 -0
  38. package/dist/cjs/hooks/useMetaTrack.js +165 -0
  39. package/dist/cjs/hooks/useMetaTrack.js.map +1 -0
  40. package/dist/cjs/hooks/usePlaybackQuality.js +131 -0
  41. package/dist/cjs/hooks/usePlaybackQuality.js.map +1 -0
  42. package/dist/cjs/hooks/usePlayerController.js +518 -0
  43. package/dist/cjs/hooks/usePlayerController.js.map +1 -0
  44. package/dist/cjs/hooks/usePlayerSelection.js +90 -0
  45. package/dist/cjs/hooks/usePlayerSelection.js.map +1 -0
  46. package/dist/cjs/hooks/useStreamState.js +360 -0
  47. package/dist/cjs/hooks/useStreamState.js.map +1 -0
  48. package/dist/cjs/hooks/useTelemetry.js +120 -0
  49. package/dist/cjs/hooks/useTelemetry.js.map +1 -0
  50. package/dist/cjs/hooks/useViewerEndpoints.js +222 -0
  51. package/dist/cjs/hooks/useViewerEndpoints.js.map +1 -0
  52. package/dist/cjs/index.js +97 -1
  53. package/dist/cjs/index.js.map +1 -1
  54. package/dist/cjs/ui/badge.js +34 -0
  55. package/dist/cjs/ui/badge.js.map +1 -0
  56. package/dist/cjs/ui/button.js +74 -0
  57. package/dist/cjs/ui/button.js.map +1 -0
  58. package/dist/cjs/ui/context-menu.js +163 -0
  59. package/dist/cjs/ui/context-menu.js.map +1 -0
  60. package/dist/cjs/ui/slider.js +60 -0
  61. package/dist/cjs/ui/slider.js.map +1 -0
  62. package/dist/esm/_virtual/_rollupPluginBabelHelpers.js +329 -0
  63. package/dist/esm/_virtual/_rollupPluginBabelHelpers.js.map +1 -0
  64. package/dist/esm/assets/logomark.svg.js +4 -0
  65. package/dist/esm/assets/logomark.svg.js.map +1 -0
  66. package/dist/esm/components/DevModePanel.js +822 -0
  67. package/dist/esm/components/DevModePanel.js.map +1 -0
  68. package/dist/esm/components/DvdLogo.js +196 -0
  69. package/dist/esm/components/DvdLogo.js.map +1 -0
  70. package/dist/esm/components/Icons.js +421 -0
  71. package/dist/esm/components/Icons.js.map +1 -0
  72. package/dist/esm/components/IdleScreen.js +582 -0
  73. package/dist/esm/components/IdleScreen.js.map +1 -0
  74. package/dist/esm/components/LoadingScreen.js +519 -0
  75. package/dist/esm/components/LoadingScreen.js.map +1 -0
  76. package/dist/esm/components/Player.js +416 -0
  77. package/dist/esm/components/Player.js.map +1 -0
  78. package/dist/esm/components/PlayerControls.js +794 -0
  79. package/dist/esm/components/PlayerControls.js.map +1 -0
  80. package/dist/esm/components/PlayerErrorBoundary.js +76 -0
  81. package/dist/esm/components/PlayerErrorBoundary.js.map +1 -0
  82. package/dist/esm/components/SeekBar.js +249 -0
  83. package/dist/esm/components/SeekBar.js.map +1 -0
  84. package/dist/esm/components/SkipIndicator.js +88 -0
  85. package/dist/esm/components/SkipIndicator.js.map +1 -0
  86. package/dist/esm/components/SpeedIndicator.js +39 -0
  87. package/dist/esm/components/SpeedIndicator.js.map +1 -0
  88. package/dist/esm/components/StatsPanel.js +198 -0
  89. package/dist/esm/components/StatsPanel.js.map +1 -0
  90. package/dist/esm/components/StreamStateOverlay.js +224 -0
  91. package/dist/esm/components/StreamStateOverlay.js.map +1 -0
  92. package/dist/esm/components/ThumbnailOverlay.js +82 -0
  93. package/dist/esm/components/ThumbnailOverlay.js.map +1 -0
  94. package/dist/esm/components/TitleOverlay.js +28 -0
  95. package/dist/esm/components/TitleOverlay.js.map +1 -0
  96. package/dist/esm/context/PlayerContext.js +41 -0
  97. package/dist/esm/context/PlayerContext.js.map +1 -0
  98. package/dist/esm/hooks/useMetaTrack.js +163 -0
  99. package/dist/esm/hooks/useMetaTrack.js.map +1 -0
  100. package/dist/esm/hooks/usePlaybackQuality.js +129 -0
  101. package/dist/esm/hooks/usePlaybackQuality.js.map +1 -0
  102. package/dist/esm/hooks/usePlayerController.js +516 -0
  103. package/dist/esm/hooks/usePlayerController.js.map +1 -0
  104. package/dist/esm/hooks/usePlayerSelection.js +88 -0
  105. package/dist/esm/hooks/usePlayerSelection.js.map +1 -0
  106. package/dist/esm/hooks/useStreamState.js +358 -0
  107. package/dist/esm/hooks/useStreamState.js.map +1 -0
  108. package/dist/esm/hooks/useTelemetry.js +118 -0
  109. package/dist/esm/hooks/useTelemetry.js.map +1 -0
  110. package/dist/esm/hooks/useViewerEndpoints.js +220 -0
  111. package/dist/esm/hooks/useViewerEndpoints.js.map +1 -0
  112. package/dist/esm/index.js +23 -1
  113. package/dist/esm/index.js.map +1 -1
  114. package/dist/esm/ui/badge.js +31 -0
  115. package/dist/esm/ui/badge.js.map +1 -0
  116. package/dist/esm/ui/button.js +52 -0
  117. package/dist/esm/ui/button.js.map +1 -0
  118. package/dist/esm/ui/context-menu.js +132 -0
  119. package/dist/esm/ui/context-menu.js.map +1 -0
  120. package/dist/esm/ui/slider.js +38 -0
  121. package/dist/esm/ui/slider.js.map +1 -0
  122. package/dist/types/components/DvdLogo.d.ts +1 -1
  123. package/dist/types/components/Icons.d.ts +1 -1
  124. package/dist/types/components/Player.d.ts +1 -1
  125. package/dist/types/components/PlayerErrorBoundary.d.ts +2 -1
  126. package/dist/types/components/StreamStateOverlay.d.ts +2 -2
  127. package/dist/types/components/SubtitleRenderer.d.ts +2 -2
  128. package/dist/types/context/PlayerContext.d.ts +2 -2
  129. package/dist/types/context/index.d.ts +2 -2
  130. package/dist/types/hooks/useMetaTrack.d.ts +3 -3
  131. package/dist/types/hooks/usePlaybackQuality.d.ts +2 -2
  132. package/dist/types/hooks/usePlayerController.d.ts +26 -3
  133. package/dist/types/hooks/usePlayerSelection.d.ts +1 -1
  134. package/dist/types/hooks/useStreamState.d.ts +1 -1
  135. package/dist/types/hooks/useTelemetry.d.ts +1 -1
  136. package/dist/types/hooks/useViewerEndpoints.d.ts +3 -3
  137. package/dist/types/index.d.ts +28 -28
  138. package/dist/types/types.d.ts +3 -3
  139. package/dist/types/ui/select.d.ts +1 -1
  140. package/package.json +22 -14
  141. package/src/components/DevModePanel.tsx +244 -143
  142. package/src/components/DvdLogo.tsx +1 -1
  143. package/src/components/Icons.tsx +105 -25
  144. package/src/components/IdleScreen.tsx +262 -128
  145. package/src/components/LoadingScreen.tsx +169 -151
  146. package/src/components/LogoOverlay.tsx +3 -6
  147. package/src/components/Player.tsx +126 -59
  148. package/src/components/PlayerControls.tsx +384 -272
  149. package/src/components/PlayerErrorBoundary.tsx +7 -13
  150. package/src/components/SeekBar.tsx +96 -88
  151. package/src/components/SkipIndicator.tsx +2 -12
  152. package/src/components/SpeedIndicator.tsx +2 -11
  153. package/src/components/StatsPanel.tsx +31 -22
  154. package/src/components/StreamStateOverlay.tsx +105 -49
  155. package/src/components/SubtitleRenderer.tsx +29 -29
  156. package/src/components/ThumbnailOverlay.tsx +5 -6
  157. package/src/components/TitleOverlay.tsx +2 -8
  158. package/src/context/PlayerContext.tsx +4 -8
  159. package/src/context/index.ts +3 -3
  160. package/src/hooks/useMetaTrack.ts +27 -27
  161. package/src/hooks/usePlaybackQuality.ts +3 -3
  162. package/src/hooks/usePlayerController.ts +246 -138
  163. package/src/hooks/usePlayerSelection.ts +6 -6
  164. package/src/hooks/useStreamState.ts +51 -56
  165. package/src/hooks/useTelemetry.ts +18 -3
  166. package/src/hooks/useViewerEndpoints.ts +34 -23
  167. package/src/index.tsx +36 -28
  168. package/src/types.ts +8 -8
  169. package/src/ui/badge.tsx +6 -5
  170. package/src/ui/button.tsx +9 -8
  171. package/src/ui/context-menu.tsx +42 -61
  172. package/src/ui/select.tsx +13 -7
  173. package/src/ui/slider.tsx +18 -29
  174. package/dist/types/components/players/DashJsPlayer.d.ts +0 -18
  175. package/dist/types/components/players/HlsJsPlayer.d.ts +0 -18
  176. package/dist/types/components/players/MewsWsPlayer/index.d.ts +0 -18
  177. package/dist/types/components/players/MistPlayer.d.ts +0 -20
  178. package/dist/types/components/players/MistWebRTCPlayer/index.d.ts +0 -20
  179. package/dist/types/components/players/NativePlayer.d.ts +0 -19
  180. package/dist/types/components/players/VideoJsPlayer.d.ts +0 -18
  181. package/src/components/players/DashJsPlayer.tsx +0 -56
  182. package/src/components/players/HlsJsPlayer.tsx +0 -56
  183. package/src/components/players/MewsWsPlayer/index.tsx +0 -56
  184. package/src/components/players/MistPlayer.tsx +0 -60
  185. package/src/components/players/MistWebRTCPlayer/index.tsx +0 -59
  186. package/src/components/players/NativePlayer.tsx +0 -58
  187. package/src/components/players/VideoJsPlayer.tsx +0 -56
@@ -5,7 +5,7 @@
5
5
  * Manages the complete player lifecycle and provides reactive state.
6
6
  */
7
7
 
8
- import { useState, useEffect, useRef, useCallback } from 'react';
8
+ import { useState, useEffect, useRef, useCallback } from "react";
9
9
  import {
10
10
  PlayerController,
11
11
  type PlayerControllerConfig,
@@ -15,13 +15,14 @@ import {
15
15
  type PlaybackQuality,
16
16
  type ContentEndpoints,
17
17
  type ContentMetadata,
18
- } from '@livepeer-frameworks/player-core';
18
+ type ClassifiedError,
19
+ } from "@livepeer-frameworks/player-core";
19
20
 
20
21
  // ============================================================================
21
22
  // Types
22
23
  // ============================================================================
23
24
 
24
- export interface UsePlayerControllerConfig extends Omit<PlayerControllerConfig, 'playerManager'> {
25
+ export interface UsePlayerControllerConfig extends Omit<PlayerControllerConfig, "playerManager"> {
25
26
  /** Enable/disable the hook */
26
27
  enabled?: boolean;
27
28
  /** Callback when state changes */
@@ -32,6 +33,20 @@ export interface UsePlayerControllerConfig extends Omit<PlayerControllerConfig,
32
33
  onError?: (error: string) => void;
33
34
  /** Callback when ready */
34
35
  onReady?: (videoElement: HTMLVideoElement) => void;
36
+ /** Callback when protocol is swapped (for toast notification) */
37
+ onProtocolSwapped?: (data: {
38
+ fromPlayer: string;
39
+ toPlayer: string;
40
+ fromProtocol: string;
41
+ toProtocol: string;
42
+ reason: string;
43
+ }) => void;
44
+ /** Callback when playback fails after all recovery attempts (for error modal) */
45
+ onPlaybackFailed?: (error: {
46
+ code: string;
47
+ message: string;
48
+ details?: ClassifiedError["details"];
49
+ }) => void;
35
50
  }
36
51
 
37
52
  export interface PlayerControllerState {
@@ -61,6 +76,8 @@ export interface PlayerControllerState {
61
76
  volume: number;
62
77
  /** Error text */
63
78
  error: string | null;
79
+ /** Error details for debugging */
80
+ errorDetails: ClassifiedError["details"] | null;
64
81
  /** Is passive error */
65
82
  isPassiveError: boolean;
66
83
  /** Has playback ever started */
@@ -92,11 +109,21 @@ export interface PlayerControllerState {
92
109
  /** Subtitles enabled */
93
110
  subtitlesEnabled: boolean;
94
111
  /** Available quality levels */
95
- qualities: Array<{ id: string; label: string; bitrate?: number; width?: number; height?: number; isAuto?: boolean; active?: boolean }>;
112
+ qualities: Array<{
113
+ id: string;
114
+ label: string;
115
+ bitrate?: number;
116
+ width?: number;
117
+ height?: number;
118
+ isAuto?: boolean;
119
+ active?: boolean;
120
+ }>;
96
121
  /** Available text/caption tracks */
97
122
  textTracks: Array<{ id: string; label: string; language?: string; active: boolean }>;
98
123
  /** Stream info for player selection (sources + tracks) */
99
124
  streamInfo: StreamInfo | null;
125
+ /** Toast message to display (auto-dismisses) */
126
+ toast: { message: string; timestamp: number } | null;
100
127
  }
101
128
 
102
129
  export interface UsePlayerControllerReturn {
@@ -132,6 +159,8 @@ export interface UsePlayerControllerReturn {
132
159
  toggleSubtitles: () => void;
133
160
  /** Clear error */
134
161
  clearError: () => void;
162
+ /** Dismiss toast notification */
163
+ dismissToast: () => void;
135
164
  /** Retry playback */
136
165
  retry: () => Promise<void>;
137
166
  /** Reload player */
@@ -153,7 +182,7 @@ export interface UsePlayerControllerReturn {
153
182
  forcePlayer?: string;
154
183
  forceType?: string;
155
184
  forceSource?: number;
156
- playbackMode?: 'auto' | 'low-latency' | 'quality' | 'vod';
185
+ playbackMode?: "auto" | "low-latency" | "quality" | "vod";
157
186
  }) => Promise<void>;
158
187
  }
159
188
 
@@ -162,7 +191,7 @@ export interface UsePlayerControllerReturn {
162
191
  // ============================================================================
163
192
 
164
193
  const initialState: PlayerControllerState = {
165
- state: 'booting',
194
+ state: "booting",
166
195
  streamState: null,
167
196
  endpoints: null,
168
197
  metadata: null,
@@ -175,6 +204,7 @@ const initialState: PlayerControllerState = {
175
204
  isMuted: true,
176
205
  volume: 1,
177
206
  error: null,
207
+ errorDetails: null,
178
208
  isPassiveError: false,
179
209
  hasPlaybackStarted: false,
180
210
  isHoldingSpeed: false,
@@ -193,16 +223,24 @@ const initialState: PlayerControllerState = {
193
223
  qualities: [],
194
224
  textTracks: [],
195
225
  streamInfo: null,
226
+ toast: null,
196
227
  };
197
228
 
198
229
  // ============================================================================
199
230
  // Hook
200
231
  // ============================================================================
201
232
 
202
- export function usePlayerController(
203
- config: UsePlayerControllerConfig
204
- ): UsePlayerControllerReturn {
205
- const { enabled = true, onStateChange, onStreamStateChange, onError, onReady, ...controllerConfig } = config;
233
+ export function usePlayerController(config: UsePlayerControllerConfig): UsePlayerControllerReturn {
234
+ const {
235
+ enabled = true,
236
+ onStateChange,
237
+ onStreamStateChange,
238
+ onError,
239
+ onReady,
240
+ onProtocolSwapped,
241
+ onPlaybackFailed,
242
+ ...controllerConfig
243
+ } = config;
206
244
 
207
245
  const containerRef = useRef<HTMLDivElement>(null);
208
246
  const controllerRef = useRef<PlayerController | null>(null);
@@ -243,7 +281,7 @@ export function usePlayerController(
243
281
  const syncState = () => {
244
282
  if (!controllerRef.current) return;
245
283
  const c = controllerRef.current;
246
- setState(prev => ({
284
+ setState((prev) => ({
247
285
  ...prev,
248
286
  isPlaying: c.isPlaying(),
249
287
  isPaused: c.isPaused(),
@@ -261,135 +299,192 @@ export function usePlayerController(
261
299
  }));
262
300
  };
263
301
 
264
- unsubs.push(controller.on('stateChange', ({ state: newState }) => {
265
- setState(prev => ({ ...prev, state: newState }));
266
- onStateChange?.(newState);
267
- }));
268
-
269
- unsubs.push(controller.on('streamStateChange', ({ state: streamState }) => {
270
- setState(prev => ({
271
- ...prev,
272
- streamState,
273
- metadata: controller.getMetadata(),
274
- isEffectivelyLive: controller.isEffectivelyLive(),
275
- shouldShowIdleScreen: controller.shouldShowIdleScreen(),
276
- }));
277
- onStreamStateChange?.(streamState);
278
- }));
279
-
280
- unsubs.push(controller.on('timeUpdate', ({ currentTime, duration }) => {
281
- setState(prev => ({ ...prev, currentTime, duration }));
282
- }));
283
-
284
- unsubs.push(controller.on('error', ({ error }) => {
285
- setState(prev => ({
286
- ...prev,
287
- error,
288
- isPassiveError: controller.isPassiveError(),
289
- }));
290
- onError?.(error);
291
- }));
292
-
293
- unsubs.push(controller.on('errorCleared', () => {
294
- setState(prev => ({ ...prev, error: null, isPassiveError: false }));
295
- }));
296
-
297
- unsubs.push(controller.on('ready', ({ videoElement }) => {
298
- setState(prev => ({
299
- ...prev,
300
- videoElement,
301
- endpoints: controller.getEndpoints(),
302
- metadata: controller.getMetadata(),
303
- streamInfo: controller.getStreamInfo(),
304
- isEffectivelyLive: controller.isEffectivelyLive(),
305
- shouldShowIdleScreen: controller.shouldShowIdleScreen(),
306
- currentPlayerInfo: controller.getCurrentPlayerInfo(),
307
- currentSourceInfo: controller.getCurrentSourceInfo(),
308
- qualities: controller.getQualities(),
309
- }));
310
- onReady?.(videoElement);
311
-
312
- // Set up video event listeners AFTER video is ready
313
- // syncState is defined below - this closure captures it
314
- const handleVideoEvent = () => {
315
- if (controllerRef.current?.shouldSuppressVideoEvents?.()) return;
316
- syncState();
317
- };
318
- videoElement.addEventListener('play', handleVideoEvent);
319
- videoElement.addEventListener('pause', handleVideoEvent);
320
- videoElement.addEventListener('waiting', handleVideoEvent);
321
- videoElement.addEventListener('playing', handleVideoEvent);
322
- unsubs.push(() => {
323
- videoElement.removeEventListener('play', handleVideoEvent);
324
- videoElement.removeEventListener('pause', handleVideoEvent);
325
- videoElement.removeEventListener('waiting', handleVideoEvent);
326
- videoElement.removeEventListener('playing', handleVideoEvent);
327
- });
328
- }));
329
-
330
- unsubs.push(controller.on('playerSelected', ({ player: _player, source }) => {
331
- setState(prev => ({
332
- ...prev,
333
- currentPlayerInfo: controller.getCurrentPlayerInfo(),
334
- currentSourceInfo: { url: source.url, type: source.type },
335
- qualities: controller.getQualities(),
336
- }));
337
- }));
338
-
339
- unsubs.push(controller.on('volumeChange', ({ volume, muted }) => {
340
- setState(prev => ({ ...prev, volume, isMuted: muted }));
341
- }));
342
-
343
- unsubs.push(controller.on('loopChange', ({ isLoopEnabled }) => {
344
- setState(prev => ({ ...prev, isLoopEnabled }));
345
- }));
346
-
347
- unsubs.push(controller.on('fullscreenChange', ({ isFullscreen }) => {
348
- setState(prev => ({ ...prev, isFullscreen }));
349
- }));
350
-
351
- unsubs.push(controller.on('pipChange', ({ isPiP }) => {
352
- setState(prev => ({ ...prev, isPiPActive: isPiP }));
353
- }));
354
-
355
- unsubs.push(controller.on('holdSpeedStart', ({ speed }) => {
356
- setState(prev => ({ ...prev, isHoldingSpeed: true, holdSpeed: speed }));
357
- }));
358
-
359
- unsubs.push(controller.on('holdSpeedEnd', () => {
360
- setState(prev => ({ ...prev, isHoldingSpeed: false }));
361
- }));
362
-
363
- unsubs.push(controller.on('hoverStart', () => {
364
- setState(prev => ({ ...prev, isHovering: true, shouldShowControls: true }));
365
- }));
366
-
367
- unsubs.push(controller.on('hoverEnd', () => {
368
- setState(prev => ({
369
- ...prev,
370
- isHovering: false,
371
- shouldShowControls: controller.shouldShowControls(),
372
- }));
373
- }));
374
-
375
- unsubs.push(controller.on('captionsChange', ({ enabled }) => {
376
- setState(prev => ({ ...prev, subtitlesEnabled: enabled }));
377
- }));
302
+ unsubs.push(
303
+ controller.on("stateChange", ({ state: newState }) => {
304
+ setState((prev) => ({ ...prev, state: newState }));
305
+ onStateChange?.(newState);
306
+ })
307
+ );
308
+
309
+ unsubs.push(
310
+ controller.on("streamStateChange", ({ state: streamState }) => {
311
+ setState((prev) => ({
312
+ ...prev,
313
+ streamState,
314
+ metadata: controller.getMetadata(),
315
+ isEffectivelyLive: controller.isEffectivelyLive(),
316
+ shouldShowIdleScreen: controller.shouldShowIdleScreen(),
317
+ }));
318
+ onStreamStateChange?.(streamState);
319
+ })
320
+ );
321
+
322
+ unsubs.push(
323
+ controller.on("timeUpdate", ({ currentTime, duration }) => {
324
+ setState((prev) => ({ ...prev, currentTime, duration }));
325
+ })
326
+ );
327
+
328
+ unsubs.push(
329
+ controller.on("error", ({ error }) => {
330
+ setState((prev) => ({
331
+ ...prev,
332
+ error,
333
+ isPassiveError: controller.isPassiveError(),
334
+ }));
335
+ onError?.(error);
336
+ })
337
+ );
338
+
339
+ unsubs.push(
340
+ controller.on("errorCleared", () => {
341
+ setState((prev) => ({ ...prev, error: null, isPassiveError: false }));
342
+ })
343
+ );
344
+
345
+ unsubs.push(
346
+ controller.on("ready", ({ videoElement }) => {
347
+ setState((prev) => ({
348
+ ...prev,
349
+ videoElement,
350
+ endpoints: controller.getEndpoints(),
351
+ metadata: controller.getMetadata(),
352
+ streamInfo: controller.getStreamInfo(),
353
+ isEffectivelyLive: controller.isEffectivelyLive(),
354
+ shouldShowIdleScreen: controller.shouldShowIdleScreen(),
355
+ currentPlayerInfo: controller.getCurrentPlayerInfo(),
356
+ currentSourceInfo: controller.getCurrentSourceInfo(),
357
+ qualities: controller.getQualities(),
358
+ }));
359
+ onReady?.(videoElement);
360
+
361
+ // Set up video event listeners AFTER video is ready
362
+ // syncState is defined below - this closure captures it
363
+ const handleVideoEvent = () => {
364
+ if (controllerRef.current?.shouldSuppressVideoEvents?.()) return;
365
+ syncState();
366
+ };
367
+ videoElement.addEventListener("play", handleVideoEvent);
368
+ videoElement.addEventListener("pause", handleVideoEvent);
369
+ videoElement.addEventListener("waiting", handleVideoEvent);
370
+ videoElement.addEventListener("playing", handleVideoEvent);
371
+ unsubs.push(() => {
372
+ videoElement.removeEventListener("play", handleVideoEvent);
373
+ videoElement.removeEventListener("pause", handleVideoEvent);
374
+ videoElement.removeEventListener("waiting", handleVideoEvent);
375
+ videoElement.removeEventListener("playing", handleVideoEvent);
376
+ });
377
+ })
378
+ );
379
+
380
+ unsubs.push(
381
+ controller.on("playerSelected", ({ player: _player, source }) => {
382
+ setState((prev) => ({
383
+ ...prev,
384
+ currentPlayerInfo: controller.getCurrentPlayerInfo(),
385
+ currentSourceInfo: { url: source.url, type: source.type },
386
+ qualities: controller.getQualities(),
387
+ }));
388
+ })
389
+ );
390
+
391
+ unsubs.push(
392
+ controller.on("volumeChange", ({ volume, muted }) => {
393
+ setState((prev) => ({ ...prev, volume, isMuted: muted }));
394
+ })
395
+ );
396
+
397
+ unsubs.push(
398
+ controller.on("loopChange", ({ isLoopEnabled }) => {
399
+ setState((prev) => ({ ...prev, isLoopEnabled }));
400
+ })
401
+ );
402
+
403
+ unsubs.push(
404
+ controller.on("fullscreenChange", ({ isFullscreen }) => {
405
+ setState((prev) => ({ ...prev, isFullscreen }));
406
+ })
407
+ );
408
+
409
+ unsubs.push(
410
+ controller.on("pipChange", ({ isPiP }) => {
411
+ setState((prev) => ({ ...prev, isPiPActive: isPiP }));
412
+ })
413
+ );
414
+
415
+ unsubs.push(
416
+ controller.on("holdSpeedStart", ({ speed }) => {
417
+ setState((prev) => ({ ...prev, isHoldingSpeed: true, holdSpeed: speed }));
418
+ })
419
+ );
420
+
421
+ unsubs.push(
422
+ controller.on("holdSpeedEnd", () => {
423
+ setState((prev) => ({ ...prev, isHoldingSpeed: false }));
424
+ })
425
+ );
426
+
427
+ unsubs.push(
428
+ controller.on("hoverStart", () => {
429
+ setState((prev) => ({ ...prev, isHovering: true, shouldShowControls: true }));
430
+ })
431
+ );
432
+
433
+ unsubs.push(
434
+ controller.on("hoverEnd", () => {
435
+ setState((prev) => ({
436
+ ...prev,
437
+ isHovering: false,
438
+ shouldShowControls: controller.shouldShowControls(),
439
+ }));
440
+ })
441
+ );
442
+
443
+ unsubs.push(
444
+ controller.on("captionsChange", ({ enabled }) => {
445
+ setState((prev) => ({ ...prev, subtitlesEnabled: enabled }));
446
+ })
447
+ );
448
+
449
+ // Error handling events - show toasts/modals
450
+ unsubs.push(
451
+ controller.on("protocolSwapped", (data) => {
452
+ const message = `Switched to ${data.toProtocol}`;
453
+ setState((prev) => ({ ...prev, toast: { message, timestamp: Date.now() } }));
454
+ onProtocolSwapped?.(data);
455
+ })
456
+ );
457
+
458
+ unsubs.push(
459
+ controller.on("playbackFailed", (data) => {
460
+ setState((prev) => ({
461
+ ...prev,
462
+ error: data.message,
463
+ errorDetails: data.details ?? null,
464
+ isPassiveError: false,
465
+ }));
466
+ onPlaybackFailed?.({
467
+ code: data.code,
468
+ message: data.message,
469
+ details: data.details,
470
+ });
471
+ })
472
+ );
378
473
 
379
474
  // Attach controller to container
380
475
  // Note: Video event listeners are set up in the 'ready' handler above
381
- controller.attach(container).catch(err => {
382
- console.warn('[usePlayerController] Attach failed:', err);
476
+ controller.attach(container).catch((err) => {
477
+ console.warn("[usePlayerController] Attach failed:", err);
383
478
  });
384
479
 
385
480
  // Set initial state
386
- setState(prev => ({
481
+ setState((prev) => ({
387
482
  ...prev,
388
483
  isLoopEnabled: controller.isLoopEnabled(),
389
484
  }));
390
485
 
391
486
  return () => {
392
- unsubs.forEach(fn => fn());
487
+ unsubs.forEach((fn) => fn());
393
488
  controller.destroy();
394
489
  controllerRef.current = null;
395
490
  setState(initialState);
@@ -443,7 +538,16 @@ export function usePlayerController(
443
538
 
444
539
  const clearError = useCallback(() => {
445
540
  controllerRef.current?.clearError();
446
- setState(prev => ({ ...prev, error: null, isPassiveError: false }));
541
+ setState((prev) => ({
542
+ ...prev,
543
+ error: null,
544
+ errorDetails: null,
545
+ isPassiveError: false,
546
+ }));
547
+ }, []);
548
+
549
+ const dismissToast = useCallback(() => {
550
+ setState((prev) => ({ ...prev, toast: null }));
447
551
  }, []);
448
552
 
449
553
  const jumpToLive = useCallback(() => {
@@ -482,14 +586,17 @@ export function usePlayerController(
482
586
  controllerRef.current?.handleTouchStart();
483
587
  }, []);
484
588
 
485
- const setDevModeOptions = useCallback(async (options: {
486
- forcePlayer?: string;
487
- forceType?: string;
488
- forceSource?: number;
489
- playbackMode?: 'auto' | 'low-latency' | 'quality' | 'vod';
490
- }) => {
491
- await controllerRef.current?.setDevModeOptions(options);
492
- }, []);
589
+ const setDevModeOptions = useCallback(
590
+ async (options: {
591
+ forcePlayer?: string;
592
+ forceType?: string;
593
+ forceSource?: number;
594
+ playbackMode?: "auto" | "low-latency" | "quality" | "vod";
595
+ }) => {
596
+ await controllerRef.current?.setDevModeOptions(options);
597
+ },
598
+ []
599
+ );
493
600
 
494
601
  return {
495
602
  containerRef,
@@ -508,6 +615,7 @@ export function usePlayerController(
508
615
  togglePiP,
509
616
  toggleSubtitles,
510
617
  clearError,
618
+ dismissToast,
511
619
  retry,
512
620
  reload,
513
621
  getQualities,
@@ -5,14 +5,14 @@
5
5
  * Uses event-driven updates instead of polling - no render spam.
6
6
  */
7
7
 
8
- import { useState, useEffect, useCallback } from 'react';
8
+ import { useState, useEffect, useCallback } from "react";
9
9
  import type {
10
10
  PlayerManager,
11
11
  PlayerSelection,
12
12
  PlayerCombination,
13
13
  StreamInfo,
14
14
  PlaybackMode,
15
- } from '@livepeer-frameworks/player-core';
15
+ } from "@livepeer-frameworks/player-core";
16
16
 
17
17
  export interface UsePlayerSelectionOptions {
18
18
  /** Stream info to compute selections for */
@@ -67,16 +67,16 @@ export function usePlayerSelection(
67
67
 
68
68
  // Subscribe to events
69
69
  useEffect(() => {
70
- const unsubSelection = manager.on('selection-changed', (sel) => {
70
+ const unsubSelection = manager.on("selection-changed", (sel) => {
71
71
  if (debug) {
72
- console.log('[usePlayerSelection] Selection changed:', sel?.player, sel?.source?.type);
72
+ console.log("[usePlayerSelection] Selection changed:", sel?.player, sel?.source?.type);
73
73
  }
74
74
  setSelection(sel);
75
75
  });
76
76
 
77
- const unsubCombos = manager.on('combinations-updated', (combos) => {
77
+ const unsubCombos = manager.on("combinations-updated", (combos) => {
78
78
  if (debug) {
79
- console.log('[usePlayerSelection] Combinations updated:', combos.length);
79
+ console.log("[usePlayerSelection] Combinations updated:", combos.length);
80
80
  }
81
81
  setCombinations(combos);
82
82
  setReady(true);