@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,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 };
|
package/src/global.d.ts
ADDED
|
@@ -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;
|