@livepeer-frameworks/player-react 0.0.3

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 (88) hide show
  1. package/dist/cjs/index.js +2 -0
  2. package/dist/cjs/index.js.map +1 -0
  3. package/dist/esm/index.js +2 -0
  4. package/dist/esm/index.js.map +1 -0
  5. package/dist/types/components/DevModePanel.d.ts +47 -0
  6. package/dist/types/components/DvdLogo.d.ts +4 -0
  7. package/dist/types/components/Icons.d.ts +33 -0
  8. package/dist/types/components/IdleScreen.d.ts +16 -0
  9. package/dist/types/components/LoadingScreen.d.ts +6 -0
  10. package/dist/types/components/LogoOverlay.d.ts +11 -0
  11. package/dist/types/components/Player.d.ts +11 -0
  12. package/dist/types/components/PlayerControls.d.ts +60 -0
  13. package/dist/types/components/PlayerErrorBoundary.d.ts +23 -0
  14. package/dist/types/components/SeekBar.d.ts +33 -0
  15. package/dist/types/components/SkipIndicator.d.ts +14 -0
  16. package/dist/types/components/SpeedIndicator.d.ts +12 -0
  17. package/dist/types/components/StatsPanel.d.ts +31 -0
  18. package/dist/types/components/StreamStateOverlay.d.ts +24 -0
  19. package/dist/types/components/SubtitleRenderer.d.ts +69 -0
  20. package/dist/types/components/ThumbnailOverlay.d.ts +4 -0
  21. package/dist/types/components/TitleOverlay.d.ts +13 -0
  22. package/dist/types/components/players/DashJsPlayer.d.ts +18 -0
  23. package/dist/types/components/players/HlsJsPlayer.d.ts +18 -0
  24. package/dist/types/components/players/MewsWsPlayer/index.d.ts +18 -0
  25. package/dist/types/components/players/MistPlayer.d.ts +20 -0
  26. package/dist/types/components/players/MistWebRTCPlayer/index.d.ts +20 -0
  27. package/dist/types/components/players/NativePlayer.d.ts +19 -0
  28. package/dist/types/components/players/VideoJsPlayer.d.ts +18 -0
  29. package/dist/types/context/PlayerContext.d.ts +40 -0
  30. package/dist/types/context/index.d.ts +5 -0
  31. package/dist/types/hooks/useMetaTrack.d.ts +54 -0
  32. package/dist/types/hooks/usePlaybackQuality.d.ts +42 -0
  33. package/dist/types/hooks/usePlayerController.d.ts +163 -0
  34. package/dist/types/hooks/usePlayerSelection.d.ts +47 -0
  35. package/dist/types/hooks/useStreamState.d.ts +27 -0
  36. package/dist/types/hooks/useTelemetry.d.ts +57 -0
  37. package/dist/types/hooks/useViewerEndpoints.d.ts +14 -0
  38. package/dist/types/index.d.ts +33 -0
  39. package/dist/types/types.d.ts +94 -0
  40. package/dist/types/ui/badge.d.ts +9 -0
  41. package/dist/types/ui/button.d.ts +11 -0
  42. package/dist/types/ui/context-menu.d.ts +27 -0
  43. package/dist/types/ui/select.d.ts +10 -0
  44. package/dist/types/ui/slider.d.ts +13 -0
  45. package/package.json +71 -0
  46. package/src/assets/logomark.svg +56 -0
  47. package/src/components/DevModePanel.tsx +822 -0
  48. package/src/components/DvdLogo.tsx +201 -0
  49. package/src/components/Icons.tsx +282 -0
  50. package/src/components/IdleScreen.tsx +664 -0
  51. package/src/components/LoadingScreen.tsx +710 -0
  52. package/src/components/LogoOverlay.tsx +75 -0
  53. package/src/components/Player.tsx +419 -0
  54. package/src/components/PlayerControls.tsx +820 -0
  55. package/src/components/PlayerErrorBoundary.tsx +70 -0
  56. package/src/components/SeekBar.tsx +291 -0
  57. package/src/components/SkipIndicator.tsx +113 -0
  58. package/src/components/SpeedIndicator.tsx +57 -0
  59. package/src/components/StatsPanel.tsx +150 -0
  60. package/src/components/StreamStateOverlay.tsx +200 -0
  61. package/src/components/SubtitleRenderer.tsx +235 -0
  62. package/src/components/ThumbnailOverlay.tsx +90 -0
  63. package/src/components/TitleOverlay.tsx +48 -0
  64. package/src/components/players/DashJsPlayer.tsx +56 -0
  65. package/src/components/players/HlsJsPlayer.tsx +56 -0
  66. package/src/components/players/MewsWsPlayer/index.tsx +56 -0
  67. package/src/components/players/MistPlayer.tsx +60 -0
  68. package/src/components/players/MistWebRTCPlayer/index.tsx +59 -0
  69. package/src/components/players/NativePlayer.tsx +58 -0
  70. package/src/components/players/VideoJsPlayer.tsx +56 -0
  71. package/src/context/PlayerContext.tsx +71 -0
  72. package/src/context/index.ts +11 -0
  73. package/src/global.d.ts +4 -0
  74. package/src/hooks/useMetaTrack.ts +187 -0
  75. package/src/hooks/usePlaybackQuality.ts +126 -0
  76. package/src/hooks/usePlayerController.ts +525 -0
  77. package/src/hooks/usePlayerSelection.ts +117 -0
  78. package/src/hooks/useStreamState.ts +381 -0
  79. package/src/hooks/useTelemetry.ts +138 -0
  80. package/src/hooks/useViewerEndpoints.ts +120 -0
  81. package/src/index.tsx +75 -0
  82. package/src/player.css +2 -0
  83. package/src/types.ts +135 -0
  84. package/src/ui/badge.tsx +27 -0
  85. package/src/ui/button.tsx +47 -0
  86. package/src/ui/context-menu.tsx +193 -0
  87. package/src/ui/select.tsx +105 -0
  88. package/src/ui/slider.tsx +67 -0
