@livepeer-frameworks/player-svelte 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/DevModePanel.svelte +266 -127
- package/dist/DevModePanel.svelte.d.ts +1 -1
- package/dist/DvdLogo.svelte +17 -21
- package/dist/Icons.svelte +5 -3
- package/dist/Icons.svelte.d.ts +6 -19
- package/dist/IdleScreen.svelte +277 -186
- package/dist/IdleScreen.svelte.d.ts +1 -1
- package/dist/LoadingScreen.svelte +190 -162
- package/dist/Player.svelte +244 -111
- package/dist/Player.svelte.d.ts +1 -1
- package/dist/PlayerControls.svelte +263 -168
- package/dist/PlayerControls.svelte.d.ts +1 -1
- package/dist/SeekBar.svelte +61 -35
- package/dist/SkipIndicator.svelte +4 -4
- package/dist/SkipIndicator.svelte.d.ts +1 -1
- package/dist/SpeedIndicator.svelte +1 -1
- package/dist/StatsPanel.svelte +76 -57
- package/dist/StatsPanel.svelte.d.ts +1 -1
- package/dist/StreamStateOverlay.svelte +143 -107
- package/dist/StreamStateOverlay.svelte.d.ts +1 -1
- package/dist/SubtitleRenderer.svelte +46 -43
- package/dist/ThumbnailOverlay.svelte +22 -19
- package/dist/TitleOverlay.svelte +6 -11
- package/dist/components/VolumeIcons.svelte +12 -6
- package/dist/global.d.ts +3 -3
- package/dist/icons/FullscreenExitIcon.svelte +1 -5
- package/dist/icons/FullscreenIcon.svelte +1 -5
- package/dist/icons/PauseIcon.svelte +1 -5
- package/dist/icons/PictureInPictureIcon.svelte +12 -6
- package/dist/icons/PlayIcon.svelte +1 -5
- package/dist/icons/SeekToLiveIcon.svelte +1 -5
- package/dist/icons/SettingsIcon.svelte +1 -5
- package/dist/icons/SkipBackIcon.svelte +1 -5
- package/dist/icons/SkipForwardIcon.svelte +1 -5
- package/dist/icons/StatsIcon.svelte +1 -5
- package/dist/icons/VolumeOffIcon.svelte +1 -5
- package/dist/icons/VolumeUpIcon.svelte +1 -5
- package/dist/icons/index.d.ts +12 -12
- package/dist/icons/index.js +12 -12
- package/dist/index.d.ts +24 -24
- package/dist/index.js +21 -21
- package/dist/stores/index.d.ts +6 -6
- package/dist/stores/index.js +6 -6
- package/dist/stores/playbackQuality.d.ts +2 -2
- package/dist/stores/playbackQuality.js +7 -7
- package/dist/stores/playerContext.d.ts +2 -2
- package/dist/stores/playerContext.js +17 -17
- package/dist/stores/playerController.d.ts +13 -4
- package/dist/stores/playerController.js +80 -56
- package/dist/stores/playerSelection.d.ts +2 -2
- package/dist/stores/playerSelection.js +7 -7
- package/dist/stores/streamState.d.ts +2 -2
- package/dist/stores/streamState.js +56 -56
- package/dist/stores/viewerEndpoints.d.ts +3 -3
- package/dist/stores/viewerEndpoints.js +21 -21
- package/dist/types.d.ts +1 -1
- package/dist/ui/Badge.svelte +9 -10
- package/dist/ui/Badge.svelte.d.ts +8 -29
- package/dist/ui/Button.svelte +16 -16
- package/dist/ui/Button.svelte.d.ts +8 -29
- package/dist/ui/Slider.svelte +21 -55
- package/dist/ui/badge.js +1 -1
- package/dist/ui/button.js +2 -2
- package/dist/ui/context-menu/ContextMenuCheckboxItem.svelte +5 -7
- package/dist/ui/context-menu/ContextMenuCheckboxItem.svelte.d.ts +6 -27
- package/dist/ui/context-menu/ContextMenuContent.svelte +2 -9
- package/dist/ui/context-menu/ContextMenuItem.svelte +1 -5
- package/dist/ui/context-menu/ContextMenuLabel.svelte +1 -5
- package/dist/ui/context-menu/ContextMenuRadioItem.svelte +5 -7
- package/dist/ui/context-menu/ContextMenuRadioItem.svelte.d.ts +6 -27
- package/dist/ui/context-menu/ContextMenuSeparator.svelte +2 -8
- package/dist/ui/context-menu/ContextMenuShortcut.svelte +2 -12
- package/dist/ui/context-menu/ContextMenuSubContent.svelte +1 -5
- package/package.json +15 -7
- package/src/DevModePanel.svelte +1 -0
- package/src/Icons.svelte +5 -3
- package/src/IdleScreen.svelte +21 -14
- package/src/LoadingScreen.svelte +20 -13
- package/src/Player.svelte +48 -2
- package/src/PlayerControls.svelte +36 -17
- package/src/SeekBar.svelte +33 -0
- package/src/StreamStateOverlay.svelte +2 -2
- package/src/stores/playerController.ts +39 -1
- package/src/stores/viewerEndpoints.ts +1 -1
- package/src/ui/Badge.svelte +7 -4
- package/src/ui/Button.svelte +13 -13
- package/src/ui/context-menu/ContextMenuCheckboxItem.svelte +4 -2
- package/src/ui/context-menu/ContextMenuRadioItem.svelte +4 -2
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
- Retry button for errors
|
|
11
11
|
-->
|
|
12
12
|
<script lang="ts">
|
|
13
|
-
import type { StreamStatus } from
|
|
13
|
+
import type { StreamStatus } from "@livepeer-frameworks/player-core";
|
|
14
14
|
|
|
15
15
|
interface Props {
|
|
16
16
|
/** Current stream status */
|
|
@@ -33,29 +33,149 @@
|
|
|
33
33
|
percentage,
|
|
34
34
|
onRetry,
|
|
35
35
|
visible = true,
|
|
36
|
-
class: className =
|
|
36
|
+
class: className = "",
|
|
37
37
|
}: Props = $props();
|
|
38
38
|
|
|
39
39
|
// Computed states
|
|
40
|
-
let showRetry = $derived(status ===
|
|
41
|
-
let showProgress = $derived(status ===
|
|
40
|
+
let showRetry = $derived(status === "ERROR" || status === "INVALID" || status === "OFFLINE");
|
|
41
|
+
let showProgress = $derived(status === "INITIALIZING" && percentage !== undefined);
|
|
42
42
|
|
|
43
43
|
// Get status label for header
|
|
44
44
|
function getStatusLabel(status: StreamStatus): string {
|
|
45
45
|
switch (status) {
|
|
46
|
-
case
|
|
47
|
-
|
|
48
|
-
case
|
|
49
|
-
|
|
50
|
-
case
|
|
51
|
-
|
|
52
|
-
case
|
|
53
|
-
|
|
54
|
-
|
|
46
|
+
case "ONLINE":
|
|
47
|
+
return "ONLINE";
|
|
48
|
+
case "OFFLINE":
|
|
49
|
+
return "OFFLINE";
|
|
50
|
+
case "INITIALIZING":
|
|
51
|
+
return "INITIALIZING";
|
|
52
|
+
case "BOOTING":
|
|
53
|
+
return "STARTING";
|
|
54
|
+
case "WAITING_FOR_DATA":
|
|
55
|
+
return "WAITING";
|
|
56
|
+
case "SHUTTING_DOWN":
|
|
57
|
+
return "ENDING";
|
|
58
|
+
case "ERROR":
|
|
59
|
+
return "ERROR";
|
|
60
|
+
case "INVALID":
|
|
61
|
+
return "INVALID";
|
|
62
|
+
default:
|
|
63
|
+
return "STATUS";
|
|
55
64
|
}
|
|
56
65
|
}
|
|
57
66
|
</script>
|
|
58
67
|
|
|
68
|
+
{#if visible && status !== "ONLINE"}
|
|
69
|
+
<div class="overlay-backdrop {className}" role="status" aria-live="polite">
|
|
70
|
+
<div class="slab">
|
|
71
|
+
<!-- Slab header - status label with icon -->
|
|
72
|
+
<div class="slab-header">
|
|
73
|
+
<!-- Status Icon -->
|
|
74
|
+
{#if status === "OFFLINE"}
|
|
75
|
+
<svg class="icon icon-offline" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
76
|
+
<path
|
|
77
|
+
stroke-linecap="round"
|
|
78
|
+
stroke-linejoin="round"
|
|
79
|
+
stroke-width="2"
|
|
80
|
+
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"
|
|
81
|
+
/>
|
|
82
|
+
</svg>
|
|
83
|
+
{:else if status === "INITIALIZING" || status === "BOOTING" || status === "WAITING_FOR_DATA"}
|
|
84
|
+
<svg class="icon icon-warning animate-spin" fill="none" viewBox="0 0 24 24">
|
|
85
|
+
<circle
|
|
86
|
+
class="opacity-25"
|
|
87
|
+
cx="12"
|
|
88
|
+
cy="12"
|
|
89
|
+
r="10"
|
|
90
|
+
stroke="currentColor"
|
|
91
|
+
stroke-width="4"
|
|
92
|
+
/>
|
|
93
|
+
<path
|
|
94
|
+
class="opacity-75"
|
|
95
|
+
fill="currentColor"
|
|
96
|
+
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"
|
|
97
|
+
/>
|
|
98
|
+
</svg>
|
|
99
|
+
{:else if status === "SHUTTING_DOWN"}
|
|
100
|
+
<svg class="icon icon-warning" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
101
|
+
<path
|
|
102
|
+
stroke-linecap="round"
|
|
103
|
+
stroke-linejoin="round"
|
|
104
|
+
stroke-width="2"
|
|
105
|
+
d="M13 10V3L4 14h7v7l9-11h-7z"
|
|
106
|
+
/>
|
|
107
|
+
</svg>
|
|
108
|
+
{:else}
|
|
109
|
+
<!-- ERROR or INVALID -->
|
|
110
|
+
<svg class="icon icon-offline" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
111
|
+
<path
|
|
112
|
+
stroke-linecap="round"
|
|
113
|
+
stroke-linejoin="round"
|
|
114
|
+
stroke-width="2"
|
|
115
|
+
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"
|
|
116
|
+
/>
|
|
117
|
+
</svg>
|
|
118
|
+
{/if}
|
|
119
|
+
|
|
120
|
+
<span>{getStatusLabel(status)}</span>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<!-- Slab body - message and progress -->
|
|
124
|
+
<div class="slab-body">
|
|
125
|
+
<p style="font-size: 0.875rem; color: hsl(var(--tn-fg, 233 23% 75%));">
|
|
126
|
+
{message}
|
|
127
|
+
</p>
|
|
128
|
+
|
|
129
|
+
{#if showProgress && percentage !== undefined}
|
|
130
|
+
<div style="margin-top: 0.75rem;">
|
|
131
|
+
<div class="progress-bar">
|
|
132
|
+
<div class="progress-fill" style="width: {Math.min(100, percentage)};"></div>
|
|
133
|
+
</div>
|
|
134
|
+
<p
|
|
135
|
+
style="margin-top: 0.375rem; font-size: 0.75rem; font-family: monospace; color: hsl(var(--tn-fg-dark, 233 23% 60%));"
|
|
136
|
+
>
|
|
137
|
+
{Math.round(percentage)}%
|
|
138
|
+
</p>
|
|
139
|
+
</div>
|
|
140
|
+
{/if}
|
|
141
|
+
|
|
142
|
+
{#if status === "OFFLINE"}
|
|
143
|
+
<p
|
|
144
|
+
style="margin-top: 0.5rem; font-size: 0.75rem; color: hsl(var(--tn-fg-dark, 233 23% 60%));"
|
|
145
|
+
>
|
|
146
|
+
The stream will start when the broadcaster goes live
|
|
147
|
+
</p>
|
|
148
|
+
{/if}
|
|
149
|
+
|
|
150
|
+
{#if status === "BOOTING" || status === "WAITING_FOR_DATA"}
|
|
151
|
+
<p
|
|
152
|
+
style="margin-top: 0.5rem; font-size: 0.75rem; color: hsl(var(--tn-fg-dark, 233 23% 60%));"
|
|
153
|
+
>
|
|
154
|
+
Please wait while the stream prepares...
|
|
155
|
+
</p>
|
|
156
|
+
{/if}
|
|
157
|
+
|
|
158
|
+
<!-- Polling indicator for non-error states -->
|
|
159
|
+
{#if !showRetry}
|
|
160
|
+
<div class="polling-indicator">
|
|
161
|
+
<span class="polling-dot"></span>
|
|
162
|
+
<span>Checking stream status...</span>
|
|
163
|
+
</div>
|
|
164
|
+
{/if}
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
<!-- Slab actions - flush retry button -->
|
|
168
|
+
{#if showRetry && onRetry}
|
|
169
|
+
<div class="slab-actions">
|
|
170
|
+
<button type="button" class="btn-flush" onclick={onRetry} aria-label="Retry connection">
|
|
171
|
+
Retry Connection
|
|
172
|
+
</button>
|
|
173
|
+
</div>
|
|
174
|
+
{/if}
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
{/if}
|
|
178
|
+
|
|
59
179
|
<style>
|
|
60
180
|
.fw-player-root .overlay-backdrop {
|
|
61
181
|
position: absolute;
|
|
@@ -161,106 +281,22 @@
|
|
|
161
281
|
}
|
|
162
282
|
|
|
163
283
|
@keyframes pulse {
|
|
164
|
-
0%,
|
|
165
|
-
|
|
284
|
+
0%,
|
|
285
|
+
100% {
|
|
286
|
+
opacity: 1;
|
|
287
|
+
}
|
|
288
|
+
50% {
|
|
289
|
+
opacity: 0.5;
|
|
290
|
+
}
|
|
166
291
|
}
|
|
167
292
|
|
|
168
293
|
@keyframes spin {
|
|
169
|
-
to {
|
|
294
|
+
to {
|
|
295
|
+
transform: rotate(360deg);
|
|
296
|
+
}
|
|
170
297
|
}
|
|
171
298
|
|
|
172
299
|
.fw-player-root .animate-spin {
|
|
173
300
|
animation: spin 1s linear infinite;
|
|
174
301
|
}
|
|
175
302
|
</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}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
- Automatic timing synchronization with video
|
|
10
10
|
-->
|
|
11
11
|
<script lang="ts">
|
|
12
|
-
import { onDestroy } from
|
|
12
|
+
import { onDestroy } from "svelte";
|
|
13
13
|
|
|
14
14
|
interface SubtitleCue {
|
|
15
15
|
id: string;
|
|
@@ -44,7 +44,10 @@
|
|
|
44
44
|
/** Subtitle cues to render (static or from meta track) */
|
|
45
45
|
cues?: SubtitleCue[];
|
|
46
46
|
/** Subscribe to meta track function (for live subtitles) */
|
|
47
|
-
subscribeToMetaTrack?: (
|
|
47
|
+
subscribeToMetaTrack?: (
|
|
48
|
+
trackId: string,
|
|
49
|
+
callback: (event: MetaTrackEvent) => void
|
|
50
|
+
) => () => void;
|
|
48
51
|
/** Meta track ID for live subtitles */
|
|
49
52
|
metaTrackId?: string;
|
|
50
53
|
/** Custom styles */
|
|
@@ -60,24 +63,24 @@
|
|
|
60
63
|
subscribeToMetaTrack,
|
|
61
64
|
metaTrackId,
|
|
62
65
|
style: customStyle,
|
|
63
|
-
class: className =
|
|
66
|
+
class: className = "",
|
|
64
67
|
}: Props = $props();
|
|
65
68
|
|
|
66
69
|
const DEFAULT_STYLE: SubtitleStyle = {
|
|
67
|
-
fontSize:
|
|
68
|
-
fontFamily:
|
|
69
|
-
color:
|
|
70
|
-
backgroundColor:
|
|
71
|
-
textShadow:
|
|
72
|
-
bottom:
|
|
73
|
-
maxWidth:
|
|
74
|
-
padding:
|
|
75
|
-
borderRadius:
|
|
70
|
+
fontSize: "1.5rem",
|
|
71
|
+
fontFamily: "system-ui, -apple-system, sans-serif",
|
|
72
|
+
color: "white",
|
|
73
|
+
backgroundColor: "rgba(0, 0, 0, 0.75)",
|
|
74
|
+
textShadow: "2px 2px 4px rgba(0, 0, 0, 0.5)",
|
|
75
|
+
bottom: "5%",
|
|
76
|
+
maxWidth: "90%",
|
|
77
|
+
padding: "0.5em 1em",
|
|
78
|
+
borderRadius: "4px",
|
|
76
79
|
};
|
|
77
80
|
|
|
78
81
|
// State
|
|
79
82
|
let liveCues = $state<SubtitleCue[]>([]);
|
|
80
|
-
let displayedText = $state<string>(
|
|
83
|
+
let displayedText = $state<string>("");
|
|
81
84
|
let _lastCueId: string | null = null;
|
|
82
85
|
let unsubscribe: (() => void) | null = null;
|
|
83
86
|
|
|
@@ -89,30 +92,30 @@
|
|
|
89
92
|
|
|
90
93
|
// Parse subtitle cue from meta track event data
|
|
91
94
|
function parseSubtitleCue(data: unknown): SubtitleCue | null {
|
|
92
|
-
if (typeof data !==
|
|
95
|
+
if (typeof data !== "object" || data === null) return null;
|
|
93
96
|
|
|
94
97
|
const obj = data as Record<string, unknown>;
|
|
95
98
|
|
|
96
|
-
const text = typeof obj.text ===
|
|
99
|
+
const text = typeof obj.text === "string" ? obj.text : String(obj.text ?? "");
|
|
97
100
|
if (!text) return null;
|
|
98
101
|
|
|
99
102
|
let startTime = 0;
|
|
100
103
|
let endTime = Infinity;
|
|
101
104
|
|
|
102
|
-
if (
|
|
103
|
-
else if (
|
|
105
|
+
if ("startTime" in obj) startTime = Number(obj.startTime);
|
|
106
|
+
else if ("start" in obj) startTime = Number(obj.start);
|
|
104
107
|
|
|
105
|
-
if (
|
|
106
|
-
else if (
|
|
108
|
+
if ("endTime" in obj) endTime = Number(obj.endTime);
|
|
109
|
+
else if ("end" in obj) endTime = Number(obj.end);
|
|
107
110
|
|
|
108
|
-
const id = typeof obj.id ===
|
|
111
|
+
const id = typeof obj.id === "string" ? obj.id : String(Date.now());
|
|
109
112
|
|
|
110
113
|
return {
|
|
111
114
|
id,
|
|
112
115
|
text,
|
|
113
116
|
startTime,
|
|
114
117
|
endTime,
|
|
115
|
-
lang: typeof obj.lang ===
|
|
118
|
+
lang: typeof obj.lang === "string" ? obj.lang : undefined,
|
|
116
119
|
};
|
|
117
120
|
}
|
|
118
121
|
|
|
@@ -127,11 +130,11 @@
|
|
|
127
130
|
}
|
|
128
131
|
|
|
129
132
|
const handleMetaEvent = (event: MetaTrackEvent) => {
|
|
130
|
-
if (event.type ===
|
|
133
|
+
if (event.type === "subtitle") {
|
|
131
134
|
const cue = parseSubtitleCue(event.data);
|
|
132
135
|
if (cue) {
|
|
133
136
|
// Deduplicate by ID
|
|
134
|
-
const existing = liveCues.find(c => c.id === cue.id);
|
|
137
|
+
const existing = liveCues.find((c) => c.id === cue.id);
|
|
135
138
|
if (!existing) {
|
|
136
139
|
// Keep last 50 cues max
|
|
137
140
|
liveCues = [...liveCues, cue].slice(-50);
|
|
@@ -153,12 +156,12 @@
|
|
|
153
156
|
// Find active cue based on current time
|
|
154
157
|
$effect(() => {
|
|
155
158
|
if (!enabled) {
|
|
156
|
-
displayedText =
|
|
159
|
+
displayedText = "";
|
|
157
160
|
return;
|
|
158
161
|
}
|
|
159
162
|
|
|
160
163
|
const currentTimeMs = currentTime * 1000;
|
|
161
|
-
const activeCue = allCues.find(cue => {
|
|
164
|
+
const activeCue = allCues.find((cue) => {
|
|
162
165
|
const start = cue.startTime;
|
|
163
166
|
const end = cue.endTime;
|
|
164
167
|
return currentTimeMs >= start && currentTimeMs < end;
|
|
@@ -168,7 +171,7 @@
|
|
|
168
171
|
displayedText = activeCue.text;
|
|
169
172
|
_lastCueId = activeCue.id;
|
|
170
173
|
} else {
|
|
171
|
-
displayedText =
|
|
174
|
+
displayedText = "";
|
|
172
175
|
_lastCueId = null;
|
|
173
176
|
}
|
|
174
177
|
});
|
|
@@ -177,7 +180,7 @@
|
|
|
177
180
|
$effect(() => {
|
|
178
181
|
const currentTimeMs = currentTime * 1000;
|
|
179
182
|
|
|
180
|
-
liveCues = liveCues.filter(cue => {
|
|
183
|
+
liveCues = liveCues.filter((cue) => {
|
|
181
184
|
const endTime = cue.endTime === Infinity ? cue.startTime + 10000 : cue.endTime;
|
|
182
185
|
return endTime >= currentTimeMs - 30000;
|
|
183
186
|
});
|
|
@@ -192,22 +195,6 @@
|
|
|
192
195
|
});
|
|
193
196
|
</script>
|
|
194
197
|
|
|
195
|
-
<style>
|
|
196
|
-
.fw-player-root .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
|
-
.fw-player-root .subtitle-text {
|
|
206
|
-
display: inline-block;
|
|
207
|
-
white-space: pre-wrap;
|
|
208
|
-
}
|
|
209
|
-
</style>
|
|
210
|
-
|
|
211
198
|
{#if enabled && displayedText}
|
|
212
199
|
<div
|
|
213
200
|
class="subtitle-container {className}"
|
|
@@ -232,3 +219,19 @@
|
|
|
232
219
|
</span>
|
|
233
220
|
</div>
|
|
234
221
|
{/if}
|
|
222
|
+
|
|
223
|
+
<style>
|
|
224
|
+
.fw-player-root .subtitle-container {
|
|
225
|
+
position: absolute;
|
|
226
|
+
left: 50%;
|
|
227
|
+
transform: translateX(-50%);
|
|
228
|
+
z-index: 30;
|
|
229
|
+
text-align: center;
|
|
230
|
+
pointer-events: none;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.fw-player-root .subtitle-text {
|
|
234
|
+
display: inline-block;
|
|
235
|
+
white-space: pre-wrap;
|
|
236
|
+
}
|
|
237
|
+
</style>
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Port of src/components/ThumbnailOverlay.tsx
|
|
4
4
|
-->
|
|
5
5
|
<script lang="ts">
|
|
6
|
-
import { cn } from
|
|
6
|
+
import { cn } from "@livepeer-frameworks/player-core";
|
|
7
7
|
|
|
8
8
|
interface Props {
|
|
9
9
|
thumbnailUrl?: string | null;
|
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
onPlay = undefined,
|
|
20
20
|
message = null,
|
|
21
21
|
showUnmuteMessage = false,
|
|
22
|
-
style =
|
|
23
|
-
class: className =
|
|
22
|
+
style = "",
|
|
23
|
+
class: className = "",
|
|
24
24
|
}: Props = $props();
|
|
25
25
|
|
|
26
26
|
function handleClick() {
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
function handleKeyDown(event: KeyboardEvent) {
|
|
31
|
-
if (event.key ===
|
|
31
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
32
32
|
event.preventDefault();
|
|
33
33
|
handleClick();
|
|
34
34
|
}
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
onkeydown={handleKeyDown}
|
|
43
43
|
{style}
|
|
44
44
|
class={cn(
|
|
45
|
-
|
|
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
46
|
className
|
|
47
47
|
)}
|
|
48
48
|
>
|
|
@@ -55,15 +55,21 @@
|
|
|
55
55
|
|
|
56
56
|
<div
|
|
57
57
|
class={cn(
|
|
58
|
-
|
|
59
|
-
!thumbnailUrl &&
|
|
58
|
+
"absolute inset-0 bg-slate-950/70",
|
|
59
|
+
!thumbnailUrl && "bg-gradient-to-br from-slate-900 via-slate-950 to-slate-900"
|
|
60
60
|
)}
|
|
61
61
|
></div>
|
|
62
62
|
|
|
63
|
-
<div
|
|
63
|
+
<div
|
|
64
|
+
class="relative z-10 flex max-w-[320px] flex-col items-center gap-4 px-6 text-center text-sm sm:gap-6"
|
|
65
|
+
>
|
|
64
66
|
{#if showUnmuteMessage}
|
|
65
|
-
<div
|
|
66
|
-
|
|
67
|
+
<div
|
|
68
|
+
class="w-full rounded-lg border border-white/15 bg-black/80 p-4 text-sm text-white shadow-lg backdrop-blur"
|
|
69
|
+
>
|
|
70
|
+
<div
|
|
71
|
+
class="mb-1 flex items-center justify-center gap-2 text-base font-semibold text-primary"
|
|
72
|
+
>
|
|
67
73
|
<span aria-hidden="true">🔇</span> Click to unmute
|
|
68
74
|
</div>
|
|
69
75
|
<p class="text-xs text-white/80">Stream is playing muted — tap to enable sound.</p>
|
|
@@ -74,21 +80,18 @@
|
|
|
74
80
|
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
81
|
aria-label="Play stream"
|
|
76
82
|
>
|
|
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
|
+
<svg viewBox="0 0 24 24" fill="currentColor" class="ml-0.5 h-8 w-8" aria-hidden="true">
|
|
83
84
|
<path d="M8 5v14l11-7z" />
|
|
84
85
|
</svg>
|
|
85
86
|
</button>
|
|
86
|
-
<div
|
|
87
|
+
<div
|
|
88
|
+
class="w-full rounded-lg border border-white/10 bg-black/70 p-5 text-white shadow-inner backdrop-blur"
|
|
89
|
+
>
|
|
87
90
|
<p class="text-base font-semibold text-primary">
|
|
88
|
-
{message ??
|
|
91
|
+
{message ?? "Click to play"}
|
|
89
92
|
</p>
|
|
90
93
|
<p class="mt-1 text-xs text-white/70">
|
|
91
|
-
{message ?
|
|
94
|
+
{message ? "Start streaming instantly" : "Jump into the live feed"}
|
|
92
95
|
</p>
|
|
93
96
|
</div>
|
|
94
97
|
{/if}
|
package/dist/TitleOverlay.svelte
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Port of src/components/TitleOverlay.tsx
|
|
4
4
|
-->
|
|
5
5
|
<script lang="ts">
|
|
6
|
-
import { cn } from
|
|
6
|
+
import { cn } from "@livepeer-frameworks/player-core";
|
|
7
7
|
|
|
8
8
|
interface Props {
|
|
9
9
|
title?: string | null;
|
|
@@ -12,12 +12,7 @@
|
|
|
12
12
|
class?: string;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
let {
|
|
16
|
-
title = null,
|
|
17
|
-
description = null,
|
|
18
|
-
isVisible,
|
|
19
|
-
class: className = '',
|
|
20
|
-
}: Props = $props();
|
|
15
|
+
let { title = null, description = null, isVisible, class: className = "" }: Props = $props();
|
|
21
16
|
|
|
22
17
|
// Don't render if no content
|
|
23
18
|
let hasContent = $derived(!!title || !!description);
|
|
@@ -26,10 +21,10 @@
|
|
|
26
21
|
{#if hasContent}
|
|
27
22
|
<div
|
|
28
23
|
class={cn(
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
isVisible ?
|
|
24
|
+
"fw-title-overlay absolute inset-x-0 top-0 z-20 pointer-events-none",
|
|
25
|
+
"bg-gradient-to-b from-black/70 via-black/40 to-transparent",
|
|
26
|
+
"px-4 py-3 transition-opacity duration-300",
|
|
27
|
+
isVisible ? "opacity-100" : "opacity-0",
|
|
33
28
|
className
|
|
34
29
|
)}
|
|
35
30
|
>
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
let {
|
|
3
3
|
size = 16,
|
|
4
|
-
color: _color =
|
|
5
|
-
className =
|
|
4
|
+
color: _color = "currentColor",
|
|
5
|
+
className = "",
|
|
6
6
|
isMuted = false,
|
|
7
|
-
volume = 1 // 0-1 range
|
|
7
|
+
volume = 1, // 0-1 range
|
|
8
8
|
}: {
|
|
9
9
|
size?: number;
|
|
10
10
|
color?: string;
|
|
@@ -24,7 +24,9 @@
|
|
|
24
24
|
class={className}
|
|
25
25
|
aria-hidden="true"
|
|
26
26
|
>
|
|
27
|
-
<path
|
|
27
|
+
<path
|
|
28
|
+
d="M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z"
|
|
29
|
+
/>
|
|
28
30
|
</svg>
|
|
29
31
|
{:else if volume < 0.5}
|
|
30
32
|
<!-- Low volume icon -->
|
|
@@ -36,7 +38,9 @@
|
|
|
36
38
|
class={className}
|
|
37
39
|
aria-hidden="true"
|
|
38
40
|
>
|
|
39
|
-
<path
|
|
41
|
+
<path
|
|
42
|
+
d="M18.5 12c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM5 9v6h4l5 5V4L9 9H5z"
|
|
43
|
+
/>
|
|
40
44
|
</svg>
|
|
41
45
|
{:else}
|
|
42
46
|
<!-- High volume icon -->
|
|
@@ -48,6 +52,8 @@
|
|
|
48
52
|
class={className}
|
|
49
53
|
aria-hidden="true"
|
|
50
54
|
>
|
|
51
|
-
<path
|
|
55
|
+
<path
|
|
56
|
+
d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"
|
|
57
|
+
/>
|
|
52
58
|
</svg>
|
|
53
59
|
{/if}
|
package/dist/global.d.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
// Asset module declarations
|
|
2
|
-
declare module
|
|
2
|
+
declare module "*.svg" {
|
|
3
3
|
const content: string;
|
|
4
4
|
export default content;
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
-
declare module
|
|
7
|
+
declare module "*.png" {
|
|
8
8
|
const content: string;
|
|
9
9
|
export default content;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
declare module
|
|
12
|
+
declare module "*.jpg" {
|
|
13
13
|
const content: string;
|
|
14
14
|
export default content;
|
|
15
15
|
}
|