@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,266 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
StreamStateOverlay.svelte - Stream status overlay
|
|
3
|
+
Port of src/components/StreamStateOverlay.tsx
|
|
4
|
+
|
|
5
|
+
Shows stream status when not playable:
|
|
6
|
+
- Status-specific icons (online, offline, initializing, shutting down, error)
|
|
7
|
+
- Progress bar for INITIALIZING state
|
|
8
|
+
- Context-aware messaging
|
|
9
|
+
- Polling indicator
|
|
10
|
+
- Retry button for errors
|
|
11
|
+
-->
|
|
12
|
+
<script lang="ts">
|
|
13
|
+
import type { StreamStatus } from '@livepeer-frameworks/player-core';
|
|
14
|
+
|
|
15
|
+
interface Props {
|
|
16
|
+
/** Current stream status */
|
|
17
|
+
status: StreamStatus;
|
|
18
|
+
/** Human-readable message */
|
|
19
|
+
message: string;
|
|
20
|
+
/** Processing percentage (for INITIALIZING state) */
|
|
21
|
+
percentage?: number;
|
|
22
|
+
/** Callback for retry button */
|
|
23
|
+
onRetry?: () => void;
|
|
24
|
+
/** Whether to show the overlay */
|
|
25
|
+
visible?: boolean;
|
|
26
|
+
/** Additional className */
|
|
27
|
+
class?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let {
|
|
31
|
+
status,
|
|
32
|
+
message,
|
|
33
|
+
percentage,
|
|
34
|
+
onRetry,
|
|
35
|
+
visible = true,
|
|
36
|
+
class: className = '',
|
|
37
|
+
}: Props = $props();
|
|
38
|
+
|
|
39
|
+
// Computed states
|
|
40
|
+
let showRetry = $derived(status === 'ERROR' || status === 'INVALID' || status === 'OFFLINE');
|
|
41
|
+
let showProgress = $derived(status === 'INITIALIZING' && percentage !== undefined);
|
|
42
|
+
|
|
43
|
+
// Get status label for header
|
|
44
|
+
function getStatusLabel(status: StreamStatus): string {
|
|
45
|
+
switch (status) {
|
|
46
|
+
case 'ONLINE': return 'ONLINE';
|
|
47
|
+
case 'OFFLINE': return 'OFFLINE';
|
|
48
|
+
case 'INITIALIZING': return 'INITIALIZING';
|
|
49
|
+
case 'BOOTING': return 'STARTING';
|
|
50
|
+
case 'WAITING_FOR_DATA': return 'WAITING';
|
|
51
|
+
case 'SHUTTING_DOWN': return 'ENDING';
|
|
52
|
+
case 'ERROR': return 'ERROR';
|
|
53
|
+
case 'INVALID': return 'INVALID';
|
|
54
|
+
default: return 'STATUS';
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
</script>
|
|
58
|
+
|
|
59
|
+
<style>
|
|
60
|
+
.overlay-backdrop {
|
|
61
|
+
position: absolute;
|
|
62
|
+
inset: 0;
|
|
63
|
+
z-index: 20;
|
|
64
|
+
display: flex;
|
|
65
|
+
align-items: center;
|
|
66
|
+
justify-content: center;
|
|
67
|
+
background-color: hsl(var(--tn-bg-dark, 235 21% 11%) / 0.8);
|
|
68
|
+
backdrop-filter: blur(4px);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.slab {
|
|
72
|
+
width: 280px;
|
|
73
|
+
max-width: 90%;
|
|
74
|
+
background-color: hsl(var(--tn-bg, 233 23% 17%) / 0.95);
|
|
75
|
+
border: 1px solid hsl(var(--tn-border, 233 23% 25%) / 0.3);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.slab-header {
|
|
79
|
+
display: flex;
|
|
80
|
+
align-items: center;
|
|
81
|
+
gap: 0.5rem;
|
|
82
|
+
padding: 0.75rem 1rem;
|
|
83
|
+
border-bottom: 1px solid hsl(var(--tn-border, 233 23% 25%) / 0.3);
|
|
84
|
+
font-size: 0.75rem;
|
|
85
|
+
font-weight: 600;
|
|
86
|
+
text-transform: uppercase;
|
|
87
|
+
letter-spacing: 0.05em;
|
|
88
|
+
color: hsl(var(--tn-fg-dark, 233 23% 60%));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.slab-body {
|
|
92
|
+
padding: 1rem;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.slab-actions {
|
|
96
|
+
border-top: 1px solid hsl(var(--tn-border, 233 23% 25%) / 0.3);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.btn-flush {
|
|
100
|
+
width: 100%;
|
|
101
|
+
padding: 0.625rem 1rem;
|
|
102
|
+
background: none;
|
|
103
|
+
border: none;
|
|
104
|
+
cursor: pointer;
|
|
105
|
+
font-size: 0.75rem;
|
|
106
|
+
font-weight: 500;
|
|
107
|
+
text-transform: uppercase;
|
|
108
|
+
letter-spacing: 0.05em;
|
|
109
|
+
color: hsl(var(--tn-blue, 217 89% 71%));
|
|
110
|
+
transition: background-color 0.15s;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.btn-flush:hover {
|
|
114
|
+
background-color: hsl(var(--tn-bg-visual, 233 23% 20%) / 0.5);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.progress-bar {
|
|
118
|
+
height: 0.375rem;
|
|
119
|
+
width: 100%;
|
|
120
|
+
overflow: hidden;
|
|
121
|
+
background-color: hsl(var(--tn-bg-visual, 233 23% 20%));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.progress-fill {
|
|
125
|
+
height: 100%;
|
|
126
|
+
transition: width 0.3s ease;
|
|
127
|
+
background-color: hsl(var(--tn-yellow, 40 70% 64%));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.icon {
|
|
131
|
+
width: 1.25rem;
|
|
132
|
+
height: 1.25rem;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.icon-online {
|
|
136
|
+
color: hsl(var(--tn-green, 115 54% 57%));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.icon-offline {
|
|
140
|
+
color: hsl(var(--tn-red, 355 68% 65%));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.icon-warning {
|
|
144
|
+
color: hsl(var(--tn-yellow, 40 70% 64%));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.polling-indicator {
|
|
148
|
+
display: flex;
|
|
149
|
+
align-items: center;
|
|
150
|
+
gap: 0.5rem;
|
|
151
|
+
margin-top: 0.75rem;
|
|
152
|
+
font-size: 0.75rem;
|
|
153
|
+
color: hsl(var(--tn-fg-dark, 233 23% 60%));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.polling-dot {
|
|
157
|
+
width: 0.375rem;
|
|
158
|
+
height: 0.375rem;
|
|
159
|
+
background-color: hsl(var(--tn-cyan, 192 78% 73%));
|
|
160
|
+
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
@keyframes pulse {
|
|
164
|
+
0%, 100% { opacity: 1; }
|
|
165
|
+
50% { opacity: 0.5; }
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
@keyframes spin {
|
|
169
|
+
to { transform: rotate(360deg); }
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.animate-spin {
|
|
173
|
+
animation: spin 1s linear infinite;
|
|
174
|
+
}
|
|
175
|
+
</style>
|
|
176
|
+
|
|
177
|
+
{#if visible && status !== 'ONLINE'}
|
|
178
|
+
<div
|
|
179
|
+
class="overlay-backdrop {className}"
|
|
180
|
+
role="status"
|
|
181
|
+
aria-live="polite"
|
|
182
|
+
>
|
|
183
|
+
<div class="slab">
|
|
184
|
+
<!-- Slab header - status label with icon -->
|
|
185
|
+
<div class="slab-header">
|
|
186
|
+
<!-- Status Icon -->
|
|
187
|
+
{#if status === 'OFFLINE'}
|
|
188
|
+
<svg class="icon icon-offline" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
189
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 5.636a9 9 0 010 12.728m0 0l-2.829-2.829m2.829 2.829L21 21M15.536 8.464a5 5 0 010 7.072m0 0l-2.829-2.829m-4.243 2.829a4.978 4.978 0 01-1.414-2.83m-1.414 5.658a9 9 0 01-2.167-9.238m7.824 2.167a1 1 0 111.414 1.414m-1.414-1.414L3 3m8.293 8.293l1.414 1.414" />
|
|
190
|
+
</svg>
|
|
191
|
+
{:else if status === 'INITIALIZING' || status === 'BOOTING' || status === 'WAITING_FOR_DATA'}
|
|
192
|
+
<svg class="icon icon-warning animate-spin" fill="none" viewBox="0 0 24 24">
|
|
193
|
+
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
|
|
194
|
+
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
|
|
195
|
+
</svg>
|
|
196
|
+
{:else if status === 'SHUTTING_DOWN'}
|
|
197
|
+
<svg class="icon icon-warning" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
198
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
|
199
|
+
</svg>
|
|
200
|
+
{:else}
|
|
201
|
+
<!-- ERROR or INVALID -->
|
|
202
|
+
<svg class="icon icon-offline" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
203
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
|
204
|
+
</svg>
|
|
205
|
+
{/if}
|
|
206
|
+
|
|
207
|
+
<span>{getStatusLabel(status)}</span>
|
|
208
|
+
</div>
|
|
209
|
+
|
|
210
|
+
<!-- Slab body - message and progress -->
|
|
211
|
+
<div class="slab-body">
|
|
212
|
+
<p style="font-size: 0.875rem; color: hsl(var(--tn-fg, 233 23% 75%));">
|
|
213
|
+
{message}
|
|
214
|
+
</p>
|
|
215
|
+
|
|
216
|
+
{#if showProgress && percentage !== undefined}
|
|
217
|
+
<div style="margin-top: 0.75rem;">
|
|
218
|
+
<div class="progress-bar">
|
|
219
|
+
<div
|
|
220
|
+
class="progress-fill"
|
|
221
|
+
style="width: {Math.min(100, percentage)}%;"
|
|
222
|
+
/>
|
|
223
|
+
</div>
|
|
224
|
+
<p style="margin-top: 0.375rem; font-size: 0.75rem; font-family: monospace; color: hsl(var(--tn-fg-dark, 233 23% 60%));">
|
|
225
|
+
{Math.round(percentage)}%
|
|
226
|
+
</p>
|
|
227
|
+
</div>
|
|
228
|
+
{/if}
|
|
229
|
+
|
|
230
|
+
{#if status === 'OFFLINE'}
|
|
231
|
+
<p style="margin-top: 0.5rem; font-size: 0.75rem; color: hsl(var(--tn-fg-dark, 233 23% 60%));">
|
|
232
|
+
The stream will start when the broadcaster goes live
|
|
233
|
+
</p>
|
|
234
|
+
{/if}
|
|
235
|
+
|
|
236
|
+
{#if status === 'BOOTING' || status === 'WAITING_FOR_DATA'}
|
|
237
|
+
<p style="margin-top: 0.5rem; font-size: 0.75rem; color: hsl(var(--tn-fg-dark, 233 23% 60%));">
|
|
238
|
+
Please wait while the stream prepares...
|
|
239
|
+
</p>
|
|
240
|
+
{/if}
|
|
241
|
+
|
|
242
|
+
<!-- Polling indicator for non-error states -->
|
|
243
|
+
{#if !showRetry}
|
|
244
|
+
<div class="polling-indicator">
|
|
245
|
+
<span class="polling-dot" />
|
|
246
|
+
<span>Checking stream status...</span>
|
|
247
|
+
</div>
|
|
248
|
+
{/if}
|
|
249
|
+
</div>
|
|
250
|
+
|
|
251
|
+
<!-- Slab actions - flush retry button -->
|
|
252
|
+
{#if showRetry && onRetry}
|
|
253
|
+
<div class="slab-actions">
|
|
254
|
+
<button
|
|
255
|
+
type="button"
|
|
256
|
+
class="btn-flush"
|
|
257
|
+
onclick={onRetry}
|
|
258
|
+
aria-label="Retry connection"
|
|
259
|
+
>
|
|
260
|
+
Retry Connection
|
|
261
|
+
</button>
|
|
262
|
+
</div>
|
|
263
|
+
{/if}
|
|
264
|
+
</div>
|
|
265
|
+
</div>
|
|
266
|
+
{/if}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
SubtitleRenderer.svelte - Live or static subtitles renderer
|
|
3
|
+
Port of src/components/SubtitleRenderer.tsx
|
|
4
|
+
|
|
5
|
+
Supports:
|
|
6
|
+
- Static cue list (pre-loaded)
|
|
7
|
+
- Live cues from meta track subscription
|
|
8
|
+
- Customizable styling (font, colors, position)
|
|
9
|
+
- Automatic timing synchronization with video
|
|
10
|
+
-->
|
|
11
|
+
<script lang="ts">
|
|
12
|
+
import { onDestroy } from 'svelte';
|
|
13
|
+
|
|
14
|
+
interface SubtitleCue {
|
|
15
|
+
id: string;
|
|
16
|
+
text: string;
|
|
17
|
+
startTime: number;
|
|
18
|
+
endTime: number;
|
|
19
|
+
lang?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface MetaTrackEvent {
|
|
23
|
+
type: string;
|
|
24
|
+
data: unknown;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface SubtitleStyle {
|
|
28
|
+
fontSize?: string;
|
|
29
|
+
fontFamily?: string;
|
|
30
|
+
color?: string;
|
|
31
|
+
backgroundColor?: string;
|
|
32
|
+
textShadow?: string;
|
|
33
|
+
bottom?: string;
|
|
34
|
+
maxWidth?: string;
|
|
35
|
+
padding?: string;
|
|
36
|
+
borderRadius?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface Props {
|
|
40
|
+
/** Current video playback time in seconds */
|
|
41
|
+
currentTime: number;
|
|
42
|
+
/** Whether subtitles are enabled */
|
|
43
|
+
enabled?: boolean;
|
|
44
|
+
/** Subtitle cues to render (static or from meta track) */
|
|
45
|
+
cues?: SubtitleCue[];
|
|
46
|
+
/** Subscribe to meta track function (for live subtitles) */
|
|
47
|
+
subscribeToMetaTrack?: (trackId: string, callback: (event: MetaTrackEvent) => void) => () => void;
|
|
48
|
+
/** Meta track ID for live subtitles */
|
|
49
|
+
metaTrackId?: string;
|
|
50
|
+
/** Custom styles */
|
|
51
|
+
style?: SubtitleStyle;
|
|
52
|
+
/** Container class name */
|
|
53
|
+
class?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let {
|
|
57
|
+
currentTime,
|
|
58
|
+
enabled = true,
|
|
59
|
+
cues: staticCues,
|
|
60
|
+
subscribeToMetaTrack,
|
|
61
|
+
metaTrackId,
|
|
62
|
+
style: customStyle,
|
|
63
|
+
class: className = '',
|
|
64
|
+
}: Props = $props();
|
|
65
|
+
|
|
66
|
+
const DEFAULT_STYLE: SubtitleStyle = {
|
|
67
|
+
fontSize: '1.5rem',
|
|
68
|
+
fontFamily: 'system-ui, -apple-system, sans-serif',
|
|
69
|
+
color: 'white',
|
|
70
|
+
backgroundColor: 'rgba(0, 0, 0, 0.75)',
|
|
71
|
+
textShadow: '2px 2px 4px rgba(0, 0, 0, 0.5)',
|
|
72
|
+
bottom: '5%',
|
|
73
|
+
maxWidth: '90%',
|
|
74
|
+
padding: '0.5em 1em',
|
|
75
|
+
borderRadius: '4px',
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// State
|
|
79
|
+
let liveCues = $state<SubtitleCue[]>([]);
|
|
80
|
+
let displayedText = $state<string>('');
|
|
81
|
+
let lastCueId: string | null = null;
|
|
82
|
+
let unsubscribe: (() => void) | null = null;
|
|
83
|
+
|
|
84
|
+
// Merged style
|
|
85
|
+
let mergedStyle = $derived({ ...DEFAULT_STYLE, ...customStyle });
|
|
86
|
+
|
|
87
|
+
// All available cues (static + live)
|
|
88
|
+
let allCues = $derived([...(staticCues ?? []), ...liveCues]);
|
|
89
|
+
|
|
90
|
+
// Parse subtitle cue from meta track event data
|
|
91
|
+
function parseSubtitleCue(data: unknown): SubtitleCue | null {
|
|
92
|
+
if (typeof data !== 'object' || data === null) return null;
|
|
93
|
+
|
|
94
|
+
const obj = data as Record<string, unknown>;
|
|
95
|
+
|
|
96
|
+
const text = typeof obj.text === 'string' ? obj.text : String(obj.text ?? '');
|
|
97
|
+
if (!text) return null;
|
|
98
|
+
|
|
99
|
+
let startTime = 0;
|
|
100
|
+
let endTime = Infinity;
|
|
101
|
+
|
|
102
|
+
if ('startTime' in obj) startTime = Number(obj.startTime);
|
|
103
|
+
else if ('start' in obj) startTime = Number(obj.start);
|
|
104
|
+
|
|
105
|
+
if ('endTime' in obj) endTime = Number(obj.endTime);
|
|
106
|
+
else if ('end' in obj) endTime = Number(obj.end);
|
|
107
|
+
|
|
108
|
+
const id = typeof obj.id === 'string' ? obj.id : String(Date.now());
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
id,
|
|
112
|
+
text,
|
|
113
|
+
startTime,
|
|
114
|
+
endTime,
|
|
115
|
+
lang: typeof obj.lang === 'string' ? obj.lang : undefined,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Subscribe to live subtitles if meta track is configured
|
|
120
|
+
$effect(() => {
|
|
121
|
+
if (!enabled || !subscribeToMetaTrack || !metaTrackId) {
|
|
122
|
+
if (unsubscribe) {
|
|
123
|
+
unsubscribe();
|
|
124
|
+
unsubscribe = null;
|
|
125
|
+
}
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const handleMetaEvent = (event: MetaTrackEvent) => {
|
|
130
|
+
if (event.type === 'subtitle') {
|
|
131
|
+
const cue = parseSubtitleCue(event.data);
|
|
132
|
+
if (cue) {
|
|
133
|
+
// Deduplicate by ID
|
|
134
|
+
const existing = liveCues.find(c => c.id === cue.id);
|
|
135
|
+
if (!existing) {
|
|
136
|
+
// Keep last 50 cues max
|
|
137
|
+
liveCues = [...liveCues, cue].slice(-50);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
unsubscribe = subscribeToMetaTrack(metaTrackId, handleMetaEvent);
|
|
144
|
+
|
|
145
|
+
return () => {
|
|
146
|
+
if (unsubscribe) {
|
|
147
|
+
unsubscribe();
|
|
148
|
+
unsubscribe = null;
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Find active cue based on current time
|
|
154
|
+
$effect(() => {
|
|
155
|
+
if (!enabled) {
|
|
156
|
+
displayedText = '';
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const currentTimeMs = currentTime * 1000;
|
|
161
|
+
const activeCue = allCues.find(cue => {
|
|
162
|
+
const start = cue.startTime;
|
|
163
|
+
const end = cue.endTime;
|
|
164
|
+
return currentTimeMs >= start && currentTimeMs < end;
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
if (activeCue) {
|
|
168
|
+
displayedText = activeCue.text;
|
|
169
|
+
lastCueId = activeCue.id;
|
|
170
|
+
} else {
|
|
171
|
+
displayedText = '';
|
|
172
|
+
lastCueId = null;
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// Clean up expired cues
|
|
177
|
+
$effect(() => {
|
|
178
|
+
const currentTimeMs = currentTime * 1000;
|
|
179
|
+
|
|
180
|
+
liveCues = liveCues.filter(cue => {
|
|
181
|
+
const endTime = cue.endTime === Infinity ? cue.startTime + 10000 : cue.endTime;
|
|
182
|
+
return endTime >= currentTimeMs - 30000;
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// Cleanup on destroy
|
|
187
|
+
onDestroy(() => {
|
|
188
|
+
if (unsubscribe) {
|
|
189
|
+
unsubscribe();
|
|
190
|
+
unsubscribe = null;
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
</script>
|
|
194
|
+
|
|
195
|
+
<style>
|
|
196
|
+
.subtitle-container {
|
|
197
|
+
position: absolute;
|
|
198
|
+
left: 50%;
|
|
199
|
+
transform: translateX(-50%);
|
|
200
|
+
z-index: 30;
|
|
201
|
+
text-align: center;
|
|
202
|
+
pointer-events: none;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.subtitle-text {
|
|
206
|
+
display: inline-block;
|
|
207
|
+
white-space: pre-wrap;
|
|
208
|
+
}
|
|
209
|
+
</style>
|
|
210
|
+
|
|
211
|
+
{#if enabled && displayedText}
|
|
212
|
+
<div
|
|
213
|
+
class="subtitle-container {className}"
|
|
214
|
+
style="bottom: {mergedStyle.bottom}; max-width: {mergedStyle.maxWidth};"
|
|
215
|
+
role="region"
|
|
216
|
+
aria-live="polite"
|
|
217
|
+
aria-label="Subtitles"
|
|
218
|
+
>
|
|
219
|
+
<span
|
|
220
|
+
class="subtitle-text"
|
|
221
|
+
style="
|
|
222
|
+
font-size: {mergedStyle.fontSize};
|
|
223
|
+
font-family: {mergedStyle.fontFamily};
|
|
224
|
+
color: {mergedStyle.color};
|
|
225
|
+
background-color: {mergedStyle.backgroundColor};
|
|
226
|
+
text-shadow: {mergedStyle.textShadow};
|
|
227
|
+
padding: {mergedStyle.padding};
|
|
228
|
+
border-radius: {mergedStyle.borderRadius};
|
|
229
|
+
"
|
|
230
|
+
>
|
|
231
|
+
{displayedText}
|
|
232
|
+
</span>
|
|
233
|
+
</div>
|
|
234
|
+
{/if}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
ThumbnailOverlay.svelte - Pre-play poster with play button
|
|
3
|
+
Port of src/components/ThumbnailOverlay.tsx
|
|
4
|
+
-->
|
|
5
|
+
<script lang="ts">
|
|
6
|
+
import { cn } from '@livepeer-frameworks/player-core';
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
thumbnailUrl?: string | null;
|
|
10
|
+
onPlay?: () => void;
|
|
11
|
+
message?: string | null;
|
|
12
|
+
showUnmuteMessage?: boolean;
|
|
13
|
+
style?: string;
|
|
14
|
+
class?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let {
|
|
18
|
+
thumbnailUrl = null,
|
|
19
|
+
onPlay = undefined,
|
|
20
|
+
message = null,
|
|
21
|
+
showUnmuteMessage = false,
|
|
22
|
+
style = '',
|
|
23
|
+
class: className = '',
|
|
24
|
+
}: Props = $props();
|
|
25
|
+
|
|
26
|
+
function handleClick() {
|
|
27
|
+
onPlay?.();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function handleKeyDown(event: KeyboardEvent) {
|
|
31
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
32
|
+
event.preventDefault();
|
|
33
|
+
handleClick();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<div
|
|
39
|
+
role="button"
|
|
40
|
+
tabindex="0"
|
|
41
|
+
onclick={handleClick}
|
|
42
|
+
onkeydown={handleKeyDown}
|
|
43
|
+
{style}
|
|
44
|
+
class={cn(
|
|
45
|
+
'fw-player-thumbnail relative flex h-full min-h-[280px] w-full cursor-pointer items-center justify-center overflow-hidden rounded-xl bg-slate-950 text-foreground outline-none transition focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-background',
|
|
46
|
+
className
|
|
47
|
+
)}
|
|
48
|
+
>
|
|
49
|
+
{#if thumbnailUrl}
|
|
50
|
+
<div
|
|
51
|
+
class="absolute inset-0 bg-cover bg-center"
|
|
52
|
+
style="background-image: url({thumbnailUrl})"
|
|
53
|
+
></div>
|
|
54
|
+
{/if}
|
|
55
|
+
|
|
56
|
+
<div
|
|
57
|
+
class={cn(
|
|
58
|
+
'absolute inset-0 bg-slate-950/70',
|
|
59
|
+
!thumbnailUrl && 'bg-gradient-to-br from-slate-900 via-slate-950 to-slate-900'
|
|
60
|
+
)}
|
|
61
|
+
></div>
|
|
62
|
+
|
|
63
|
+
<div class="relative z-10 flex max-w-[320px] flex-col items-center gap-4 px-6 text-center text-sm sm:gap-6">
|
|
64
|
+
{#if showUnmuteMessage}
|
|
65
|
+
<div class="w-full rounded-lg border border-white/15 bg-black/80 p-4 text-sm text-white shadow-lg backdrop-blur">
|
|
66
|
+
<div class="mb-1 flex items-center justify-center gap-2 text-base font-semibold text-primary">
|
|
67
|
+
<span aria-hidden="true">🔇</span> Click to unmute
|
|
68
|
+
</div>
|
|
69
|
+
<p class="text-xs text-white/80">Stream is playing muted — tap to enable sound.</p>
|
|
70
|
+
</div>
|
|
71
|
+
{:else}
|
|
72
|
+
<button
|
|
73
|
+
type="button"
|
|
74
|
+
class="h-20 w-20 rounded-full bg-primary/90 text-primary-foreground shadow-lg shadow-primary/40 transition hover:bg-primary focus-visible:bg-primary flex items-center justify-center"
|
|
75
|
+
aria-label="Play stream"
|
|
76
|
+
>
|
|
77
|
+
<svg
|
|
78
|
+
viewBox="0 0 24 24"
|
|
79
|
+
fill="currentColor"
|
|
80
|
+
class="ml-0.5 h-8 w-8"
|
|
81
|
+
aria-hidden="true"
|
|
82
|
+
>
|
|
83
|
+
<path d="M8 5v14l11-7z" />
|
|
84
|
+
</svg>
|
|
85
|
+
</button>
|
|
86
|
+
<div class="w-full rounded-lg border border-white/10 bg-black/70 p-5 text-white shadow-inner backdrop-blur">
|
|
87
|
+
<p class="text-base font-semibold text-primary">
|
|
88
|
+
{message ?? 'Click to play'}
|
|
89
|
+
</p>
|
|
90
|
+
<p class="mt-1 text-xs text-white/70">
|
|
91
|
+
{message ? 'Start streaming instantly' : 'Jump into the live feed'}
|
|
92
|
+
</p>
|
|
93
|
+
</div>
|
|
94
|
+
{/if}
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
TitleOverlay.svelte - Title/description overlay at top of player
|
|
3
|
+
Port of src/components/TitleOverlay.tsx
|
|
4
|
+
-->
|
|
5
|
+
<script lang="ts">
|
|
6
|
+
import { cn } from '@livepeer-frameworks/player-core';
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
title?: string | null;
|
|
10
|
+
description?: string | null;
|
|
11
|
+
isVisible: boolean;
|
|
12
|
+
class?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let {
|
|
16
|
+
title = null,
|
|
17
|
+
description = null,
|
|
18
|
+
isVisible,
|
|
19
|
+
class: className = '',
|
|
20
|
+
}: Props = $props();
|
|
21
|
+
|
|
22
|
+
// Don't render if no content
|
|
23
|
+
let hasContent = $derived(!!title || !!description);
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
{#if hasContent}
|
|
27
|
+
<div
|
|
28
|
+
class={cn(
|
|
29
|
+
'fw-title-overlay absolute inset-x-0 top-0 z-20 pointer-events-none',
|
|
30
|
+
'bg-gradient-to-b from-black/70 via-black/40 to-transparent',
|
|
31
|
+
'px-4 py-3 transition-opacity duration-300',
|
|
32
|
+
isVisible ? 'opacity-100' : 'opacity-0',
|
|
33
|
+
className
|
|
34
|
+
)}
|
|
35
|
+
>
|
|
36
|
+
{#if title}
|
|
37
|
+
<h2 class="text-white text-sm font-medium truncate max-w-[80%]">
|
|
38
|
+
{title}
|
|
39
|
+
</h2>
|
|
40
|
+
{/if}
|
|
41
|
+
{#if description}
|
|
42
|
+
<p class="text-white/70 text-xs mt-0.5 line-clamp-2 max-w-[70%]">
|
|
43
|
+
{description}
|
|
44
|
+
</p>
|
|
45
|
+
{/if}
|
|
46
|
+
</div>
|
|
47
|
+
{/if}
|