@livepeer-frameworks/player-react 0.1.0 → 0.1.2
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 +7 -9
- package/dist/cjs/_virtual/_rollupPluginBabelHelpers.js +359 -0
- package/dist/cjs/_virtual/_rollupPluginBabelHelpers.js.map +1 -0
- package/dist/cjs/assets/logomark.svg.js +8 -0
- package/dist/cjs/assets/logomark.svg.js.map +1 -0
- package/dist/cjs/components/DevModePanel.js +826 -0
- package/dist/cjs/components/DevModePanel.js.map +1 -0
- package/dist/cjs/components/DvdLogo.js +200 -0
- package/dist/cjs/components/DvdLogo.js.map +1 -0
- package/dist/cjs/components/Icons.js +439 -0
- package/dist/cjs/components/Icons.js.map +1 -0
- package/dist/cjs/components/IdleScreen.js +587 -0
- package/dist/cjs/components/IdleScreen.js.map +1 -0
- package/dist/cjs/components/LoadingScreen.js +523 -0
- package/dist/cjs/components/LoadingScreen.js.map +1 -0
- package/dist/cjs/components/Player.js +420 -0
- package/dist/cjs/components/Player.js.map +1 -0
- package/dist/cjs/components/PlayerControls.js +798 -0
- package/dist/cjs/components/PlayerControls.js.map +1 -0
- package/dist/cjs/components/PlayerErrorBoundary.js +80 -0
- package/dist/cjs/components/PlayerErrorBoundary.js.map +1 -0
- package/dist/cjs/components/SeekBar.js +253 -0
- package/dist/cjs/components/SeekBar.js.map +1 -0
- package/dist/cjs/components/SkipIndicator.js +92 -0
- package/dist/cjs/components/SkipIndicator.js.map +1 -0
- package/dist/cjs/components/SpeedIndicator.js +43 -0
- package/dist/cjs/components/SpeedIndicator.js.map +1 -0
- package/dist/cjs/components/StatsPanel.js +202 -0
- package/dist/cjs/components/StatsPanel.js.map +1 -0
- package/dist/cjs/components/StreamStateOverlay.js +229 -0
- package/dist/cjs/components/StreamStateOverlay.js.map +1 -0
- package/dist/cjs/components/ThumbnailOverlay.js +86 -0
- package/dist/cjs/components/ThumbnailOverlay.js.map +1 -0
- package/dist/cjs/components/TitleOverlay.js +32 -0
- package/dist/cjs/components/TitleOverlay.js.map +1 -0
- package/dist/cjs/context/PlayerContext.js +46 -0
- package/dist/cjs/context/PlayerContext.js.map +1 -0
- package/dist/cjs/hooks/useMetaTrack.js +165 -0
- package/dist/cjs/hooks/useMetaTrack.js.map +1 -0
- package/dist/cjs/hooks/usePlaybackQuality.js +131 -0
- package/dist/cjs/hooks/usePlaybackQuality.js.map +1 -0
- package/dist/cjs/hooks/usePlayerController.js +518 -0
- package/dist/cjs/hooks/usePlayerController.js.map +1 -0
- package/dist/cjs/hooks/usePlayerSelection.js +90 -0
- package/dist/cjs/hooks/usePlayerSelection.js.map +1 -0
- package/dist/cjs/hooks/useStreamState.js +360 -0
- package/dist/cjs/hooks/useStreamState.js.map +1 -0
- package/dist/cjs/hooks/useTelemetry.js +120 -0
- package/dist/cjs/hooks/useTelemetry.js.map +1 -0
- package/dist/cjs/hooks/useViewerEndpoints.js +222 -0
- package/dist/cjs/hooks/useViewerEndpoints.js.map +1 -0
- package/dist/cjs/index.js +97 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/ui/badge.js +34 -0
- package/dist/cjs/ui/badge.js.map +1 -0
- package/dist/cjs/ui/button.js +74 -0
- package/dist/cjs/ui/button.js.map +1 -0
- package/dist/cjs/ui/context-menu.js +163 -0
- package/dist/cjs/ui/context-menu.js.map +1 -0
- package/dist/cjs/ui/slider.js +60 -0
- package/dist/cjs/ui/slider.js.map +1 -0
- package/dist/esm/_virtual/_rollupPluginBabelHelpers.js +329 -0
- package/dist/esm/_virtual/_rollupPluginBabelHelpers.js.map +1 -0
- package/dist/esm/assets/logomark.svg.js +4 -0
- package/dist/esm/assets/logomark.svg.js.map +1 -0
- package/dist/esm/components/DevModePanel.js +822 -0
- package/dist/esm/components/DevModePanel.js.map +1 -0
- package/dist/esm/components/DvdLogo.js +196 -0
- package/dist/esm/components/DvdLogo.js.map +1 -0
- package/dist/esm/components/Icons.js +421 -0
- package/dist/esm/components/Icons.js.map +1 -0
- package/dist/esm/components/IdleScreen.js +582 -0
- package/dist/esm/components/IdleScreen.js.map +1 -0
- package/dist/esm/components/LoadingScreen.js +519 -0
- package/dist/esm/components/LoadingScreen.js.map +1 -0
- package/dist/esm/components/Player.js +416 -0
- package/dist/esm/components/Player.js.map +1 -0
- package/dist/esm/components/PlayerControls.js +794 -0
- package/dist/esm/components/PlayerControls.js.map +1 -0
- package/dist/esm/components/PlayerErrorBoundary.js +76 -0
- package/dist/esm/components/PlayerErrorBoundary.js.map +1 -0
- package/dist/esm/components/SeekBar.js +249 -0
- package/dist/esm/components/SeekBar.js.map +1 -0
- package/dist/esm/components/SkipIndicator.js +88 -0
- package/dist/esm/components/SkipIndicator.js.map +1 -0
- package/dist/esm/components/SpeedIndicator.js +39 -0
- package/dist/esm/components/SpeedIndicator.js.map +1 -0
- package/dist/esm/components/StatsPanel.js +198 -0
- package/dist/esm/components/StatsPanel.js.map +1 -0
- package/dist/esm/components/StreamStateOverlay.js +224 -0
- package/dist/esm/components/StreamStateOverlay.js.map +1 -0
- package/dist/esm/components/ThumbnailOverlay.js +82 -0
- package/dist/esm/components/ThumbnailOverlay.js.map +1 -0
- package/dist/esm/components/TitleOverlay.js +28 -0
- package/dist/esm/components/TitleOverlay.js.map +1 -0
- package/dist/esm/context/PlayerContext.js +41 -0
- package/dist/esm/context/PlayerContext.js.map +1 -0
- package/dist/esm/hooks/useMetaTrack.js +163 -0
- package/dist/esm/hooks/useMetaTrack.js.map +1 -0
- package/dist/esm/hooks/usePlaybackQuality.js +129 -0
- package/dist/esm/hooks/usePlaybackQuality.js.map +1 -0
- package/dist/esm/hooks/usePlayerController.js +516 -0
- package/dist/esm/hooks/usePlayerController.js.map +1 -0
- package/dist/esm/hooks/usePlayerSelection.js +88 -0
- package/dist/esm/hooks/usePlayerSelection.js.map +1 -0
- package/dist/esm/hooks/useStreamState.js +358 -0
- package/dist/esm/hooks/useStreamState.js.map +1 -0
- package/dist/esm/hooks/useTelemetry.js +118 -0
- package/dist/esm/hooks/useTelemetry.js.map +1 -0
- package/dist/esm/hooks/useViewerEndpoints.js +220 -0
- package/dist/esm/hooks/useViewerEndpoints.js.map +1 -0
- package/dist/esm/index.js +23 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/ui/badge.js +31 -0
- package/dist/esm/ui/badge.js.map +1 -0
- package/dist/esm/ui/button.js +52 -0
- package/dist/esm/ui/button.js.map +1 -0
- package/dist/esm/ui/context-menu.js +132 -0
- package/dist/esm/ui/context-menu.js.map +1 -0
- package/dist/esm/ui/slider.js +38 -0
- package/dist/esm/ui/slider.js.map +1 -0
- package/dist/types/components/DvdLogo.d.ts +1 -1
- package/dist/types/components/Icons.d.ts +1 -1
- package/dist/types/components/Player.d.ts +1 -1
- package/dist/types/components/PlayerErrorBoundary.d.ts +2 -1
- package/dist/types/components/StreamStateOverlay.d.ts +2 -2
- package/dist/types/components/SubtitleRenderer.d.ts +2 -2
- package/dist/types/context/PlayerContext.d.ts +2 -2
- package/dist/types/context/index.d.ts +2 -2
- package/dist/types/hooks/useMetaTrack.d.ts +3 -3
- package/dist/types/hooks/usePlaybackQuality.d.ts +2 -2
- package/dist/types/hooks/usePlayerController.d.ts +26 -3
- package/dist/types/hooks/usePlayerSelection.d.ts +1 -1
- package/dist/types/hooks/useStreamState.d.ts +1 -1
- package/dist/types/hooks/useTelemetry.d.ts +1 -1
- package/dist/types/hooks/useViewerEndpoints.d.ts +3 -3
- package/dist/types/index.d.ts +28 -28
- package/dist/types/types.d.ts +3 -3
- package/dist/types/ui/select.d.ts +1 -1
- package/package.json +22 -14
- package/src/components/DevModePanel.tsx +244 -143
- package/src/components/DvdLogo.tsx +1 -1
- package/src/components/Icons.tsx +105 -25
- package/src/components/IdleScreen.tsx +262 -128
- package/src/components/LoadingScreen.tsx +169 -151
- package/src/components/LogoOverlay.tsx +3 -6
- package/src/components/Player.tsx +126 -59
- package/src/components/PlayerControls.tsx +384 -272
- package/src/components/PlayerErrorBoundary.tsx +7 -13
- package/src/components/SeekBar.tsx +96 -88
- package/src/components/SkipIndicator.tsx +2 -12
- package/src/components/SpeedIndicator.tsx +2 -11
- package/src/components/StatsPanel.tsx +31 -22
- package/src/components/StreamStateOverlay.tsx +105 -49
- package/src/components/SubtitleRenderer.tsx +29 -29
- package/src/components/ThumbnailOverlay.tsx +5 -6
- package/src/components/TitleOverlay.tsx +2 -8
- package/src/context/PlayerContext.tsx +4 -8
- package/src/context/index.ts +3 -3
- package/src/hooks/useMetaTrack.ts +27 -27
- package/src/hooks/usePlaybackQuality.ts +3 -3
- package/src/hooks/usePlayerController.ts +246 -138
- package/src/hooks/usePlayerSelection.ts +6 -6
- package/src/hooks/useStreamState.ts +51 -56
- package/src/hooks/useTelemetry.ts +18 -3
- package/src/hooks/useViewerEndpoints.ts +34 -23
- package/src/index.tsx +36 -28
- package/src/types.ts +8 -8
- package/src/ui/badge.tsx +6 -5
- package/src/ui/button.tsx +9 -8
- package/src/ui/context-menu.tsx +42 -61
- package/src/ui/select.tsx +13 -7
- package/src/ui/slider.tsx +18 -29
- package/dist/types/components/players/DashJsPlayer.d.ts +0 -18
- package/dist/types/components/players/HlsJsPlayer.d.ts +0 -18
- package/dist/types/components/players/MewsWsPlayer/index.d.ts +0 -18
- package/dist/types/components/players/MistPlayer.d.ts +0 -20
- package/dist/types/components/players/MistWebRTCPlayer/index.d.ts +0 -20
- package/dist/types/components/players/NativePlayer.d.ts +0 -19
- package/dist/types/components/players/VideoJsPlayer.d.ts +0 -18
- package/src/components/players/DashJsPlayer.tsx +0 -56
- package/src/components/players/HlsJsPlayer.tsx +0 -56
- package/src/components/players/MewsWsPlayer/index.tsx +0 -56
- package/src/components/players/MistPlayer.tsx +0 -60
- package/src/components/players/MistWebRTCPlayer/index.tsx +0 -59
- package/src/components/players/NativePlayer.tsx +0 -58
- package/src/components/players/VideoJsPlayer.tsx +0 -56
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
SkipBackIcon,
|
|
24
24
|
SkipForwardIcon,
|
|
25
25
|
VolumeIcon,
|
|
26
|
-
SettingsIcon
|
|
26
|
+
SettingsIcon,
|
|
27
27
|
} from "./Icons";
|
|
28
28
|
import type { MistStreamInfo, PlaybackMode } from "../types";
|
|
29
29
|
|
|
@@ -51,7 +51,15 @@ interface PlayerControlsProps {
|
|
|
51
51
|
/** Video element - passed from parent hook */
|
|
52
52
|
videoElement?: HTMLVideoElement | null;
|
|
53
53
|
/** Available quality levels - passed from parent hook */
|
|
54
|
-
qualities?: Array<{
|
|
54
|
+
qualities?: Array<{
|
|
55
|
+
id: string;
|
|
56
|
+
label: string;
|
|
57
|
+
bitrate?: number;
|
|
58
|
+
width?: number;
|
|
59
|
+
height?: number;
|
|
60
|
+
isAuto?: boolean;
|
|
61
|
+
active?: boolean;
|
|
62
|
+
}>;
|
|
55
63
|
/** Callback to select quality */
|
|
56
64
|
onSelectQuality?: (id: string) => void;
|
|
57
65
|
/** Is player muted */
|
|
@@ -78,16 +86,15 @@ interface PlayerControlsProps {
|
|
|
78
86
|
onJumpToLive?: () => void;
|
|
79
87
|
}
|
|
80
88
|
|
|
81
|
-
|
|
82
89
|
const PlayerControls: React.FC<PlayerControlsProps> = ({
|
|
83
90
|
currentTime,
|
|
84
91
|
duration,
|
|
85
92
|
isVisible = true,
|
|
86
|
-
className,
|
|
93
|
+
className: _className,
|
|
87
94
|
onSeek,
|
|
88
95
|
mistStreamInfo,
|
|
89
96
|
disabled = false,
|
|
90
|
-
playbackMode =
|
|
97
|
+
playbackMode = "auto",
|
|
91
98
|
onModeChange,
|
|
92
99
|
sourceType,
|
|
93
100
|
isContentLive,
|
|
@@ -120,9 +127,10 @@ const PlayerControls: React.FC<PlayerControlsProps> = ({
|
|
|
120
127
|
if (propVideoElement) return propVideoElement;
|
|
121
128
|
if (contextVideo) return contextVideo;
|
|
122
129
|
if (player?.getVideoElement?.()) return player.getVideoElement();
|
|
123
|
-
const domVideo =
|
|
124
|
-
|
|
125
|
-
|
|
130
|
+
const domVideo =
|
|
131
|
+
(document.querySelector(".fw-player-video") as HTMLVideoElement | null) ??
|
|
132
|
+
(document.querySelector('[data-player-container="true"] video') as HTMLVideoElement | null) ??
|
|
133
|
+
(document.querySelector(".fw-player-container video") as HTMLVideoElement | null);
|
|
126
134
|
return domVideo;
|
|
127
135
|
}, [propVideoElement, contextVideo, player]);
|
|
128
136
|
|
|
@@ -183,7 +191,7 @@ const PlayerControls: React.FC<PlayerControlsProps> = ({
|
|
|
183
191
|
// Fallback to Mist track metadata for players without quality API
|
|
184
192
|
if (mistTracks) {
|
|
185
193
|
return Object.entries(mistTracks)
|
|
186
|
-
.filter(([, t]) => t.type ===
|
|
194
|
+
.filter(([, t]) => t.type === "video")
|
|
187
195
|
.map(([id, t]) => ({
|
|
188
196
|
id,
|
|
189
197
|
label: t.height ? `${t.height}p` : t.codec,
|
|
@@ -214,7 +222,9 @@ const PlayerControls: React.FC<PlayerControlsProps> = ({
|
|
|
214
222
|
const isPlaying = propIsPlaying ?? internalIsPlaying;
|
|
215
223
|
const isMuted = propIsMuted ?? internalIsMuted;
|
|
216
224
|
const isFullscreen = propIsFullscreen ?? internalIsFullscreen;
|
|
217
|
-
const
|
|
225
|
+
const actualVolume = propVolume !== undefined ? Math.round(propVolume * 100) : internalVolume;
|
|
226
|
+
// Show 0 when muted, actual volume otherwise
|
|
227
|
+
const volumeValue = isMuted ? 0 : actualVolume;
|
|
218
228
|
const [qualityValue, setQualityValue] = useState<string>("auto");
|
|
219
229
|
const [captionValue, setCaptionValue] = useState<string>("none");
|
|
220
230
|
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
|
@@ -227,81 +237,106 @@ const PlayerControls: React.FC<PlayerControlsProps> = ({
|
|
|
227
237
|
|
|
228
238
|
const handleWindowClick = (event: MouseEvent) => {
|
|
229
239
|
const target = event.target as HTMLElement;
|
|
230
|
-
if (target && !target.closest(
|
|
240
|
+
if (target && !target.closest(".fw-settings-menu")) {
|
|
231
241
|
setIsSettingsOpen(false);
|
|
232
242
|
}
|
|
233
243
|
};
|
|
234
244
|
|
|
235
245
|
// Use setTimeout to avoid immediate close from the same click that opened it
|
|
236
246
|
const timeoutId = setTimeout(() => {
|
|
237
|
-
window.addEventListener(
|
|
247
|
+
window.addEventListener("click", handleWindowClick);
|
|
238
248
|
}, 0);
|
|
239
249
|
|
|
240
250
|
return () => {
|
|
241
251
|
clearTimeout(timeoutId);
|
|
242
|
-
window.removeEventListener(
|
|
252
|
+
window.removeEventListener("click", handleWindowClick);
|
|
243
253
|
};
|
|
244
254
|
}, [isSettingsOpen]);
|
|
245
255
|
|
|
246
256
|
// Core utility-based calculations
|
|
247
|
-
const deriveBufferWindowMs = useCallback(
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
const
|
|
265
|
-
|
|
257
|
+
const deriveBufferWindowMs = useCallback(
|
|
258
|
+
(tracks?: Record<string, { firstms?: number; lastms?: number }>) => {
|
|
259
|
+
if (!tracks) return undefined;
|
|
260
|
+
const list = Object.values(tracks);
|
|
261
|
+
if (list.length === 0) return undefined;
|
|
262
|
+
const firstmsValues = list.map((t) => t.firstms).filter((v): v is number => v !== undefined);
|
|
263
|
+
const lastmsValues = list.map((t) => t.lastms).filter((v): v is number => v !== undefined);
|
|
264
|
+
if (firstmsValues.length === 0 || lastmsValues.length === 0) return undefined;
|
|
265
|
+
const firstms = Math.max(...firstmsValues);
|
|
266
|
+
const lastms = Math.min(...lastmsValues);
|
|
267
|
+
const window = lastms - firstms;
|
|
268
|
+
if (!Number.isFinite(window) || window <= 0) return undefined;
|
|
269
|
+
return window;
|
|
270
|
+
},
|
|
271
|
+
[]
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
const bufferWindowMs =
|
|
275
|
+
mistStreamInfo?.meta?.buffer_window ??
|
|
276
|
+
deriveBufferWindowMs(
|
|
277
|
+
mistStreamInfo?.meta?.tracks as
|
|
278
|
+
| Record<string, { firstms?: number; lastms?: number }>
|
|
279
|
+
| undefined
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
const isLive = useMemo(
|
|
283
|
+
() => isLiveContent(isContentLive, mistStreamInfo, duration),
|
|
284
|
+
[isContentLive, mistStreamInfo, duration]
|
|
285
|
+
);
|
|
266
286
|
|
|
267
287
|
const isWebRTC = useMemo(() => isMediaStreamSource(video), [video]);
|
|
268
288
|
|
|
269
289
|
const supportsPlaybackRate = useMemo(() => coreSupportsPlaybackRate(video), [video]);
|
|
270
290
|
|
|
271
291
|
// Seekable range using core calculation (allow controller override)
|
|
272
|
-
const allowMediaStreamDvr =
|
|
273
|
-
(
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
292
|
+
const allowMediaStreamDvr =
|
|
293
|
+
isMediaStreamSource(video) &&
|
|
294
|
+
bufferWindowMs !== undefined &&
|
|
295
|
+
bufferWindowMs > 0 &&
|
|
296
|
+
sourceType !== "whep" &&
|
|
297
|
+
sourceType !== "webrtc";
|
|
298
|
+
const { seekableStart: calcSeekableStart, liveEdge: calcLiveEdge } = useMemo(
|
|
299
|
+
() =>
|
|
300
|
+
calculateSeekableRange({
|
|
301
|
+
isLive,
|
|
302
|
+
video,
|
|
303
|
+
mistStreamInfo,
|
|
304
|
+
currentTime,
|
|
305
|
+
duration,
|
|
306
|
+
allowMediaStreamDvr,
|
|
307
|
+
}),
|
|
308
|
+
[isLive, video, mistStreamInfo, currentTime, duration, allowMediaStreamDvr]
|
|
309
|
+
);
|
|
283
310
|
const controllerSeekableStart = player?.getSeekableStart?.();
|
|
284
311
|
const controllerLiveEdge = player?.getLiveEdge?.();
|
|
285
|
-
const useControllerRange =
|
|
312
|
+
const useControllerRange =
|
|
313
|
+
Number.isFinite(controllerSeekableStart) &&
|
|
286
314
|
Number.isFinite(controllerLiveEdge) &&
|
|
287
315
|
(controllerLiveEdge as number) >= (controllerSeekableStart as number) &&
|
|
288
316
|
((controllerLiveEdge as number) > 0 || (controllerSeekableStart as number) > 0);
|
|
289
|
-
const seekableStart = useControllerRange
|
|
317
|
+
const seekableStart = useControllerRange
|
|
318
|
+
? (controllerSeekableStart as number)
|
|
319
|
+
: calcSeekableStart;
|
|
290
320
|
const liveEdge = useControllerRange ? (controllerLiveEdge as number) : calcLiveEdge;
|
|
291
321
|
|
|
292
|
-
const hasDvrWindow =
|
|
322
|
+
const hasDvrWindow =
|
|
323
|
+
isLive &&
|
|
324
|
+
Number.isFinite(liveEdge) &&
|
|
325
|
+
Number.isFinite(seekableStart) &&
|
|
326
|
+
liveEdge > seekableStart;
|
|
293
327
|
const commitOnRelease = isLive;
|
|
294
328
|
|
|
295
329
|
// Live thresholds with buffer window scaling
|
|
296
|
-
const liveThresholds = useMemo(
|
|
297
|
-
calculateLiveThresholds(sourceType, isWebRTC, bufferWindowMs),
|
|
298
|
-
[sourceType, isWebRTC, bufferWindowMs]
|
|
330
|
+
const liveThresholds = useMemo(
|
|
331
|
+
() => calculateLiveThresholds(sourceType, isWebRTC, bufferWindowMs),
|
|
332
|
+
[sourceType, isWebRTC, bufferWindowMs]
|
|
333
|
+
);
|
|
299
334
|
|
|
300
335
|
// Can seek - prefer PlayerController's computed value (includes player-specific canSeek)
|
|
301
336
|
// Fall back to utility function when controller not available
|
|
302
337
|
const baseCanSeek = useMemo(() => {
|
|
303
338
|
// PlayerController already computes canSeek with player-specific logic
|
|
304
|
-
if (player && typeof (player as any).canSeekStream ===
|
|
339
|
+
if (player && typeof (player as any).canSeekStream === "function") {
|
|
305
340
|
return (player as any).canSeekStream();
|
|
306
341
|
}
|
|
307
342
|
// Fallback when no controller
|
|
@@ -353,7 +388,8 @@ const PlayerControls: React.FC<PlayerControlsProps> = ({
|
|
|
353
388
|
video.addEventListener("playing", updatePlayingState);
|
|
354
389
|
video.addEventListener("volumechange", updateMutedState);
|
|
355
390
|
video.addEventListener("ratechange", updatePlaybackRate);
|
|
356
|
-
if (typeof document !== "undefined")
|
|
391
|
+
if (typeof document !== "undefined")
|
|
392
|
+
document.addEventListener("fullscreenchange", updateFullscreenState);
|
|
357
393
|
|
|
358
394
|
return () => {
|
|
359
395
|
video.removeEventListener("play", updatePlayingState);
|
|
@@ -361,7 +397,8 @@ const PlayerControls: React.FC<PlayerControlsProps> = ({
|
|
|
361
397
|
video.removeEventListener("playing", updatePlayingState);
|
|
362
398
|
video.removeEventListener("volumechange", updateMutedState);
|
|
363
399
|
video.removeEventListener("ratechange", updatePlaybackRate);
|
|
364
|
-
if (typeof document !== "undefined")
|
|
400
|
+
if (typeof document !== "undefined")
|
|
401
|
+
document.removeEventListener("fullscreenchange", updateFullscreenState);
|
|
365
402
|
};
|
|
366
403
|
}, [video, isLive]);
|
|
367
404
|
|
|
@@ -392,7 +429,10 @@ const PlayerControls: React.FC<PlayerControlsProps> = ({
|
|
|
392
429
|
}, [video]);
|
|
393
430
|
|
|
394
431
|
useEffect(() => {
|
|
395
|
-
if (!video) {
|
|
432
|
+
if (!video) {
|
|
433
|
+
setHasAudio(true);
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
396
436
|
const checkAudio = () => {
|
|
397
437
|
if (video.srcObject instanceof MediaStream) {
|
|
398
438
|
const audioTracks = video.srcObject.getAudioTracks();
|
|
@@ -459,7 +499,7 @@ const PlayerControls: React.FC<PlayerControlsProps> = ({
|
|
|
459
499
|
return;
|
|
460
500
|
}
|
|
461
501
|
// Fallback: direct video/player manipulation
|
|
462
|
-
const v = video ?? document.querySelector(
|
|
502
|
+
const v = video ?? (document.querySelector(".fw-player-video") as HTMLVideoElement | null);
|
|
463
503
|
if (!v) return;
|
|
464
504
|
const nextMuted = !(player?.isMuted?.() ?? v.muted);
|
|
465
505
|
player?.setMuted?.(nextMuted);
|
|
@@ -469,22 +509,25 @@ const PlayerControls: React.FC<PlayerControlsProps> = ({
|
|
|
469
509
|
else setInternalVolume(Math.round(v.volume * 100));
|
|
470
510
|
};
|
|
471
511
|
|
|
472
|
-
const handleVolumeChange = (
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
onVolumeChange
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
512
|
+
const handleVolumeChange = useCallback(
|
|
513
|
+
(value: number[]) => {
|
|
514
|
+
if (disabled) return;
|
|
515
|
+
const next = Math.max(0, Math.min(100, value[0] ?? 0));
|
|
516
|
+
// Prefer prop callback from usePlayerController
|
|
517
|
+
if (onVolumeChange) {
|
|
518
|
+
onVolumeChange(next / 100);
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
// Fallback: direct video manipulation
|
|
522
|
+
const v = video ?? (document.querySelector(".fw-player-video") as HTMLVideoElement | null);
|
|
523
|
+
if (!v) return;
|
|
524
|
+
v.volume = next / 100;
|
|
525
|
+
v.muted = next === 0;
|
|
526
|
+
setInternalVolume(next);
|
|
527
|
+
setInternalIsMuted(next === 0);
|
|
528
|
+
},
|
|
529
|
+
[disabled, onVolumeChange, video]
|
|
530
|
+
);
|
|
488
531
|
|
|
489
532
|
const handleFullscreen = () => {
|
|
490
533
|
if (disabled) return;
|
|
@@ -495,7 +538,9 @@ const PlayerControls: React.FC<PlayerControlsProps> = ({
|
|
|
495
538
|
}
|
|
496
539
|
// Fallback: direct DOM manipulation
|
|
497
540
|
if (typeof document === "undefined") return;
|
|
498
|
-
const container = document.querySelector(
|
|
541
|
+
const container = document.querySelector(
|
|
542
|
+
'[data-player-container="true"]'
|
|
543
|
+
) as HTMLElement | null;
|
|
499
544
|
if (!container) return;
|
|
500
545
|
if (document.fullscreenElement) document.exitFullscreen().catch(() => {});
|
|
501
546
|
else container.requestFullscreen().catch(() => {});
|
|
@@ -543,29 +588,47 @@ const PlayerControls: React.FC<PlayerControlsProps> = ({
|
|
|
543
588
|
};
|
|
544
589
|
|
|
545
590
|
// Time display - using core formatTimeDisplay
|
|
546
|
-
const timeDisplay = useMemo(
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
591
|
+
const timeDisplay = useMemo(
|
|
592
|
+
() =>
|
|
593
|
+
formatTimeDisplay({
|
|
594
|
+
isLive,
|
|
595
|
+
currentTime,
|
|
596
|
+
duration,
|
|
597
|
+
liveEdge,
|
|
598
|
+
seekableStart,
|
|
599
|
+
unixoffset: mistStreamInfo?.unixoffset,
|
|
600
|
+
}),
|
|
601
|
+
[isLive, currentTime, duration, liveEdge, seekableStart, mistStreamInfo?.unixoffset]
|
|
602
|
+
);
|
|
554
603
|
|
|
555
604
|
const [isVolumeHovered, setIsVolumeHovered] = useState(false);
|
|
556
605
|
const [isVolumeFocused, setIsVolumeFocused] = useState(false);
|
|
557
606
|
const isVolumeExpanded = isVolumeHovered || isVolumeFocused;
|
|
607
|
+
const volumeGroupRef = useRef<HTMLDivElement>(null);
|
|
608
|
+
|
|
609
|
+
// Non-passive wheel listener for volume control
|
|
610
|
+
useEffect(() => {
|
|
611
|
+
const el = volumeGroupRef.current;
|
|
612
|
+
if (!el) return;
|
|
613
|
+
const handler = (e: WheelEvent) => {
|
|
614
|
+
if (disabled || !hasAudio) return;
|
|
615
|
+
e.preventDefault();
|
|
616
|
+
const delta = e.deltaY < 0 ? 5 : -5;
|
|
617
|
+
handleVolumeChange([actualVolume + delta]);
|
|
618
|
+
};
|
|
619
|
+
el.addEventListener("wheel", handler, { passive: false });
|
|
620
|
+
return () => el.removeEventListener("wheel", handler);
|
|
621
|
+
}, [disabled, hasAudio, actualVolume, handleVolumeChange]);
|
|
558
622
|
|
|
559
623
|
return (
|
|
560
|
-
<div
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
624
|
+
<div
|
|
625
|
+
className={cn(
|
|
626
|
+
"fw-player-surface fw-controls-wrapper",
|
|
627
|
+
isVisible ? "fw-controls-wrapper--visible" : "fw-controls-wrapper--hidden"
|
|
628
|
+
)}
|
|
629
|
+
>
|
|
564
630
|
{/* Bottom Row: Controls with SeekBar on top */}
|
|
565
|
-
<div
|
|
566
|
-
className="fw-control-bar pointer-events-auto"
|
|
567
|
-
onClick={(e) => e.stopPropagation()}
|
|
568
|
-
>
|
|
631
|
+
<div className="fw-control-bar pointer-events-auto" onClick={(e) => e.stopPropagation()}>
|
|
569
632
|
{/* SeekBar - sits directly on top of control buttons */}
|
|
570
633
|
{canSeek && (
|
|
571
634
|
<div className="fw-seek-wrapper">
|
|
@@ -591,222 +654,271 @@ const PlayerControls: React.FC<PlayerControlsProps> = ({
|
|
|
591
654
|
|
|
592
655
|
{/* Control buttons row */}
|
|
593
656
|
<div className="fw-controls-row">
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
657
|
+
{/* Left: Controls & Time */}
|
|
658
|
+
<div className="fw-controls-left">
|
|
659
|
+
<div className="fw-control-group">
|
|
660
|
+
<button
|
|
661
|
+
type="button"
|
|
662
|
+
className="fw-btn-flush"
|
|
663
|
+
aria-label={isPlaying ? "Pause" : "Play"}
|
|
664
|
+
onClick={handlePlayPause}
|
|
665
|
+
>
|
|
666
|
+
<PlayPauseIcon isPlaying={isPlaying} size={18} />
|
|
667
|
+
</button>
|
|
668
|
+
{canSeek && (
|
|
669
|
+
<>
|
|
670
|
+
<button
|
|
671
|
+
type="button"
|
|
672
|
+
className="fw-btn-flush hidden sm:flex"
|
|
673
|
+
aria-label="Skip back 10 seconds"
|
|
674
|
+
onClick={handleSkipBack}
|
|
675
|
+
>
|
|
676
|
+
<SkipBackIcon size={16} />
|
|
677
|
+
</button>
|
|
678
|
+
<button
|
|
679
|
+
type="button"
|
|
680
|
+
className="fw-btn-flush hidden sm:flex"
|
|
681
|
+
aria-label="Skip forward 10 seconds"
|
|
682
|
+
onClick={handleSkipForward}
|
|
683
|
+
>
|
|
684
|
+
<SkipForwardIcon size={16} />
|
|
685
|
+
</button>
|
|
686
|
+
</>
|
|
687
|
+
)}
|
|
688
|
+
</div>
|
|
611
689
|
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
{/* Volume icon - part of the pill */}
|
|
636
|
-
<button
|
|
637
|
-
type="button"
|
|
638
|
-
className="fw-volume-btn"
|
|
639
|
-
aria-label={!hasAudio ? "No audio" : (isMuted ? "Unmute" : "Mute")}
|
|
640
|
-
onClick={hasAudio ? handleMute : undefined}
|
|
641
|
-
disabled={!hasAudio}
|
|
690
|
+
{/* Volume pill - cohesive hover element (slab style) */}
|
|
691
|
+
<div
|
|
692
|
+
ref={volumeGroupRef}
|
|
693
|
+
className={cn(
|
|
694
|
+
"fw-volume-group",
|
|
695
|
+
isVolumeExpanded && "fw-volume-group--expanded",
|
|
696
|
+
!hasAudio && "fw-volume-group--disabled"
|
|
697
|
+
)}
|
|
698
|
+
onMouseEnter={() => hasAudio && setIsVolumeHovered(true)}
|
|
699
|
+
onMouseLeave={() => {
|
|
700
|
+
setIsVolumeHovered(false);
|
|
701
|
+
setIsVolumeFocused(false);
|
|
702
|
+
}}
|
|
703
|
+
onFocusCapture={() => hasAudio && setIsVolumeFocused(true)}
|
|
704
|
+
onBlurCapture={(e) => {
|
|
705
|
+
if (!e.currentTarget.contains(e.relatedTarget as Node)) setIsVolumeFocused(false);
|
|
706
|
+
}}
|
|
707
|
+
onClick={(e) => {
|
|
708
|
+
// Click on the pill (not slider) toggles mute
|
|
709
|
+
if (hasAudio && e.target === e.currentTarget) {
|
|
710
|
+
handleMute();
|
|
711
|
+
}
|
|
712
|
+
}}
|
|
642
713
|
>
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
)}>
|
|
650
|
-
<Slider
|
|
651
|
-
orientation="horizontal"
|
|
652
|
-
aria-label="Volume"
|
|
653
|
-
max={100}
|
|
654
|
-
step={1}
|
|
655
|
-
value={[volumeValue]}
|
|
656
|
-
onValueChange={handleVolumeChange}
|
|
657
|
-
className="w-full"
|
|
714
|
+
{/* Volume icon - part of the pill */}
|
|
715
|
+
<button
|
|
716
|
+
type="button"
|
|
717
|
+
className="fw-volume-btn"
|
|
718
|
+
aria-label={!hasAudio ? "No audio" : isMuted ? "Unmute" : "Mute"}
|
|
719
|
+
onClick={hasAudio ? handleMute : undefined}
|
|
658
720
|
disabled={!hasAudio}
|
|
659
|
-
|
|
721
|
+
>
|
|
722
|
+
<VolumeIcon isMuted={isMuted || !hasAudio} size={16} />
|
|
723
|
+
</button>
|
|
724
|
+
{/* Slider - expands within the pill */}
|
|
725
|
+
<div
|
|
726
|
+
className={cn(
|
|
727
|
+
"fw-volume-slider-wrapper",
|
|
728
|
+
isVolumeExpanded
|
|
729
|
+
? "fw-volume-slider-wrapper--expanded"
|
|
730
|
+
: "fw-volume-slider-wrapper--collapsed"
|
|
731
|
+
)}
|
|
732
|
+
>
|
|
733
|
+
<Slider
|
|
734
|
+
orientation="horizontal"
|
|
735
|
+
aria-label="Volume"
|
|
736
|
+
max={100}
|
|
737
|
+
step={1}
|
|
738
|
+
value={[volumeValue]}
|
|
739
|
+
onValueChange={handleVolumeChange}
|
|
740
|
+
className="w-full"
|
|
741
|
+
disabled={!hasAudio}
|
|
742
|
+
/>
|
|
743
|
+
</div>
|
|
660
744
|
</div>
|
|
661
|
-
</div>
|
|
662
745
|
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
746
|
+
<div className="fw-control-group">
|
|
747
|
+
<span className="fw-time-display">{timeDisplay}</span>
|
|
748
|
+
</div>
|
|
749
|
+
|
|
750
|
+
{isLive && (
|
|
751
|
+
<div className="fw-control-group">
|
|
752
|
+
<button
|
|
753
|
+
type="button"
|
|
754
|
+
onClick={handleGoLive}
|
|
755
|
+
disabled={!hasDvrWindow || isNearLiveState}
|
|
756
|
+
className={cn(
|
|
757
|
+
"fw-live-badge",
|
|
758
|
+
!hasDvrWindow || isNearLiveState
|
|
759
|
+
? "fw-live-badge--active"
|
|
760
|
+
: "fw-live-badge--behind"
|
|
761
|
+
)}
|
|
762
|
+
title={
|
|
763
|
+
!hasDvrWindow ? "Live only" : isNearLiveState ? "At live edge" : "Jump to live"
|
|
764
|
+
}
|
|
765
|
+
>
|
|
766
|
+
LIVE
|
|
767
|
+
{!isNearLiveState && hasDvrWindow && <SeekToLiveIcon size={10} />}
|
|
768
|
+
</button>
|
|
769
|
+
</div>
|
|
770
|
+
)}
|
|
667
771
|
</div>
|
|
668
772
|
|
|
669
|
-
{
|
|
670
|
-
|
|
773
|
+
{/* Right Group: Settings, Fullscreen */}
|
|
774
|
+
<div className="fw-controls-right">
|
|
775
|
+
<div className="fw-control-group relative">
|
|
671
776
|
<button
|
|
672
777
|
type="button"
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
(!hasDvrWindow || isNearLiveState) ? "fw-live-badge--active" : "fw-live-badge--behind"
|
|
678
|
-
)}
|
|
679
|
-
title={!hasDvrWindow ? "Live only" : (isNearLiveState ? "At live edge" : "Jump to live")}
|
|
778
|
+
className={cn("fw-btn-flush group", isSettingsOpen && "fw-btn-flush--active")}
|
|
779
|
+
aria-label="Settings"
|
|
780
|
+
title="Settings"
|
|
781
|
+
onClick={() => setIsSettingsOpen(!isSettingsOpen)}
|
|
680
782
|
>
|
|
681
|
-
|
|
682
|
-
{!isNearLiveState && hasDvrWindow && <SeekToLiveIcon size={10} />}
|
|
783
|
+
<SettingsIcon size={16} className="transition-transform group-hover:rotate-90" />
|
|
683
784
|
</button>
|
|
684
|
-
</div>
|
|
685
|
-
)}
|
|
686
|
-
</div>
|
|
687
785
|
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
onClick={() => { onModeChange(mode); setIsSettingsOpen(false); }}
|
|
717
|
-
>
|
|
718
|
-
{mode === 'low-latency' ? 'Fast' : mode === 'quality' ? 'Stable' : 'Auto'}
|
|
719
|
-
</button>
|
|
720
|
-
))}
|
|
786
|
+
{/* Settings Popup */}
|
|
787
|
+
{isSettingsOpen && (
|
|
788
|
+
<div className="fw-player-surface fw-settings-menu">
|
|
789
|
+
{/* Playback Mode - only show for live content (not VOD/clips) */}
|
|
790
|
+
{onModeChange && isContentLive !== false && (
|
|
791
|
+
<div className="fw-settings-section">
|
|
792
|
+
<div className="fw-settings-label">Mode</div>
|
|
793
|
+
<div className="fw-settings-options">
|
|
794
|
+
{(["auto", "low-latency", "quality"] as const).map((mode) => (
|
|
795
|
+
<button
|
|
796
|
+
key={mode}
|
|
797
|
+
className={cn(
|
|
798
|
+
"fw-settings-btn",
|
|
799
|
+
playbackMode === mode && "fw-settings-btn--active"
|
|
800
|
+
)}
|
|
801
|
+
onClick={() => {
|
|
802
|
+
onModeChange(mode);
|
|
803
|
+
setIsSettingsOpen(false);
|
|
804
|
+
}}
|
|
805
|
+
>
|
|
806
|
+
{mode === "low-latency"
|
|
807
|
+
? "Fast"
|
|
808
|
+
: mode === "quality"
|
|
809
|
+
? "Stable"
|
|
810
|
+
: "Auto"}
|
|
811
|
+
</button>
|
|
812
|
+
))}
|
|
813
|
+
</div>
|
|
721
814
|
</div>
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
815
|
+
)}
|
|
816
|
+
{supportsPlaybackRate && (
|
|
817
|
+
<div className="fw-settings-section">
|
|
818
|
+
<div className="fw-settings-label">Speed</div>
|
|
819
|
+
<div className="fw-settings-options fw-settings-options--wrap">
|
|
820
|
+
{SPEED_PRESETS.map((rate) => (
|
|
821
|
+
<button
|
|
822
|
+
key={rate}
|
|
823
|
+
className={cn(
|
|
824
|
+
"fw-settings-btn",
|
|
825
|
+
playbackRate === rate && "fw-settings-btn--active"
|
|
826
|
+
)}
|
|
827
|
+
onClick={() => {
|
|
828
|
+
handleSpeedChange(String(rate));
|
|
829
|
+
setIsSettingsOpen(false);
|
|
830
|
+
}}
|
|
831
|
+
>
|
|
832
|
+
{rate}x
|
|
833
|
+
</button>
|
|
834
|
+
))}
|
|
835
|
+
</div>
|
|
740
836
|
</div>
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
<div className="fw-settings-list">
|
|
747
|
-
<button
|
|
748
|
-
className={cn(
|
|
749
|
-
"fw-settings-list-item",
|
|
750
|
-
qualityValue === 'auto' && "fw-settings-list-item--active"
|
|
751
|
-
)}
|
|
752
|
-
onClick={() => { handleQualityChange('auto'); setIsSettingsOpen(false); }}
|
|
753
|
-
>
|
|
754
|
-
Auto
|
|
755
|
-
</button>
|
|
756
|
-
{qualities.map((q) => (
|
|
837
|
+
)}
|
|
838
|
+
{qualities.length > 0 && (
|
|
839
|
+
<div className="fw-settings-section">
|
|
840
|
+
<div className="fw-settings-label">Quality</div>
|
|
841
|
+
<div className="fw-settings-list">
|
|
757
842
|
<button
|
|
758
|
-
key={q.id}
|
|
759
843
|
className={cn(
|
|
760
844
|
"fw-settings-list-item",
|
|
761
|
-
qualityValue ===
|
|
845
|
+
qualityValue === "auto" && "fw-settings-list-item--active"
|
|
762
846
|
)}
|
|
763
|
-
onClick={() => {
|
|
847
|
+
onClick={() => {
|
|
848
|
+
handleQualityChange("auto");
|
|
849
|
+
setIsSettingsOpen(false);
|
|
850
|
+
}}
|
|
764
851
|
>
|
|
765
|
-
|
|
852
|
+
Auto
|
|
766
853
|
</button>
|
|
767
|
-
|
|
854
|
+
{qualities.map((q) => (
|
|
855
|
+
<button
|
|
856
|
+
key={q.id}
|
|
857
|
+
className={cn(
|
|
858
|
+
"fw-settings-list-item",
|
|
859
|
+
qualityValue === q.id && "fw-settings-list-item--active"
|
|
860
|
+
)}
|
|
861
|
+
onClick={() => {
|
|
862
|
+
handleQualityChange(q.id);
|
|
863
|
+
setIsSettingsOpen(false);
|
|
864
|
+
}}
|
|
865
|
+
>
|
|
866
|
+
{q.label}
|
|
867
|
+
</button>
|
|
868
|
+
))}
|
|
869
|
+
</div>
|
|
768
870
|
</div>
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
<div className="fw-settings-list">
|
|
775
|
-
<button
|
|
776
|
-
className={cn(
|
|
777
|
-
"fw-settings-list-item",
|
|
778
|
-
captionValue === 'none' && "fw-settings-list-item--active"
|
|
779
|
-
)}
|
|
780
|
-
onClick={() => { handleCaptionChange('none'); setIsSettingsOpen(false); }}
|
|
781
|
-
>
|
|
782
|
-
Off
|
|
783
|
-
</button>
|
|
784
|
-
{textTracks.map((t) => (
|
|
871
|
+
)}
|
|
872
|
+
{textTracks.length > 0 && (
|
|
873
|
+
<div className="fw-settings-section">
|
|
874
|
+
<div className="fw-settings-label">Captions</div>
|
|
875
|
+
<div className="fw-settings-list">
|
|
785
876
|
<button
|
|
786
|
-
key={t.id}
|
|
787
877
|
className={cn(
|
|
788
878
|
"fw-settings-list-item",
|
|
789
|
-
captionValue ===
|
|
879
|
+
captionValue === "none" && "fw-settings-list-item--active"
|
|
790
880
|
)}
|
|
791
|
-
onClick={() => {
|
|
881
|
+
onClick={() => {
|
|
882
|
+
handleCaptionChange("none");
|
|
883
|
+
setIsSettingsOpen(false);
|
|
884
|
+
}}
|
|
792
885
|
>
|
|
793
|
-
|
|
886
|
+
Off
|
|
794
887
|
</button>
|
|
795
|
-
|
|
888
|
+
{textTracks.map((t) => (
|
|
889
|
+
<button
|
|
890
|
+
key={t.id}
|
|
891
|
+
className={cn(
|
|
892
|
+
"fw-settings-list-item",
|
|
893
|
+
captionValue === t.id && "fw-settings-list-item--active"
|
|
894
|
+
)}
|
|
895
|
+
onClick={() => {
|
|
896
|
+
handleCaptionChange(t.id);
|
|
897
|
+
setIsSettingsOpen(false);
|
|
898
|
+
}}
|
|
899
|
+
>
|
|
900
|
+
{t.label || t.id}
|
|
901
|
+
</button>
|
|
902
|
+
))}
|
|
903
|
+
</div>
|
|
796
904
|
</div>
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
</div>
|
|
905
|
+
)}
|
|
906
|
+
</div>
|
|
907
|
+
)}
|
|
908
|
+
</div>
|
|
802
909
|
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
910
|
+
<div className="fw-control-group">
|
|
911
|
+
<button
|
|
912
|
+
type="button"
|
|
913
|
+
className="fw-btn-flush"
|
|
914
|
+
aria-label="Toggle fullscreen"
|
|
915
|
+
onClick={handleFullscreen}
|
|
916
|
+
>
|
|
917
|
+
<FullscreenToggleIcon isFullscreen={isFullscreen} size={16} />
|
|
918
|
+
</button>
|
|
919
|
+
</div>
|
|
807
920
|
</div>
|
|
808
921
|
</div>
|
|
809
|
-
</div>
|
|
810
922
|
</div>
|
|
811
923
|
</div>
|
|
812
924
|
);
|