@livepeer-frameworks/player-svelte 0.2.1 → 0.2.5
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/README.md +4 -3
- package/dist/DevModePanel.svelte +20 -10
- package/dist/IdleScreen.svelte +5 -3
- package/dist/LoadingScreen.svelte +5 -3
- package/dist/Player.svelte +28 -16
- package/dist/PlayerControls.svelte +104 -63
- package/dist/PlayerControls.svelte.d.ts +4 -0
- package/dist/SeekBar.svelte +15 -14
- package/dist/SeekBar.svelte.d.ts +4 -4
- package/dist/StreamStateOverlay.svelte +6 -4
- package/dist/SubtitleRenderer.svelte +3 -3
- package/dist/SubtitleRenderer.svelte.d.ts +1 -1
- package/dist/controls/FullscreenButton.svelte +5 -3
- package/dist/controls/LiveBadge.svelte +5 -3
- package/dist/controls/PlayButton.svelte +5 -3
- package/dist/controls/SettingsMenu.svelte +12 -4
- package/dist/controls/SkipButton.svelte +7 -4
- package/dist/controls/VolumeControl.svelte +5 -3
- package/dist/stores/playerController.d.ts +12 -4
- package/dist/stores/playerController.js +14 -1
- package/package.json +5 -5
- package/src/DevModePanel.svelte +20 -10
- package/src/IdleScreen.svelte +5 -3
- package/src/LoadingScreen.svelte +5 -3
- package/src/Player.svelte +28 -16
- package/src/PlayerControls.svelte +104 -63
- package/src/SeekBar.svelte +15 -14
- package/src/StreamStateOverlay.svelte +6 -4
- package/src/SubtitleRenderer.svelte +3 -3
- package/src/controls/FullscreenButton.svelte +5 -3
- package/src/controls/LiveBadge.svelte +5 -3
- package/src/controls/PlayButton.svelte +5 -3
- package/src/controls/SettingsMenu.svelte +12 -4
- package/src/controls/SkipButton.svelte +7 -4
- package/src/controls/VolumeControl.svelte +5 -3
- package/src/stores/playerController.ts +29 -5
package/README.md
CHANGED
|
@@ -16,14 +16,15 @@ npm i @livepeer-frameworks/player-svelte
|
|
|
16
16
|
|
|
17
17
|
```svelte
|
|
18
18
|
<script lang="ts">
|
|
19
|
-
import { Player } from
|
|
19
|
+
import { Player } from "@livepeer-frameworks/player-svelte";
|
|
20
20
|
</script>
|
|
21
21
|
|
|
22
22
|
<div style="width: 100%; height: 500px;">
|
|
23
23
|
<Player
|
|
24
24
|
contentType="live"
|
|
25
|
-
contentId="pk_..."
|
|
26
|
-
|
|
25
|
+
contentId="pk_..."
|
|
26
|
+
// playbackId
|
|
27
|
+
options={{ gatewayUrl: "https://your-bridge/graphql" }}
|
|
27
28
|
/>
|
|
28
29
|
</div>
|
|
29
30
|
```
|
package/dist/DevModePanel.svelte
CHANGED
|
@@ -375,7 +375,9 @@
|
|
|
375
375
|
{:else}
|
|
376
376
|
{#each allCombinations as combo, index}
|
|
377
377
|
{@const isCodecIncompat = (combo as any).codecIncompatible === true}
|
|
378
|
-
{@const
|
|
378
|
+
{@const isPartial = ((combo as any).missingTracks?.length ?? 0) > 0}
|
|
379
|
+
{@const isWarn = isCodecIncompat || isPartial}
|
|
380
|
+
{@const shouldShow = combo.compatible || isWarn || showDisabledPlayers}
|
|
379
381
|
{@const isActive = activeComboIndex === index}
|
|
380
382
|
{@const typeLabel =
|
|
381
383
|
SOURCE_TYPE_LABELS[combo.sourceType] || combo.sourceType.split("/").pop()}
|
|
@@ -396,8 +398,8 @@
|
|
|
396
398
|
class={cn(
|
|
397
399
|
"fw-dev-combo-btn",
|
|
398
400
|
isActive && "fw-dev-combo-btn--active",
|
|
399
|
-
!combo.compatible && !
|
|
400
|
-
|
|
401
|
+
!combo.compatible && !isWarn && "fw-dev-combo-btn--disabled",
|
|
402
|
+
isWarn && "fw-dev-combo-btn--codec-warn"
|
|
401
403
|
)}
|
|
402
404
|
>
|
|
403
405
|
<!-- Rank -->
|
|
@@ -406,14 +408,14 @@
|
|
|
406
408
|
"fw-dev-combo-rank",
|
|
407
409
|
isActive
|
|
408
410
|
? "fw-dev-combo-rank--active"
|
|
409
|
-
: !combo.compatible && !
|
|
411
|
+
: !combo.compatible && !isWarn
|
|
410
412
|
? "fw-dev-combo-rank--disabled"
|
|
411
|
-
:
|
|
413
|
+
: isWarn
|
|
412
414
|
? "fw-dev-combo-rank--warn"
|
|
413
415
|
: ""
|
|
414
416
|
)}
|
|
415
417
|
>
|
|
416
|
-
{combo.compatible ? index + 1 :
|
|
418
|
+
{combo.compatible && !isPartial ? index + 1 : isWarn ? "⚠" : "—"}
|
|
417
419
|
</span>
|
|
418
420
|
<!-- Player + Protocol -->
|
|
419
421
|
<span class="fw-dev-combo-name">
|
|
@@ -422,8 +424,8 @@
|
|
|
422
424
|
<span
|
|
423
425
|
class={cn(
|
|
424
426
|
"fw-dev-combo-type",
|
|
425
|
-
|
|
426
|
-
!combo.compatible && !
|
|
427
|
+
isWarn && "fw-dev-combo-type--warn",
|
|
428
|
+
!combo.compatible && !isWarn && "fw-dev-combo-type--disabled"
|
|
427
429
|
)}>{typeLabel}</span
|
|
428
430
|
>
|
|
429
431
|
</span>
|
|
@@ -431,9 +433,9 @@
|
|
|
431
433
|
<span
|
|
432
434
|
class={cn(
|
|
433
435
|
"fw-dev-combo-score",
|
|
434
|
-
!combo.compatible && !
|
|
436
|
+
!combo.compatible && !isWarn
|
|
435
437
|
? "fw-dev-combo-score--disabled"
|
|
436
|
-
:
|
|
438
|
+
: isWarn
|
|
437
439
|
? "fw-dev-combo-score--low"
|
|
438
440
|
: combo.score >= 2
|
|
439
441
|
? "fw-dev-combo-score--high"
|
|
@@ -463,6 +465,14 @@
|
|
|
463
465
|
</div>
|
|
464
466
|
{/if}
|
|
465
467
|
</div>
|
|
468
|
+
{#if combo.note}
|
|
469
|
+
<div class="fw-dev-tooltip-note">{combo.note}</div>
|
|
470
|
+
{/if}
|
|
471
|
+
{#if isPartial}
|
|
472
|
+
<div class="fw-dev-tooltip-note">
|
|
473
|
+
No compatible {(combo as any).missingTracks.join(", ")} codec
|
|
474
|
+
</div>
|
|
475
|
+
{/if}
|
|
466
476
|
{#if combo.compatible && combo.scoreBreakdown}
|
|
467
477
|
<div class="fw-dev-tooltip-score">Score: {combo.score.toFixed(2)}</div>
|
|
468
478
|
<div class="fw-dev-tooltip-row">
|
package/dist/IdleScreen.svelte
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
-->
|
|
15
15
|
<script lang="ts">
|
|
16
16
|
import { onMount, onDestroy, getContext } from "svelte";
|
|
17
|
+
import { readable } from "svelte/store";
|
|
17
18
|
import type { Readable } from "svelte/store";
|
|
18
19
|
import { createTranslator, type TranslateFn } from "@livepeer-frameworks/player-core";
|
|
19
20
|
import type { StreamStatus } from "@livepeer-frameworks/player-core";
|
|
@@ -28,9 +29,10 @@
|
|
|
28
29
|
onRetry?: () => void;
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
const translatorStore: Readable<TranslateFn> =
|
|
33
|
+
getContext<Readable<TranslateFn> | undefined>("fw-translator") ??
|
|
34
|
+
readable(createTranslator({ locale: "en" }));
|
|
35
|
+
let t: TranslateFn = $derived($translatorStore);
|
|
34
36
|
|
|
35
37
|
let {
|
|
36
38
|
status = "OFFLINE",
|
|
@@ -13,14 +13,16 @@
|
|
|
13
13
|
-->
|
|
14
14
|
<script lang="ts">
|
|
15
15
|
import { onMount, onDestroy, getContext } from "svelte";
|
|
16
|
+
import { readable } from "svelte/store";
|
|
16
17
|
import type { Readable } from "svelte/store";
|
|
17
18
|
import { createTranslator, type TranslateFn } from "@livepeer-frameworks/player-core";
|
|
18
19
|
import DvdLogo from "./DvdLogo.svelte";
|
|
19
20
|
import logomarkAsset from "./assets/logomark.svg";
|
|
20
21
|
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
const translatorStore: Readable<TranslateFn> =
|
|
23
|
+
getContext<Readable<TranslateFn> | undefined>("fw-translator") ??
|
|
24
|
+
readable(createTranslator({ locale: "en" }));
|
|
25
|
+
let t: TranslateFn = $derived($translatorStore);
|
|
24
26
|
|
|
25
27
|
interface Props {
|
|
26
28
|
message?: string;
|
package/dist/Player.svelte
CHANGED
|
@@ -90,8 +90,12 @@
|
|
|
90
90
|
let isDevPanelOpen = $state(false);
|
|
91
91
|
let skipDirection: SkipDirection = $state(null);
|
|
92
92
|
|
|
93
|
-
let activeTheme = $state<FwThemePreset>(
|
|
94
|
-
let activeLocale = $state<FwLocale>(
|
|
93
|
+
let activeTheme = $state<FwThemePreset>("default");
|
|
94
|
+
let activeLocale = $state<FwLocale>("en");
|
|
95
|
+
$effect(() => {
|
|
96
|
+
if (options.theme) activeTheme = options.theme;
|
|
97
|
+
if (options.locale) activeLocale = options.locale;
|
|
98
|
+
});
|
|
95
99
|
|
|
96
100
|
// Sync locale state to i18n store and provide translator context
|
|
97
101
|
$effect(() => {
|
|
@@ -202,7 +206,7 @@
|
|
|
202
206
|
isPlaying: false,
|
|
203
207
|
isPaused: true,
|
|
204
208
|
isBuffering: false,
|
|
205
|
-
isMuted:
|
|
209
|
+
isMuted: false,
|
|
206
210
|
volume: 1,
|
|
207
211
|
error: null as string | null,
|
|
208
212
|
isPassiveError: false,
|
|
@@ -221,6 +225,10 @@
|
|
|
221
225
|
playbackQuality: null as any,
|
|
222
226
|
subtitlesEnabled: false,
|
|
223
227
|
toast: null as { message: string; timestamp: number } | null,
|
|
228
|
+
controllerSeekableStart: 0,
|
|
229
|
+
controllerLiveEdge: 0,
|
|
230
|
+
controllerCanSeek: false,
|
|
231
|
+
controllerHasAudio: true,
|
|
224
232
|
});
|
|
225
233
|
|
|
226
234
|
// Track if we've already attached to prevent double-attach race
|
|
@@ -247,7 +255,7 @@
|
|
|
247
255
|
mistUrl: options?.mistUrl,
|
|
248
256
|
authToken: options?.authToken,
|
|
249
257
|
autoplay: options?.autoplay !== false,
|
|
250
|
-
muted: options?.muted
|
|
258
|
+
muted: options?.muted === true,
|
|
251
259
|
controls: options?.stockControls === true,
|
|
252
260
|
poster: thumbnailUrl || undefined,
|
|
253
261
|
debug: options?.debug,
|
|
@@ -587,7 +595,7 @@
|
|
|
587
595
|
>
|
|
588
596
|
{$translatorStore("retry")}
|
|
589
597
|
</button>
|
|
590
|
-
{#if playerStore?.getController()?.canAttemptFallback()}
|
|
598
|
+
{#if options?.devMode && playerStore?.getController()?.canAttemptFallback()}
|
|
591
599
|
<button
|
|
592
600
|
type="button"
|
|
593
601
|
class="fw-error-btn fw-error-btn--secondary"
|
|
@@ -600,17 +608,19 @@
|
|
|
600
608
|
{$translatorStore("tryNext")}
|
|
601
609
|
</button>
|
|
602
610
|
{/if}
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
611
|
+
{#if options?.devMode}
|
|
612
|
+
<button
|
|
613
|
+
type="button"
|
|
614
|
+
class="fw-error-btn fw-error-btn--secondary"
|
|
615
|
+
onclick={() => {
|
|
616
|
+
playerStore?.clearError();
|
|
617
|
+
playerStore?.reload();
|
|
618
|
+
}}
|
|
619
|
+
aria-label={$translatorStore("reloadPlayer")}
|
|
620
|
+
>
|
|
621
|
+
{$translatorStore("reloadPlayer")}
|
|
622
|
+
</button>
|
|
623
|
+
{/if}
|
|
614
624
|
</div>
|
|
615
625
|
</div>
|
|
616
626
|
</div>
|
|
@@ -665,6 +675,8 @@
|
|
|
665
675
|
onStatsToggle={() => (isStatsOpen = !isStatsOpen)}
|
|
666
676
|
isContentLive={storeState.isEffectivelyLive}
|
|
667
677
|
onJumpToLive={() => playerStore?.getController()?.jumpToLive()}
|
|
678
|
+
controllerSeekableStart={storeState?.controllerSeekableStart ?? 0}
|
|
679
|
+
controllerLiveEdge={storeState?.controllerLiveEdge ?? 0}
|
|
668
680
|
{activeLocale}
|
|
669
681
|
onLocaleChange={(l) => {
|
|
670
682
|
activeLocale = l;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { getContext } from "svelte";
|
|
3
|
+
import { readable } from "svelte/store";
|
|
3
4
|
import type { Readable } from "svelte/store";
|
|
4
5
|
import {
|
|
5
6
|
cn,
|
|
@@ -39,9 +40,10 @@
|
|
|
39
40
|
} from "./icons";
|
|
40
41
|
|
|
41
42
|
// i18n: get translator from context, fall back to default English
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
const translatorStore: Readable<TranslateFn> =
|
|
44
|
+
getContext<Readable<TranslateFn> | undefined>("fw-translator") ??
|
|
45
|
+
readable(createTranslator({ locale: "en" }));
|
|
46
|
+
let t: TranslateFn = $derived($translatorStore);
|
|
45
47
|
|
|
46
48
|
// Props - aligned with React PlayerControls
|
|
47
49
|
interface Props {
|
|
@@ -63,6 +65,10 @@
|
|
|
63
65
|
onJumpToLive?: () => void;
|
|
64
66
|
activeLocale?: FwLocale;
|
|
65
67
|
onLocaleChange?: (locale: FwLocale) => void;
|
|
68
|
+
/** Controller-derived seekable start (ms) — preferred over player direct */
|
|
69
|
+
controllerSeekableStart?: number;
|
|
70
|
+
/** Controller-derived live edge (ms) — preferred over player direct */
|
|
71
|
+
controllerLiveEdge?: number;
|
|
66
72
|
}
|
|
67
73
|
|
|
68
74
|
let {
|
|
@@ -82,6 +88,8 @@
|
|
|
82
88
|
onJumpToLive = undefined,
|
|
83
89
|
activeLocale = undefined,
|
|
84
90
|
onLocaleChange = undefined,
|
|
91
|
+
controllerSeekableStart = undefined,
|
|
92
|
+
controllerLiveEdge = undefined,
|
|
85
93
|
}: Props = $props();
|
|
86
94
|
|
|
87
95
|
// Video element discovery
|
|
@@ -141,40 +149,77 @@
|
|
|
141
149
|
|
|
142
150
|
// Audio detection: trust MistServer metadata first, then DOM fallback
|
|
143
151
|
$effect(() => {
|
|
144
|
-
// Primary: trust MistServer stream metadata (matches ddvtech embed approach)
|
|
145
|
-
if (mistStreamInfo?.hasAudio !== undefined) {
|
|
146
|
-
hasAudio = mistStreamInfo.hasAudio;
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
152
|
if (!video) {
|
|
151
153
|
hasAudio = true;
|
|
152
154
|
return;
|
|
153
155
|
}
|
|
156
|
+
const currentVideo = video;
|
|
157
|
+
|
|
158
|
+
let boundStream: MediaStream | null = null;
|
|
159
|
+
let streamListener: (() => void) | null = null;
|
|
160
|
+
|
|
161
|
+
const unbindStream = () => {
|
|
162
|
+
if (boundStream && streamListener) {
|
|
163
|
+
boundStream.removeEventListener("addtrack", streamListener);
|
|
164
|
+
boundStream.removeEventListener("removetrack", streamListener);
|
|
165
|
+
}
|
|
166
|
+
boundStream = null;
|
|
167
|
+
streamListener = null;
|
|
168
|
+
};
|
|
154
169
|
|
|
155
170
|
const checkAudio = () => {
|
|
156
|
-
|
|
157
|
-
|
|
171
|
+
// Prefer actual MediaStream tracks over metadata to avoid phantom controls.
|
|
172
|
+
if (currentVideo.srcObject instanceof MediaStream) {
|
|
173
|
+
const stream = currentVideo.srcObject;
|
|
174
|
+
if (stream !== boundStream) {
|
|
175
|
+
unbindStream();
|
|
176
|
+
boundStream = stream;
|
|
177
|
+
streamListener = () => {
|
|
178
|
+
hasAudio = stream.getAudioTracks().length > 0;
|
|
179
|
+
};
|
|
180
|
+
stream.addEventListener("addtrack", streamListener);
|
|
181
|
+
stream.addEventListener("removetrack", streamListener);
|
|
182
|
+
}
|
|
183
|
+
hasAudio = stream.getAudioTracks().length > 0;
|
|
158
184
|
return;
|
|
159
185
|
}
|
|
160
|
-
|
|
186
|
+
|
|
187
|
+
unbindStream();
|
|
188
|
+
|
|
189
|
+
// Fallback: metadata for non-MediaStream sources.
|
|
190
|
+
if (mistStreamInfo?.hasAudio !== undefined) {
|
|
191
|
+
hasAudio = mistStreamInfo.hasAudio;
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const videoAny = currentVideo as any;
|
|
161
196
|
if (videoAny.audioTracks && videoAny.audioTracks.length !== undefined) {
|
|
162
197
|
hasAudio = videoAny.audioTracks.length > 0;
|
|
163
198
|
return;
|
|
164
199
|
}
|
|
200
|
+
|
|
165
201
|
hasAudio = true;
|
|
166
202
|
};
|
|
203
|
+
|
|
167
204
|
checkAudio();
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
205
|
+
currentVideo.addEventListener("loadedmetadata", checkAudio);
|
|
206
|
+
currentVideo.addEventListener("loadeddata", checkAudio);
|
|
207
|
+
currentVideo.addEventListener("durationchange", checkAudio);
|
|
208
|
+
|
|
209
|
+
const audioTracks = (currentVideo as any).audioTracks;
|
|
171
210
|
if (audioTracks?.addEventListener) {
|
|
172
211
|
audioTracks.addEventListener("addtrack", checkAudio);
|
|
212
|
+
audioTracks.addEventListener("removetrack", checkAudio);
|
|
173
213
|
}
|
|
214
|
+
|
|
174
215
|
return () => {
|
|
175
|
-
|
|
216
|
+
unbindStream();
|
|
217
|
+
currentVideo.removeEventListener("loadedmetadata", checkAudio);
|
|
218
|
+
currentVideo.removeEventListener("loadeddata", checkAudio);
|
|
219
|
+
currentVideo.removeEventListener("durationchange", checkAudio);
|
|
176
220
|
if (audioTracks?.removeEventListener) {
|
|
177
221
|
audioTracks.removeEventListener("addtrack", checkAudio);
|
|
222
|
+
audioTracks.removeEventListener("removetrack", checkAudio);
|
|
178
223
|
}
|
|
179
224
|
};
|
|
180
225
|
});
|
|
@@ -224,16 +269,22 @@
|
|
|
224
269
|
let isWebRTC = $derived(isMediaStreamSource(video));
|
|
225
270
|
let supportsPlaybackRate = $derived(coreSupportsPlaybackRate(video));
|
|
226
271
|
function deriveBufferWindowMs(
|
|
227
|
-
tracks?: Record<string, { firstms?: number; lastms?: number }>
|
|
272
|
+
tracks?: Record<string, { type?: string; firstms?: number; lastms?: number }>
|
|
228
273
|
): number | undefined {
|
|
229
274
|
if (!tracks) return undefined;
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
275
|
+
const trackValues = Object.values(tracks).filter(
|
|
276
|
+
(t) => t.type !== "meta" && (t.lastms === undefined || t.lastms > 0)
|
|
277
|
+
);
|
|
278
|
+
if (trackValues.length === 0) return undefined;
|
|
279
|
+
const firstmsValues = trackValues
|
|
280
|
+
.map((t) => t.firstms)
|
|
281
|
+
.filter((v): v is number => v !== undefined);
|
|
282
|
+
const lastmsValues = trackValues
|
|
283
|
+
.map((t) => t.lastms)
|
|
284
|
+
.filter((v): v is number => v !== undefined);
|
|
234
285
|
if (firstmsValues.length === 0 || lastmsValues.length === 0) return undefined;
|
|
235
|
-
const firstms = Math.
|
|
236
|
-
const lastms = Math.
|
|
286
|
+
const firstms = Math.min(...firstmsValues);
|
|
287
|
+
const lastms = Math.max(...lastmsValues);
|
|
237
288
|
const window = lastms - firstms;
|
|
238
289
|
if (!Number.isFinite(window) || window <= 0) return undefined;
|
|
239
290
|
return window;
|
|
@@ -243,48 +294,38 @@
|
|
|
243
294
|
mistStreamInfo?.meta?.buffer_window ??
|
|
244
295
|
deriveBufferWindowMs(
|
|
245
296
|
mistStreamInfo?.meta?.tracks as
|
|
246
|
-
| Record<string, { firstms?: number; lastms?: number }>
|
|
297
|
+
| Record<string, { type?: string; firstms?: number; lastms?: number }>
|
|
247
298
|
| undefined
|
|
248
299
|
)
|
|
249
300
|
);
|
|
250
301
|
|
|
251
|
-
function getPlayerSeekableRange(): { seekableStart: number; liveEdge: number } | null {
|
|
252
|
-
const player = globalPlayerManager.getCurrentPlayer();
|
|
253
|
-
if (player && typeof (player as any).getSeekableRange === "function") {
|
|
254
|
-
const range = (player as any).getSeekableRange();
|
|
255
|
-
if (
|
|
256
|
-
range &&
|
|
257
|
-
Number.isFinite(range.start) &&
|
|
258
|
-
Number.isFinite(range.end) &&
|
|
259
|
-
range.end >= range.start
|
|
260
|
-
) {
|
|
261
|
-
return { seekableStart: range.start, liveEdge: range.end };
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
return null;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
302
|
let allowMediaStreamDvr = $derived(
|
|
268
|
-
isMediaStreamSource(video) &&
|
|
269
|
-
bufferWindowMs !== undefined &&
|
|
270
|
-
bufferWindowMs > 0 &&
|
|
271
|
-
sourceType !== "whep" &&
|
|
272
|
-
sourceType !== "webrtc"
|
|
303
|
+
isMediaStreamSource(video) && bufferWindowMs !== undefined && bufferWindowMs > 0
|
|
273
304
|
);
|
|
274
305
|
|
|
275
|
-
// Seekable range
|
|
276
|
-
let
|
|
277
|
-
(
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
306
|
+
// Seekable range: prefer controller-derived values (same pattern as React/WC)
|
|
307
|
+
let calcRange = $derived(
|
|
308
|
+
calculateSeekableRange({
|
|
309
|
+
isLive,
|
|
310
|
+
video,
|
|
311
|
+
mistStreamInfo,
|
|
312
|
+
currentTime,
|
|
313
|
+
duration,
|
|
314
|
+
allowMediaStreamDvr,
|
|
315
|
+
})
|
|
316
|
+
);
|
|
317
|
+
let useControllerRange = $derived(
|
|
318
|
+
Number.isFinite(controllerSeekableStart) &&
|
|
319
|
+
Number.isFinite(controllerLiveEdge) &&
|
|
320
|
+
(controllerLiveEdge as number) >= (controllerSeekableStart as number) &&
|
|
321
|
+
((controllerLiveEdge as number) > 0 || (controllerSeekableStart as number) > 0)
|
|
287
322
|
);
|
|
323
|
+
let seekableRange = $derived({
|
|
324
|
+
seekableStart: useControllerRange
|
|
325
|
+
? (controllerSeekableStart as number)
|
|
326
|
+
: calcRange.seekableStart,
|
|
327
|
+
liveEdge: useControllerRange ? (controllerLiveEdge as number) : calcRange.liveEdge,
|
|
328
|
+
});
|
|
288
329
|
let seekableStart = $derived(seekableRange.seekableStart);
|
|
289
330
|
let liveEdge = $derived(seekableRange.liveEdge);
|
|
290
331
|
let hasDvrWindow = $derived(
|
|
@@ -422,24 +463,24 @@
|
|
|
422
463
|
}
|
|
423
464
|
|
|
424
465
|
function handleSkipBack() {
|
|
425
|
-
const newTime = Math.max(0, currentTime -
|
|
466
|
+
const newTime = Math.max(0, currentTime - 10000);
|
|
426
467
|
if (onseek) {
|
|
427
468
|
onseek(newTime);
|
|
428
469
|
return;
|
|
429
470
|
}
|
|
430
471
|
const v = findVideoElement();
|
|
431
|
-
if (v) v.currentTime = newTime;
|
|
472
|
+
if (v) v.currentTime = newTime / 1000;
|
|
432
473
|
}
|
|
433
474
|
|
|
434
475
|
function handleSkipForward() {
|
|
435
|
-
const maxTime = Number.isFinite(duration) ? duration : currentTime +
|
|
436
|
-
const newTime = Math.min(maxTime, currentTime +
|
|
476
|
+
const maxTime = Number.isFinite(duration) ? duration : currentTime + 10000;
|
|
477
|
+
const newTime = Math.min(maxTime, currentTime + 10000);
|
|
437
478
|
if (onseek) {
|
|
438
479
|
onseek(newTime);
|
|
439
480
|
return;
|
|
440
481
|
}
|
|
441
482
|
const v = findVideoElement();
|
|
442
|
-
if (v) v.currentTime = newTime;
|
|
483
|
+
if (v) v.currentTime = newTime / 1000;
|
|
443
484
|
}
|
|
444
485
|
|
|
445
486
|
function handleMute() {
|
|
@@ -592,7 +633,7 @@
|
|
|
592
633
|
if (onseek) {
|
|
593
634
|
onseek(time);
|
|
594
635
|
} else if (video) {
|
|
595
|
-
video.currentTime = time;
|
|
636
|
+
video.currentTime = time / 1000;
|
|
596
637
|
}
|
|
597
638
|
}}
|
|
598
639
|
/>
|
|
@@ -19,6 +19,10 @@ interface Props {
|
|
|
19
19
|
onJumpToLive?: () => void;
|
|
20
20
|
activeLocale?: FwLocale;
|
|
21
21
|
onLocaleChange?: (locale: FwLocale) => void;
|
|
22
|
+
/** Controller-derived seekable start (ms) — preferred over player direct */
|
|
23
|
+
controllerSeekableStart?: number;
|
|
24
|
+
/** Controller-derived live edge (ms) — preferred over player direct */
|
|
25
|
+
controllerLiveEdge?: number;
|
|
22
26
|
}
|
|
23
27
|
declare const PlayerControls: import("svelte").Component<Props, {}, "">;
|
|
24
28
|
type PlayerControls = ReturnType<typeof PlayerControls>;
|
package/dist/SeekBar.svelte
CHANGED
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
import { cn } from "@livepeer-frameworks/player-core";
|
|
7
7
|
|
|
8
8
|
interface Props {
|
|
9
|
-
/** Current playback time in
|
|
9
|
+
/** Current playback time in milliseconds */
|
|
10
10
|
currentTime: number;
|
|
11
|
-
/** Total duration in
|
|
11
|
+
/** Total duration in milliseconds */
|
|
12
12
|
duration: number;
|
|
13
13
|
/** Buffered time ranges from video element */
|
|
14
14
|
buffered?: TimeRanges;
|
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
class?: string;
|
|
21
21
|
/** Whether this is a live stream */
|
|
22
22
|
isLive?: boolean;
|
|
23
|
-
/** For live: start of seekable DVR window (
|
|
23
|
+
/** For live: start of seekable DVR window (ms) */
|
|
24
24
|
seekableStart?: number;
|
|
25
|
-
/** For live: current live edge position (
|
|
25
|
+
/** For live: current live edge position (ms) */
|
|
26
26
|
liveEdge?: number;
|
|
27
27
|
/** Defer seeking until drag release */
|
|
28
28
|
commitOnRelease?: boolean;
|
|
@@ -80,8 +80,9 @@
|
|
|
80
80
|
|
|
81
81
|
const segments: Array<{ startPercent: number; endPercent: number }> = [];
|
|
82
82
|
for (let i = 0; i < buffered.length; i++) {
|
|
83
|
-
|
|
84
|
-
const
|
|
83
|
+
// buffered TimeRanges are in seconds (browser API), convert to ms
|
|
84
|
+
const start = buffered.start(i) * 1000;
|
|
85
|
+
const end = buffered.end(i) * 1000;
|
|
85
86
|
|
|
86
87
|
const relativeStart = start - rangeStart;
|
|
87
88
|
const relativeEnd = end - rangeStart;
|
|
@@ -95,9 +96,9 @@
|
|
|
95
96
|
});
|
|
96
97
|
|
|
97
98
|
// Format time as MM:SS or HH:MM:SS
|
|
98
|
-
function formatTime(
|
|
99
|
-
if (!Number.isFinite(
|
|
100
|
-
const total = Math.floor(
|
|
99
|
+
function formatTime(ms: number): string {
|
|
100
|
+
if (!Number.isFinite(ms) || ms < 0) return "0:00";
|
|
101
|
+
const total = Math.floor(ms / 1000);
|
|
101
102
|
const hours = Math.floor(total / 3600);
|
|
102
103
|
const minutes = Math.floor((total % 3600) / 60);
|
|
103
104
|
const secs = total % 60;
|
|
@@ -108,10 +109,10 @@
|
|
|
108
109
|
}
|
|
109
110
|
|
|
110
111
|
// Format relative time for live streams
|
|
111
|
-
function formatLiveTime(
|
|
112
|
-
const
|
|
113
|
-
if (
|
|
114
|
-
const total = Math.floor(
|
|
112
|
+
function formatLiveTime(ms: number, edgeMs: number): string {
|
|
113
|
+
const behindMs = edgeMs - ms;
|
|
114
|
+
if (behindMs < 1000) return "LIVE";
|
|
115
|
+
const total = Math.floor(behindMs / 1000);
|
|
115
116
|
const hours = Math.floor(total / 3600);
|
|
116
117
|
const minutes = Math.floor((total % 3600) / 60);
|
|
117
118
|
const secs = total % 60;
|
|
@@ -218,7 +219,7 @@
|
|
|
218
219
|
// Handle keyboard navigation for accessibility
|
|
219
220
|
function handleKeyDown(e: KeyboardEvent) {
|
|
220
221
|
if (disabled) return;
|
|
221
|
-
const step = e.shiftKey ?
|
|
222
|
+
const step = e.shiftKey ? 10000 : 5000;
|
|
222
223
|
const rangeEnd = isLive ? effectiveLiveEdge : duration;
|
|
223
224
|
const rangeStart = isLive ? seekableStart : 0;
|
|
224
225
|
|
package/dist/SeekBar.svelte.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
interface Props {
|
|
2
|
-
/** Current playback time in
|
|
2
|
+
/** Current playback time in milliseconds */
|
|
3
3
|
currentTime: number;
|
|
4
|
-
/** Total duration in
|
|
4
|
+
/** Total duration in milliseconds */
|
|
5
5
|
duration: number;
|
|
6
6
|
/** Buffered time ranges from video element */
|
|
7
7
|
buffered?: TimeRanges;
|
|
@@ -13,9 +13,9 @@ interface Props {
|
|
|
13
13
|
class?: string;
|
|
14
14
|
/** Whether this is a live stream */
|
|
15
15
|
isLive?: boolean;
|
|
16
|
-
/** For live: start of seekable DVR window (
|
|
16
|
+
/** For live: start of seekable DVR window (ms) */
|
|
17
17
|
seekableStart?: number;
|
|
18
|
-
/** For live: current live edge position (
|
|
18
|
+
/** For live: current live edge position (ms) */
|
|
19
19
|
liveEdge?: number;
|
|
20
20
|
/** Defer seeking until drag release */
|
|
21
21
|
commitOnRelease?: boolean;
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
-->
|
|
12
12
|
<script lang="ts">
|
|
13
13
|
import { getContext } from "svelte";
|
|
14
|
+
import { readable } from "svelte/store";
|
|
14
15
|
import type { Readable } from "svelte/store";
|
|
15
16
|
import { createTranslator, type TranslateFn } from "@livepeer-frameworks/player-core";
|
|
16
17
|
import type { StreamStatus } from "@livepeer-frameworks/player-core";
|
|
@@ -39,9 +40,10 @@
|
|
|
39
40
|
class: className = "",
|
|
40
41
|
}: Props = $props();
|
|
41
42
|
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
const translatorStore: Readable<TranslateFn> =
|
|
44
|
+
getContext<Readable<TranslateFn> | undefined>("fw-translator") ??
|
|
45
|
+
readable(createTranslator({ locale: "en" }));
|
|
46
|
+
let t: TranslateFn = $derived($translatorStore);
|
|
45
47
|
|
|
46
48
|
// Computed states
|
|
47
49
|
let showRetry = $derived(status === "ERROR" || status === "INVALID" || status === "OFFLINE");
|
|
@@ -136,7 +138,7 @@
|
|
|
136
138
|
{#if showProgress && percentage !== undefined}
|
|
137
139
|
<div style="margin-top: 0.75rem;">
|
|
138
140
|
<div class="progress-bar">
|
|
139
|
-
<div class="progress-fill" style="width: {Math.min(100, percentage)}
|
|
141
|
+
<div class="progress-fill" style="width: {Math.min(100, percentage)}%;"></div>
|
|
140
142
|
</div>
|
|
141
143
|
<p
|
|
142
144
|
style="margin-top: 0.375rem; font-size: 0.75rem; font-family: monospace; color: hsl(var(--tn-fg-dark, 233 23% 60%));"
|