@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.
Files changed (169) hide show
  1. package/dist/DevModePanel.svelte +650 -0
  2. package/dist/DevModePanel.svelte.d.ts +31 -0
  3. package/dist/DvdLogo.svelte +213 -0
  4. package/dist/DvdLogo.svelte.d.ts +7 -0
  5. package/dist/Icons.svelte +27 -0
  6. package/dist/Icons.svelte.d.ts +25 -0
  7. package/dist/IdleScreen.svelte +752 -0
  8. package/dist/IdleScreen.svelte.d.ts +11 -0
  9. package/dist/LoadingScreen.svelte +689 -0
  10. package/dist/LoadingScreen.svelte.d.ts +7 -0
  11. package/dist/Player.svelte +482 -0
  12. package/dist/Player.svelte.d.ts +26 -0
  13. package/dist/PlayerControls.svelte +739 -0
  14. package/dist/PlayerControls.svelte.d.ts +20 -0
  15. package/dist/SeekBar.svelte +274 -0
  16. package/dist/SeekBar.svelte.d.ts +25 -0
  17. package/dist/SkipIndicator.svelte +95 -0
  18. package/dist/SkipIndicator.svelte.d.ts +14 -0
  19. package/dist/SpeedIndicator.svelte +38 -0
  20. package/dist/SpeedIndicator.svelte.d.ts +8 -0
  21. package/dist/StatsPanel.svelte +155 -0
  22. package/dist/StatsPanel.svelte.d.ts +27 -0
  23. package/dist/StreamStateOverlay.svelte +266 -0
  24. package/dist/StreamStateOverlay.svelte.d.ts +18 -0
  25. package/dist/SubtitleRenderer.svelte +234 -0
  26. package/dist/SubtitleRenderer.svelte.d.ts +41 -0
  27. package/dist/ThumbnailOverlay.svelte +96 -0
  28. package/dist/ThumbnailOverlay.svelte.d.ts +11 -0
  29. package/dist/TitleOverlay.svelte +47 -0
  30. package/dist/TitleOverlay.svelte.d.ts +9 -0
  31. package/dist/assets/logomark.svg +56 -0
  32. package/dist/components/VolumeIcons.svelte +53 -0
  33. package/dist/components/VolumeIcons.svelte.d.ts +10 -0
  34. package/dist/global.d.ts +15 -0
  35. package/dist/icons/FullscreenExitIcon.svelte +33 -0
  36. package/dist/icons/FullscreenExitIcon.svelte.d.ts +8 -0
  37. package/dist/icons/FullscreenIcon.svelte +33 -0
  38. package/dist/icons/FullscreenIcon.svelte.d.ts +8 -0
  39. package/dist/icons/PauseIcon.svelte +28 -0
  40. package/dist/icons/PauseIcon.svelte.d.ts +8 -0
  41. package/dist/icons/PictureInPictureIcon.svelte +28 -0
  42. package/dist/icons/PictureInPictureIcon.svelte.d.ts +8 -0
  43. package/dist/icons/PlayIcon.svelte +27 -0
  44. package/dist/icons/PlayIcon.svelte.d.ts +8 -0
  45. package/dist/icons/SeekToLiveIcon.svelte +30 -0
  46. package/dist/icons/SeekToLiveIcon.svelte.d.ts +8 -0
  47. package/dist/icons/SettingsIcon.svelte +40 -0
  48. package/dist/icons/SettingsIcon.svelte.d.ts +8 -0
  49. package/dist/icons/SkipBackIcon.svelte +32 -0
  50. package/dist/icons/SkipBackIcon.svelte.d.ts +8 -0
  51. package/dist/icons/SkipForwardIcon.svelte +32 -0
  52. package/dist/icons/SkipForwardIcon.svelte.d.ts +8 -0
  53. package/dist/icons/StatsIcon.svelte +29 -0
  54. package/dist/icons/StatsIcon.svelte.d.ts +8 -0
  55. package/dist/icons/VolumeOffIcon.svelte +29 -0
  56. package/dist/icons/VolumeOffIcon.svelte.d.ts +8 -0
  57. package/dist/icons/VolumeUpIcon.svelte +34 -0
  58. package/dist/icons/VolumeUpIcon.svelte.d.ts +8 -0
  59. package/dist/icons/index.d.ts +17 -0
  60. package/dist/icons/index.js +17 -0
  61. package/dist/index.d.ts +50 -0
  62. package/dist/index.js +54 -0
  63. package/dist/player.css +2 -0
  64. package/dist/stores/index.d.ts +15 -0
  65. package/dist/stores/index.js +21 -0
  66. package/dist/stores/playbackQuality.d.ts +43 -0
  67. package/dist/stores/playbackQuality.js +107 -0
  68. package/dist/stores/playerContext.d.ts +73 -0
  69. package/dist/stores/playerContext.js +166 -0
  70. package/dist/stores/playerController.d.ts +178 -0
  71. package/dist/stores/playerController.js +358 -0
  72. package/dist/stores/playerSelection.d.ts +84 -0
  73. package/dist/stores/playerSelection.js +159 -0
  74. package/dist/stores/streamState.d.ts +44 -0
  75. package/dist/stores/streamState.js +314 -0
  76. package/dist/stores/viewerEndpoints.d.ts +48 -0
  77. package/dist/stores/viewerEndpoints.js +178 -0
  78. package/dist/types.d.ts +4 -0
  79. package/dist/types.js +4 -0
  80. package/dist/ui/Badge.svelte +21 -0
  81. package/dist/ui/Badge.svelte.d.ts +32 -0
  82. package/dist/ui/Button.svelte +42 -0
  83. package/dist/ui/Button.svelte.d.ts +35 -0
  84. package/dist/ui/Slider.svelte +100 -0
  85. package/dist/ui/Slider.svelte.d.ts +17 -0
  86. package/dist/ui/badge.d.ts +6 -0
  87. package/dist/ui/badge.js +10 -0
  88. package/dist/ui/button.d.ts +8 -0
  89. package/dist/ui/button.js +21 -0
  90. package/dist/ui/context-menu/ContextMenuCheckboxItem.svelte +34 -0
  91. package/dist/ui/context-menu/ContextMenuCheckboxItem.svelte.d.ts +31 -0
  92. package/dist/ui/context-menu/ContextMenuContent.svelte +17 -0
  93. package/dist/ui/context-menu/ContextMenuContent.svelte.d.ts +7 -0
  94. package/dist/ui/context-menu/ContextMenuItem.svelte +22 -0
  95. package/dist/ui/context-menu/ContextMenuItem.svelte.d.ts +8 -0
  96. package/dist/ui/context-menu/ContextMenuLabel.svelte +22 -0
  97. package/dist/ui/context-menu/ContextMenuLabel.svelte.d.ts +8 -0
  98. package/dist/ui/context-menu/ContextMenuPortal.svelte +11 -0
  99. package/dist/ui/context-menu/ContextMenuPortal.svelte.d.ts +6 -0
  100. package/dist/ui/context-menu/ContextMenuRadioItem.svelte +21 -0
  101. package/dist/ui/context-menu/ContextMenuRadioItem.svelte.d.ts +31 -0
  102. package/dist/ui/context-menu/ContextMenuSeparator.svelte +14 -0
  103. package/dist/ui/context-menu/ContextMenuSeparator.svelte.d.ts +6 -0
  104. package/dist/ui/context-menu/ContextMenuShortcut.svelte +19 -0
  105. package/dist/ui/context-menu/ContextMenuShortcut.svelte.d.ts +7 -0
  106. package/dist/ui/context-menu/ContextMenuSubContent.svelte +20 -0
  107. package/dist/ui/context-menu/ContextMenuSubContent.svelte.d.ts +7 -0
  108. package/dist/ui/context-menu/ContextMenuSubTrigger.svelte +34 -0
  109. package/dist/ui/context-menu/ContextMenuSubTrigger.svelte.d.ts +8 -0
  110. package/dist/ui/context-menu/index.d.ts +17 -0
  111. package/dist/ui/context-menu/index.js +17 -0
  112. package/package.json +51 -0
  113. package/src/DevModePanel.svelte +650 -0
  114. package/src/DvdLogo.svelte +213 -0
  115. package/src/Icons.svelte +27 -0
  116. package/src/IdleScreen.svelte +739 -0
  117. package/src/LoadingScreen.svelte +674 -0
  118. package/src/Player.svelte +483 -0
  119. package/src/PlayerControls.svelte +752 -0
  120. package/src/SeekBar.svelte +274 -0
  121. package/src/SkipIndicator.svelte +95 -0
  122. package/src/SpeedIndicator.svelte +37 -0
  123. package/src/StatsPanel.svelte +155 -0
  124. package/src/StreamStateOverlay.svelte +266 -0
  125. package/src/SubtitleRenderer.svelte +234 -0
  126. package/src/ThumbnailOverlay.svelte +96 -0
  127. package/src/TitleOverlay.svelte +47 -0
  128. package/src/assets/logomark.svg +56 -0
  129. package/src/components/VolumeIcons.svelte +53 -0
  130. package/src/global.d.ts +15 -0
  131. package/src/icons/FullscreenExitIcon.svelte +33 -0
  132. package/src/icons/FullscreenIcon.svelte +33 -0
  133. package/src/icons/PauseIcon.svelte +28 -0
  134. package/src/icons/PictureInPictureIcon.svelte +28 -0
  135. package/src/icons/PlayIcon.svelte +27 -0
  136. package/src/icons/SeekToLiveIcon.svelte +30 -0
  137. package/src/icons/SettingsIcon.svelte +40 -0
  138. package/src/icons/SkipBackIcon.svelte +32 -0
  139. package/src/icons/SkipForwardIcon.svelte +32 -0
  140. package/src/icons/StatsIcon.svelte +29 -0
  141. package/src/icons/VolumeOffIcon.svelte +29 -0
  142. package/src/icons/VolumeUpIcon.svelte +34 -0
  143. package/src/icons/index.ts +18 -0
  144. package/src/index.ts +84 -0
  145. package/src/player.css +2 -0
  146. package/src/stores/index.ts +88 -0
  147. package/src/stores/playbackQuality.ts +137 -0
  148. package/src/stores/playerContext.ts +221 -0
  149. package/src/stores/playerController.ts +568 -0
  150. package/src/stores/playerSelection.ts +216 -0
  151. package/src/stores/streamState.ts +367 -0
  152. package/src/stores/viewerEndpoints.ts +224 -0
  153. package/src/types.ts +6 -0
  154. package/src/ui/Badge.svelte +21 -0
  155. package/src/ui/Button.svelte +42 -0
  156. package/src/ui/Slider.svelte +100 -0
  157. package/src/ui/badge.ts +20 -0
  158. package/src/ui/button.ts +35 -0
  159. package/src/ui/context-menu/ContextMenuCheckboxItem.svelte +34 -0
  160. package/src/ui/context-menu/ContextMenuContent.svelte +17 -0
  161. package/src/ui/context-menu/ContextMenuItem.svelte +22 -0
  162. package/src/ui/context-menu/ContextMenuLabel.svelte +22 -0
  163. package/src/ui/context-menu/ContextMenuPortal.svelte +11 -0
  164. package/src/ui/context-menu/ContextMenuRadioItem.svelte +21 -0
  165. package/src/ui/context-menu/ContextMenuSeparator.svelte +14 -0
  166. package/src/ui/context-menu/ContextMenuShortcut.svelte +19 -0
  167. package/src/ui/context-menu/ContextMenuSubContent.svelte +20 -0
  168. package/src/ui/context-menu/ContextMenuSubTrigger.svelte +34 -0
  169. 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;