@ssgc/hls-player 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +169 -0
- package/dist/HlsPlayer.d.ts +96 -0
- package/dist/components/GoLiveButton.d.ts +15 -0
- package/dist/components/PlaybackControls.d.ts +11 -0
- package/dist/components/SpeedMenu.d.ts +14 -0
- package/dist/hooks/useClickOutside.d.ts +2 -0
- package/dist/hooks/useHlsEngine.d.ts +23 -0
- package/dist/hooks/useLiveEdge.d.ts +18 -0
- package/dist/hooks/usePlaybackSpeed.d.ts +23 -0
- package/dist/hooks/useRecordingSearch.d.ts +22 -0
- package/dist/hooks/useVideoPlaybackState.d.ts +9 -0
- package/dist/index.cjs +4 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.es.js +936 -0
- package/dist/index.es.js.map +1 -0
- package/dist/types.d.ts +59 -0
- package/dist/utils/hlsPlayerUtils.d.ts +30 -0
- package/dist/utils/hlsPlaylist.d.ts +37 -0
- package/package.json +73 -0
package/README.md
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# @ssgc/hls-player
|
|
2
|
+
|
|
3
|
+
An HLS video player for live and recorded camera streams, with a playback
|
|
4
|
+
speed control and an optional Go Live button.
|
|
5
|
+
|
|
6
|
+
It draws its own controls rather than using the browser's, so it looks the
|
|
7
|
+
same everywhere and so the browser's own speed menu does not compete with
|
|
8
|
+
this one. All styling is inline — there is no stylesheet to load.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install @ssgc/hls-player
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
`react` and `react-dom` are peer dependencies. `hls.js` and `lucide-react`
|
|
17
|
+
are installed with the package.
|
|
18
|
+
|
|
19
|
+
## Use
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
import { HlsPlayer } from "@ssgc/hls-player";
|
|
23
|
+
|
|
24
|
+
<HlsPlayer src="https://example.com/camera-3/index.m3u8" />;
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Go Live
|
|
28
|
+
|
|
29
|
+
There are two ways a system serves live footage, so there are two modes.
|
|
30
|
+
|
|
31
|
+
### "source" - live and recorded are different addresses
|
|
32
|
+
|
|
33
|
+
This is the usual case with a camera system, and the default. You are watching
|
|
34
|
+
a recording; going live means loading a different `.m3u8`.
|
|
35
|
+
|
|
36
|
+
The player does not fetch anything. It shows the button while a recording is
|
|
37
|
+
playing, tells you it was pressed, and your app fetches the live address and
|
|
38
|
+
hands it back as a new `src`. Once the stream is live the button goes away on
|
|
39
|
+
its own.
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
const [url, setUrl] = useState(clipUrl);
|
|
43
|
+
|
|
44
|
+
<HlsPlayer
|
|
45
|
+
src={url}
|
|
46
|
+
showGoLive
|
|
47
|
+
goLiveMode="source"
|
|
48
|
+
onGoLive={async () => {
|
|
49
|
+
const { m3u8 } = await api.getLiveStream(cameraId);
|
|
50
|
+
setUrl(m3u8);
|
|
51
|
+
}}
|
|
52
|
+
onExitLive={async () => {
|
|
53
|
+
const { m3u8 } = await api.getRecording(cameraId, timestamp);
|
|
54
|
+
setUrl(m3u8);
|
|
55
|
+
}}
|
|
56
|
+
/>;
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Getting back to the recording
|
|
60
|
+
|
|
61
|
+
Once the stream is live the same button turns into the way back, labelled
|
|
62
|
+
"Back to recording", and calls `onExitLive`. The app fetches the recorded address and hands it over as a new `src`.
|
|
63
|
+
|
|
64
|
+
Leave `onExitLive` out and the button simply disappears once the stream is
|
|
65
|
+
live, which suits a screen that has its own timeline for picking a moment to
|
|
66
|
+
go back to.
|
|
67
|
+
|
|
68
|
+
### "seek" - one stream holds both
|
|
69
|
+
|
|
70
|
+
Some streams keep a rolling window of recent footage, so going live just means
|
|
71
|
+
jumping to the newest moment. The player can do that itself.
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
<HlsPlayer src={streamUrl} showGoLive goLiveMode="seek" liveEdgeToleranceSeconds={5} />
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Here the button appears only on a live stream, and stays disabled until the
|
|
78
|
+
viewer is further behind than `liveEdgeToleranceSeconds`.
|
|
79
|
+
|
|
80
|
+
That tolerance matters. An HLS player sits several seconds behind the newest
|
|
81
|
+
moment by design, so treating any gap at all as "behind" leaves the button
|
|
82
|
+
flickering on and off while the viewer is, in practice, live. Five seconds is
|
|
83
|
+
the default; lower it for low-latency setups.
|
|
84
|
+
|
|
85
|
+
Nothing on the server is needed for the "seek" mode. The newest playable
|
|
86
|
+
moment is read from the stream itself, through the browser's own `seekable`
|
|
87
|
+
range.
|
|
88
|
+
|
|
89
|
+
## Options
|
|
90
|
+
|
|
91
|
+
| Prop | Default | What it does |
|
|
92
|
+
| --- | --- | --- |
|
|
93
|
+
| `src` | — | Address of the `.m3u8` stream. Required. |
|
|
94
|
+
| `autoPlay` | `true` | Start playing as soon as it loads. |
|
|
95
|
+
| `muted` | `true` | Start muted. Browsers block autoplay with sound. |
|
|
96
|
+
| `height` | `480` | Height of the video. |
|
|
97
|
+
| `className` | — | Class on the wrapper element. |
|
|
98
|
+
| `showControls` | `true` | The play/pause and seek bar. |
|
|
99
|
+
| `showSpeedMenu` | `true` | The speed button. |
|
|
100
|
+
| `speedOptions` | `[1, 2, 3, 5, 10]` | Speeds the menu offers. |
|
|
101
|
+
| `initialSpeed` | `1` | Speed to start at. |
|
|
102
|
+
| `showGoLive` | `false` | Offer the Go Live button. |
|
|
103
|
+
| `goLiveMode` | `"source"` | `"source"` fetches a new address, `"seek"` jumps within the stream. |
|
|
104
|
+
| `goLiveLabel` | `"Go Live"` | Wording on it. |
|
|
105
|
+
| `exitLiveLabel` | `"Back to recording"` | Wording once live. |
|
|
106
|
+
| `onExitLive` | — | Called to leave live. Omit it and the button hides once live. |
|
|
107
|
+
| `allowSpeedWhenLive` | `false` | Let the speed menu stay usable on a live stream. |
|
|
108
|
+
| `liveEdgeToleranceSeconds` | `5` | `"seek"` mode only. How far behind counts as behind. |
|
|
109
|
+
| `onGoLive` | — | Called when the button is pressed. |
|
|
110
|
+
| `onSpeedChange` | — | Called when the speed changes. |
|
|
111
|
+
| `onError` | — | Called on a playback failure. |
|
|
112
|
+
| `onLiveChange` | — | Called once it knows live or recorded. |
|
|
113
|
+
| `onEngineChange` | — | Called with the engine it used. |
|
|
114
|
+
| `debug` | `false` | Print engine selection and recovery steps to the console. |
|
|
115
|
+
|
|
116
|
+
## Speed on a live stream
|
|
117
|
+
|
|
118
|
+
The speed menu is greyed out while a live stream is playing, and the rate
|
|
119
|
+
drops back to 1x. There are no frames ahead of live to fast-forward into, so
|
|
120
|
+
a higher rate only runs the player into the end of the stream and stalls.
|
|
121
|
+
|
|
122
|
+
Pass `allowSpeedWhenLive` if your stream keeps enough of a rolling window
|
|
123
|
+
behind live for that to be useful.
|
|
124
|
+
|
|
125
|
+
## How playback works
|
|
126
|
+
|
|
127
|
+
Browsers do not agree on HLS. Safari plays it directly; everything else needs
|
|
128
|
+
the hls.js library. The player picks whichever applies, and falls back to
|
|
129
|
+
hls.js if the native path repeatedly fails — which happens on Chrome when an
|
|
130
|
+
extension makes the browser claim HLS support it does not have.
|
|
131
|
+
|
|
132
|
+
High speeds are not what they look like. `playbackRate` does not make the
|
|
133
|
+
browser skip frames, it still decodes every one, and at 5x or 10x that
|
|
134
|
+
outruns the decoder and playback stalls. So above 5x the player runs at a
|
|
135
|
+
rate the decoder can sustain and skips forward on a timer to make up the
|
|
136
|
+
difference, only ever into video already downloaded. It is the same trick
|
|
137
|
+
DVR and VMS players use.
|
|
138
|
+
|
|
139
|
+
Buffer targets scale with the selected speed, in both seconds and bytes,
|
|
140
|
+
since consuming video N times faster leaves the network 1/N as long to keep
|
|
141
|
+
up.
|
|
142
|
+
|
|
143
|
+
## Building on it
|
|
144
|
+
|
|
145
|
+
The parts are exported too, for an app that wants its own controls:
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
import {
|
|
149
|
+
useHlsEngine, // loads and recovers the stream
|
|
150
|
+
usePlaybackSpeed, // speed selection, including the high-speed trick
|
|
151
|
+
useVideoPlaybackState, // playing, position, length
|
|
152
|
+
useLiveEdge, // how far behind live, and the jump
|
|
153
|
+
PlaybackControls,
|
|
154
|
+
SpeedMenu,
|
|
155
|
+
GoLiveButton,
|
|
156
|
+
} from "@ssgc/hls-player";
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Development
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
npm install
|
|
163
|
+
npm run dev # demo page on http://localhost:5175
|
|
164
|
+
npm run build
|
|
165
|
+
npm run lint
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
The demo page takes any stream address and lets you toggle Go Live and its
|
|
169
|
+
tolerance, to see the button enable as you scrub backwards.
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
import { PlaybackEngine, PlayerError, RecordingChunk, RecordingSearch } from './types';
|
|
3
|
+
export interface HlsPlayerProps {
|
|
4
|
+
/**
|
|
5
|
+
* Address of the stream to play. Leave it out, or pass an empty string,
|
|
6
|
+
* and the player shows `emptyMessage` in place of the video.
|
|
7
|
+
*/
|
|
8
|
+
src?: string;
|
|
9
|
+
/** Shown instead of the video while there is no `src`. */
|
|
10
|
+
emptyMessage?: ReactNode;
|
|
11
|
+
/**
|
|
12
|
+
* The host's recordings search. Called when there is no `src`, and whatever
|
|
13
|
+
* it returns is rebuilt into a playable playlist — see `useRecordingSearch`.
|
|
14
|
+
* This package has no API client of its own, so the call is injected.
|
|
15
|
+
*/
|
|
16
|
+
recordingSearch?: RecordingSearch;
|
|
17
|
+
/**
|
|
18
|
+
* Epoch ms of the moment being investigated. The chunk covering it is the
|
|
19
|
+
* one played; without it playback starts at the earliest chunk.
|
|
20
|
+
*/
|
|
21
|
+
recordingAt?: number | null;
|
|
22
|
+
/** Seconds of footage per rebuilt chunk. Defaults to 120. */
|
|
23
|
+
recordingChunkSeconds?: number;
|
|
24
|
+
/** Handed every chunk built from the search response, oldest first. */
|
|
25
|
+
onRecordingChunks?: (chunks: RecordingChunk[]) => void;
|
|
26
|
+
/** Shown while the recordings search is in flight. */
|
|
27
|
+
loadingMessage?: ReactNode;
|
|
28
|
+
autoPlay?: boolean;
|
|
29
|
+
muted?: boolean;
|
|
30
|
+
/** Height of the video in pixels */
|
|
31
|
+
height?: number | string;
|
|
32
|
+
className?: string;
|
|
33
|
+
/** Hide the play/pause and seek bar. */
|
|
34
|
+
showControls?: boolean;
|
|
35
|
+
/** Hide the speed menu. */
|
|
36
|
+
showSpeedMenu?: boolean;
|
|
37
|
+
/** Speeds the menu offers. */
|
|
38
|
+
speedOptions?: number[];
|
|
39
|
+
/** Speed to start at. */
|
|
40
|
+
initialSpeed?: number;
|
|
41
|
+
/** Offer a Go Live button. */
|
|
42
|
+
showGoLive?: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* What going live means for this stream.
|
|
45
|
+
*
|
|
46
|
+
* "source" is for a camera system that serves live footage and recordings
|
|
47
|
+
* at two different addresses. The button shows while a recording is
|
|
48
|
+
* playing, calls `onGoLive`, and the app is expected to fetch the live
|
|
49
|
+
* address and hand it back as a new `src`. The button then goes away,
|
|
50
|
+
* because the stream is live.
|
|
51
|
+
*
|
|
52
|
+
* "seek" is for a single stream holding both, where going live only means
|
|
53
|
+
* jumping to its newest moment.
|
|
54
|
+
*/
|
|
55
|
+
goLiveMode?: "source" | "seek";
|
|
56
|
+
/** Wording on the button. */
|
|
57
|
+
goLiveLabel?: string;
|
|
58
|
+
/**
|
|
59
|
+
* Wording once the stream is live and the button offers the way back.
|
|
60
|
+
* Only used by "source" mode, and only when `onExitLive` is given.
|
|
61
|
+
*/
|
|
62
|
+
exitLiveLabel?: string;
|
|
63
|
+
/**
|
|
64
|
+
* Called when the viewer wants the recording back. The app fetches the
|
|
65
|
+
* recorded address and hands it over as a new `src`, the mirror of
|
|
66
|
+
* `onGoLive`. Leave it out and the button simply disappears once live.
|
|
67
|
+
*/
|
|
68
|
+
onExitLive?: () => void;
|
|
69
|
+
/**
|
|
70
|
+
* Let the speed menu stay usable on a live stream. Off by default: there is
|
|
71
|
+
* nothing ahead of live to fast-forward into, so a higher speed only runs
|
|
72
|
+
* the player into the end of the stream and stalls.
|
|
73
|
+
*/
|
|
74
|
+
allowSpeedWhenLive?: boolean;
|
|
75
|
+
/**
|
|
76
|
+
* Only used by "seek" mode. How many seconds behind the newest moment
|
|
77
|
+
* counts as behind. A player sits a few seconds back by design, so a very
|
|
78
|
+
* small number makes the button flicker on and off while the viewer is
|
|
79
|
+
* effectively live.
|
|
80
|
+
*/
|
|
81
|
+
liveEdgeToleranceSeconds?: number;
|
|
82
|
+
/**
|
|
83
|
+
* Called when Go Live is pressed. In "source" mode this is where the app
|
|
84
|
+
* fetches the live address and updates `src`.
|
|
85
|
+
*/
|
|
86
|
+
onGoLive?: () => void;
|
|
87
|
+
onSpeedChange?: (speed: number) => void;
|
|
88
|
+
onError?: (error: PlayerError) => void;
|
|
89
|
+
/** Print what the player is doing to the console. Off by default. */
|
|
90
|
+
debug?: boolean;
|
|
91
|
+
/** Called once the player knows whether this is live or a recorded clip. */
|
|
92
|
+
onLiveChange?: (isLive: boolean | null) => void;
|
|
93
|
+
/** Called with the engine that ended up being used. */
|
|
94
|
+
onEngineChange?: (engine: PlaybackEngine | null) => void;
|
|
95
|
+
}
|
|
96
|
+
export default function HlsPlayer({ src, emptyMessage, recordingSearch, recordingAt, recordingChunkSeconds, onRecordingChunks, loadingMessage, autoPlay, muted, height, className, showControls, showSpeedMenu, speedOptions, initialSpeed, showGoLive, goLiveMode, goLiveLabel, exitLiveLabel, onExitLive, allowSpeedWhenLive, liveEdgeToleranceSeconds, onGoLive, onSpeedChange, onError, onLiveChange, onEngineChange, debug, }: Readonly<HlsPlayerProps>): import("react").JSX.Element;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
interface GoLiveButtonProps {
|
|
2
|
+
enabled: boolean;
|
|
3
|
+
label: string;
|
|
4
|
+
/** Hover text explaining the current state. */
|
|
5
|
+
hint: string;
|
|
6
|
+
/**
|
|
7
|
+
* True when the stream is already live and this button is the way back to
|
|
8
|
+
* the recording. It is drawn plainly then, because red with a dot reads as
|
|
9
|
+
* "you are live" rather than as a way out of it.
|
|
10
|
+
*/
|
|
11
|
+
live?: boolean;
|
|
12
|
+
onGoLive: () => void;
|
|
13
|
+
}
|
|
14
|
+
export default function GoLiveButton({ enabled, label, hint, live, onGoLive, }: Readonly<GoLiveButtonProps>): import("react").JSX.Element;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ReactNode, RefObject } from 'react';
|
|
2
|
+
interface PlaybackControlsProps {
|
|
3
|
+
videoRef: RefObject<HTMLVideoElement | null>;
|
|
4
|
+
isPlaying: boolean;
|
|
5
|
+
currentTime: number;
|
|
6
|
+
duration: number;
|
|
7
|
+
isLive: boolean;
|
|
8
|
+
goLive?: ReactNode;
|
|
9
|
+
}
|
|
10
|
+
export default function PlaybackControls({ videoRef, isPlaying, currentTime, duration, isLive, goLive, }: Readonly<PlaybackControlsProps>): import("react").JSX.Element;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
interface SpeedMenuProps {
|
|
2
|
+
speed: number;
|
|
3
|
+
setSpeed: (speed: number) => void;
|
|
4
|
+
showSpeedMenu: boolean;
|
|
5
|
+
setShowSpeedMenu: React.Dispatch<React.SetStateAction<boolean>>;
|
|
6
|
+
speedMenuRef: React.RefObject<HTMLDivElement | null>;
|
|
7
|
+
options?: number[];
|
|
8
|
+
/** Greyed out and unclickable while a live stream is playing. */
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
/** Hover text explaining why it is greyed out. */
|
|
11
|
+
disabledHint?: string;
|
|
12
|
+
}
|
|
13
|
+
export default function SpeedMenu({ speed, setSpeed, showSpeedMenu, setShowSpeedMenu, speedMenuRef, options, disabled, disabledHint, }: Readonly<SpeedMenuProps>): import("react").JSX.Element;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { RefObject } from 'react';
|
|
2
|
+
import { default as Hls } from 'hls.js';
|
|
3
|
+
import { PlaybackEngine, PlayerError } from '../types';
|
|
4
|
+
interface UseHlsEngineArgs {
|
|
5
|
+
src?: string;
|
|
6
|
+
videoRef: RefObject<HTMLVideoElement | null>;
|
|
7
|
+
hlsRef: RefObject<Hls | null>;
|
|
8
|
+
engineRef: RefObject<PlaybackEngine | null>;
|
|
9
|
+
speedRef: RefObject<number>;
|
|
10
|
+
applyPlaybackSpeed: (video: HTMLVideoElement, targetSpeed: number) => void;
|
|
11
|
+
clearJumpScan: () => void;
|
|
12
|
+
autoPlay: boolean;
|
|
13
|
+
onError?: (error: PlayerError) => void;
|
|
14
|
+
/** Print what the player is doing to the console. Off by default. */
|
|
15
|
+
debug?: boolean;
|
|
16
|
+
}
|
|
17
|
+
export interface HlsEngineApi {
|
|
18
|
+
url: string;
|
|
19
|
+
isLive: boolean | null;
|
|
20
|
+
engine: PlaybackEngine | null;
|
|
21
|
+
}
|
|
22
|
+
export declare function useHlsEngine({ src, videoRef, hlsRef, engineRef, speedRef, applyPlaybackSpeed, clearJumpScan, autoPlay, onError, debug, }: UseHlsEngineArgs): HlsEngineApi;
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { RefObject } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* How far behind counts as behind, in seconds.
|
|
4
|
+
*
|
|
5
|
+
* An HLS player sits a few segments back from the newest moment by design,
|
|
6
|
+
* so treating any gap at all as "behind" leaves the button flickering on and
|
|
7
|
+
* off while the viewer is, for all practical purposes, live.
|
|
8
|
+
*/
|
|
9
|
+
export declare const DEFAULT_LIVE_EDGE_TOLERANCE_SECONDS = 5;
|
|
10
|
+
export interface LiveEdgeApi {
|
|
11
|
+
/** Seconds between the viewer and the newest moment. Null while unknown. */
|
|
12
|
+
secondsBehind: number | null;
|
|
13
|
+
/** True when the viewer is far enough back for Go Live to be worth offering. */
|
|
14
|
+
behindLiveEdge: boolean;
|
|
15
|
+
/** Jump to the newest moment and resume playing. */
|
|
16
|
+
goLive: () => void;
|
|
17
|
+
}
|
|
18
|
+
export declare function useLiveEdge(videoRef: RefObject<HTMLVideoElement | null>, isLive: boolean | null, toleranceSeconds?: number): LiveEdgeApi;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { RefObject } from 'react';
|
|
2
|
+
import { default as Hls } from 'hls.js';
|
|
3
|
+
import { PlaybackEngine } from '../types';
|
|
4
|
+
interface UsePlaybackSpeedArgs {
|
|
5
|
+
videoRef: RefObject<HTMLVideoElement | null>;
|
|
6
|
+
hlsRef: RefObject<Hls | null>;
|
|
7
|
+
engineRef: RefObject<PlaybackEngine | null>;
|
|
8
|
+
/** Starting speed. */
|
|
9
|
+
initialSpeed?: number;
|
|
10
|
+
onSpeedChange?: (speed: number) => void;
|
|
11
|
+
}
|
|
12
|
+
export interface PlaybackSpeedApi {
|
|
13
|
+
speed: number;
|
|
14
|
+
setSpeed: (speed: number) => void;
|
|
15
|
+
showSpeedMenu: boolean;
|
|
16
|
+
setShowSpeedMenu: React.Dispatch<React.SetStateAction<boolean>>;
|
|
17
|
+
speedMenuRef: RefObject<HTMLDivElement | null>;
|
|
18
|
+
speedRef: RefObject<number>;
|
|
19
|
+
applyPlaybackSpeed: (video: HTMLVideoElement, targetSpeed: number) => void;
|
|
20
|
+
clearJumpScan: () => void;
|
|
21
|
+
}
|
|
22
|
+
export declare function usePlaybackSpeed({ videoRef, hlsRef, engineRef, initialSpeed, onSpeedChange, }: UsePlaybackSpeedArgs): PlaybackSpeedApi;
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { PlayerError, RecordingChunk, RecordingSearch } from '../types';
|
|
2
|
+
interface UseRecordingSearchArgs {
|
|
3
|
+
/** The host's call. Leave it out and the hook stays idle. */
|
|
4
|
+
search?: RecordingSearch;
|
|
5
|
+
/** Epoch ms of the moment to watch; picks the chunk covering it. */
|
|
6
|
+
at?: number | null;
|
|
7
|
+
/** Seconds of footage per chunk. */
|
|
8
|
+
chunkSeconds?: number;
|
|
9
|
+
onError?: (error: PlayerError) => void;
|
|
10
|
+
}
|
|
11
|
+
export interface RecordingSearchApi {
|
|
12
|
+
/** Every chunk built from the response, oldest first. */
|
|
13
|
+
chunks: RecordingChunk[];
|
|
14
|
+
/** The chunk being watched. */
|
|
15
|
+
chunk: RecordingChunk | null;
|
|
16
|
+
/** Address for `chunk`, or null when there is nothing to play. */
|
|
17
|
+
url: string | null;
|
|
18
|
+
loading: boolean;
|
|
19
|
+
error: PlayerError | null;
|
|
20
|
+
}
|
|
21
|
+
export declare function useRecordingSearch({ search, at, chunkSeconds, onError, }: UseRecordingSearchArgs): RecordingSearchApi;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { RefObject } from 'react';
|
|
2
|
+
export interface VideoPlaybackState {
|
|
3
|
+
isPlaying: boolean;
|
|
4
|
+
/** Seconds from the start of the stream. */
|
|
5
|
+
currentTime: number;
|
|
6
|
+
/** Seconds. Zero for a live stream, which has no fixed length. */
|
|
7
|
+
duration: number;
|
|
8
|
+
}
|
|
9
|
+
export declare function useVideoPlaybackState(videoRef: RefObject<HTMLVideoElement | null>, url: string, autoPlay: boolean): VideoPlaybackState;
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const m=require("react/jsx-runtime"),o=require("react"),ce=require("lucide-react"),L=require("hls.js");function pe({enabled:e,label:r,hint:n,live:t=!1,onGoLive:i}){const l=e&&!t;return m.jsxs("button",{type:"button",onClick:a=>{a.stopPropagation(),i()},onMouseDown:a=>a.stopPropagation(),disabled:!e,title:n,style:{display:"flex",alignItems:"center",gap:6,padding:"5px 10px",background:l?"rgba(211,47,47,0.85)":"rgba(0,0,0,0.55)",color:e?"#fff":"rgba(255,255,255,0.45)",border:"1px solid rgba(255,255,255,0.22)",borderRadius:6,cursor:e?"pointer":"default",fontSize:12,fontWeight:600,letterSpacing:"0.02em",whiteSpace:"nowrap",backdropFilter:"blur(4px)",WebkitBackdropFilter:"blur(4px)"},children:[!t&&m.jsx("span",{style:{width:7,height:7,borderRadius:"50%",background:e?"#fff":"rgba(255,255,255,0.45)"}}),r]})}const ge=[1,2,3,5,10],Ve=30,qe=600;function te(e){return Math.min(Ve*e,qe)}const Ke=60*1e3*1e3,Ye=600*1e3*1e3;function re(e){return Math.min(Ke*e,Ye)}const me=5,Y=2,Z=250;function he(e){const r=e.currentTime;for(let n=0;n<e.buffered.length;n+=1)if(e.buffered.start(n)<=r+.01&&r<=e.buffered.end(n))return Math.max(0,e.buffered.end(n)-r);return 0}function ne(e){return e.seekable.length===0?null:e.seekable.end(e.seekable.length-1)}function Q(e){const r=ne(e);return r===null?null:Math.max(0,r-e.currentTime)}function F(e){const r=ne(e);r!==null&&(e.currentTime=Math.max(0,r-1))}function Se(){return/^((?!chrome|android).)*safari/i.test(navigator.userAgent)}function G(e){if(!e||!isFinite(e)||e<=0)return"0:00";const r=Math.floor(e),n=Math.floor(r/60),t=r%60;return`${n}:${t.toString().padStart(2,"0")}`}function ee(e){return`${e.replace(/#reload\d*$/,"")}#reload${Date.now()}`}function be(e){return e.replace(/#reload\d*$/,"")}const de=300,Ze=210;function Ee({videoRef:e,isPlaying:r,currentTime:n,duration:t,isLive:i,goLive:l}){const a=o.useRef(null),[g,f]=o.useState(0);o.useEffect(()=>{const d=a.current;if(!d||typeof ResizeObserver>"u")return;const s=new ResizeObserver(([h])=>{f(h.contentRect.width)});return s.observe(d),()=>s.disconnect()},[]);const c=t>0,p=g>0,S=!p||g>=de,E=!p||g>=Ze,b=()=>{const d=e.current;d&&(d.paused?d.play().catch(()=>{}):d.pause())},x=d=>{const s=e.current;!s||!isFinite(s.duration)||(s.currentTime=Math.max(0,Math.min(1,d))*s.duration)};return m.jsxs("div",{ref:a,style:{position:"absolute",left:12,right:12,bottom:12,display:"flex",alignItems:"center",gap:p&&g<de?8:12,minWidth:0,pointerEvents:"auto"},children:[m.jsx("button",{type:"button",onClick:d=>{d.stopPropagation(),b()},onMouseDown:d=>d.stopPropagation(),style:{display:"flex",flex:"0 0 auto",background:"rgba(0,0,0,0.6)",color:"#fff",border:"none",padding:"6px 10px",borderRadius:6,cursor:"pointer"},"aria-label":r?"Pause":"Play",title:r?"Pause":"Play",children:r?m.jsx(ce.Pause,{size:16}):m.jsx(ce.Play,{size:16})}),c?m.jsx("div",{onClick:d=>{d.stopPropagation();const s=d.currentTarget.getBoundingClientRect();x((d.clientX-s.left)/s.width)},onMouseDown:d=>d.stopPropagation(),onKeyDown:d=>{const s=e.current;if(!s||!isFinite(s.duration))return;const h=s.duration>0?s.duration*.02:5;d.key==="ArrowRight"?(d.stopPropagation(),s.currentTime=Math.min(s.duration,s.currentTime+h)):d.key==="ArrowLeft"&&(d.stopPropagation(),s.currentTime=Math.max(0,s.currentTime-h))},role:"slider",tabIndex:0,"aria-label":"Seek","aria-valuemin":0,"aria-valuemax":t,"aria-valuenow":n,style:{flex:"1 1 0",minWidth:0,height:8,background:"rgba(255,255,255,0.12)",borderRadius:6,position:"relative",overflow:"hidden",cursor:"pointer"},title:"Seek",children:m.jsx("div",{style:{position:"absolute",left:0,top:0,bottom:0,width:`${t>0?Math.min(100,Math.max(0,n/t*100)):0}%`,background:"rgba(66,153,225,0.85)",borderRadius:6}})}):m.jsx("span",{style:{flex:"1 1 0",minWidth:0}}),(E||!c&&i)&&m.jsx("div",{style:{flex:"0 0 auto",color:"#fff",fontSize:12,fontVariantNumeric:"tabular-nums",whiteSpace:"nowrap"},children:c?S?`${G(n)}`:G(n):i?"Live":G(n)}),l&&m.jsx("div",{style:{display:"flex",flex:"0 0 auto"},children:l})]})}function Qe(){return m.jsxs("svg",{width:"14",height:"14",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round","aria-hidden":"true",children:[m.jsx("path",{d:"M5 16a7 7 0 0 1 14 0"}),m.jsx("line",{x1:"12",y1:"16",x2:"9",y2:"9"}),m.jsx("circle",{cx:"12",cy:"16",r:"1.5",fill:"currentColor",stroke:"none"})]})}function ye({speed:e,setSpeed:r,showSpeedMenu:n,setShowSpeedMenu:t,speedMenuRef:i,options:l=ge,disabled:a=!1,disabledHint:g="Speed is not available on a live stream"}){return m.jsxs("div",{ref:i,style:{position:"absolute",top:10,right:10,zIndex:2},onMouseDown:f=>f.stopPropagation(),children:[m.jsxs("button",{type:"button",onClick:f=>{f.stopPropagation(),t(c=>!c)},disabled:a,title:a?g:"Playback speed","aria-haspopup":"menu","aria-expanded":n&&!a,style:{display:"flex",alignItems:"center",gap:5,padding:"5px 10px",background:"rgba(0,0,0,0.60)",color:a?"rgba(255,255,255,0.40)":"#fff",border:"1px solid rgba(255,255,255,0.22)",borderRadius:6,cursor:a?"default":"pointer",fontSize:13,fontWeight:600,backdropFilter:"blur(4px)",WebkitBackdropFilter:"blur(4px)"},children:[m.jsx(Qe,{}),e,"×"]}),n&&!a&&m.jsx("div",{role:"menu",style:{position:"absolute",top:"calc(100% + 6px)",right:0,background:"rgba(18,18,18,0.92)",border:"1px solid rgba(255,255,255,0.14)",borderRadius:8,padding:"4px 0",minWidth:90,backdropFilter:"blur(10px)",WebkitBackdropFilter:"blur(10px)",boxShadow:"0 4px 16px rgba(0,0,0,0.45)"},children:l.map(f=>m.jsxs("button",{type:"button",role:"menuitemradio","aria-checked":f===e,onClick:c=>{c.stopPropagation(),r(f),t(!1)},style:{display:"flex",alignItems:"center",gap:8,width:"100%",padding:"7px 14px",background:f===e?"rgba(26,115,232,0.55)":"transparent",border:"none",color:f===e?"#fff":"rgba(255,255,255,0.75)",fontWeight:f===e?700:400,fontSize:13,cursor:"pointer",textAlign:"left"},children:[m.jsx("span",{style:{width:12,fontSize:10,color:"#4caf50"},children:f===e?"✓":""}),f,"×"]},f))})]})}function xe({src:e,videoRef:r,hlsRef:n,engineRef:t,speedRef:i,applyPlaybackSpeed:l,clearJumpScan:a,autoPlay:g,onError:f,debug:c=!1}){const p=o.useRef(c);p.current=c;const S=(...u)=>{p.current&&console.warn(...u)},E=(...u)=>{p.current&&console.error(...u)},[b,x]=o.useState(e??""),[d,s]=o.useState(null),[h,T]=o.useState(null),M=o.useRef(null),I=o.useRef(!1),_=o.useRef(f);return _.current=f,o.useEffect(()=>{x(e??"")},[e]),o.useEffect(()=>{const u=r.current;if(!u||!b)return;const D=y=>{M.current=y,s(y)},j=y=>{t.current=y,T(y)};D(null),t.current=null,T(null),a(),n.current&&(n.current.destroy(),n.current=null);const O=()=>{t.current==="Native HLS"&&M.current===null&&D(!isFinite(u.duration)),l(u,i.current)};u.addEventListener("loadedmetadata",O);const B=()=>{u.removeEventListener("loadedmetadata",O)},H=be(b);if(!I.current&&Se()&&u.canPlayType("application/vnd.apple.mpegurl")){j("Native HLS"),u.src=H,u.playbackRate=i.current,u.play().catch(()=>{});let y=0,k=0,w=0;const N=setInterval(()=>{u.paused||(u.currentTime===y&&u.readyState>=2?(k+=1,k>=2&&(M.current===!0?(S("[HlsPlayer/Native] Stall — jumping to live edge"),F(u)):S("[HlsPlayer/Native] Stall — resuming recorded playback"),u.playbackRate=i.current,u.play().catch(()=>{}),k=0)):k=0,y=u.currentTime)},3e3),A=()=>{if(u.error?.code===4&&/empty src/i.test(u.error?.message||"")){S("[HlsPlayer/Native] Ignoring spurious empty-src error",u.error);return}if(w+=1,S("[HlsPlayer/Native] Media error",u.error),_.current?.(u.error),w>5){S("[HlsPlayer/Native] Giving up after repeated errors — canPlayType claimed HLS support but playback never worked. Falling back to HLS.js."),I.current=!0,clearInterval(N),u.removeEventListener("error",A),x(C=>ee(C));return}const R=u.src;u.src="",setTimeout(()=>{u.src=R,M.current===!0&&F(u),u.playbackRate=i.current,u.play().catch(()=>{})},1500)};return u.addEventListener("error",A),()=>{clearInterval(N),u.removeEventListener("error",A),B(),u.src=""}}if(L.isSupported()){j("HLS.js");let y=0,k=null;const w=C=>{k||(S("[HlsPlayer/HLS.js] Hard reload:",C),k=setTimeout(()=>{k=null,x(v=>ee(v))},1500))},N=te(i.current),A={enableWorker:!0,lowLatencyMode:!1,liveSyncDurationCount:3,liveMaxLatencyDurationCount:6,fragLoadingMaxRetry:2,fragLoadingRetryDelay:500,fragLoadingMaxRetryTimeout:4e3,manifestLoadingMaxRetry:3,levelLoadingMaxRetry:3,maxBufferLength:N,maxMaxBufferLength:N,maxBufferSize:re(i.current)},R=new L(A);return n.current=R,R.loadSource(H),R.attachMedia(u),R.on(L.Events.LEVEL_LOADED,(C,v)=>{D(v.details.live)}),R.on(L.Events.MANIFEST_PARSED,()=>{l(u,i.current),g&&u.play().catch(()=>{})}),R.on(L.Events.ERROR,(C,v)=>{if((v.details===L.ErrorDetails.FRAG_LOAD_ERROR||v.details===L.ErrorDetails.FRAG_LOAD_TIMEOUT)&&!v.fatal){setTimeout(()=>{M.current===!0&&F(u),l(u,i.current),u.play().catch(()=>{})},500);return}if(v.fatal)switch(E("[HlsPlayer/HLS.js] Fatal error",v.type,v.details),_.current?.(v),v.type){case L.ErrorTypes.MEDIA_ERROR:y<3?(y+=1,R.recoverMediaError()):w("repeated media errors");break;case L.ErrorTypes.NETWORK_ERROR:v.details===L.ErrorDetails.MANIFEST_LOAD_ERROR||v.details===L.ErrorDetails.MANIFEST_LOAD_TIMEOUT?w("manifest unreachable"):setTimeout(()=>R.startLoad(),2e3);break;default:w("unrecoverable error");break}}),()=>{k&&clearTimeout(k),a(),B(),R.destroy(),n.current=null}}return S("[HlsPlayer] HLS is not supported in this browser"),_.current?.(new Error("HLS playback is not supported in this browser.")),()=>{B()}},[b]),{url:b,isLive:d,engine:h}}const se=5,et=1e3;function ve(e,r,n=se){const[t,i]=o.useState(null);o.useEffect(()=>{if(r!==!0){i(null);return}const a=()=>{const f=e.current;i(f?Q(f):null)};a();const g=setInterval(a,et);return()=>clearInterval(g)},[r,e]);const l=o.useCallback(()=>{const a=e.current;a&&(F(a),a.play().catch(()=>{}),i(Q(a)))},[e]);return{secondsBehind:t,behindLiveEdge:t!==null&&t>n,goLive:l}}function ke(e,r,n){const t=o.useRef(n);o.useEffect(()=>{t.current=n}),o.useEffect(()=>{if(!r)return;const i=l=>{e.current&&!e.current.contains(l.target)&&t.current()};return document.addEventListener("mousedown",i),()=>document.removeEventListener("mousedown",i)},[r,e])}function Re({videoRef:e,hlsRef:r,engineRef:n,initialSpeed:t=1,onSpeedChange:i}){const[l,a]=o.useState(t),[g,f]=o.useState(!1),c=o.useRef(t),p=o.useRef(null),S=o.useRef(null),E=o.useRef(i);E.current=i;const b=o.useCallback(()=>{S.current&&(clearInterval(S.current),S.current=null)},[]),x=o.useCallback((s,h)=>{if(b(),n.current!=="HLS.js"||h<me){s.playbackRate=h;return}s.playbackRate=Y;const T=(h-Y)*(Z/1e3);S.current=setInterval(()=>{if(s.paused||s.seeking)return;const I=Math.max(0,he(s)-2),_=Math.min(T,I);if(_<=0)return;const u=s.seekable.length>0?s.seekable.end(s.seekable.length-1):s.duration,D=s.currentTime+_;if(isFinite(u)&&D>=u-.5){b();return}s.currentTime=D},Z)},[b,n]),d=o.useCallback(s=>{a(s)},[]);return o.useEffect(()=>{c.current=l,E.current?.(l)},[l]),o.useEffect(()=>{const s=e.current;if(!s)return;x(s,l);const h=r.current;if(h){const T=te(l);h.config.maxBufferLength=T,h.config.maxMaxBufferLength=T,h.config.maxBufferSize=re(l)}return()=>b()},[l,x,b,r,e]),ke(p,g,()=>f(!1)),{speed:l,setSpeed:d,showSpeedMenu:g,setShowSpeedMenu:f,speedMenuRef:p,speedRef:c,applyPlaybackSpeed:x,clearJumpScan:b}}const J=120,tt=/^#EXTINF:([\d.]+),?/,rt=/seg_(\d{8})_(\d{6})_/;function Le(e){const r=rt.exec(e);if(!r)return null;const[,n,t]=r,i=Number(n.slice(0,4)),l=Number(n.slice(4,6)),a=Number(n.slice(6,8)),g=Number(t.slice(0,2)),f=Number(t.slice(2,4)),c=Number(t.slice(4,6)),p=new Date(i,l-1,a,g,f,c);return Number.isNaN(p.getTime())||p.getFullYear()!==i||p.getMonth()!==l-1||p.getDate()!==a||p.getHours()!==g||p.getMinutes()!==f||p.getSeconds()!==c?null:p.getTime()}function Te(e){const r=(e??"").split(/\r?\n/).filter(a=>a.trim()!==""),n=[],t=[];let i=null,l=!1;for(const a of r){const g=tt.exec(a);if(g){l=!0,i=Number(g[1]);continue}if(a.startsWith("#")){l||n.push(a);continue}i!==null&&(t.push({durationSec:i,uri:a,timestamp:Le(a)}),i=null)}return{headerLines:n,segments:t}}function Me(e,r=J){const n=[];let t=[],i=0;for(const l of e)t.push(l),i+=l.durationSec,i>=r&&(n.push(t),t=[],i=0);return t.length>0&&n.push(t),n}function _e(e,r){const n=[...e];for(const t of r)n.push(`#EXTINF:${t.durationSec},`,t.uri);return n.push("#EXT-X-ENDLIST"),`${n.join(`
|
|
2
|
+
`)}
|
|
3
|
+
`}const fe=e=>e===null?"--:--":new Date(e).toTimeString().slice(0,5);function we(e,r=J){return(e??[]).flatMap((t,i)=>{const{spaceId:l,cameraId:a,feed:g}=t,{headerLines:f,segments:c}=Te(g);return c.length===0?[]:Me(c,r).map((p,S)=>{const E=p[0].timestamp,b=p.reduce((d,s)=>d+s.durationSec,0),x=E===null?null:E+b*1e3;return{id:`${l}-${a}-${i}-${S}`,spaceId:l,cameraId:a,startTime:E,endTime:x,durationSec:b,startLabel:fe(E),endLabel:fe(x),segmentCount:p.length,playlistText:_e(f,p)}})}).sort((t,i)=>t.startTime===null||i.startTime===null?0:t.startTime-i.startTime)}function Pe(e){const r=new Blob([new TextEncoder().encode(e)],{type:"application/vnd.apple.mpegurl"});return URL.createObjectURL(r)}function De(e,r){return e.length===0?null:r==null?e[0]:e.find(t=>t.startTime!==null&&t.endTime!==null&&r>=t.startTime&&r<t.endTime)??e[0]}function Ne({search:e,at:r,chunkSeconds:n=J,onError:t}){const[i,l]=o.useState([]),[a,g]=o.useState(!1),[f,c]=o.useState(null),[p,S]=o.useState(null),E=o.useRef(t);E.current=t,o.useEffect(()=>{if(!e){l([]),g(!1),c(null);return}let s=!0;return g(!0),c(null),e().then(h=>{s&&l(Array.isArray(h)?h:[])}).catch(h=>{s&&(l([]),c(h),E.current?.(h))}).finally(()=>{s&&g(!1)}),()=>{s=!1}},[e]);const b=o.useMemo(()=>we(i,n),[i,n]),x=o.useMemo(()=>De(b,r),[b,r]),d=x?.playlistText??null;return o.useEffect(()=>{if(d===null){S(null);return}const s=Pe(d);return S(s),()=>{URL.revokeObjectURL(s),S(null)}},[d]),{chunks:b,chunk:x,url:p,loading:a,error:f}}function Ae(e,r,n){const[t,i]=o.useState(n),[l,a]=o.useState(0),[g,f]=o.useState(0);return o.useEffect(()=>{const c=e.current;if(!c)return;const p=()=>a(c.currentTime||0),S=()=>f(isFinite(c.duration)?c.duration:0),E=()=>i(!0),b=()=>i(!1);return c.addEventListener("timeupdate",p),c.addEventListener("loadedmetadata",S),c.addEventListener("playing",E),c.addEventListener("pause",b),a(c.currentTime||0),f(isFinite(c.duration)?c.duration:0),()=>{c.removeEventListener("timeupdate",p),c.removeEventListener("loadedmetadata",S),c.removeEventListener("playing",E),c.removeEventListener("pause",b)}},[r]),{isPlaying:t,currentTime:l,duration:g}}function nt({src:e,emptyMessage:r="No video to display",recordingSearch:n,recordingAt:t,recordingChunkSeconds:i,onRecordingChunks:l,loadingMessage:a="Loading recording…",autoPlay:g=!0,muted:f=!0,height:c=480,className:p,showControls:S=!0,showSpeedMenu:E=!0,speedOptions:b,initialSpeed:x=1,showGoLive:d=!1,goLiveMode:s="source",goLiveLabel:h="Go Live",exitLiveLabel:T="Back to recording",onExitLive:M,allowSpeedWhenLive:I=!1,liveEdgeToleranceSeconds:_=se,onGoLive:u,onSpeedChange:D,onError:j,onLiveChange:O,onEngineChange:B,debug:H=!1}){const y=o.useRef(null),k=o.useRef(null),w=o.useRef(null),N=typeof e=="string"&&e.trim()!=="",{chunks:A,url:R,loading:C}=Ne({search:N?void 0:n,at:t,chunkSeconds:i,onError:j}),v=o.useRef(l);v.current=l,o.useEffect(()=>{v.current?.(A)},[A]);const U=N?e:R??void 0,{speed:Ce,setSpeed:X,showSpeedMenu:Ie,setShowSpeedMenu:Be,speedMenuRef:je,speedRef:V,applyPlaybackSpeed:Oe,clearJumpScan:Fe}=Re({videoRef:y,hlsRef:k,engineRef:w,initialSpeed:x,onSpeedChange:D}),{url:He,isLive:P,engine:oe}=xe({src:U,videoRef:y,hlsRef:k,engineRef:w,speedRef:V,applyPlaybackSpeed:Oe,clearJumpScan:Fe,autoPlay:g,onError:j,debug:H}),{isPlaying:Ue,currentTime:We,duration:$e}=Ae(y,He,g),{secondsBehind:ie,behindLiveEdge:ze,goLive:Ge}=ve(y,P,_),ae=o.useRef(O);ae.current=O,o.useEffect(()=>{ae.current?.(P)},[P]);const ue=o.useRef(B);ue.current=B,o.useEffect(()=>{ue.current?.(oe)},[oe]);const q=s==="source"&&P===!0;let W=!1,$=!1,z=h,le=h;d&&(s==="source"?q?(W=!!M,$=!0,le=T,z=`${T} — leave the live stream`):(W=!0,$=P===!1,z=P===null?"Checking the stream…":`${h} — switch to the live stream`):(W=P===!0,$=ze,z=ie===null?h:`${h} — ${Math.round(ie)}s behind`));const Je=()=>{if(q){M?.();return}s==="seek"&&Ge(),u?.()},K=!I&&P===!0;return o.useEffect(()=>{K&&V.current!==1&&X(1)},[K,X,V]),typeof U=="string"&&U.trim()!==""?m.jsxs("div",{className:p,style:{position:"relative",width:"100%"},"data-testid":"hls-player",children:[m.jsx("video",{ref:y,controls:!1,autoPlay:g,muted:f,playsInline:!0,onContextMenu:Xe=>Xe.preventDefault(),controlsList:"nodownload nofullscreen noremoteplayback noplaybackrate",disablePictureInPicture:!0,disableRemotePlayback:!0,style:{width:"100%",height:c,display:"block",background:"#000"},children:m.jsx("track",{kind:"captions"})}),S&&m.jsx(Ee,{videoRef:y,isPlaying:Ue,currentTime:We,duration:$e,isLive:P===!0,goLive:W?m.jsx(pe,{enabled:$,label:le,hint:z,live:q,onGoLive:Je}):void 0}),E&&m.jsx(ye,{speed:Ce,setSpeed:X,showSpeedMenu:Ie,setShowSpeedMenu:Be,speedMenuRef:je,options:b,disabled:K})]}):m.jsx("div",{className:p,style:{position:"relative",width:"100%"},"data-testid":"hls-player",children:m.jsx("div",{"data-testid":C?"hls-player-loading":"hls-player-empty",style:{width:"100%",height:c,display:"flex",alignItems:"center",justifyContent:"center",background:"#000",color:"#94a3b8",fontSize:14,textAlign:"center",padding:16,boxSizing:"border-box"},children:C?a:r})})}exports.CHUNK_DURATION_SECONDS=J;exports.DEFAULT_LIVE_EDGE_TOLERANCE_SECONDS=se;exports.GoLiveButton=pe;exports.HlsPlayer=nt;exports.JUMP_SCAN_BASE_RATE=Y;exports.JUMP_SCAN_INTERVAL_MS=Z;exports.JUMP_SCAN_THRESHOLD=me;exports.PlaybackControls=Ee;exports.SPEED_OPTIONS=ge;exports.SpeedMenu=ye;exports.buildChunkPlaylistText=_e;exports.buildRecordingChunks=we;exports.chunkAt=De;exports.chunkSegments=Me;exports.formatTime=G;exports.getBufferedAhead=he;exports.getLiveEdge=ne;exports.getScaledBufferSeconds=te;exports.getScaledBufferSizeBytes=re;exports.getSecondsBehindLive=Q;exports.isSafariBrowser=Se;exports.jumpToLiveEdge=F;exports.makeBlobUrl=Pe;exports.parseM3u8=Te;exports.parseSegmentTimestamp=Le;exports.useClickOutside=ke;exports.useHlsEngine=xe;exports.useLiveEdge=ve;exports.usePlaybackSpeed=Re;exports.useRecordingSearch=Ne;exports.useVideoPlaybackState=Ae;exports.withReloadMarker=ee;exports.withoutReloadMarker=be;
|
|
4
|
+
//# sourceMappingURL=index.cjs.map
|