@livepeer-frameworks/player-react 0.0.4 → 0.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.
Files changed (54) hide show
  1. package/README.md +16 -5
  2. package/dist/cjs/index.js +1 -1
  3. package/dist/cjs/index.js.map +1 -1
  4. package/dist/esm/index.js +1 -1
  5. package/dist/esm/index.js.map +1 -1
  6. package/dist/types/components/PlayerControls.d.ts +2 -0
  7. package/dist/types/components/StatsPanel.d.ts +2 -14
  8. package/dist/types/hooks/useMetaTrack.d.ts +1 -1
  9. package/dist/types/hooks/usePlayerController.d.ts +2 -0
  10. package/dist/types/hooks/useStreamState.d.ts +1 -1
  11. package/dist/types/hooks/useTelemetry.d.ts +1 -1
  12. package/dist/types/hooks/useViewerEndpoints.d.ts +2 -2
  13. package/dist/types/types.d.ts +1 -1
  14. package/dist/types/ui/button.d.ts +1 -1
  15. package/package.json +1 -1
  16. package/src/components/DevModePanel.tsx +249 -170
  17. package/src/components/Icons.tsx +105 -25
  18. package/src/components/IdleScreen.tsx +262 -142
  19. package/src/components/LoadingScreen.tsx +171 -153
  20. package/src/components/LogoOverlay.tsx +3 -6
  21. package/src/components/Player.tsx +86 -74
  22. package/src/components/PlayerControls.tsx +351 -263
  23. package/src/components/PlayerErrorBoundary.tsx +6 -13
  24. package/src/components/SeekBar.tsx +96 -88
  25. package/src/components/SkipIndicator.tsx +2 -12
  26. package/src/components/SpeedIndicator.tsx +2 -11
  27. package/src/components/StatsPanel.tsx +65 -34
  28. package/src/components/StreamStateOverlay.tsx +105 -49
  29. package/src/components/SubtitleRenderer.tsx +29 -29
  30. package/src/components/ThumbnailOverlay.tsx +5 -6
  31. package/src/components/TitleOverlay.tsx +2 -8
  32. package/src/components/players/DashJsPlayer.tsx +13 -11
  33. package/src/components/players/HlsJsPlayer.tsx +13 -11
  34. package/src/components/players/MewsWsPlayer/index.tsx +13 -11
  35. package/src/components/players/MistPlayer.tsx +13 -11
  36. package/src/components/players/MistWebRTCPlayer/index.tsx +19 -10
  37. package/src/components/players/NativePlayer.tsx +10 -12
  38. package/src/components/players/VideoJsPlayer.tsx +13 -11
  39. package/src/context/PlayerContext.tsx +4 -8
  40. package/src/context/index.ts +3 -3
  41. package/src/hooks/useMetaTrack.ts +28 -28
  42. package/src/hooks/usePlaybackQuality.ts +3 -3
  43. package/src/hooks/usePlayerController.ts +186 -140
  44. package/src/hooks/usePlayerSelection.ts +6 -6
  45. package/src/hooks/useStreamState.ts +53 -58
  46. package/src/hooks/useTelemetry.ts +19 -4
  47. package/src/hooks/useViewerEndpoints.ts +40 -30
  48. package/src/index.tsx +36 -28
  49. package/src/types.ts +9 -9
  50. package/src/ui/badge.tsx +6 -5
  51. package/src/ui/button.tsx +9 -8
  52. package/src/ui/context-menu.tsx +42 -61
  53. package/src/ui/select.tsx +13 -7
  54. package/src/ui/slider.tsx +18 -29
@@ -5,26 +5,23 @@
5
5
  * Manages the complete player lifecycle and provides reactive state.
6
6
  */
7
7
 
8
- import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
8
+ import { useState, useEffect, useRef, useCallback } from "react";
9
9
  import {
10
10
  PlayerController,
11
11
  type PlayerControllerConfig,
12
- type PlayerControllerEvents,
13
12
  type PlayerState,
14
13
  type StreamState,
15
- type StreamSource,
16
14
  type StreamInfo,
17
15
  type PlaybackQuality,
18
16
  type ContentEndpoints,
19
17
  type ContentMetadata,
20
- type MistStreamInfo,
21
- } from '@livepeer-frameworks/player-core';
18
+ } from "@livepeer-frameworks/player-core";
22
19
 
