@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.
- package/dist/cjs/index.js +2 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/types/components/DevModePanel.d.ts +47 -0
- package/dist/types/components/DvdLogo.d.ts +4 -0
- package/dist/types/components/Icons.d.ts +33 -0
- package/dist/types/components/IdleScreen.d.ts +16 -0
- package/dist/types/components/LoadingScreen.d.ts +6 -0
- package/dist/types/components/LogoOverlay.d.ts +11 -0
- package/dist/types/components/Player.d.ts +11 -0
- package/dist/types/components/PlayerControls.d.ts +60 -0
- package/dist/types/components/PlayerErrorBoundary.d.ts +23 -0
- package/dist/types/components/SeekBar.d.ts +33 -0
- package/dist/types/components/SkipIndicator.d.ts +14 -0
- package/dist/types/components/SpeedIndicator.d.ts +12 -0
- package/dist/types/components/StatsPanel.d.ts +31 -0
- package/dist/types/components/StreamStateOverlay.d.ts +24 -0
- package/dist/types/components/SubtitleRenderer.d.ts +69 -0
- package/dist/types/components/ThumbnailOverlay.d.ts +4 -0
- package/dist/types/components/TitleOverlay.d.ts +13 -0
- package/dist/types/components/players/DashJsPlayer.d.ts +18 -0
- package/dist/types/components/players/HlsJsPlayer.d.ts +18 -0
- package/dist/types/components/players/MewsWsPlayer/index.d.ts +18 -0
- package/dist/types/components/players/MistPlayer.d.ts +20 -0
- package/dist/types/components/players/MistWebRTCPlayer/index.d.ts +20 -0
- package/dist/types/components/players/NativePlayer.d.ts +19 -0
- package/dist/types/components/players/VideoJsPlayer.d.ts +18 -0
- package/dist/types/context/PlayerContext.d.ts +40 -0
- package/dist/types/context/index.d.ts +5 -0
- package/dist/types/hooks/useMetaTrack.d.ts +54 -0
- package/dist/types/hooks/usePlaybackQuality.d.ts +42 -0
- package/dist/types/hooks/usePlayerController.d.ts +163 -0
- package/dist/types/hooks/usePlayerSelection.d.ts +47 -0
- package/dist/types/hooks/useStreamState.d.ts +27 -0
- package/dist/types/hooks/useTelemetry.d.ts +57 -0
- package/dist/types/hooks/useViewerEndpoints.d.ts +14 -0
- package/dist/types/index.d.ts +33 -0
- package/dist/types/types.d.ts +94 -0
- package/dist/types/ui/badge.d.ts +9 -0
- package/dist/types/ui/button.d.ts +11 -0
- package/dist/types/ui/context-menu.d.ts +27 -0
- package/dist/types/ui/select.d.ts +10 -0
- package/dist/types/ui/slider.d.ts +13 -0
- package/package.json +71 -0
- package/src/assets/logomark.svg +56 -0
- package/src/components/DevModePanel.tsx +822 -0
- package/src/components/DvdLogo.tsx +201 -0
- package/src/components/Icons.tsx +282 -0
- package/src/components/IdleScreen.tsx +664 -0
- package/src/components/LoadingScreen.tsx +710 -0
- package/src/components/LogoOverlay.tsx +75 -0
- package/src/components/Player.tsx +419 -0
- package/src/components/PlayerControls.tsx +820 -0
- package/src/components/PlayerErrorBoundary.tsx +70 -0
- package/src/components/SeekBar.tsx +291 -0
- package/src/components/SkipIndicator.tsx +113 -0
- package/src/components/SpeedIndicator.tsx +57 -0
- package/src/components/StatsPanel.tsx +150 -0
- package/src/components/StreamStateOverlay.tsx +200 -0
- package/src/components/SubtitleRenderer.tsx +235 -0
- package/src/components/ThumbnailOverlay.tsx +90 -0
- package/src/components/TitleOverlay.tsx +48 -0
- package/src/components/players/DashJsPlayer.tsx +56 -0
- package/src/components/players/HlsJsPlayer.tsx +56 -0
- package/src/components/players/MewsWsPlayer/index.tsx +56 -0
- package/src/components/players/MistPlayer.tsx +60 -0
- package/src/components/players/MistWebRTCPlayer/index.tsx +59 -0
- package/src/components/players/NativePlayer.tsx +58 -0
- package/src/components/players/VideoJsPlayer.tsx +56 -0
- package/src/context/PlayerContext.tsx +71 -0
- package/src/context/index.ts +11 -0
- package/src/global.d.ts +4 -0
- package/src/hooks/useMetaTrack.ts +187 -0
- package/src/hooks/usePlaybackQuality.ts +126 -0
- package/src/hooks/usePlayerController.ts +525 -0
- package/src/hooks/usePlayerSelection.ts +117 -0
- package/src/hooks/useStreamState.ts +381 -0
- package/src/hooks/useTelemetry.ts +138 -0
- package/src/hooks/useViewerEndpoints.ts +120 -0
- package/src/index.tsx +75 -0
- package/src/player.css +2 -0
- package/src/types.ts +135 -0
- package/src/ui/badge.tsx +27 -0
- package/src/ui/button.tsx +47 -0
- package/src/ui/context-menu.tsx +193 -0
- package/src/ui/select.tsx +105 -0
- 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
|
+
}
|