@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,75 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { cn } from "@livepeer-frameworks/player-core";
|
|
3
|
+
|
|
4
|
+
interface LogoOverlayProps {
|
|
5
|
+
src: string;
|
|
6
|
+
show?: boolean;
|
|
7
|
+
position?: "top-left" | "top-right" | "bottom-left" | "bottom-right";
|
|
8
|
+
width?: number;
|
|
9
|
+
height?: number | "auto";
|
|
10
|
+
clickUrl?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const POSITION_MAP: Record<
|
|
14
|
+
NonNullable<LogoOverlayProps["position"]>,
|
|
15
|
+
string
|
|
16
|
+
> = {
|
|
17
|
+
"top-left": "left-3 top-3 sm:left-4 sm:top-4",
|
|
18
|
+
"top-right": "right-3 top-3 sm:right-4 sm:top-4",
|
|
19
|
+
"bottom-left": "left-3 bottom-3 sm:left-4 sm:bottom-4",
|
|
20
|
+
"bottom-right": "right-3 bottom-3 sm:right-4 sm:bottom-4"
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const LogoOverlay: React.FC<LogoOverlayProps> = ({
|
|
24
|
+
src,
|
|
25
|
+
show = true,
|
|
26
|
+
position = "bottom-right",
|
|
27
|
+
width = 96,
|
|
28
|
+
height = "auto",
|
|
29
|
+
clickUrl
|
|
30
|
+
}) => {
|
|
31
|
+
if (!show) return null;
|
|
32
|
+
|
|
33
|
+
const content = (
|
|
34
|
+
<img
|
|
35
|
+
src={src}
|
|
36
|
+
alt="FrameWorks logo"
|
|
37
|
+
width={width}
|
|
38
|
+
height={height === "auto" ? undefined : height}
|
|
39
|
+
className={cn(
|
|
40
|
+
"max-h-[72px] rounded-md border border-white/10 bg-black/40 p-2 shadow-lg backdrop-blur transition",
|
|
41
|
+
clickUrl ? "hover:bg-black/60" : ""
|
|
42
|
+
)}
|
|
43
|
+
style={{ width, height: height === "auto" ? undefined : height }}
|
|
44
|
+
/>
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
if (clickUrl) {
|
|
48
|
+
return (
|
|
49
|
+
<a
|
|
50
|
+
href={clickUrl}
|
|
51
|
+
target="_blank"
|
|
52
|
+
rel="noreferrer"
|
|
53
|
+
className={cn(
|
|
54
|
+
"fw-player-surface absolute z-40 inline-flex items-center justify-center opacity-90",
|
|
55
|
+
POSITION_MAP[position]
|
|
56
|
+
)}
|
|
57
|
+
>
|
|
58
|
+
{content}
|
|
59
|
+
</a>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<div
|
|
65
|
+
className={cn(
|
|
66
|
+
"fw-player-surface absolute z-40 inline-flex items-center justify-center opacity-90",
|
|
67
|
+
POSITION_MAP[position]
|
|
68
|
+
)}
|
|
69
|
+
>
|
|
70
|
+
{content}
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export default LogoOverlay;
|
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
import React, { useState, useCallback, useMemo } from "react";
|
|
2
|
+
import IdleScreen from "./IdleScreen";
|
|
3
|
+
import ThumbnailOverlay from "./ThumbnailOverlay";
|
|
4
|
+
import TitleOverlay from "./TitleOverlay";
|
|
5
|
+
import StatsPanel from "./StatsPanel";
|
|
6
|
+
import PlayerControls from "./PlayerControls";
|
|
7
|
+
import DevModePanel from "./DevModePanel";
|
|
8
|
+
import SpeedIndicator from "./SpeedIndicator";
|
|
9
|
+
import SkipIndicator, { SkipDirection } from "./SkipIndicator";
|
|
10
|
+
import { StatsIcon, SettingsIcon, PictureInPictureIcon } from "./Icons";
|
|
11
|
+
import { PlayerProps } from "../types";
|
|
12
|
+
import { usePlayerController } from "../hooks/usePlayerController";
|
|
13
|
+
import { cn } from "@livepeer-frameworks/player-core";
|
|
14
|
+
import type { PlaybackMode, EndpointInfo } from "@livepeer-frameworks/player-core";
|
|
15
|
+
import {
|
|
16
|
+
ContextMenu,
|
|
17
|
+
ContextMenuContent,
|
|
18
|
+
ContextMenuItem,
|
|
19
|
+
ContextMenuSeparator,
|
|
20
|
+
ContextMenuTrigger,
|
|
21
|
+
} from "../ui/context-menu";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Inner player component that uses PlayerController via hook
|
|
25
|
+
*/
|
|
26
|
+
const PlayerInner: React.FC<PlayerProps> = ({
|
|
27
|
+
contentId,
|
|
28
|
+
contentType,
|
|
29
|
+
thumbnailUrl = null,
|
|
30
|
+
options,
|
|
31
|
+
endpoints: propsEndpoints,
|
|
32
|
+
onStateChange
|
|
33
|
+
}) => {
|
|
34
|
+
// ============================================================================
|
|
35
|
+
// UI-only State (stays in wrapper)
|
|
36
|
+
// ============================================================================
|
|
37
|
+
const [isStatsOpen, setIsStatsOpen] = useState(false);
|
|
38
|
+
const [isDevPanelOpen, setIsDevPanelOpen] = useState(false);
|
|
39
|
+
const [skipDirection, setSkipDirection] = useState<SkipDirection>(null);
|
|
40
|
+
|
|
41
|
+
// Playback mode preference (persistent)
|
|
42
|
+
const [devPlaybackMode, setDevPlaybackMode] = useState<PlaybackMode>(options?.playbackMode || 'auto');
|
|
43
|
+
|
|
44
|
+
// ============================================================================
|
|
45
|
+
// PlayerController Hook - ALL business logic
|
|
46
|
+
// ============================================================================
|
|
47
|
+
const {
|
|
48
|
+
containerRef,
|
|
49
|
+
state,
|
|
50
|
+
controller,
|
|
51
|
+
togglePlay,
|
|
52
|
+
toggleMute,
|
|
53
|
+
toggleLoop,
|
|
54
|
+
toggleFullscreen,
|
|
55
|
+
togglePiP,
|
|
56
|
+
setVolume,
|
|
57
|
+
selectQuality,
|
|
58
|
+
clearError,
|
|
59
|
+
retry,
|
|
60
|
+
reload,
|
|
61
|
+
jumpToLive,
|
|
62
|
+
handleMouseEnter,
|
|
63
|
+
handleMouseLeave,
|
|
64
|
+
handleMouseMove,
|
|
65
|
+
setDevModeOptions,
|
|
66
|
+
} = usePlayerController({
|
|
67
|
+
contentId,
|
|
68
|
+
contentType,
|
|
69
|
+
endpoints: propsEndpoints,
|
|
70
|
+
gatewayUrl: options?.gatewayUrl,
|
|
71
|
+
mistUrl: options?.mistUrl,
|
|
72
|
+
authToken: options?.authToken,
|
|
73
|
+
autoplay: options?.autoplay !== false,
|
|
74
|
+
muted: options?.muted !== false,
|
|
75
|
+
controls: options?.stockControls === true,
|
|
76
|
+
poster: thumbnailUrl || undefined,
|
|
77
|
+
debug: options?.debug,
|
|
78
|
+
onStateChange: (playerState) => {
|
|
79
|
+
onStateChange?.(playerState);
|
|
80
|
+
},
|
|
81
|
+
onError: (error) => {
|
|
82
|
+
console.warn('[Player] Error:', error);
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// ============================================================================
|
|
87
|
+
// Dev Mode Callbacks
|
|
88
|
+
// ============================================================================
|
|
89
|
+
const handleDevSettingsChange = useCallback((settings: {
|
|
90
|
+
forcePlayer?: string;
|
|
91
|
+
forceType?: string;
|
|
92
|
+
forceSource?: number;
|
|
93
|
+
}) => {
|
|
94
|
+
// One-shot selection - controller handles the state
|
|
95
|
+
setDevModeOptions({
|
|
96
|
+
forcePlayer: settings.forcePlayer,
|
|
97
|
+
forceType: settings.forceType,
|
|
98
|
+
forceSource: settings.forceSource,
|
|
99
|
+
});
|
|
100
|
+
}, [setDevModeOptions]);
|
|
101
|
+
|
|
102
|
+
const handleModeChange = useCallback((mode: PlaybackMode) => {
|
|
103
|
+
setDevPlaybackMode(mode);
|
|
104
|
+
// Mode is a persistent preference
|
|
105
|
+
setDevModeOptions({ playbackMode: mode });
|
|
106
|
+
}, [setDevModeOptions]);
|
|
107
|
+
|
|
108
|
+
const handleReload = useCallback(() => {
|
|
109
|
+
clearError();
|
|
110
|
+
reload();
|
|
111
|
+
}, [clearError, reload]);
|
|
112
|
+
|
|
113
|
+
const handleStatsToggle = useCallback(() => {
|
|
114
|
+
setIsStatsOpen(prev => !prev);
|
|
115
|
+
}, []);
|
|
116
|
+
|
|
117
|
+
// Clear skip indicator after animation
|
|
118
|
+
const handleSkipIndicatorHide = useCallback(() => {
|
|
119
|
+
setSkipDirection(null);
|
|
120
|
+
}, []);
|
|
121
|
+
|
|
122
|
+
// ============================================================================
|
|
123
|
+
// Derived Values
|
|
124
|
+
// ============================================================================
|
|
125
|
+
const primaryEndpoint = state.endpoints?.primary as EndpointInfo | undefined;
|
|
126
|
+
const isLegacyPlayer = state.currentPlayerInfo?.shortname === 'mist-legacy';
|
|
127
|
+
const useStockControls = options?.stockControls === true || isLegacyPlayer;
|
|
128
|
+
|
|
129
|
+
// Title overlay visibility: show on hover or when paused
|
|
130
|
+
const showTitleOverlay = (state.isHovering || state.isPaused) &&
|
|
131
|
+
!state.shouldShowIdleScreen && !state.isBuffering && !state.error;
|
|
132
|
+
|
|
133
|
+
// Buffering spinner: only during active playback
|
|
134
|
+
const showBufferingSpinner = !state.shouldShowIdleScreen &&
|
|
135
|
+
state.isBuffering && !state.error && state.hasPlaybackStarted;
|
|
136
|
+
|
|
137
|
+
// Click-to-play overlay support
|
|
138
|
+
const supportsOverlay = false;
|
|
139
|
+
|
|
140
|
+
// ============================================================================
|
|
141
|
+
// Waiting for Endpoint (shown as overlay, not early return)
|
|
142
|
+
// ============================================================================
|
|
143
|
+
const showWaitingForEndpoint = !state.endpoints?.primary && state.state !== 'booting';
|
|
144
|
+
const waitingMessage = options?.gatewayUrl
|
|
145
|
+
? (state.state === 'gateway_loading' ? 'Resolving viewing endpoint...' : 'Waiting for endpoint...')
|
|
146
|
+
: 'Waiting for endpoint...';
|
|
147
|
+
|
|
148
|
+
// ============================================================================
|
|
149
|
+
// Render
|
|
150
|
+
// ============================================================================
|
|
151
|
+
return (
|
|
152
|
+
<ContextMenu>
|
|
153
|
+
<ContextMenuTrigger asChild>
|
|
154
|
+
<div
|
|
155
|
+
className={cn(
|
|
156
|
+
"fw-player-surface fw-player-root w-full h-full overflow-hidden",
|
|
157
|
+
options?.devMode && "flex"
|
|
158
|
+
)}
|
|
159
|
+
data-player-container="true"
|
|
160
|
+
tabIndex={0}
|
|
161
|
+
onMouseEnter={handleMouseEnter}
|
|
162
|
+
onMouseLeave={handleMouseLeave}
|
|
163
|
+
onMouseMove={handleMouseMove}
|
|
164
|
+
>
|
|
165
|
+
{/* Player area */}
|
|
166
|
+
<div className={cn(
|
|
167
|
+
"relative",
|
|
168
|
+
options?.devMode ? "flex-1 min-w-0" : "w-full h-full"
|
|
169
|
+
)}>
|
|
170
|
+
{/* Video container - PlayerController attaches here */}
|
|
171
|
+
<div ref={containerRef} className="fw-player-container" />
|
|
172
|
+
|
|
173
|
+
{/* Title/Description overlay */}
|
|
174
|
+
<TitleOverlay
|
|
175
|
+
title={state.metadata?.title}
|
|
176
|
+
description={state.metadata?.description}
|
|
177
|
+
isVisible={showTitleOverlay}
|
|
178
|
+
/>
|
|
179
|
+
|
|
180
|
+
{/* Stats panel */}
|
|
181
|
+
<StatsPanel
|
|
182
|
+
isOpen={isStatsOpen}
|
|
183
|
+
onClose={handleStatsToggle}
|
|
184
|
+
metadata={state.metadata}
|
|
185
|
+
streamState={state.streamState?.isOnline ? {
|
|
186
|
+
status: state.streamState.status,
|
|
187
|
+
viewers: state.metadata?.viewers,
|
|
188
|
+
tracks: state.streamState.streamInfo?.meta?.tracks
|
|
189
|
+
? Object.values(state.streamState.streamInfo.meta.tracks).map(t => ({
|
|
190
|
+
type: t.type,
|
|
191
|
+
codec: t.codec,
|
|
192
|
+
width: t.width,
|
|
193
|
+
height: t.height,
|
|
194
|
+
bps: t.bps,
|
|
195
|
+
}))
|
|
196
|
+
: [],
|
|
197
|
+
} : null}
|
|
198
|
+
quality={state.playbackQuality}
|
|
199
|
+
videoElement={state.videoElement}
|
|
200
|
+
protocol={primaryEndpoint?.protocol}
|
|
201
|
+
nodeId={primaryEndpoint?.nodeId}
|
|
202
|
+
geoDistance={primaryEndpoint?.geoDistance}
|
|
203
|
+
/>
|
|
204
|
+
|
|
205
|
+
{/* Dev Mode Panel toggle */}
|
|
206
|
+
{options?.devMode && !isDevPanelOpen && (
|
|
207
|
+
<DevModePanel
|
|
208
|
+
onSettingsChange={handleDevSettingsChange}
|
|
209
|
+
playbackMode={devPlaybackMode}
|
|
210
|
+
onModeChange={handleModeChange}
|
|
211
|
+
onReload={handleReload}
|
|
212
|
+
streamInfo={state.streamInfo}
|
|
213
|
+
mistStreamInfo={state.streamState?.streamInfo}
|
|
214
|
+
currentPlayer={state.currentPlayerInfo}
|
|
215
|
+
currentSource={state.currentSourceInfo}
|
|
216
|
+
videoElement={state.videoElement}
|
|
217
|
+
protocol={primaryEndpoint?.protocol}
|
|
218
|
+
nodeId={primaryEndpoint?.nodeId}
|
|
219
|
+
isVisible={false}
|
|
220
|
+
isOpen={false}
|
|
221
|
+
onOpenChange={setIsDevPanelOpen}
|
|
222
|
+
/>
|
|
223
|
+
)}
|
|
224
|
+
|
|
225
|
+
{/* Speed indicator */}
|
|
226
|
+
{state.isHoldingSpeed && (
|
|
227
|
+
<SpeedIndicator isVisible={true} speed={state.holdSpeed} />
|
|
228
|
+
)}
|
|
229
|
+
|
|
230
|
+
{/* Skip indicator */}
|
|
231
|
+
<SkipIndicator
|
|
232
|
+
direction={skipDirection}
|
|
233
|
+
seconds={10}
|
|
234
|
+
onHide={handleSkipIndicatorHide}
|
|
235
|
+
/>
|
|
236
|
+
|
|
237
|
+
{/* Waiting for endpoint overlay */}
|
|
238
|
+
{showWaitingForEndpoint && (
|
|
239
|
+
<IdleScreen status="OFFLINE" message={waitingMessage} />
|
|
240
|
+
)}
|
|
241
|
+
|
|
242
|
+
{/* Idle screen */}
|
|
243
|
+
{!showWaitingForEndpoint && state.shouldShowIdleScreen && (
|
|
244
|
+
<IdleScreen
|
|
245
|
+
status={state.isEffectivelyLive ? state.streamState?.status : undefined}
|
|
246
|
+
message={state.isEffectivelyLive ? state.streamState?.message : 'Loading video...'}
|
|
247
|
+
percentage={state.isEffectivelyLive ? state.streamState?.percentage : undefined}
|
|
248
|
+
/>
|
|
249
|
+
)}
|
|
250
|
+
|
|
251
|
+
{/* Buffering spinner */}
|
|
252
|
+
{showBufferingSpinner && (
|
|
253
|
+
<div
|
|
254
|
+
role="status"
|
|
255
|
+
aria-live="polite"
|
|
256
|
+
className="fw-player-surface absolute inset-0 flex items-center justify-center bg-black/40 backdrop-blur-sm z-20"
|
|
257
|
+
>
|
|
258
|
+
<div className="flex items-center gap-3 rounded-lg border border-white/10 bg-black/70 px-4 py-3 text-sm text-white shadow-lg">
|
|
259
|
+
<div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin"></div>
|
|
260
|
+
<span>Buffering...</span>
|
|
261
|
+
</div>
|
|
262
|
+
</div>
|
|
263
|
+
)}
|
|
264
|
+
|
|
265
|
+
{/* Error overlay */}
|
|
266
|
+
{!state.shouldShowIdleScreen && state.error && (
|
|
267
|
+
<div
|
|
268
|
+
role="alert"
|
|
269
|
+
aria-live="assertive"
|
|
270
|
+
className={cn(
|
|
271
|
+
"fw-error-overlay",
|
|
272
|
+
state.isPassiveError ? "fw-error-overlay--passive" : "fw-error-overlay--fullscreen"
|
|
273
|
+
)}
|
|
274
|
+
>
|
|
275
|
+
<div className={cn(
|
|
276
|
+
"fw-error-popup",
|
|
277
|
+
state.isPassiveError ? "fw-error-popup--passive" : "fw-error-popup--fullscreen"
|
|
278
|
+
)}>
|
|
279
|
+
<div className={cn(
|
|
280
|
+
"fw-error-header",
|
|
281
|
+
state.isPassiveError ? "fw-error-header--warning" : "fw-error-header--error"
|
|
282
|
+
)}>
|
|
283
|
+
<span className={cn(
|
|
284
|
+
"fw-error-title",
|
|
285
|
+
state.isPassiveError ? "fw-error-title--warning" : "fw-error-title--error"
|
|
286
|
+
)}>
|
|
287
|
+
{state.isPassiveError ? "Warning" : "Error"}
|
|
288
|
+
</span>
|
|
289
|
+
<button
|
|
290
|
+
type="button"
|
|
291
|
+
className="fw-error-close"
|
|
292
|
+
onClick={clearError}
|
|
293
|
+
aria-label="Dismiss"
|
|
294
|
+
>
|
|
295
|
+
<svg width="12" height="12" viewBox="0 0 12 12" fill="none">
|
|
296
|
+
<path d="M9 3L3 9M3 3L9 9" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/>
|
|
297
|
+
</svg>
|
|
298
|
+
</button>
|
|
299
|
+
</div>
|
|
300
|
+
<div className="fw-error-body">
|
|
301
|
+
<p className="fw-error-message">Playback issue</p>
|
|
302
|
+
</div>
|
|
303
|
+
<div className="fw-error-actions">
|
|
304
|
+
<button
|
|
305
|
+
type="button"
|
|
306
|
+
className="fw-error-btn"
|
|
307
|
+
aria-label="Retry playback"
|
|
308
|
+
onClick={() => { clearError(); retry(); }}
|
|
309
|
+
>
|
|
310
|
+
Retry
|
|
311
|
+
</button>
|
|
312
|
+
</div>
|
|
313
|
+
</div>
|
|
314
|
+
</div>
|
|
315
|
+
)}
|
|
316
|
+
|
|
317
|
+
{/* Player controls */}
|
|
318
|
+
{!useStockControls && (
|
|
319
|
+
<PlayerControls
|
|
320
|
+
currentTime={state.currentTime}
|
|
321
|
+
duration={state.duration}
|
|
322
|
+
isVisible={state.shouldShowControls}
|
|
323
|
+
onSeek={(t) => controller?.seek(t)}
|
|
324
|
+
showStatsButton={false}
|
|
325
|
+
isStatsOpen={isStatsOpen}
|
|
326
|
+
onStatsToggle={handleStatsToggle}
|
|
327
|
+
mistStreamInfo={state.streamState?.streamInfo}
|
|
328
|
+
disabled={!state.videoElement}
|
|
329
|
+
playbackMode={devPlaybackMode}
|
|
330
|
+
onModeChange={handleModeChange}
|
|
331
|
+
sourceType={state.currentSourceInfo?.type}
|
|
332
|
+
isContentLive={state.isEffectivelyLive}
|
|
333
|
+
// Props from usePlayerController hook
|
|
334
|
+
videoElement={state.videoElement}
|
|
335
|
+
qualities={state.qualities}
|
|
336
|
+
onSelectQuality={selectQuality}
|
|
337
|
+
isMuted={state.isMuted}
|
|
338
|
+
volume={state.volume}
|
|
339
|
+
onVolumeChange={setVolume}
|
|
340
|
+
onToggleMute={toggleMute}
|
|
341
|
+
isPlaying={state.isPlaying}
|
|
342
|
+
onTogglePlay={togglePlay}
|
|
343
|
+
onToggleFullscreen={toggleFullscreen}
|
|
344
|
+
isFullscreen={state.isFullscreen}
|
|
345
|
+
isLoopEnabled={state.isLoopEnabled}
|
|
346
|
+
onToggleLoop={toggleLoop}
|
|
347
|
+
onJumpToLive={jumpToLive}
|
|
348
|
+
/>
|
|
349
|
+
)}
|
|
350
|
+
</div>
|
|
351
|
+
|
|
352
|
+
{/* Dev Mode Panel - side panel */}
|
|
353
|
+
{options?.devMode && isDevPanelOpen && (
|
|
354
|
+
<DevModePanel
|
|
355
|
+
onSettingsChange={handleDevSettingsChange}
|
|
356
|
+
playbackMode={devPlaybackMode}
|
|
357
|
+
onModeChange={handleModeChange}
|
|
358
|
+
onReload={handleReload}
|
|
359
|
+
streamInfo={state.streamInfo}
|
|
360
|
+
mistStreamInfo={state.streamState?.streamInfo}
|
|
361
|
+
currentPlayer={state.currentPlayerInfo}
|
|
362
|
+
currentSource={state.currentSourceInfo}
|
|
363
|
+
videoElement={state.videoElement}
|
|
364
|
+
protocol={primaryEndpoint?.protocol}
|
|
365
|
+
nodeId={primaryEndpoint?.nodeId}
|
|
366
|
+
isVisible={true}
|
|
367
|
+
isOpen={true}
|
|
368
|
+
onOpenChange={setIsDevPanelOpen}
|
|
369
|
+
/>
|
|
370
|
+
)}
|
|
371
|
+
</div>
|
|
372
|
+
</ContextMenuTrigger>
|
|
373
|
+
|
|
374
|
+
{/* Context menu */}
|
|
375
|
+
<ContextMenuContent>
|
|
376
|
+
<ContextMenuItem onClick={handleStatsToggle} className="gap-2">
|
|
377
|
+
<StatsIcon size={14} className="opacity-70 flex-shrink-0" />
|
|
378
|
+
<span>{isStatsOpen ? "Hide Stats" : "Stats"}</span>
|
|
379
|
+
</ContextMenuItem>
|
|
380
|
+
{options?.devMode && (
|
|
381
|
+
<>
|
|
382
|
+
<ContextMenuSeparator />
|
|
383
|
+
<ContextMenuItem onClick={() => setIsDevPanelOpen(!isDevPanelOpen)} className="gap-2">
|
|
384
|
+
<SettingsIcon size={14} className="opacity-70 flex-shrink-0" />
|
|
385
|
+
<span>{isDevPanelOpen ? "Hide Settings" : "Settings"}</span>
|
|
386
|
+
</ContextMenuItem>
|
|
387
|
+
</>
|
|
388
|
+
)}
|
|
389
|
+
<ContextMenuSeparator />
|
|
390
|
+
<ContextMenuItem onClick={togglePiP} className="gap-2">
|
|
391
|
+
<PictureInPictureIcon size={14} className="opacity-70 flex-shrink-0" />
|
|
392
|
+
<span>Picture-in-Picture</span>
|
|
393
|
+
</ContextMenuItem>
|
|
394
|
+
<ContextMenuItem onClick={toggleLoop} className="gap-2">
|
|
395
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="opacity-70 flex-shrink-0">
|
|
396
|
+
<polyline points="17 1 21 5 17 9"></polyline>
|
|
397
|
+
<path d="M3 11V9a4 4 0 0 1 4-4h14"></path>
|
|
398
|
+
<polyline points="7 23 3 19 7 15"></polyline>
|
|
399
|
+
<path d="M21 13v2a4 4 0 0 1-4 4H3"></path>
|
|
400
|
+
</svg>
|
|
401
|
+
<span>{state.isLoopEnabled ? "Disable Loop" : "Enable Loop"}</span>
|
|
402
|
+
</ContextMenuItem>
|
|
403
|
+
</ContextMenuContent>
|
|
404
|
+
</ContextMenu>
|
|
405
|
+
);
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Main Player component.
|
|
410
|
+
*
|
|
411
|
+
* Note: PlayerProvider is available if you need to use context-based access
|
|
412
|
+
* across multiple components. PlayerInner manages its own PlayerController
|
|
413
|
+
* via usePlayerController hook.
|
|
414
|
+
*/
|
|
415
|
+
const Player: React.FC<PlayerProps> = (props) => {
|
|
416
|
+
return <PlayerInner {...props} />;
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
export default Player;
|