@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,56 @@
1
+ /**
2
+ * MEWS WebSocket Player - React Wrapper
3
+ *
4
+ * Low-latency WebSocket MP4 streaming using MediaSource Extensions.
5
+ * The implementation is in @livepeer-frameworks/player-core.
6
+ */
7
+
8
+ import React, { useEffect, useRef } from 'react';
9
+ import { MewsWsPlayerImpl } from '@livepeer-frameworks/player-core';
10
+
11
+ // Re-export the implementation from core for backwards compatibility
12
+ export { MewsWsPlayerImpl };
13
+
14
+ type Props = {
15
+ wsUrl: string;
16
+ muted?: boolean;
17
+ autoPlay?: boolean;
18
+ controls?: boolean;
19
+ onError?: (e: Error) => void;
20
+ };
21
+
22
+ // React component wrapper
23
+ const MewsWsPlayer: React.FC<Props> = ({
24
+ wsUrl,
25
+ muted = true,
26
+ autoPlay = true,
27
+ controls = true,
28
+ onError
29
+ }) => {
30
+ const containerRef = useRef<HTMLDivElement>(null);
31
+ const playerRef = useRef<MewsWsPlayerImpl | null>(null);
32
+
33
+ useEffect(() => {
34
+ if (!containerRef.current) return;
35
+
36
+ const player = new MewsWsPlayerImpl();
37
+ playerRef.current = player;
38
+
39
+ player.initialize(
40
+ containerRef.current,
41
+ { url: wsUrl, type: 'ws/video/mp4' },
42
+ { autoplay: autoPlay, muted, controls }
43
+ ).catch((e) => {
44
+ onError?.(e instanceof Error ? e : new Error(String(e)));
45
+ });
46
+
47
+ return () => {
48
+ player.destroy();
49
+ playerRef.current = null;
50
+ };
51
+ }, [wsUrl, muted, autoPlay, controls, onError]);
52
+
53
+ return <div ref={containerRef} style={{ width: '100%', height: '100%' }} />;
54
+ };
55
+
56
+ export default MewsWsPlayer;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * MistServer Legacy Player - React Wrapper
3
+ *
4
+ * Fallback player using MistServer's native player.js.
5
+ * The implementation is in @livepeer-frameworks/player-core.
6
+ */
7
+
8
+ import React, { useEffect, useRef } from 'react';
9
+ import { MistPlayerImpl } from '@livepeer-frameworks/player-core';
10
+
11
+ // Re-export the implementation from core for backwards compatibility
12
+ export { MistPlayerImpl };
13
+
14
+ type Props = {
15
+ src: string;
16
+ streamName?: string;
17
+ muted?: boolean;
18
+ autoPlay?: boolean;
19
+ controls?: boolean;
20
+ devMode?: boolean;
21
+ onError?: (e: Error) => void;
22
+ };
23
+
24
+ // React component wrapper
25
+ const MistPlayer: React.FC<Props> = ({
26
+ src,
27
+ streamName,
28
+ muted = true,
29
+ autoPlay = true,
30
+ controls = true,
31
+ devMode = false,
32
+ onError
33
+ }) => {
34
+ const containerRef = useRef<HTMLDivElement>(null);
35
+ const playerRef = useRef<MistPlayerImpl | null>(null);
36
+
37
+ useEffect(() => {
38
+ if (!containerRef.current) return;
39
+
40
+ const player = new MistPlayerImpl();
41
+ playerRef.current = player;
42
+
43
+ player.initialize(
44
+ containerRef.current,
45
+ { url: src, type: 'mist/legacy', streamName },
46
+ { autoplay: autoPlay, muted, controls, devMode }
47
+ ).catch((e) => {
48
+ onError?.(e instanceof Error ? e : new Error(String(e)));
49
+ });
50
+
51
+ return () => {
52
+ player.destroy();
53
+ playerRef.current = null;
54
+ };
55
+ }, [src, streamName, muted, autoPlay, controls, devMode, onError]);
56
+
57
+ return <div ref={containerRef} style={{ width: '100%', height: '100%' }} />;
58
+ };
59
+
60
+ export default MistPlayer;
@@ -0,0 +1,59 @@
1
+ /**
2
+ * MistWebRTC Player - React Wrapper
3
+ *
4
+ * MistServer native WebRTC with signaling for DVR support.
5
+ * The implementation is in @livepeer-frameworks/player-core.
6
+ */
7
+
8
+ import React, { useRef, useEffect } from 'react';
9
+ import { MistWebRTCPlayerImpl } from '@livepeer-frameworks/player-core';
10
+
11
+ // Re-export the implementation from core for backwards compatibility
12
+ export { MistWebRTCPlayerImpl };
13
+
14
+ interface Props {
15
+ src: string;
16
+ autoPlay?: boolean;
17
+ muted?: boolean;
18
+ controls?: boolean;
19
+ poster?: string;
20
+ onReady?: (video: HTMLVideoElement) => void;
21
+ onError?: (error: Error) => void;
22
+ }
23
+
24
+ export const MistWebRTCPlayer: React.FC<Props> = ({
25
+ src,
26
+ autoPlay = true,
27
+ muted = true,
28
+ controls = true,
29
+ poster,
30
+ onReady,
31
+ onError,
32
+ }) => {
33
+ const containerRef = useRef<HTMLDivElement>(null);
34
+ const playerRef = useRef<MistWebRTCPlayerImpl | null>(null);
35
+
36
+ useEffect(() => {
37
+ if (!containerRef.current) return;
38
+
39
+ const player = new MistWebRTCPlayerImpl();
40
+ playerRef.current = player;
41
+
42
+ player.initialize(
43
+ containerRef.current,
44
+ { url: src, type: 'webrtc' },
45
+ { autoplay: autoPlay, muted, controls, poster, onReady, onError: (e) => onError?.(typeof e === 'string' ? new Error(e) : e) }
46
+ ).catch((e) => {
47
+ onError?.(e instanceof Error ? e : new Error(String(e)));
48
+ });
49
+
50
+ return () => {
51
+ player.destroy();
52
+ playerRef.current = null;
53
+ };
54
+ }, [src, autoPlay, muted, controls, poster, onReady, onError]);
55
+
56
+ return <div ref={containerRef} style={{ width: '100%', height: '100%' }} />;
57
+ };
58
+
59
+ export default MistWebRTCPlayerImpl;
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Native Player - React Wrapper
3
+ *
4
+ * HTML5 video and WHEP WebRTC playback.
5
+ * The implementation is in @livepeer-frameworks/player-core.
6
+ */
7
+
8
+ import React, { useEffect, useRef } from 'react';
9
+ import { NativePlayerImpl, DirectPlaybackPlayerImpl } from '@livepeer-frameworks/player-core';
10
+
11
+ // Re-export the implementations from core for backwards compatibility
12
+ export { NativePlayerImpl, DirectPlaybackPlayerImpl };
13
+
14
+ type Props = {
15
+ src: string;
16
+ type?: string;
17
+ muted?: boolean;
18
+ autoPlay?: boolean;
19
+ controls?: boolean;
20
+ onError?: (e: Error) => void;
21
+ };
22
+
23
+ // React component wrapper
24
+ const NativePlayer: React.FC<Props> = ({
25
+ src,
26
+ type = 'html5/video/mp4',
27
+ muted = true,
28
+ autoPlay = true,
29
+ controls = true,
30
+ onError
31
+ }) => {
32
+ const containerRef = useRef<HTMLDivElement>(null);
33
+ const playerRef = useRef<NativePlayerImpl | null>(null);
34
+
35
+ useEffect(() => {
36
+ if (!containerRef.current) return;
37
+
38
+ const player = new NativePlayerImpl();
39
+ playerRef.current = player;
40
+
41
+ player.initialize(
42
+ containerRef.current,
43
+ { url: src, type },
44
+ { autoplay: autoPlay, muted, controls }
45
+ ).catch((e) => {
46
+ onError?.(e instanceof Error ? e : new Error(String(e)));
47
+ });
48
+
49
+ return () => {
50
+ player.destroy();
51
+ playerRef.current = null;
52
+ };
53
+ }, [src, type, muted, autoPlay, controls, onError]);
54
+
55
+ return <div ref={containerRef} style={{ width: '100%', height: '100%' }} />;
56
+ };
57
+
58
+ export default NativePlayer;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Video.js Player - React Wrapper
3
+ *
4
+ * HLS streaming via Video.js with VHS (videojs-http-streaming).
5
+ * The implementation is in @livepeer-frameworks/player-core.
6
+ */
7
+
8
+ import React, { useEffect, useRef } from 'react';
9
+ import { VideoJsPlayerImpl } from '@livepeer-frameworks/player-core';
10
+
11
+ // Re-export the implementation from core for backwards compatibility
12
+ export { VideoJsPlayerImpl };
13
+
14
+ type Props = {
15
+ src: string;
16
+ muted?: boolean;
17
+ autoPlay?: boolean;
18
+ controls?: boolean;
19
+ onError?: (e: Error) => void;
20
+ };
21
+
22
+ // React component wrapper
23
+ const VideoJsPlayer: React.FC<Props> = ({
24
+ src,
25
+ muted = true,
26
+ autoPlay = true,
27
+ controls = true,
28
+ onError
29
+ }) => {
30
+ const containerRef = useRef<HTMLDivElement>(null);
31
+ const playerRef = useRef<VideoJsPlayerImpl | null>(null);
32
+
33
+ useEffect(() => {
34
+ if (!containerRef.current) return;
35
+
36
+ const player = new VideoJsPlayerImpl();
37
+ playerRef.current = player;
38
+
39
+ player.initialize(
40
+ containerRef.current,
41
+ { url: src, type: 'html5/application/vnd.apple.mpegurl' },
42
+ { autoplay: autoPlay, muted, controls }
43
+ ).catch((e) => {
44
+ onError?.(e instanceof Error ? e : new Error(String(e)));
45
+ });
46
+
47
+ return () => {
48
+ player.destroy();
49
+ playerRef.current = null;
50
+ };
51
+ }, [src, muted, autoPlay, controls, onError]);
52
+
53
+ return <div ref={containerRef} style={{ width: '100%', height: '100%' }} />;
54
+ };
55
+
56
+ export default VideoJsPlayer;
@@ -0,0 +1,71 @@
1
+ /**
2
+ * PlayerContext
3
+ *
4
+ * React context for sharing PlayerController state across components.
5
+ * Follows the "context wraps hook" pattern (same as npm_studio).
6
+ *
7
+ * Usage:
8
+ * ```tsx
9
+ * <PlayerProvider config={{ contentId: 'stream-1', contentType: 'live' }}>
10
+ * <PlayerControls />
11
+ * </PlayerProvider>
12
+ * ```
13
+ */
14
+
15
+ import React, { createContext, useContext, type ReactNode } from 'react';
16
+ import {
17
+ usePlayerController,
18
+ type UsePlayerControllerConfig,
19
+ type UsePlayerControllerReturn,
20
+ } from '../hooks/usePlayerController';
21
+
22
+ // Context holds the full hook return value
23
+ const PlayerContext = createContext<UsePlayerControllerReturn | null>(null);
24
+
25
+ export interface PlayerProviderProps {
26
+ children: ReactNode;
27
+ /** Configuration for the player controller */
28
+ config: UsePlayerControllerConfig;
29
+ }
30
+
31
+ /**
32
+ * Provider component that wraps Player and its controls.
33
+ * Calls usePlayerController internally and shares state via context.
34
+ */
35
+ export function PlayerProvider({ children, config }: PlayerProviderProps) {
36
+ const playerController = usePlayerController(config);
37
+
38
+ return (
39
+ <PlayerContext.Provider value={playerController}>
40
+ {children}
41
+ </PlayerContext.Provider>
42
+ );
43
+ }
44
+
45
+ /**
46
+ * Hook to access player context.
47
+ * Must be used within a PlayerProvider.
48
+ */
49
+ export function usePlayerContext(): UsePlayerControllerReturn {
50
+ const context = useContext(PlayerContext);
51
+ if (!context) {
52
+ throw new Error('usePlayerContext must be used within a PlayerProvider');
53
+ }
54
+ return context;
55
+ }
56
+
57
+ /**
58
+ * Hook to optionally access player context.
59
+ * Returns null if not within a PlayerProvider (no error thrown).
60
+ * Use this when component may or may not be within a PlayerProvider.
61
+ */
62
+ export function usePlayerContextOptional(): UsePlayerControllerReturn | null {
63
+ return useContext(PlayerContext);
64
+ }
65
+
66
+ // Export context for advanced use cases
67
+ export { PlayerContext };
68
+
69
+ // Type exports
70
+ export type { UsePlayerControllerReturn as PlayerContextValue };
71
+ export type { UsePlayerControllerConfig };
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Context exports
3
+ */
4
+
5
+ export {
6
+ PlayerProvider,
7
+ usePlayerContext,
8
+ usePlayerContextOptional,
9
+ PlayerContext
10
+ } from './PlayerContext';
11
+ export type { PlayerContextValue, UsePlayerControllerConfig } from './PlayerContext';
@@ -0,0 +1,4 @@
1
+ declare module "*.svg" {
2
+ const content: string;
3
+ export default content;
4
+ }
@@ -0,0 +1,187 @@
1
+ import { useEffect, useState, useRef, useCallback } from 'react';
2
+ import { MetaTrackManager, type MetaTrackSubscription, type MetaTrackEvent } from '@livepeer-frameworks/player-core';
3
+ import type { UseMetaTrackOptions } from '../types';
4
+
5
+ export interface UseMetaTrackReturn {
6
+ /** Whether connected to MistServer WebSocket */
7
+ isConnected: boolean;
8
+ /** Connection state */
9
+ connectionState: 'disconnected' | 'connecting' | 'connected' | 'reconnecting';
10
+ /** List of subscribed track IDs */
11
+ subscribedTracks: string[];
12
+ /** Subscribe to a meta track */
13
+ subscribe: (trackId: string, callback: (event: MetaTrackEvent) => void) => () => void;
14
+ /** Unsubscribe from a meta track */
15
+ unsubscribe: (trackId: string, callback: (event: MetaTrackEvent) => void) => void;
16
+ /** Manually connect */
17
+ connect: () => void;
18
+ /** Manually disconnect */
19
+ disconnect: () => void;
20
+ /** Update playback time for timed event dispatch (call on video timeupdate) */
21
+ setPlaybackTime: (timeInSeconds: number) => void;
22
+ /** Handle seek event (call on video seeking/seeked) */
23
+ onSeek: (newTimeInSeconds: number) => void;
24
+ }
25
+
26
+ /**
27
+ * Hook for subscribing to real-time metadata from MistServer
28
+ *
29
+ * Uses native MistServer WebSocket protocol for low-latency metadata delivery:
30
+ * - Subtitles/captions
31
+ * - Live scores
32
+ * - Timed events
33
+ * - Chapter markers
34
+ *
35
+ * @example
36
+ * ```tsx
37
+ * const { isConnected, subscribe } = useMetaTrack({
38
+ * mistBaseUrl: 'https://mist.example.com',
39
+ * streamName: 'my-stream',
40
+ * enabled: true,
41
+ * });
42
+ *
43
+ * useEffect(() => {
44
+ * if (!isConnected) return;
45
+ *
46
+ * const unsubscribe = subscribe('1', (event) => {
47
+ * if (event.type === 'subtitle') {
48
+ * setSubtitle(event.data as SubtitleCue);
49
+ * }
50
+ * });
51
+ *
52
+ * return unsubscribe;
53
+ * }, [isConnected, subscribe]);
54
+ * ```
55
+ */
56
+ export function useMetaTrack(options: UseMetaTrackOptions): UseMetaTrackReturn {
57
+ const {
58
+ mistBaseUrl,
59
+ streamName,
60
+ subscriptions: initialSubscriptions,
61
+ enabled = true,
62
+ } = options;
63
+
64
+ const [isConnected, setIsConnected] = useState(false);
65
+ const [connectionState, setConnectionState] = useState<'disconnected' | 'connecting' | 'connected' | 'reconnecting'>('disconnected');
66
+ const [subscribedTracks, setSubscribedTracks] = useState<string[]>([]);
67
+ const managerRef = useRef<MetaTrackManager | null>(null);
68
+
69
+ // Create manager instance
70
+ useEffect(() => {
71
+ if (!enabled || !mistBaseUrl || !streamName) {
72
+ if (managerRef.current) {
73
+ managerRef.current.disconnect();
74
+ managerRef.current = null;
75
+ }
76
+ setIsConnected(false);
77
+ setConnectionState('disconnected');
78
+ return;
79
+ }
80
+
81
+ managerRef.current = new MetaTrackManager({
82
+ mistBaseUrl,
83
+ streamName,
84
+ subscriptions: initialSubscriptions,
85
+ debug: false,
86
+ });
87
+
88
+ // Start polling connection state
89
+ const pollState = () => {
90
+ if (managerRef.current) {
91
+ const state = managerRef.current.getState();
92
+ setConnectionState(state);
93
+ setIsConnected(state === 'connected');
94
+ setSubscribedTracks(managerRef.current.getSubscribedTracks());
95
+ }
96
+ };
97
+
98
+ const pollInterval = setInterval(pollState, 500);
99
+
100
+ // Connect
101
+ managerRef.current.connect();
102
+ pollState();
103
+
104
+ return () => {
105
+ clearInterval(pollInterval);
106
+ if (managerRef.current) {
107
+ managerRef.current.disconnect();
108
+ managerRef.current = null;
109
+ }
110
+ setIsConnected(false);
111
+ setConnectionState('disconnected');
112
+ };
113
+ }, [enabled, mistBaseUrl, streamName, initialSubscriptions]);
114
+
115
+ /**
116
+ * Subscribe to a meta track
117
+ */
118
+ const subscribe = useCallback((trackId: string, callback: (event: MetaTrackEvent) => void): () => void => {
119
+ if (!managerRef.current) {
120
+ return () => {};
121
+ }
122
+
123
+ const unsubscribe = managerRef.current.subscribe(trackId, callback);
124
+ setSubscribedTracks(managerRef.current.getSubscribedTracks());
125
+
126
+ return () => {
127
+ unsubscribe();
128
+ if (managerRef.current) {
129
+ setSubscribedTracks(managerRef.current.getSubscribedTracks());
130
+ }
131
+ };
132
+ }, []);
133
+
134
+ /**
135
+ * Unsubscribe from a meta track
136
+ */
137
+ const unsubscribe = useCallback((trackId: string, callback: (event: MetaTrackEvent) => void) => {
138
+ if (managerRef.current) {
139
+ managerRef.current.unsubscribe(trackId, callback);
140
+ setSubscribedTracks(managerRef.current.getSubscribedTracks());
141
+ }
142
+ }, []);
143
+
144
+ /**
145
+ * Manually connect
146
+ */
147
+ const connect = useCallback(() => {
148
+ managerRef.current?.connect();
149
+ }, []);
150
+
151
+ /**
152
+ * Manually disconnect
153
+ */
154
+ const disconnect = useCallback(() => {
155
+ managerRef.current?.disconnect();
156
+ }, []);
157
+
158
+ /**
159
+ * Update playback time for timed event dispatch
160
+ * Call this on video timeupdate events to keep subtitle/chapter timing in sync
161
+ */
162
+ const setPlaybackTime = useCallback((timeInSeconds: number) => {
163
+ managerRef.current?.setPlaybackTime(timeInSeconds);
164
+ }, []);
165
+
166
+ /**
167
+ * Handle seek event - clears buffered events and resets state
168
+ * Call this on video seeking/seeked events
169
+ */
170
+ const onSeek = useCallback((newTimeInSeconds: number) => {
171
+ managerRef.current?.onSeek(newTimeInSeconds);
172
+ }, []);
173
+
174
+ return {
175
+ isConnected,
176
+ connectionState,
177
+ subscribedTracks,
178
+ subscribe,
179
+ unsubscribe,
180
+ connect,
181
+ disconnect,
182
+ setPlaybackTime,
183
+ onSeek,
184
+ };
185
+ }
186
+
187
+ export default useMetaTrack;
@@ -0,0 +1,126 @@
1
+ import { useEffect, useState, useRef, useCallback } from 'react';
2
+ import { QualityMonitor, type PlaybackQuality } from '@livepeer-frameworks/player-core';
3
+ import type { UsePlaybackQualityOptions } from '../types';
4
+
5
+ /**
6
+ * Hook to monitor video playback quality
7
+ *
8
+ * Tracks:
9
+ * - Buffer health (seconds ahead)
10
+ * - Stall count
11
+ * - Frame drop rate
12
+ * - Estimated bitrate
13
+ * - Latency (live streams)
14
+ * - Composite quality score (0-100)
15
+ *
16
+ * @example
17
+ * ```tsx
18
+ * const { quality, isMonitoring } = usePlaybackQuality({
19
+ * videoElement,
20
+ * enabled: true,
21
+ * thresholds: { minScore: 60 },
22
+ * onQualityDegraded: (q) => console.log('Quality dropped:', q.score),
23
+ * });
24
+ *
25
+ * return <div>Quality: {quality?.score ?? '--'}</div>;
26
+ * ```
27
+ */
28
+ export function usePlaybackQuality(options: UsePlaybackQualityOptions) {
29
+ const {
30
+ videoElement,
31
+ enabled = true,
32
+ sampleInterval = 500,
33
+ thresholds,
34
+ onQualityDegraded,
35
+ } = options;
36
+
37
+ const [quality, setQuality] = useState<PlaybackQuality | null>(null);
38
+ const [isMonitoring, setIsMonitoring] = useState(false);
39
+ const monitorRef = useRef<QualityMonitor | null>(null);
40
+
41
+ // Create/update monitor instance
42
+ useEffect(() => {
43
+ monitorRef.current = new QualityMonitor({
44
+ sampleInterval,
45
+ thresholds,
46
+ onQualityDegraded,
47
+ onSample: setQuality,
48
+ });
49
+
50
+ return () => {
51
+ monitorRef.current?.stop();
52
+ monitorRef.current = null;
53
+ };
54
+ }, [sampleInterval, thresholds, onQualityDegraded]);
55
+
56
+ // Start/stop monitoring based on videoElement and enabled state
57
+ useEffect(() => {
58
+ if (!enabled || !videoElement || !monitorRef.current) {
59
+ monitorRef.current?.stop();
60
+ setIsMonitoring(false);
61
+ return;
62
+ }
63
+
64
+ monitorRef.current.start(videoElement);
65
+ setIsMonitoring(true);
66
+
67
+ return () => {
68
+ monitorRef.current?.stop();
69
+ setIsMonitoring(false);
70
+ };
71
+ }, [videoElement, enabled]);
72
+
73
+ /**
74
+ * Get current quality snapshot
75
+ */
76
+ const getCurrentQuality = useCallback((): PlaybackQuality | null => {
77
+ return monitorRef.current?.getCurrentQuality() ?? null;
78
+ }, []);
79
+
80
+ /**
81
+ * Get rolling average quality
82
+ */
83
+ const getAverageQuality = useCallback((): PlaybackQuality | null => {
84
+ return monitorRef.current?.getAverageQuality() ?? null;
85
+ }, []);
86
+
87
+ /**
88
+ * Get quality history
89
+ */
90
+ const getHistory = useCallback((): PlaybackQuality[] => {
91
+ return monitorRef.current?.getHistory() ?? [];
92
+ }, []);
93
+
94
+ /**
95
+ * Reset stall counters
96
+ */
97
+ const resetStallCounters = useCallback(() => {
98
+ monitorRef.current?.resetStallCounters();
99
+ }, []);
100
+
101
+ /**
102
+ * Get total stall time
103
+ */
104
+ const getTotalStallMs = useCallback((): number => {
105
+ return monitorRef.current?.getTotalStallMs() ?? 0;
106
+ }, []);
107
+
108
+ return {
109
+ /** Current quality metrics */
110
+ quality,
111
+ /** Whether monitoring is active */
112
+ isMonitoring,
113
+ /** Get current quality snapshot */
114
+ getCurrentQuality,
115
+ /** Get rolling average quality */
116
+ getAverageQuality,
117
+ /** Get quality history */
118
+ getHistory,
119
+ /** Reset stall counters */
120
+ resetStallCounters,
121
+ /** Get total stall time in ms */
122
+ getTotalStallMs,
123
+ };
124
+ }
125
+
126
+ export default usePlaybackQuality;