@livepeer-frameworks/player-svelte 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/DevModePanel.svelte +650 -0
- package/dist/DevModePanel.svelte.d.ts +31 -0
- package/dist/DvdLogo.svelte +213 -0
- package/dist/DvdLogo.svelte.d.ts +7 -0
- package/dist/Icons.svelte +27 -0
- package/dist/Icons.svelte.d.ts +25 -0
- package/dist/IdleScreen.svelte +752 -0
- package/dist/IdleScreen.svelte.d.ts +11 -0
- package/dist/LoadingScreen.svelte +689 -0
- package/dist/LoadingScreen.svelte.d.ts +7 -0
- package/dist/Player.svelte +482 -0
- package/dist/Player.svelte.d.ts +26 -0
- package/dist/PlayerControls.svelte +739 -0
- package/dist/PlayerControls.svelte.d.ts +20 -0
- package/dist/SeekBar.svelte +274 -0
- package/dist/SeekBar.svelte.d.ts +25 -0
- package/dist/SkipIndicator.svelte +95 -0
- package/dist/SkipIndicator.svelte.d.ts +14 -0
- package/dist/SpeedIndicator.svelte +38 -0
- package/dist/SpeedIndicator.svelte.d.ts +8 -0
- package/dist/StatsPanel.svelte +155 -0
- package/dist/StatsPanel.svelte.d.ts +27 -0
- package/dist/StreamStateOverlay.svelte +266 -0
- package/dist/StreamStateOverlay.svelte.d.ts +18 -0
- package/dist/SubtitleRenderer.svelte +234 -0
- package/dist/SubtitleRenderer.svelte.d.ts +41 -0
- package/dist/ThumbnailOverlay.svelte +96 -0
- package/dist/ThumbnailOverlay.svelte.d.ts +11 -0
- package/dist/TitleOverlay.svelte +47 -0
- package/dist/TitleOverlay.svelte.d.ts +9 -0
- package/dist/assets/logomark.svg +56 -0
- package/dist/components/VolumeIcons.svelte +53 -0
- package/dist/components/VolumeIcons.svelte.d.ts +10 -0
- package/dist/global.d.ts +15 -0
- package/dist/icons/FullscreenExitIcon.svelte +33 -0
- package/dist/icons/FullscreenExitIcon.svelte.d.ts +8 -0
- package/dist/icons/FullscreenIcon.svelte +33 -0
- package/dist/icons/FullscreenIcon.svelte.d.ts +8 -0
- package/dist/icons/PauseIcon.svelte +28 -0
- package/dist/icons/PauseIcon.svelte.d.ts +8 -0
- package/dist/icons/PictureInPictureIcon.svelte +28 -0
- package/dist/icons/PictureInPictureIcon.svelte.d.ts +8 -0
- package/dist/icons/PlayIcon.svelte +27 -0
- package/dist/icons/PlayIcon.svelte.d.ts +8 -0
- package/dist/icons/SeekToLiveIcon.svelte +30 -0
- package/dist/icons/SeekToLiveIcon.svelte.d.ts +8 -0
- package/dist/icons/SettingsIcon.svelte +40 -0
- package/dist/icons/SettingsIcon.svelte.d.ts +8 -0
- package/dist/icons/SkipBackIcon.svelte +32 -0
- package/dist/icons/SkipBackIcon.svelte.d.ts +8 -0
- package/dist/icons/SkipForwardIcon.svelte +32 -0
- package/dist/icons/SkipForwardIcon.svelte.d.ts +8 -0
- package/dist/icons/StatsIcon.svelte +29 -0
- package/dist/icons/StatsIcon.svelte.d.ts +8 -0
- package/dist/icons/VolumeOffIcon.svelte +29 -0
- package/dist/icons/VolumeOffIcon.svelte.d.ts +8 -0
- package/dist/icons/VolumeUpIcon.svelte +34 -0
- package/dist/icons/VolumeUpIcon.svelte.d.ts +8 -0
- package/dist/icons/index.d.ts +17 -0
- package/dist/icons/index.js +17 -0
- package/dist/index.d.ts +50 -0
- package/dist/index.js +54 -0
- package/dist/player.css +2 -0
- package/dist/stores/index.d.ts +15 -0
- package/dist/stores/index.js +21 -0
- package/dist/stores/playbackQuality.d.ts +43 -0
- package/dist/stores/playbackQuality.js +107 -0
- package/dist/stores/playerContext.d.ts +73 -0
- package/dist/stores/playerContext.js +166 -0
- package/dist/stores/playerController.d.ts +178 -0
- package/dist/stores/playerController.js +358 -0
- package/dist/stores/playerSelection.d.ts +84 -0
- package/dist/stores/playerSelection.js +159 -0
- package/dist/stores/streamState.d.ts +44 -0
- package/dist/stores/streamState.js +314 -0
- package/dist/stores/viewerEndpoints.d.ts +48 -0
- package/dist/stores/viewerEndpoints.js +178 -0
- package/dist/types.d.ts +4 -0
- package/dist/types.js +4 -0
- package/dist/ui/Badge.svelte +21 -0
- package/dist/ui/Badge.svelte.d.ts +32 -0
- package/dist/ui/Button.svelte +42 -0
- package/dist/ui/Button.svelte.d.ts +35 -0
- package/dist/ui/Slider.svelte +100 -0
- package/dist/ui/Slider.svelte.d.ts +17 -0
- package/dist/ui/badge.d.ts +6 -0
- package/dist/ui/badge.js +10 -0
- package/dist/ui/button.d.ts +8 -0
- package/dist/ui/button.js +21 -0
- package/dist/ui/context-menu/ContextMenuCheckboxItem.svelte +34 -0
- package/dist/ui/context-menu/ContextMenuCheckboxItem.svelte.d.ts +31 -0
- package/dist/ui/context-menu/ContextMenuContent.svelte +17 -0
- package/dist/ui/context-menu/ContextMenuContent.svelte.d.ts +7 -0
- package/dist/ui/context-menu/ContextMenuItem.svelte +22 -0
- package/dist/ui/context-menu/ContextMenuItem.svelte.d.ts +8 -0
- package/dist/ui/context-menu/ContextMenuLabel.svelte +22 -0
- package/dist/ui/context-menu/ContextMenuLabel.svelte.d.ts +8 -0
- package/dist/ui/context-menu/ContextMenuPortal.svelte +11 -0
- package/dist/ui/context-menu/ContextMenuPortal.svelte.d.ts +6 -0
- package/dist/ui/context-menu/ContextMenuRadioItem.svelte +21 -0
- package/dist/ui/context-menu/ContextMenuRadioItem.svelte.d.ts +31 -0
- package/dist/ui/context-menu/ContextMenuSeparator.svelte +14 -0
- package/dist/ui/context-menu/ContextMenuSeparator.svelte.d.ts +6 -0
- package/dist/ui/context-menu/ContextMenuShortcut.svelte +19 -0
- package/dist/ui/context-menu/ContextMenuShortcut.svelte.d.ts +7 -0
- package/dist/ui/context-menu/ContextMenuSubContent.svelte +20 -0
- package/dist/ui/context-menu/ContextMenuSubContent.svelte.d.ts +7 -0
- package/dist/ui/context-menu/ContextMenuSubTrigger.svelte +34 -0
- package/dist/ui/context-menu/ContextMenuSubTrigger.svelte.d.ts +8 -0
- package/dist/ui/context-menu/index.d.ts +17 -0
- package/dist/ui/context-menu/index.js +17 -0
- package/package.json +51 -0
- package/src/DevModePanel.svelte +650 -0
- package/src/DvdLogo.svelte +213 -0
- package/src/Icons.svelte +27 -0
- package/src/IdleScreen.svelte +739 -0
- package/src/LoadingScreen.svelte +674 -0
- package/src/Player.svelte +483 -0
- package/src/PlayerControls.svelte +752 -0
- package/src/SeekBar.svelte +274 -0
- package/src/SkipIndicator.svelte +95 -0
- package/src/SpeedIndicator.svelte +37 -0
- package/src/StatsPanel.svelte +155 -0
- package/src/StreamStateOverlay.svelte +266 -0
- package/src/SubtitleRenderer.svelte +234 -0
- package/src/ThumbnailOverlay.svelte +96 -0
- package/src/TitleOverlay.svelte +47 -0
- package/src/assets/logomark.svg +56 -0
- package/src/components/VolumeIcons.svelte +53 -0
- package/src/global.d.ts +15 -0
- package/src/icons/FullscreenExitIcon.svelte +33 -0
- package/src/icons/FullscreenIcon.svelte +33 -0
- package/src/icons/PauseIcon.svelte +28 -0
- package/src/icons/PictureInPictureIcon.svelte +28 -0
- package/src/icons/PlayIcon.svelte +27 -0
- package/src/icons/SeekToLiveIcon.svelte +30 -0
- package/src/icons/SettingsIcon.svelte +40 -0
- package/src/icons/SkipBackIcon.svelte +32 -0
- package/src/icons/SkipForwardIcon.svelte +32 -0
- package/src/icons/StatsIcon.svelte +29 -0
- package/src/icons/VolumeOffIcon.svelte +29 -0
- package/src/icons/VolumeUpIcon.svelte +34 -0
- package/src/icons/index.ts +18 -0
- package/src/index.ts +84 -0
- package/src/player.css +2 -0
- package/src/stores/index.ts +88 -0
- package/src/stores/playbackQuality.ts +137 -0
- package/src/stores/playerContext.ts +221 -0
- package/src/stores/playerController.ts +568 -0
- package/src/stores/playerSelection.ts +216 -0
- package/src/stores/streamState.ts +367 -0
- package/src/stores/viewerEndpoints.ts +224 -0
- package/src/types.ts +6 -0
- package/src/ui/Badge.svelte +21 -0
- package/src/ui/Button.svelte +42 -0
- package/src/ui/Slider.svelte +100 -0
- package/src/ui/badge.ts +20 -0
- package/src/ui/button.ts +35 -0
- package/src/ui/context-menu/ContextMenuCheckboxItem.svelte +34 -0
- package/src/ui/context-menu/ContextMenuContent.svelte +17 -0
- package/src/ui/context-menu/ContextMenuItem.svelte +22 -0
- package/src/ui/context-menu/ContextMenuLabel.svelte +22 -0
- package/src/ui/context-menu/ContextMenuPortal.svelte +11 -0
- package/src/ui/context-menu/ContextMenuRadioItem.svelte +21 -0
- package/src/ui/context-menu/ContextMenuSeparator.svelte +14 -0
- package/src/ui/context-menu/ContextMenuShortcut.svelte +19 -0
- package/src/ui/context-menu/ContextMenuSubContent.svelte +20 -0
- package/src/ui/context-menu/ContextMenuSubTrigger.svelte +34 -0
- package/src/ui/context-menu/index.ts +36 -0
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Player.svelte - Full-featured video player using PlayerController
|
|
3
|
+
Thin wrapper over PlayerController from @livepeer-frameworks/player-core
|
|
4
|
+
-->
|
|
5
|
+
<script lang="ts">
|
|
6
|
+
import { onMount, onDestroy } from 'svelte';
|
|
7
|
+
import IdleScreen from './IdleScreen.svelte';
|
|
8
|
+
import LoadingScreen from './LoadingScreen.svelte';
|
|
9
|
+
import SubtitleRenderer from './SubtitleRenderer.svelte';
|
|
10
|
+
import PlayerControls from './PlayerControls.svelte';
|
|
11
|
+
import SpeedIndicator from './SpeedIndicator.svelte';
|
|
12
|
+
import SkipIndicator from './SkipIndicator.svelte';
|
|
13
|
+
import TitleOverlay from './TitleOverlay.svelte';
|
|
14
|
+
import StatsPanel from './StatsPanel.svelte';
|
|
15
|
+
import DevModePanel from './DevModePanel.svelte';
|
|
16
|
+
import {
|
|
17
|
+
ContextMenu,
|
|
18
|
+
ContextMenuTrigger,
|
|
19
|
+
ContextMenuPortal,
|
|
20
|
+
ContextMenuContent,
|
|
21
|
+
ContextMenuItem,
|
|
22
|
+
ContextMenuSeparator,
|
|
23
|
+
} from './ui/context-menu';
|
|
24
|
+
import { StatsIcon, SettingsIcon, PictureInPictureIcon } from './icons';
|
|
25
|
+
import { cn, type PlaybackMode, type ContentEndpoints, type ContentMetadata, type PlayerState, type PlayerStateContext, type ContentType, type EndpointInfo } from '@livepeer-frameworks/player-core';
|
|
26
|
+
import { createPlayerControllerStore, type PlayerControllerStore } from './stores/playerController';
|
|
27
|
+
import type { SkipDirection } from './SkipIndicator.svelte';
|
|
28
|
+
|
|
29
|
+
// Props - aligned with React Player
|
|
30
|
+
interface Props {
|
|
31
|
+
contentId: string;
|
|
32
|
+
contentType?: ContentType;
|
|
33
|
+
thumbnailUrl?: string | null;
|
|
34
|
+
endpoints?: ContentEndpoints;
|
|
35
|
+
options?: {
|
|
36
|
+
gatewayUrl?: string;
|
|
37
|
+
mistUrl?: string;
|
|
38
|
+
authToken?: string;
|
|
39
|
+
autoplay?: boolean;
|
|
40
|
+
muted?: boolean;
|
|
41
|
+
controls?: boolean;
|
|
42
|
+
stockControls?: boolean;
|
|
43
|
+
devMode?: boolean;
|
|
44
|
+
debug?: boolean;
|
|
45
|
+
forcePlayer?: string;
|
|
46
|
+
forceType?: string;
|
|
47
|
+
forceSource?: number;
|
|
48
|
+
playbackMode?: PlaybackMode;
|
|
49
|
+
};
|
|
50
|
+
onStateChange?: (state: PlayerState, context?: PlayerStateContext) => void;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let {
|
|
54
|
+
contentId,
|
|
55
|
+
contentType = 'live',
|
|
56
|
+
thumbnailUrl = null,
|
|
57
|
+
endpoints = undefined,
|
|
58
|
+
options = {},
|
|
59
|
+
onStateChange = undefined,
|
|
60
|
+
}: Props = $props();
|
|
61
|
+
|
|
62
|
+
// ============================================================================
|
|
63
|
+
// UI-only State (stays in wrapper)
|
|
64
|
+
// ============================================================================
|
|
65
|
+
let isStatsOpen = $state(false);
|
|
66
|
+
let isDevPanelOpen = $state(false);
|
|
67
|
+
let skipDirection: SkipDirection = $state(null);
|
|
68
|
+
|
|
69
|
+
// Playback mode preference (persistent)
|
|
70
|
+
let devPlaybackMode: PlaybackMode = $state(options?.playbackMode || 'auto');
|
|
71
|
+
|
|
72
|
+
// Container ref
|
|
73
|
+
let containerRef: HTMLElement | undefined = $state();
|
|
74
|
+
let playerRootRef: HTMLDivElement | undefined = $state();
|
|
75
|
+
|
|
76
|
+
// ============================================================================
|
|
77
|
+
// PlayerController Store - ALL business logic
|
|
78
|
+
// ============================================================================
|
|
79
|
+
let playerStore: PlayerControllerStore | null = $state(null);
|
|
80
|
+
let storeState = $state({
|
|
81
|
+
state: 'booting' as PlayerState,
|
|
82
|
+
streamState: null as any,
|
|
83
|
+
endpoints: null as any,
|
|
84
|
+
metadata: null as any,
|
|
85
|
+
videoElement: null as HTMLVideoElement | null,
|
|
86
|
+
currentTime: 0,
|
|
87
|
+
duration: NaN,
|
|
88
|
+
isPlaying: false,
|
|
89
|
+
isPaused: true,
|
|
90
|
+
isBuffering: false,
|
|
91
|
+
isMuted: true,
|
|
92
|
+
volume: 1,
|
|
93
|
+
error: null as string | null,
|
|
94
|
+
isPassiveError: false,
|
|
95
|
+
hasPlaybackStarted: false,
|
|
96
|
+
isHoldingSpeed: false,
|
|
97
|
+
holdSpeed: 2,
|
|
98
|
+
isHovering: false,
|
|
99
|
+
shouldShowControls: false,
|
|
100
|
+
isLoopEnabled: false,
|
|
101
|
+
isFullscreen: false,
|
|
102
|
+
isPiPActive: false,
|
|
103
|
+
isEffectivelyLive: false,
|
|
104
|
+
shouldShowIdleScreen: true,
|
|
105
|
+
currentPlayerInfo: null as { name: string; shortname: string } | null,
|
|
106
|
+
currentSourceInfo: null as { url: string; type: string } | null,
|
|
107
|
+
playbackQuality: null as any,
|
|
108
|
+
subtitlesEnabled: false,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Track if we've already attached to prevent double-attach race
|
|
112
|
+
let hasAttached = false;
|
|
113
|
+
|
|
114
|
+
// Debug helper
|
|
115
|
+
const debug = (msg: string) => {
|
|
116
|
+
if (options?.debug) {
|
|
117
|
+
console.log(`[Player.svelte] ${msg}`);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// Create store on mount
|
|
122
|
+
onMount(() => {
|
|
123
|
+
debug(`onMount - contentId: ${contentId}, contentType: ${contentType}`);
|
|
124
|
+
debug(`onMount - gatewayUrl: ${options?.gatewayUrl}, mistUrl: ${options?.mistUrl}`);
|
|
125
|
+
debug(`onMount - endpoints: ${endpoints ? 'provided' : 'not provided'}`);
|
|
126
|
+
|
|
127
|
+
playerStore = createPlayerControllerStore({
|
|
128
|
+
contentId,
|
|
129
|
+
contentType,
|
|
130
|
+
endpoints,
|
|
131
|
+
gatewayUrl: options?.gatewayUrl,
|
|
132
|
+
mistUrl: options?.mistUrl,
|
|
133
|
+
authToken: options?.authToken,
|
|
134
|
+
autoplay: options?.autoplay !== false,
|
|
135
|
+
muted: options?.muted !== false,
|
|
136
|
+
controls: options?.stockControls === true,
|
|
137
|
+
poster: thumbnailUrl || undefined,
|
|
138
|
+
debug: options?.debug,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
debug('playerStore created');
|
|
142
|
+
|
|
143
|
+
// Subscribe to store state
|
|
144
|
+
const unsubscribe = playerStore.subscribe(state => {
|
|
145
|
+
storeState = state;
|
|
146
|
+
// Forward state changes to prop callback
|
|
147
|
+
if (onStateChange && state.state) {
|
|
148
|
+
onStateChange(state.state);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
return () => {
|
|
153
|
+
debug('cleanup - destroying playerStore');
|
|
154
|
+
unsubscribe();
|
|
155
|
+
playerStore?.destroy();
|
|
156
|
+
playerStore = null;
|
|
157
|
+
hasAttached = false;
|
|
158
|
+
};
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// Attach when container becomes available (only once)
|
|
162
|
+
$effect(() => {
|
|
163
|
+
debug(`$effect - containerRef: ${!!containerRef}, playerStore: ${!!playerStore}, hasAttached: ${hasAttached}`);
|
|
164
|
+
if (containerRef && playerStore && !hasAttached) {
|
|
165
|
+
hasAttached = true;
|
|
166
|
+
debug('attaching to container');
|
|
167
|
+
playerStore.attach(containerRef).then(() => {
|
|
168
|
+
debug('attach completed');
|
|
169
|
+
}).catch((err) => {
|
|
170
|
+
debug(`attach failed: ${err}`);
|
|
171
|
+
console.error('[Player.svelte] attach failed:', err);
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// ============================================================================
|
|
177
|
+
// Dev Mode Callbacks
|
|
178
|
+
// ============================================================================
|
|
179
|
+
function handleDevSettingsChange(settings: { forcePlayer?: string; forceType?: string; forceSource?: number }) {
|
|
180
|
+
// One-shot selection - controller handles the state
|
|
181
|
+
playerStore?.setDevModeOptions({
|
|
182
|
+
forcePlayer: settings.forcePlayer,
|
|
183
|
+
forceType: settings.forceType,
|
|
184
|
+
forceSource: settings.forceSource,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function handleModeChange(mode: PlaybackMode) {
|
|
189
|
+
devPlaybackMode = mode;
|
|
190
|
+
// Mode is a persistent preference
|
|
191
|
+
playerStore?.setDevModeOptions({ playbackMode: mode });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function handleReload() {
|
|
195
|
+
playerStore?.clearError();
|
|
196
|
+
playerStore?.reload();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function handleSkipIndicatorHide() {
|
|
200
|
+
skipDirection = null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ============================================================================
|
|
204
|
+
// Derived Values
|
|
205
|
+
// ============================================================================
|
|
206
|
+
let primaryEndpoint = $derived(storeState.endpoints?.primary as EndpointInfo | undefined);
|
|
207
|
+
let isLegacyPlayer = $derived(storeState.currentPlayerInfo?.shortname === 'mist-legacy');
|
|
208
|
+
let useStockControls = $derived(options?.stockControls === true || isLegacyPlayer);
|
|
209
|
+
let metadata = $derived(storeState.metadata);
|
|
210
|
+
|
|
211
|
+
// Title overlay visibility: show on hover or when paused
|
|
212
|
+
let showTitleOverlay = $derived(
|
|
213
|
+
(storeState.isHovering || storeState.isPaused) &&
|
|
214
|
+
!storeState.shouldShowIdleScreen &&
|
|
215
|
+
!storeState.isBuffering &&
|
|
216
|
+
!storeState.error
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
// Buffering spinner: only during active playback
|
|
220
|
+
let showBufferingSpinner = $derived(
|
|
221
|
+
!storeState.shouldShowIdleScreen &&
|
|
222
|
+
storeState.isBuffering &&
|
|
223
|
+
!storeState.error &&
|
|
224
|
+
storeState.hasPlaybackStarted
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
// Waiting for endpoint (shown as overlay, not early return)
|
|
228
|
+
let showWaitingForEndpoint = $derived(
|
|
229
|
+
!storeState.endpoints?.primary && storeState.state !== 'booting'
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
let waitingMessage = $derived(
|
|
233
|
+
options?.gatewayUrl
|
|
234
|
+
? (storeState.state === 'gateway_loading' ? 'Resolving viewing endpoint...' : 'Waiting for endpoint...')
|
|
235
|
+
: 'Waiting for endpoint...'
|
|
236
|
+
);
|
|
237
|
+
</script>
|
|
238
|
+
|
|
239
|
+
<ContextMenu>
|
|
240
|
+
<ContextMenuTrigger>
|
|
241
|
+
{#snippet child({ props })}
|
|
242
|
+
<div
|
|
243
|
+
bind:this={playerRootRef}
|
|
244
|
+
{...props}
|
|
245
|
+
class={cn(
|
|
246
|
+
'fw-player-surface fw-player-root relative w-full h-full bg-black',
|
|
247
|
+
options?.devMode && 'flex'
|
|
248
|
+
)}
|
|
249
|
+
data-player-container="true"
|
|
250
|
+
tabindex="0"
|
|
251
|
+
onmouseenter={() => playerStore?.handleMouseEnter()}
|
|
252
|
+
onmouseleave={() => playerStore?.handleMouseLeave()}
|
|
253
|
+
onmousemove={() => playerStore?.handleMouseMove()}
|
|
254
|
+
>
|
|
255
|
+
<!-- Player area -->
|
|
256
|
+
<div class={cn('relative', options?.devMode ? 'flex-1 min-w-0 h-full' : 'w-full h-full')}>
|
|
257
|
+
<!-- Video container - PlayerController attaches here -->
|
|
258
|
+
<div bind:this={containerRef} class="fw-player-container w-full h-full"></div>
|
|
259
|
+
|
|
260
|
+
<!-- Subtitle renderer -->
|
|
261
|
+
{#if storeState.subtitlesEnabled}
|
|
262
|
+
<SubtitleRenderer
|
|
263
|
+
currentTime={storeState.currentTime}
|
|
264
|
+
enabled={storeState.subtitlesEnabled}
|
|
265
|
+
/>
|
|
266
|
+
{/if}
|
|
267
|
+
|
|
268
|
+
<!-- Title overlay -->
|
|
269
|
+
<TitleOverlay
|
|
270
|
+
title={metadata?.title}
|
|
271
|
+
description={metadata?.description}
|
|
272
|
+
isVisible={showTitleOverlay}
|
|
273
|
+
/>
|
|
274
|
+
|
|
275
|
+
<!-- Stats panel -->
|
|
276
|
+
<StatsPanel
|
|
277
|
+
isOpen={isStatsOpen}
|
|
278
|
+
onClose={() => isStatsOpen = false}
|
|
279
|
+
{metadata}
|
|
280
|
+
streamState={storeState.streamState?.isOnline ? {
|
|
281
|
+
status: storeState.streamState.status,
|
|
282
|
+
viewers: metadata?.viewers,
|
|
283
|
+
tracks: storeState.streamState.streamInfo?.meta?.tracks
|
|
284
|
+
? Object.values(storeState.streamState.streamInfo.meta.tracks).map((t: any) => ({
|
|
285
|
+
type: t.type,
|
|
286
|
+
codec: t.codec,
|
|
287
|
+
width: t.width,
|
|
288
|
+
height: t.height,
|
|
289
|
+
bps: t.bps,
|
|
290
|
+
}))
|
|
291
|
+
: [],
|
|
292
|
+
} : null}
|
|
293
|
+
quality={storeState.playbackQuality}
|
|
294
|
+
videoElement={storeState.videoElement}
|
|
295
|
+
protocol={primaryEndpoint?.protocol}
|
|
296
|
+
nodeId={primaryEndpoint?.nodeId}
|
|
297
|
+
geoDistance={primaryEndpoint?.geoDistance}
|
|
298
|
+
/>
|
|
299
|
+
|
|
300
|
+
<!-- Dev mode toggle (when panel closed) -->
|
|
301
|
+
{#if options?.devMode && !isDevPanelOpen}
|
|
302
|
+
<DevModePanel
|
|
303
|
+
onSettingsChange={handleDevSettingsChange}
|
|
304
|
+
playbackMode={devPlaybackMode}
|
|
305
|
+
onModeChange={handleModeChange}
|
|
306
|
+
onReload={handleReload}
|
|
307
|
+
streamInfo={storeState.currentSourceInfo ? {
|
|
308
|
+
source: [{ url: storeState.currentSourceInfo.url, type: storeState.currentSourceInfo.type }],
|
|
309
|
+
meta: { tracks: [] }
|
|
310
|
+
} : null}
|
|
311
|
+
mistStreamInfo={storeState.streamState?.streamInfo}
|
|
312
|
+
currentPlayer={storeState.currentPlayerInfo}
|
|
313
|
+
currentSource={storeState.currentSourceInfo}
|
|
314
|
+
videoElement={storeState.videoElement}
|
|
315
|
+
protocol={primaryEndpoint?.protocol}
|
|
316
|
+
nodeId={primaryEndpoint?.nodeId}
|
|
317
|
+
isVisible={storeState.isHovering || storeState.isPaused}
|
|
318
|
+
isOpen={false}
|
|
319
|
+
onOpenChange={(open) => isDevPanelOpen = open}
|
|
320
|
+
/>
|
|
321
|
+
{/if}
|
|
322
|
+
|
|
323
|
+
<!-- Speed indicator overlay -->
|
|
324
|
+
{#if storeState.isHoldingSpeed}
|
|
325
|
+
<SpeedIndicator isVisible={true} speed={storeState.holdSpeed} />
|
|
326
|
+
{/if}
|
|
327
|
+
|
|
328
|
+
<!-- Skip indicator overlay -->
|
|
329
|
+
<SkipIndicator direction={skipDirection} seconds={10} onhide={handleSkipIndicatorHide} />
|
|
330
|
+
|
|
331
|
+
<!-- Waiting for endpoint overlay -->
|
|
332
|
+
{#if showWaitingForEndpoint}
|
|
333
|
+
<IdleScreen status="OFFLINE" message={waitingMessage} />
|
|
334
|
+
{/if}
|
|
335
|
+
|
|
336
|
+
<!-- Idle screen -->
|
|
337
|
+
{#if !showWaitingForEndpoint && storeState.shouldShowIdleScreen}
|
|
338
|
+
<IdleScreen
|
|
339
|
+
status={storeState.isEffectivelyLive ? storeState.streamState?.status : undefined}
|
|
340
|
+
message={storeState.isEffectivelyLive ? storeState.streamState?.message : 'Loading video...'}
|
|
341
|
+
percentage={storeState.isEffectivelyLive ? storeState.streamState?.percentage : undefined}
|
|
342
|
+
/>
|
|
343
|
+
{/if}
|
|
344
|
+
|
|
345
|
+
<!-- Buffering spinner -->
|
|
346
|
+
{#if showBufferingSpinner}
|
|
347
|
+
<div class="absolute inset-0 flex items-center justify-center bg-black/40 backdrop-blur-sm z-20">
|
|
348
|
+
<div class="flex items-center gap-3 rounded-lg border border-white/10 bg-black/70 px-4 py-3 text-sm text-white shadow-lg">
|
|
349
|
+
<div class="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin"></div>
|
|
350
|
+
<span>Buffering...</span>
|
|
351
|
+
</div>
|
|
352
|
+
</div>
|
|
353
|
+
{/if}
|
|
354
|
+
|
|
355
|
+
<!-- Error overlay -->
|
|
356
|
+
{#if storeState.error && !storeState.shouldShowIdleScreen}
|
|
357
|
+
<div
|
|
358
|
+
role="alert"
|
|
359
|
+
aria-live="assertive"
|
|
360
|
+
class={cn(
|
|
361
|
+
'fw-error-overlay',
|
|
362
|
+
storeState.isPassiveError ? 'fw-error-overlay--passive' : 'fw-error-overlay--fullscreen'
|
|
363
|
+
)}
|
|
364
|
+
>
|
|
365
|
+
<div class={cn(
|
|
366
|
+
'fw-error-popup',
|
|
367
|
+
storeState.isPassiveError ? 'fw-error-popup--passive' : 'fw-error-popup--fullscreen'
|
|
368
|
+
)}>
|
|
369
|
+
<div class={cn(
|
|
370
|
+
'fw-error-header',
|
|
371
|
+
storeState.isPassiveError ? 'fw-error-header--warning' : 'fw-error-header--error'
|
|
372
|
+
)}>
|
|
373
|
+
<span class={cn(
|
|
374
|
+
'fw-error-title',
|
|
375
|
+
storeState.isPassiveError ? 'fw-error-title--warning' : 'fw-error-title--error'
|
|
376
|
+
)}>
|
|
377
|
+
{storeState.isPassiveError ? 'Warning' : 'Error'}
|
|
378
|
+
</span>
|
|
379
|
+
<button
|
|
380
|
+
type="button"
|
|
381
|
+
class="fw-error-close"
|
|
382
|
+
onclick={() => playerStore?.clearError()}
|
|
383
|
+
aria-label="Dismiss"
|
|
384
|
+
>
|
|
385
|
+
<svg width="12" height="12" viewBox="0 0 12 12" fill="none">
|
|
386
|
+
<path d="M9 3L3 9M3 3L9 9" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
|
387
|
+
</svg>
|
|
388
|
+
</button>
|
|
389
|
+
</div>
|
|
390
|
+
<div class="fw-error-body">
|
|
391
|
+
<p class="fw-error-message">Playback issue</p>
|
|
392
|
+
</div>
|
|
393
|
+
<div class="fw-error-actions">
|
|
394
|
+
<button
|
|
395
|
+
type="button"
|
|
396
|
+
class="fw-error-btn"
|
|
397
|
+
onclick={() => { playerStore?.clearError(); playerStore?.retry(); }}
|
|
398
|
+
aria-label="Retry playback"
|
|
399
|
+
>
|
|
400
|
+
Retry
|
|
401
|
+
</button>
|
|
402
|
+
</div>
|
|
403
|
+
</div>
|
|
404
|
+
</div>
|
|
405
|
+
{/if}
|
|
406
|
+
|
|
407
|
+
<!-- Player controls -->
|
|
408
|
+
{#if !useStockControls}
|
|
409
|
+
<PlayerControls
|
|
410
|
+
currentTime={storeState.currentTime}
|
|
411
|
+
duration={storeState.duration}
|
|
412
|
+
isVisible={storeState.shouldShowControls}
|
|
413
|
+
onseek={(t) => playerStore?.seek(t)}
|
|
414
|
+
disabled={!storeState.videoElement}
|
|
415
|
+
sourceType={storeState.currentSourceInfo?.type}
|
|
416
|
+
playbackMode={devPlaybackMode}
|
|
417
|
+
onModeChange={handleModeChange}
|
|
418
|
+
mistStreamInfo={storeState.streamState?.streamInfo}
|
|
419
|
+
showStatsButton={false}
|
|
420
|
+
isStatsOpen={isStatsOpen}
|
|
421
|
+
onStatsToggle={() => isStatsOpen = !isStatsOpen}
|
|
422
|
+
isContentLive={storeState.isEffectivelyLive}
|
|
423
|
+
/>
|
|
424
|
+
{/if}
|
|
425
|
+
</div>
|
|
426
|
+
|
|
427
|
+
<!-- Dev mode panel (when open) -->
|
|
428
|
+
{#if options?.devMode && isDevPanelOpen}
|
|
429
|
+
<DevModePanel
|
|
430
|
+
onSettingsChange={handleDevSettingsChange}
|
|
431
|
+
playbackMode={devPlaybackMode}
|
|
432
|
+
onModeChange={handleModeChange}
|
|
433
|
+
onReload={handleReload}
|
|
434
|
+
streamInfo={storeState.currentSourceInfo ? {
|
|
435
|
+
source: [{ url: storeState.currentSourceInfo.url, type: storeState.currentSourceInfo.type }],
|
|
436
|
+
meta: { tracks: [] }
|
|
437
|
+
} : null}
|
|
438
|
+
mistStreamInfo={storeState.streamState?.streamInfo}
|
|
439
|
+
currentPlayer={storeState.currentPlayerInfo}
|
|
440
|
+
currentSource={storeState.currentSourceInfo}
|
|
441
|
+
videoElement={storeState.videoElement}
|
|
442
|
+
protocol={primaryEndpoint?.protocol}
|
|
443
|
+
nodeId={primaryEndpoint?.nodeId}
|
|
444
|
+
isVisible={true}
|
|
445
|
+
isOpen={true}
|
|
446
|
+
onOpenChange={(open) => isDevPanelOpen = open}
|
|
447
|
+
/>
|
|
448
|
+
{/if}
|
|
449
|
+
</div>
|
|
450
|
+
{/snippet}
|
|
451
|
+
</ContextMenuTrigger>
|
|
452
|
+
|
|
453
|
+
<ContextMenuPortal>
|
|
454
|
+
<ContextMenuContent>
|
|
455
|
+
<ContextMenuItem onSelect={() => { isStatsOpen = !isStatsOpen; }}>
|
|
456
|
+
<StatsIcon size={14} class="opacity-70 flex-shrink-0 mr-2" />
|
|
457
|
+
{isStatsOpen ? 'Hide Stats' : 'Stats'}
|
|
458
|
+
</ContextMenuItem>
|
|
459
|
+
{#if options?.devMode}
|
|
460
|
+
<ContextMenuSeparator />
|
|
461
|
+
<ContextMenuItem onSelect={() => { isDevPanelOpen = !isDevPanelOpen; }}>
|
|
462
|
+
<SettingsIcon size={14} class="opacity-70 flex-shrink-0 mr-2" />
|
|
463
|
+
{isDevPanelOpen ? 'Hide Settings' : 'Settings'}
|
|
464
|
+
</ContextMenuItem>
|
|
465
|
+
{/if}
|
|
466
|
+
<ContextMenuSeparator />
|
|
467
|
+
<ContextMenuItem onSelect={() => playerStore?.togglePiP()}>
|
|
468
|
+
<PictureInPictureIcon size={14} class="opacity-70 flex-shrink-0 mr-2" />
|
|
469
|
+
Picture-in-Picture
|
|
470
|
+
</ContextMenuItem>
|
|
471
|
+
<ContextMenuItem onSelect={() => playerStore?.toggleLoop()}>
|
|
472
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-70 flex-shrink-0 mr-2">
|
|
473
|
+
<polyline points="17 1 21 5 17 9"></polyline>
|
|
474
|
+
<path d="M3 11V9a4 4 0 0 1 4-4h14"></path>
|
|
475
|
+
<polyline points="7 23 3 19 7 15"></polyline>
|
|
476
|
+
<path d="M21 13v2a4 4 0 0 1-4 4H3"></path>
|
|
477
|
+
</svg>
|
|
478
|
+
{storeState.isLoopEnabled ? 'Disable Loop' : 'Enable Loop'}
|
|
479
|
+
</ContextMenuItem>
|
|
480
|
+
</ContextMenuContent>
|
|
481
|
+
</ContextMenuPortal>
|
|
482
|
+
</ContextMenu>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type PlaybackMode, type ContentEndpoints, type PlayerState, type PlayerStateContext, type ContentType } from '@livepeer-frameworks/player-core';
|
|
2
|
+
interface Props {
|
|
3
|
+
contentId: string;
|
|
4
|
+
contentType?: ContentType;
|
|
5
|
+
thumbnailUrl?: string | null;
|
|
6
|
+
endpoints?: ContentEndpoints;
|
|
7
|
+
options?: {
|
|
8
|
+
gatewayUrl?: string;
|
|
9
|
+
mistUrl?: string;
|
|
10
|
+
authToken?: string;
|
|
11
|
+
autoplay?: boolean;
|
|
12
|
+
muted?: boolean;
|
|
13
|
+
controls?: boolean;
|
|
14
|
+
stockControls?: boolean;
|
|
15
|
+
devMode?: boolean;
|
|
16
|
+
debug?: boolean;
|
|
17
|
+
forcePlayer?: string;
|
|
18
|
+
forceType?: string;
|
|
19
|
+
forceSource?: number;
|
|
20
|
+
playbackMode?: PlaybackMode;
|
|
21
|
+
};
|
|
22
|
+
onStateChange?: (state: PlayerState, context?: PlayerStateContext) => void;
|
|
23
|
+
}
|
|
24
|
+
declare const Player: import("svelte").Component<Props, {}, "">;
|
|
25
|
+
type Player = ReturnType<typeof Player>;
|
|
26
|
+
export default Player;
|