23
20
  // ============================================================================
24
21
  // Types
25
22
  // ============================================================================
26
23
 
27
- export interface UsePlayerControllerConfig extends Omit<PlayerControllerConfig, 'playerManager'> {
24
+ export interface UsePlayerControllerConfig extends Omit<PlayerControllerConfig, "playerManager"> {
28
25
  /** Enable/disable the hook */
29
26
  enabled?: boolean;
30
27
  /** Callback when state changes */
@@ -95,7 +92,15 @@ export interface PlayerControllerState {
95
92
  /** Subtitles enabled */
96
93
  subtitlesEnabled: boolean;
97
94
  /** Available quality levels */
98
- qualities: Array<{ id: string; label: string; bitrate?: number; width?: number; height?: number; isAuto?: boolean; active?: boolean }>;
95
+ qualities: Array<{
96
+ id: string;
97
+ label: string;
98
+ bitrate?: number;
99
+ width?: number;
100
+ height?: number;
101
+ isAuto?: boolean;
102
+ active?: boolean;
103
+ }>;
99
104
  /** Available text/caption tracks */
100
105
  textTracks: Array<{ id: string; label: string; language?: string; active: boolean }>;
101
106
  /** Stream info for player selection (sources + tracks) */
@@ -156,7 +161,7 @@ export interface UsePlayerControllerReturn {
156
161
  forcePlayer?: string;
157
162
  forceType?: string;
158
163
  forceSource?: number;
159
- playbackMode?: 'auto' | 'low-latency' | 'quality' | 'vod';
164
+ playbackMode?: "auto" | "low-latency" | "quality" | "vod";
160
165
  }) => Promise<void>;
161
166
  }
162
167
 
@@ -165,7 +170,7 @@ export interface UsePlayerControllerReturn {
165
170
  // ============================================================================
166
171
 
167
172
  const initialState: PlayerControllerState = {
168
- state: 'booting',
173
+ state: "booting",
169
174
  streamState: null,
170
175
  endpoints: null,
171
176
  metadata: null,
@@ -202,10 +207,15 @@ const initialState: PlayerControllerState = {
202
207
  // Hook
203
208
  // ============================================================================
204
209
 
205
- export function usePlayerController(
206
- config: UsePlayerControllerConfig
207
- ): UsePlayerControllerReturn {
208
- const { enabled = true, onStateChange, onStreamStateChange, onError, onReady, ...controllerConfig } = config;
210
+ export function usePlayerController(config: UsePlayerControllerConfig): UsePlayerControllerReturn {
211
+ const {
212
+ enabled = true,
213
+ onStateChange,
214
+ onStreamStateChange,
215
+ onError,
216
+ onReady,
217
+ ...controllerConfig
218
+ } = config;
209
219
 
210
220
  const containerRef = useRef<HTMLDivElement>(null);
211
221
  const controllerRef = useRef<PlayerController | null>(null);
@@ -246,7 +256,7 @@ export function usePlayerController(
246
256
  const syncState = () => {
247
257
  if (!controllerRef.current) return;
248
258
  const c = controllerRef.current;
249
- setState(prev => ({
259
+ setState((prev) => ({
250
260
  ...prev,
251
261
  isPlaying: c.isPlaying(),
252
262
  isPaused: c.isPaused(),
@@ -264,134 +274,167 @@ export function usePlayerController(
264
274
  }));
265
275
  };
266
276
 
267
- unsubs.push(controller.on('stateChange', ({ state: newState }) => {
268
- setState(prev => ({ ...prev, state: newState }));
269
- onStateChange?.(newState);
270
- }));
271
-
272
- unsubs.push(controller.on('streamStateChange', ({ state: streamState }) => {
273
- setState(prev => ({
274
- ...prev,
275
- streamState,
276
- isEffectivelyLive: controller.isEffectivelyLive(),
277
- shouldShowIdleScreen: controller.shouldShowIdleScreen(),
278
- }));
279
- onStreamStateChange?.(streamState);
280
- }));
281
-
282
- unsubs.push(controller.on('timeUpdate', ({ currentTime, duration }) => {
283
- setState(prev => ({ ...prev, currentTime, duration }));
284
- }));
285
-
286
- unsubs.push(controller.on('error', ({ error }) => {
287
- setState(prev => ({
288
- ...prev,
289
- error,
290
- isPassiveError: controller.isPassiveError(),
291
- }));
292
- onError?.(error);
293
- }));
294
-
295
- unsubs.push(controller.on('errorCleared', () => {
296
- setState(prev => ({ ...prev, error: null, isPassiveError: false }));
297
- }));
298
-
299
- unsubs.push(controller.on('ready', ({ videoElement }) => {
300
- setState(prev => ({
301
- ...prev,
302
- videoElement,
303
- endpoints: controller.getEndpoints(),
304
- metadata: controller.getMetadata(),
305
- streamInfo: controller.getStreamInfo(),
306
- isEffectivelyLive: controller.isEffectivelyLive(),
307
- shouldShowIdleScreen: controller.shouldShowIdleScreen(),
308
- currentPlayerInfo: controller.getCurrentPlayerInfo(),
309
- currentSourceInfo: controller.getCurrentSourceInfo(),
310
- qualities: controller.getQualities(),
311
- }));
312
- onReady?.(videoElement);
313
-
314
- // Set up video event listeners AFTER video is ready
315
- // syncState is defined below - this closure captures it
316
- const handleVideoEvent = () => {
317
- if (controllerRef.current?.shouldSuppressVideoEvents?.()) return;
318
- syncState();
319
- };
320
- videoElement.addEventListener('play', handleVideoEvent);
321
- videoElement.addEventListener('pause', handleVideoEvent);
322
- videoElement.addEventListener('waiting', handleVideoEvent);
323
- videoElement.addEventListener('playing', handleVideoEvent);
324
- unsubs.push(() => {
325
- videoElement.removeEventListener('play', handleVideoEvent);
326
- videoElement.removeEventListener('pause', handleVideoEvent);
327
- videoElement.removeEventListener('waiting', handleVideoEvent);
328
- videoElement.removeEventListener('playing', handleVideoEvent);
329
- });
330
- }));
331
-
332
- unsubs.push(controller.on('playerSelected', ({ player, source }) => {
333
- setState(prev => ({
334
- ...prev,
335
- currentPlayerInfo: controller.getCurrentPlayerInfo(),
336
- currentSourceInfo: { url: source.url, type: source.type },
337
- qualities: controller.getQualities(),
338
- }));
339
- }));
340
-
341
- unsubs.push(controller.on('volumeChange', ({ volume, muted }) => {
342
- setState(prev => ({ ...prev, volume, isMuted: muted }));
343
- }));
344
-
345
- unsubs.push(controller.on('loopChange', ({ isLoopEnabled }) => {
346
- setState(prev => ({ ...prev, isLoopEnabled }));
347
- }));
348
-
349
- unsubs.push(controller.on('fullscreenChange', ({ isFullscreen }) => {
350
- setState(prev => ({ ...prev, isFullscreen }));
351
- }));
352
-
353
- unsubs.push(controller.on('pipChange', ({ isPiP }) => {
354
- setState(prev => ({ ...prev, isPiPActive: isPiP }));
355
- }));
356
-
357
- unsubs.push(controller.on('holdSpeedStart', ({ speed }) => {
358
- setState(prev => ({ ...prev, isHoldingSpeed: true, holdSpeed: speed }));
359
- }));
360
-
361
- unsubs.push(controller.on('holdSpeedEnd', () => {
362
- setState(prev => ({ ...prev, isHoldingSpeed: false }));
363
- }));
364
-
365
- unsubs.push(controller.on('hoverStart', () => {
366
- setState(prev => ({ ...prev, isHovering: true, shouldShowControls: true }));
367
- }));
368
-
369
- unsubs.push(controller.on('hoverEnd', () => {
370
- setState(prev => ({
371
- ...prev,
372
- isHovering: false,
373
- shouldShowControls: controller.shouldShowControls(),
374
- }));
375
- }));
376
-
377
- unsubs.push(controller.on('captionsChange', ({ enabled }) => {
378
- setState(prev => ({ ...prev, subtitlesEnabled: enabled }));
379
- }));
277
+ unsubs.push(
278
+ controller.on("stateChange", ({ state: newState }) => {
279
+ setState((prev) => ({ ...prev, state: newState }));
280
+ onStateChange?.(newState);
281
+ })
282
+ );
283
+
284
+ unsubs.push(
285
+ controller.on("streamStateChange", ({ state: streamState }) => {
286
+ setState((prev) => ({
287
+ ...prev,
288
+ streamState,
289
+ metadata: controller.getMetadata(),
290
+ isEffectivelyLive: controller.isEffectivelyLive(),
291
+ shouldShowIdleScreen: controller.shouldShowIdleScreen(),
292
+ }));
293
+ onStreamStateChange?.(streamState);
294
+ })
295
+ );
296
+
297
+ unsubs.push(
298
+ controller.on("timeUpdate", ({ currentTime, duration }) => {
299
+ setState((prev) => ({ ...prev, currentTime, duration }));
300
+ })
301
+ );
302
+
303
+ unsubs.push(
304
+ controller.on("error", ({ error }) => {
305
+ setState((prev) => ({
306
+ ...prev,
307
+ error,
308
+ isPassiveError: controller.isPassiveError(),
309
+ }));
310
+ onError?.(error);
311
+ })
312
+ );
313
+
314
+ unsubs.push(
315
+ controller.on("errorCleared", () => {
316
+ setState((prev) => ({ ...prev, error: null, isPassiveError: false }));
317
+ })
318
+ );
319
+
320
+ unsubs.push(
321
+ controller.on("ready", ({ videoElement }) => {
322
+ setState((prev) => ({
323
+ ...prev,
324
+ videoElement,
325
+ endpoints: controller.getEndpoints(),
326
+ metadata: controller.getMetadata(),
327
+ streamInfo: controller.getStreamInfo(),
328
+ isEffectivelyLive: controller.isEffectivelyLive(),
329
+ shouldShowIdleScreen: controller.shouldShowIdleScreen(),
330
+ currentPlayerInfo: controller.getCurrentPlayerInfo(),
331
+ currentSourceInfo: controller.getCurrentSourceInfo(),
332
+ qualities: controller.getQualities(),
333
+ }));
334
+ onReady?.(videoElement);
335
+
336
+ // Set up video event listeners AFTER video is ready
337
+ // syncState is defined below - this closure captures it
338
+ const handleVideoEvent = () => {
339
+ if (controllerRef.current?.shouldSuppressVideoEvents?.()) return;
340
+ syncState();
341
+ };
342
+ videoElement.addEventListener("play", handleVideoEvent);
343
+ videoElement.addEventListener("pause", handleVideoEvent);
344
+ videoElement.addEventListener("waiting", handleVideoEvent);
345
+ videoElement.addEventListener("playing", handleVideoEvent);
346
+ unsubs.push(() => {
347
+ videoElement.removeEventListener("play", handleVideoEvent);
348
+ videoElement.removeEventListener("pause", handleVideoEvent);
349
+ videoElement.removeEventListener("waiting", handleVideoEvent);
350
+ videoElement.removeEventListener("playing", handleVideoEvent);
351
+ });
352
+ })
353
+ );
354
+
355
+ unsubs.push(
356
+ controller.on("playerSelected", ({ player: _player, source }) => {
357
+ setState((prev) => ({
358
+ ...prev,
359
+ currentPlayerInfo: controller.getCurrentPlayerInfo(),
360
+ currentSourceInfo: { url: source.url, type: source.type },
361
+ qualities: controller.getQualities(),
362
+ }));
363
+ })
364
+ );
365
+
366
+ unsubs.push(
367
+ controller.on("volumeChange", ({ volume, muted }) => {
368
+ setState((prev) => ({ ...prev, volume, isMuted: muted }));
369
+ })
370
+ );
371
+
372
+ unsubs.push(
373
+ controller.on("loopChange", ({ isLoopEnabled }) => {
374
+ setState((prev) => ({ ...prev, isLoopEnabled }));
375
+ })
376
+ );
377
+
378
+ unsubs.push(
379
+ controller.on("fullscreenChange", ({ isFullscreen }) => {
380
+ setState((prev) => ({ ...prev, isFullscreen }));
381
+ })
382
+ );
383
+
384
+ unsubs.push(
385
+ controller.on("pipChange", ({ isPiP }) => {
386
+ setState((prev) => ({ ...prev, isPiPActive: isPiP }));
387
+ })
388
+ );
389
+
390
+ unsubs.push(
391
+ controller.on("holdSpeedStart", ({ speed }) => {
392
+ setState((prev) => ({ ...prev, isHoldingSpeed: true, holdSpeed: speed }));
393
+ })
394
+ );
395
+
396
+ unsubs.push(
397
+ controller.on("holdSpeedEnd", () => {
398
+ setState((prev) => ({ ...prev, isHoldingSpeed: false }));
399
+ })
400
+ );
401
+
402
+ unsubs.push(
403
+ controller.on("hoverStart", () => {
404
+ setState((prev) => ({ ...prev, isHovering: true, shouldShowControls: true }));
405
+ })
406
+ );
407
+
408
+ unsubs.push(
409
+ controller.on("hoverEnd", () => {
410
+ setState((prev) => ({
411
+ ...prev,
412
+ isHovering: false,
413
+ shouldShowControls: controller.shouldShowControls(),
414
+ }));
415
+ })
416
+ );
417
+
418
+ unsubs.push(
419
+ controller.on("captionsChange", ({ enabled }) => {
420
+ setState((prev) => ({ ...prev, subtitlesEnabled: enabled }));
421
+ })
422
+ );
380
423
 
381
424
  // Attach controller to container
382
425
  // Note: Video event listeners are set up in the 'ready' handler above
383
- controller.attach(container).catch(err => {
384
- console.warn('[usePlayerController] Attach failed:', err);
426
+ controller.attach(container).catch((err) => {
427
+ console.warn("[usePlayerController] Attach failed:", err);
385
428
  });
386
429
 
387
430
  // Set initial state
388
- setState(prev => ({
431
+ setState((prev) => ({
389
432
  ...prev,
390
433
  isLoopEnabled: controller.isLoopEnabled(),
391
434
  }));
392
435
 
393
436
  return () => {
394
- unsubs.forEach(fn => fn());
437
+ unsubs.forEach((fn) => fn());
395
438
  controller.destroy();
396
439
  controllerRef.current = null;
397
440
  setState(initialState);
@@ -445,7 +488,7 @@ export function usePlayerController(
445
488
 
446
489
  const clearError = useCallback(() => {
447
490
  controllerRef.current?.clearError();
448
- setState(prev => ({ ...prev, error: null, isPassiveError: false }));
491
+ setState((prev) => ({ ...prev, error: null, isPassiveError: false }));
449
492
  }, []);
450
493
 
451
494
  const jumpToLive = useCallback(() => {
@@ -484,14 +527,17 @@ export function usePlayerController(
484
527
  controllerRef.current?.handleTouchStart();
485
528
  }, []);
486
529
 
487
- const setDevModeOptions = useCallback(async (options: {
488
- forcePlayer?: string;
489
- forceType?: string;
490
- forceSource?: number;
491
- playbackMode?: 'auto' | 'low-latency' | 'quality' | 'vod';
492
- }) => {
493
- await controllerRef.current?.setDevModeOptions(options);
494
- }, []);
530
+ const setDevModeOptions = useCallback(
531
+ async (options: {
532
+ forcePlayer?: string;
533
+ forceType?: string;
534
+ forceSource?: number;
535
+ playbackMode?: "auto" | "low-latency" | "quality" | "vod";
536
+ }) => {
537
+ await controllerRef.current?.setDevModeOptions(options);
538
+ },
539
+ []
540
+ );
495
541
 
496
542
  return {
497
543
  containerRef,
@@ -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);