@@ -0,0 +1,525 @@
1
+ /**
2
+ * usePlayerController.ts
3
+ *
4
+ * React hook that wraps PlayerController for declarative usage.
5
+ * Manages the complete player lifecycle and provides reactive state.
6
+ */
7
+
8
+ import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
9
+ import {
10
+ PlayerController,
11
+ type PlayerControllerConfig,
12
+ type PlayerControllerEvents,
13
+ type PlayerState,
14
+ type StreamState,
15
+ type StreamSource,
16
+ type StreamInfo,
17
+ type PlaybackQuality,
18
+ type ContentEndpoints,
19
+ type ContentMetadata,
20
+ type MistStreamInfo,
21
+ } from '@livepeer-frameworks/player-core';
22
+
23
+ // ============================================================================
24
+ // Types
25
+ // ============================================================================
26
+
27
+ export interface UsePlayerControllerConfig extends Omit<PlayerControllerConfig, 'playerManager'> {
28
+ /** Enable/disable the hook */
29
+ enabled?: boolean;
30
+ /** Callback when state changes */
31
+ onStateChange?: (state: PlayerState) => void;
32
+ /** Callback when stream state changes */
33
+ onStreamStateChange?: (state: StreamState) => void;
34
+ /** Callback when error occurs */
35
+ onError?: (error: string) => void;
36
+ /** Callback when ready */
37
+ onReady?: (videoElement: HTMLVideoElement) => void;
38
+ }
39
+
40
+ export interface PlayerControllerState {
41
+ /** Current player state */
42
+ state: PlayerState;
43
+ /** Stream state (for live streams) */
44
+ streamState: StreamState | null;
45
+ /** Resolved endpoints */
46
+ endpoints: ContentEndpoints | null;
47
+ /** Content metadata */
48
+ metadata: ContentMetadata | null;
49
+ /** Video element (null if not ready) */
50
+ videoElement: HTMLVideoElement | null;
51
+ /** Current time */
52
+ currentTime: number;
53
+ /** Duration */
54
+ duration: number;
55
+ /** Is playing */
56
+ isPlaying: boolean;
57
+ /** Is paused */
58
+ isPaused: boolean;
59
+ /** Is buffering */
60
+ isBuffering: boolean;
61
+ /** Is muted */
62
+ isMuted: boolean;
63
+ /** Volume (0-1) */
64
+ volume: number;
65
+ /** Error text */
66
+ error: string | null;
67
+ /** Is passive error */
68
+ isPassiveError: boolean;
69
+ /** Has playback ever started */
70
+ hasPlaybackStarted: boolean;
71
+ /** Is holding speed (2x gesture) */
72
+ isHoldingSpeed: boolean;
73
+ /** Current hold speed */
74
+ holdSpeed: number;
75
+ /** Is hovering (controls visible) */
76
+ isHovering: boolean;
77
+ /** Should show controls */
78
+ shouldShowControls: boolean;
79
+ /** Is loop enabled */
80
+ isLoopEnabled: boolean;
81
+ /** Is fullscreen */
82
+ isFullscreen: boolean;
83
+ /** Is PiP active */
84
+ isPiPActive: boolean;
85
+ /** Is effectively live (live or DVR recording) */
86
+ isEffectivelyLive: boolean;
87
+ /** Should show idle screen */
88
+ shouldShowIdleScreen: boolean;
89
+ /** Current player info */
90
+ currentPlayerInfo: { name: string; shortname: string } | null;
91
+ /** Current source info */
92
+ currentSourceInfo: { url: string; type: string } | null;
93
+ /** Playback quality metrics */
94
+ playbackQuality: PlaybackQuality | null;
95
+ /** Subtitles enabled */
96
+ subtitlesEnabled: boolean;
97
+ /** Available quality levels */
98
+ qualities: Array<{ id: string; label: string; bitrate?: number; width?: number; height?: number; isAuto?: boolean; active?: boolean }>;
99
+ /** Available text/caption tracks */
100
+ textTracks: Array<{ id: string; label: string; language?: string; active: boolean }>;
101
+ /** Stream info for player selection (sources + tracks) */
102
+ streamInfo: StreamInfo | null;
103
+ }
104
+
105
+ export interface UsePlayerControllerReturn {
106
+ /** Container ref to attach to your player container div */
107
+ containerRef: React.RefObject<HTMLDivElement | null>;
108
+ /** Current state (reactive) */
109
+ state: PlayerControllerState;
110
+ /** Controller instance (for direct method calls) */
111
+ controller: PlayerController | null;
112
+ /** Play */
113
+ play: () => Promise<void>;
114
+ /** Pause */
115
+ pause: () => void;
116
+ /** Toggle play/pause */
117
+ togglePlay: () => void;
118
+ /** Seek to time */
119
+ seek: (time: number) => void;
120
+ /** Seek by delta */
121
+ seekBy: (delta: number) => void;
122
+ /** Jump to live edge (for live streams) */
123
+ jumpToLive: () => void;
124
+ /** Set volume */
125
+ setVolume: (volume: number) => void;
126
+ /** Toggle mute */
127
+ toggleMute: () => void;
128
+ /** Toggle loop */
129
+ toggleLoop: () => void;
130
+ /** Toggle fullscreen */
131
+ toggleFullscreen: () => Promise<void>;
132
+ /** Toggle PiP */
133
+ togglePiP: () => Promise<void>;
134
+ /** Toggle subtitles */
135
+ toggleSubtitles: () => void;
136
+ /** Clear error */
137
+ clearError: () => void;
138
+ /** Retry playback */
139
+ retry: () => Promise<void>;
140
+ /** Reload player */
141
+ reload: () => Promise<void>;
142
+ /** Get qualities */
143
+ getQualities: () => Array<{ id: string; label: string; bitrate?: number }>;
144
+ /** Select quality */
145
+ selectQuality: (id: string) => void;
146
+ /** Handle mouse enter (for controls visibility) */
147
+ handleMouseEnter: () => void;
148
+ /** Handle mouse leave (for controls visibility) */
149
+ handleMouseLeave: () => void;
150
+ /** Handle mouse move (for controls visibility) */
151
+ handleMouseMove: () => void;
152
+ /** Handle touch start (for controls visibility) */
153
+ handleTouchStart: () => void;
154
+ /** Set dev mode options (force player, type, source) */
155
+ setDevModeOptions: (options: {
156
+ forcePlayer?: string;
157
+ forceType?: string;
158
+ forceSource?: number;
159
+ playbackMode?: 'auto' | 'low-latency' | 'quality' | 'vod';
160
+ }) => Promise<void>;
161
+ }
162
+
163
+ // ============================================================================
164
+ // Initial State
165
+ // ============================================================================
166
+
167
+ const initialState: PlayerControllerState = {
168
+ state: 'booting',
169
+ streamState: null,
170
+ endpoints: null,
171
+ metadata: null,
172
+ videoElement: null,
173
+ currentTime: 0,
174
+ duration: NaN,
175
+ isPlaying: false,
176
+ isPaused: true,
177
+ isBuffering: false,
178
+ isMuted: true,
179
+ volume: 1,
180
+ error: null,
181
+ isPassiveError: false,
182
+ hasPlaybackStarted: false,
183
+ isHoldingSpeed: false,
184
+ holdSpeed: 2,
185
+ isHovering: false,
186
+ shouldShowControls: false,
187
+ isLoopEnabled: false,
188
+ isFullscreen: false,
189
+ isPiPActive: false,
190
+ isEffectivelyLive: false,
191
+ shouldShowIdleScreen: true,
192
+ currentPlayerInfo: null,
193
+ currentSourceInfo: null,
194
+ playbackQuality: null,
195
+ subtitlesEnabled: false,
196
+ qualities: [],
197
+ textTracks: [],
198
+ streamInfo: null,
199
+ };
200
+
201
+ // ============================================================================
202
+ // Hook
203
+ // ============================================================================
204
+
205
+ export function usePlayerController(
206
+ config: UsePlayerControllerConfig
207
+ ): UsePlayerControllerReturn {
208
+ const { enabled = true, onStateChange, onStreamStateChange, onError, onReady, ...controllerConfig } = config;
209
+
210
+ const containerRef = useRef<HTMLDivElement>(null);
211
+ const controllerRef = useRef<PlayerController | null>(null);
212
+ const [state, setState] = useState<PlayerControllerState>(initialState);
213
+
214
+ // Stable config ref for effect dependencies
215
+ const configRef = useRef(controllerConfig);
216
+ configRef.current = controllerConfig;
217
+
218
+ // Create and attach controller
219
+ useEffect(() => {
220
+ if (!enabled) return;
221
+
222
+ const container = containerRef.current;
223
+ if (!container) return;
224
+
225
+ // Create controller
226
+ const controller = new PlayerController({
227
+ contentId: configRef.current.contentId,
228
+ contentType: configRef.current.contentType,
229
+ endpoints: configRef.current.endpoints,
230
+ gatewayUrl: configRef.current.gatewayUrl,
231
+ mistUrl: configRef.current.mistUrl,
232
+ authToken: configRef.current.authToken,
233
+ autoplay: configRef.current.autoplay,
234
+ muted: configRef.current.muted,
235
+ controls: configRef.current.controls,
236
+ poster: configRef.current.poster,
237
+ debug: configRef.current.debug,
238
+ });
239
+
240
+ controllerRef.current = controller;
241
+
242
+ // Subscribe to events
243
+ const unsubs: Array<() => void> = [];
244
+
245
+ // Sync state from controller - called on video events
246
+ const syncState = () => {
247
+ if (!controllerRef.current) return;
248
+ const c = controllerRef.current;
249
+ setState(prev => ({
250
+ ...prev,
251
+ isPlaying: c.isPlaying(),
252
+ isPaused: c.isPaused(),
253
+ isBuffering: c.isBuffering(),
254
+ isMuted: c.isMuted(),
255
+ volume: c.getVolume(),
256
+ hasPlaybackStarted: c.hasPlaybackStarted(),
257
+ shouldShowControls: c.shouldShowControls(),
258
+ shouldShowIdleScreen: c.shouldShowIdleScreen(),
259
+ playbackQuality: c.getPlaybackQuality(),
260
+ isLoopEnabled: c.isLoopEnabled(),
261
+ subtitlesEnabled: c.isSubtitlesEnabled(),
262
+ qualities: c.getQualities(),
263
+ streamInfo: c.getStreamInfo(),
264
+ }));
265
+ };
266
+
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
+ }));
380
+
381
+ // Attach controller to container
382
+ // 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);
385
+ });
386
+
387
+ // Set initial state
388
+ setState(prev => ({
389
+ ...prev,
390
+ isLoopEnabled: controller.isLoopEnabled(),
391
+ }));
392
+
393
+ return () => {
394
+ unsubs.forEach(fn => fn());
395
+ controller.destroy();
396
+ controllerRef.current = null;
397
+ setState(initialState);
398
+ };
399
+ }, [enabled, config.contentId, config.contentType]); // Re-create on content change
400
+
401
+ // Stable action callbacks
402
+ const play = useCallback(async () => {
403
+ await controllerRef.current?.play();
404
+ }, []);
405
+
406
+ const pause = useCallback(() => {
407
+ controllerRef.current?.pause();
408
+ }, []);
409
+
410
+ const togglePlay = useCallback(() => {
411
+ controllerRef.current?.togglePlay();
412
+ }, []);
413
+
414
+ const seek = useCallback((time: number) => {
415
+ controllerRef.current?.seek(time);
416
+ }, []);
417
+
418
+ const seekBy = useCallback((delta: number) => {
419
+ controllerRef.current?.seekBy(delta);
420
+ }, []);
421
+
422
+ const setVolume = useCallback((volume: number) => {
423
+ controllerRef.current?.setVolume(volume);
424
+ }, []);
425
+
426
+ const toggleMute = useCallback(() => {
427
+ controllerRef.current?.toggleMute();
428
+ }, []);
429
+
430
+ const toggleLoop = useCallback(() => {
431
+ controllerRef.current?.toggleLoop();
432
+ }, []);
433
+
434
+ const toggleFullscreen = useCallback(async () => {
435
+ await controllerRef.current?.toggleFullscreen();
436
+ }, []);
437
+
438
+ const togglePiP = useCallback(async () => {
439
+ await controllerRef.current?.togglePictureInPicture();
440
+ }, []);
441
+
442
+ const toggleSubtitles = useCallback(() => {
443
+ controllerRef.current?.toggleSubtitles();
444
+ }, []);
445
+
446
+ const clearError = useCallback(() => {
447
+ controllerRef.current?.clearError();
448
+ setState(prev => ({ ...prev, error: null, isPassiveError: false }));
449
+ }, []);
450
+
451
+ const jumpToLive = useCallback(() => {
452
+ controllerRef.current?.jumpToLive();
453
+ }, []);
454
+
455
+ const retry = useCallback(async () => {
456
+ await controllerRef.current?.retry();
457
+ }, []);
458
+
459
+ const reload = useCallback(async () => {
460
+ await controllerRef.current?.reload();
461
+ }, []);
462
+
463
+ const getQualities = useCallback(() => {
464
+ return controllerRef.current?.getQualities() ?? [];
465
+ }, []);
466
+
467
+ const selectQuality = useCallback((id: string) => {
468
+ controllerRef.current?.selectQuality(id);
469
+ }, []);
470
+
471
+ const handleMouseEnter = useCallback(() => {
472
+ controllerRef.current?.handleMouseEnter();
473
+ }, []);
474
+
475
+ const handleMouseLeave = useCallback(() => {
476
+ controllerRef.current?.handleMouseLeave();
477
+ }, []);
478
+
479
+ const handleMouseMove = useCallback(() => {
480
+ controllerRef.current?.handleMouseMove();
481
+ }, []);
482
+
483
+ const handleTouchStart = useCallback(() => {
484
+ controllerRef.current?.handleTouchStart();
485
+ }, []);
486
+
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
+ }, []);
495
+
496
+ return {
497
+ containerRef,
498
+ state,
499
+ controller: controllerRef.current,
500
+ play,
501
+ pause,
502
+ togglePlay,
503
+ seek,
504
+ seekBy,
505
+ jumpToLive,
506
+ setVolume,
507
+ toggleMute,
508
+ toggleLoop,
509
+ toggleFullscreen,
510
+ togglePiP,
511
+ toggleSubtitles,
512
+ clearError,
513
+ retry,
514
+ reload,
515
+ getQualities,
516
+ selectQuality,
517
+ handleMouseEnter,
518
+ handleMouseLeave,
519
+ handleMouseMove,
520
+ handleTouchStart,
521
+ setDevModeOptions,
522
+ };
523
+ }
524
+
525
+ export default usePlayerController;
@@ -0,0 +1,117 @@
1
+ /**
2
+ * usePlayerSelection
3
+ *
4
+ * React hook for subscribing to PlayerManager selection events.
5
+ * Uses event-driven updates instead of polling - no render spam.
6
+ */
7
+
8
+ import { useState, useEffect, useCallback } from 'react';
9
+ import type {
10
+ PlayerManager,
11
+ PlayerSelection,
12
+ PlayerCombination,
13
+ StreamInfo,
14
+ PlaybackMode,
15
+ } from '@livepeer-frameworks/player-core';
16
+
17
+ export interface UsePlayerSelectionOptions {
18
+ /** Stream info to compute selections for */
19
+ streamInfo: StreamInfo | null;
20
+ /** Playback mode override */
21
+ playbackMode?: PlaybackMode;
22
+ /** Enable debug logging */
23
+ debug?: boolean;
24
+ }
25
+
26
+ export interface UsePlayerSelectionReturn {
27
+ /** Current best selection (null if no compatible player) */
28
+ selection: PlayerSelection | null;
29
+ /** All player+source combinations with scores */
30
+ combinations: PlayerCombination[];
31
+ /** Whether initial computation has completed */
32
+ ready: boolean;
33
+ /** Force recomputation (invalidates cache) */
34
+ refresh: () => void;
35
+ }
36
+
37
+ /**
38
+ * Subscribe to player selection changes from a PlayerManager.
39
+ *
40
+ * This hook uses the event system in PlayerManager, which means:
41
+ * - Initial computation happens once when streamInfo is provided
42
+ * - Updates only fire when selection actually changes (different player+source)
43
+ * - No render spam from React strict mode or frequent re-renders
44
+ *
45
+ * @example
46
+ * ```tsx
47
+ * const { selection, combinations, ready } = usePlayerSelection(globalPlayerManager, {
48
+ * streamInfo,
49
+ * playbackMode: 'auto',
50
+ * });
51
+ *
52
+ * if (!ready) return <Loading />;
53
+ * if (!selection) return <NoPlayerAvailable />;
54
+ *
55
+ * return <div>Selected: {selection.player} + {selection.source.type}</div>;
56
+ * ```
57
+ */
58
+ export function usePlayerSelection(
59
+ manager: PlayerManager,
60
+ options: UsePlayerSelectionOptions
61
+ ): UsePlayerSelectionReturn {
62
+ const { streamInfo, playbackMode, debug } = options;
63
+
64
+ const [selection, setSelection] = useState<PlayerSelection | null>(null);
65
+ const [combinations, setCombinations] = useState<PlayerCombination[]>([]);
66
+ const [ready, setReady] = useState(false);
67
+
68
+ // Subscribe to events
69
+ useEffect(() => {
70
+ const unsubSelection = manager.on('selection-changed', (sel) => {
71
+ if (debug) {
72
+ console.log('[usePlayerSelection] Selection changed:', sel?.player, sel?.source?.type);
73
+ }
74
+ setSelection(sel);
75
+ });
76
+
77
+ const unsubCombos = manager.on('combinations-updated', (combos) => {
78
+ if (debug) {
79
+ console.log('[usePlayerSelection] Combinations updated:', combos.length);
80
+ }
81
+ setCombinations(combos);
82
+ setReady(true);
83
+ });
84
+
85
+ return () => {
86
+ unsubSelection();
87
+ unsubCombos();
88
+ };
89
+ }, [manager, debug]);
90
+
91
+ // Trigger initial computation when streamInfo changes
92
+ useEffect(() => {
93
+ if (!streamInfo) {
94
+ setSelection(null);
95
+ setCombinations([]);
96
+ setReady(false);
97
+ return;
98
+ }
99
+
100
+ // This will use cache if available, or compute + emit events if not
101
+ manager.getAllCombinations(streamInfo, playbackMode);
102
+ }, [manager, streamInfo, playbackMode]);
103
+
104
+ // Manual refresh function
105
+ const refresh = useCallback(() => {
106
+ if (!streamInfo) return;
107
+ manager.invalidateCache();
108
+ manager.getAllCombinations(streamInfo, playbackMode);
109
+ }, [manager, streamInfo, playbackMode]);
110
+
111
+ return {
112
+ selection,
113
+ combinations,
114
+ ready,
115
+ refresh,
116
+ };
117
+ }