@livepeer-frameworks/player-svelte 0.2.1 → 0.2.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 (35) hide show
  1. package/dist/DevModePanel.svelte +20 -10
  2. package/dist/IdleScreen.svelte +5 -3
  3. package/dist/LoadingScreen.svelte +5 -3
  4. package/dist/Player.svelte +18 -14
  5. package/dist/PlayerControls.svelte +53 -45
  6. package/dist/PlayerControls.svelte.d.ts +4 -0
  7. package/dist/SeekBar.svelte +15 -14
  8. package/dist/SeekBar.svelte.d.ts +4 -4
  9. package/dist/StreamStateOverlay.svelte +6 -4
  10. package/dist/SubtitleRenderer.svelte +3 -3
  11. package/dist/SubtitleRenderer.svelte.d.ts +1 -1
  12. package/dist/controls/FullscreenButton.svelte +5 -3
  13. package/dist/controls/LiveBadge.svelte +5 -3
  14. package/dist/controls/PlayButton.svelte +5 -3
  15. package/dist/controls/SettingsMenu.svelte +5 -3
  16. package/dist/controls/SkipButton.svelte +7 -4
  17. package/dist/controls/VolumeControl.svelte +5 -3
  18. package/dist/stores/playerController.d.ts +4 -4
  19. package/dist/stores/playerController.js +1 -1
  20. package/package.json +2 -2
  21. package/src/DevModePanel.svelte +20 -10
  22. package/src/IdleScreen.svelte +5 -3
  23. package/src/LoadingScreen.svelte +5 -3
  24. package/src/Player.svelte +18 -14
  25. package/src/PlayerControls.svelte +53 -45
  26. package/src/SeekBar.svelte +15 -14
  27. package/src/StreamStateOverlay.svelte +6 -4
  28. package/src/SubtitleRenderer.svelte +3 -3
  29. package/src/controls/FullscreenButton.svelte +5 -3
  30. package/src/controls/LiveBadge.svelte +5 -3
  31. package/src/controls/PlayButton.svelte +5 -3
  32. package/src/controls/SettingsMenu.svelte +5 -3
  33. package/src/controls/SkipButton.svelte +7 -4
  34. package/src/controls/VolumeControl.svelte +5 -3
  35. package/src/stores/playerController.ts +5 -5
@@ -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 shouldShow = combo.compatible || isCodecIncompat || showDisabledPlayers}
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 && !isCodecIncompat && "fw-dev-combo-btn--disabled",
400
- isCodecIncompat && "fw-dev-combo-btn--codec-warn"
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 && !isCodecIncompat
411
+ : !combo.compatible && !isWarn
410
412
  ? "fw-dev-combo-rank--disabled"
411
- : isCodecIncompat
413
+ : isWarn
412
414
  ? "fw-dev-combo-rank--warn"
413
415
  : ""
414
416
  )}
415
417
  >
416
- {combo.compatible ? index + 1 : isCodecIncompat ? "⚠" : "—"}
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
- isCodecIncompat && "fw-dev-combo-type--warn",
426
- !combo.compatible && !isCodecIncompat && "fw-dev-combo-type--disabled"
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 && !isCodecIncompat
436
+ !combo.compatible && !isWarn
435
437
  ? "fw-dev-combo-score--disabled"
436
- : isCodecIncompat
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">
@@ -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 translatorCtx = getContext<Readable<TranslateFn> | undefined>("fw-translator");
32
- const fallbackT = createTranslator({ locale: "en" });
33
- let t: TranslateFn = $derived(translatorCtx ? $translatorCtx : fallbackT);
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 translatorCtx = getContext<Readable<TranslateFn> | undefined>("fw-translator");
22
- const fallbackT = createTranslator({ locale: "en" });
23
- let t: TranslateFn = $derived(translatorCtx ? $translatorCtx : fallbackT);
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;
@@ -202,7 +202,7 @@
202
202
  isPlaying: false,
203
203
  isPaused: true,
204
204
  isBuffering: false,
205
- isMuted: true,
205
+ isMuted: false,
206
206
  volume: 1,
207
207
  error: null as string | null,
208
208
  isPassiveError: false,
@@ -247,7 +247,7 @@
247
247
  mistUrl: options?.mistUrl,
248
248
  authToken: options?.authToken,
249
249
  autoplay: options?.autoplay !== false,
250
- muted: options?.muted !== false,
250
+ muted: options?.muted === true,
251
251
  controls: options?.stockControls === true,
252
252
  poster: thumbnailUrl || undefined,
253
253
  debug: options?.debug,
@@ -587,7 +587,7 @@
587
587
  >
588
588
  {$translatorStore("retry")}
589
589
  </button>
590
- {#if playerStore?.getController()?.canAttemptFallback()}
590
+ {#if options?.devMode && playerStore?.getController()?.canAttemptFallback()}
591
591
  <button
592
592
  type="button"
593
593
  class="fw-error-btn fw-error-btn--secondary"
@@ -600,17 +600,19 @@
600
600
  {$translatorStore("tryNext")}
601
601
  </button>
602
602
  {/if}
603
- <button
604
- type="button"
605
- class="fw-error-btn fw-error-btn--secondary"
606
- onclick={() => {
607
- playerStore?.clearError();
608
- playerStore?.reload();
609
- }}
610
- aria-label={$translatorStore("reloadPlayer")}
611
- >
612
- {$translatorStore("reloadPlayer")}
613
- </button>
603
+ {#if options?.devMode}
604
+ <button
605
+ type="button"
606
+ class="fw-error-btn fw-error-btn--secondary"
607
+ onclick={() => {
608
+ playerStore?.clearError();
609
+ playerStore?.reload();
610
+ }}
611
+ aria-label={$translatorStore("reloadPlayer")}
612
+ >
613
+ {$translatorStore("reloadPlayer")}
614
+ </button>
615
+ {/if}
614
616
  </div>
615
617
  </div>
616
618
  </div>
@@ -665,6 +667,8 @@
665
667
  onStatsToggle={() => (isStatsOpen = !isStatsOpen)}
666
668
  isContentLive={storeState.isEffectivelyLive}
667
669
  onJumpToLive={() => playerStore?.getController()?.jumpToLive()}
670
+ controllerSeekableStart={playerStore?.getController()?.getSeekableStart()}
671
+ controllerLiveEdge={playerStore?.getController()?.getLiveEdge()}
668
672
  {activeLocale}
669
673
  onLocaleChange={(l) => {
670
674
  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 translatorCtx = getContext<Readable<TranslateFn> | undefined>("fw-translator");
43
- const fallbackT = createTranslator({ locale: "en" });
44
- let t: TranslateFn = $derived(translatorCtx ? $translatorCtx : fallbackT);
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
@@ -224,16 +232,22 @@
224
232
  let isWebRTC = $derived(isMediaStreamSource(video));
225
233
  let supportsPlaybackRate = $derived(coreSupportsPlaybackRate(video));
226
234
  function deriveBufferWindowMs(
227
- tracks?: Record<string, { firstms?: number; lastms?: number }>
235
+ tracks?: Record<string, { type?: string; firstms?: number; lastms?: number }>
228
236
  ): number | undefined {
229
237
  if (!tracks) return undefined;
230
- const list = Object.values(tracks);
231
- if (list.length === 0) return undefined;
232
- const firstmsValues = list.map((t) => t.firstms).filter((v): v is number => v !== undefined);
233
- const lastmsValues = list.map((t) => t.lastms).filter((v): v is number => v !== undefined);
238
+ const trackValues = Object.values(tracks).filter(
239
+ (t) => t.type !== "meta" && (t.lastms === undefined || t.lastms > 0)
240
+ );
241
+ if (trackValues.length === 0) return undefined;
242
+ const firstmsValues = trackValues
243
+ .map((t) => t.firstms)
244
+ .filter((v): v is number => v !== undefined);
245
+ const lastmsValues = trackValues
246
+ .map((t) => t.lastms)
247
+ .filter((v): v is number => v !== undefined);
234
248
  if (firstmsValues.length === 0 || lastmsValues.length === 0) return undefined;
235
- const firstms = Math.max(...firstmsValues);
236
- const lastms = Math.min(...lastmsValues);
249
+ const firstms = Math.min(...firstmsValues);
250
+ const lastms = Math.max(...lastmsValues);
237
251
  const window = lastms - firstms;
238
252
  if (!Number.isFinite(window) || window <= 0) return undefined;
239
253
  return window;
@@ -243,27 +257,11 @@
243
257
  mistStreamInfo?.meta?.buffer_window ??
244
258
  deriveBufferWindowMs(
245
259
  mistStreamInfo?.meta?.tracks as
246
- | Record<string, { firstms?: number; lastms?: number }>
260
+ | Record<string, { type?: string; firstms?: number; lastms?: number }>
247
261
  | undefined
248
262
  )
249
263
  );
250
264
 
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
265
  let allowMediaStreamDvr = $derived(
268
266
  isMediaStreamSource(video) &&
269
267
  bufferWindowMs !== undefined &&
@@ -272,19 +270,29 @@
272
270
  sourceType !== "webrtc"
273
271
  );
274
272
 
275
- // Seekable range using core calculation (allow player override)
276
- let seekableRange = $derived.by(
277
- () =>
278
- getPlayerSeekableRange() ??
279
- calculateSeekableRange({
280
- isLive,
281
- video,
282
- mistStreamInfo,
283
- currentTime,
284
- duration,
285
- allowMediaStreamDvr,
286
- })
273
+ // Seekable range: prefer controller-derived values (same pattern as React/WC)
274
+ let calcRange = $derived(
275
+ calculateSeekableRange({
276
+ isLive,
277
+ video,
278
+ mistStreamInfo,
279
+ currentTime,
280
+ duration,
281
+ allowMediaStreamDvr,
282
+ })
283
+ );
284
+ let useControllerRange = $derived(
285
+ Number.isFinite(controllerSeekableStart) &&
286
+ Number.isFinite(controllerLiveEdge) &&
287
+ (controllerLiveEdge as number) >= (controllerSeekableStart as number) &&
288
+ ((controllerLiveEdge as number) > 0 || (controllerSeekableStart as number) > 0)
287
289
  );
290
+ let seekableRange = $derived({
291
+ seekableStart: useControllerRange
292
+ ? (controllerSeekableStart as number)
293
+ : calcRange.seekableStart,
294
+ liveEdge: useControllerRange ? (controllerLiveEdge as number) : calcRange.liveEdge,
295
+ });
288
296
  let seekableStart = $derived(seekableRange.seekableStart);
289
297
  let liveEdge = $derived(seekableRange.liveEdge);
290
298
  let hasDvrWindow = $derived(
@@ -422,24 +430,24 @@
422
430
  }
423
431
 
424
432
  function handleSkipBack() {
425
- const newTime = Math.max(0, currentTime - 10);
433
+ const newTime = Math.max(0, currentTime - 10000);
426
434
  if (onseek) {
427
435
  onseek(newTime);
428
436
  return;
429
437
  }
430
438
  const v = findVideoElement();
431
- if (v) v.currentTime = newTime;
439
+ if (v) v.currentTime = newTime / 1000;
432
440
  }
433
441
 
434
442
  function handleSkipForward() {
435
- const maxTime = Number.isFinite(duration) ? duration : currentTime + 10;
436
- const newTime = Math.min(maxTime, currentTime + 10);
443
+ const maxTime = Number.isFinite(duration) ? duration : currentTime + 10000;
444
+ const newTime = Math.min(maxTime, currentTime + 10000);
437
445
  if (onseek) {
438
446
  onseek(newTime);
439
447
  return;
440
448
  }
441
449
  const v = findVideoElement();
442
- if (v) v.currentTime = newTime;
450
+ if (v) v.currentTime = newTime / 1000;
443
451
  }
444
452
 
445
453
  function handleMute() {
@@ -592,7 +600,7 @@
592
600
  if (onseek) {
593
601
  onseek(time);
594
602
  } else if (video) {
595
- video.currentTime = time;
603
+ video.currentTime = time / 1000;
596
604
  }
597
605
  }}
598
606
  />
@@ -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>;
@@ -6,9 +6,9 @@
6
6
  import { cn } from "@livepeer-frameworks/player-core";
7
7
 
8
8
  interface Props {
9
- /** Current playback time in seconds */
9
+ /** Current playback time in milliseconds */
10
10
  currentTime: number;
11
- /** Total duration in seconds */
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 (seconds) */
23
+ /** For live: start of seekable DVR window (ms) */
24
24
  seekableStart?: number;
25
- /** For live: current live edge position (seconds) */
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
- const start = buffered.start(i);
84
- const end = buffered.end(i);
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(seconds: number): string {
99
- if (!Number.isFinite(seconds) || seconds < 0) return "0:00";
100
- const total = Math.floor(seconds);
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(seconds: number, edge: number): string {
112
- const behindSeconds = edge - seconds;
113
- if (behindSeconds < 1) return "LIVE";
114
- const total = Math.floor(behindSeconds);
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 ? 10 : 5; // 5s default, 10s with shift
222
+ const step = e.shiftKey ? 10000 : 5000;
222
223
  const rangeEnd = isLive ? effectiveLiveEdge : duration;
223
224
  const rangeStart = isLive ? seekableStart : 0;
224
225
 
@@ -1,7 +1,7 @@
1
1
  interface Props {
2
- /** Current playback time in seconds */
2
+ /** Current playback time in milliseconds */
3
3
  currentTime: number;
4
- /** Total duration in seconds */
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 (seconds) */
16
+ /** For live: start of seekable DVR window (ms) */
17
17
  seekableStart?: number;
18
- /** For live: current live edge position (seconds) */
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 translatorCtx = getContext<Readable<TranslateFn> | undefined>("fw-translator");
43
- const fallbackT = createTranslator({ locale: "en" });
44
- let t: TranslateFn = $derived(translatorCtx ? $translatorCtx : fallbackT);
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)};"></div>
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%));"
@@ -37,7 +37,7 @@
37
37
  }
38
38
 
39
39
  interface Props {
40
- /** Current video playback time in seconds */
40
+ /** Current video playback time in milliseconds */
41
41
  currentTime: number;
42
42
  /** Whether subtitles are enabled */
43
43
  enabled?: boolean;
@@ -160,7 +160,7 @@
160
160
  return;
161
161
  }
162
162
 
163
- const currentTimeMs = currentTime * 1000;
163
+ const currentTimeMs = currentTime;
164
164
  const activeCue = allCues.find((cue) => {
165
165
  const start = cue.startTime;
166
166
  const end = cue.endTime;
@@ -178,7 +178,7 @@
178
178
 
179
179
  // Clean up expired cues
180
180
  $effect(() => {
181
- const currentTimeMs = currentTime * 1000;
181
+ const currentTimeMs = currentTime;
182
182
 
183
183
  liveCues = liveCues.filter((cue) => {
184
184
  const endTime = cue.endTime === Infinity ? cue.startTime + 10000 : cue.endTime;
@@ -21,7 +21,7 @@ interface SubtitleStyle {
21
21
  borderRadius?: string;
22
22
  }
23
23
  interface Props {
24
- /** Current video playback time in seconds */
24
+ /** Current video playback time in milliseconds */
25
25
  currentTime: number;
26
26
  /** Whether subtitles are enabled */
27
27
  enabled?: boolean;
@@ -1,13 +1,15 @@
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 { createTranslator, type TranslateFn } from "@livepeer-frameworks/player-core";
5
6
  import { FullscreenIcon, FullscreenExitIcon } from "../icons";
6
7
 
7
8
  let pc: any = getContext("fw-player-controller");
8
- const translatorCtx = getContext<Readable<TranslateFn> | undefined>("fw-translator");
9
- const fallbackT = createTranslator({ locale: "en" });
10
- let t: TranslateFn = $derived(translatorCtx ? $translatorCtx : fallbackT);
9
+ const translatorStore: Readable<TranslateFn> =
10
+ getContext<Readable<TranslateFn> | undefined>("fw-translator") ??
11
+ readable(createTranslator({ locale: "en" }));
12
+ let t: TranslateFn = $derived($translatorStore);
11
13
  </script>
12
14
 
13
15
  <button
@@ -1,13 +1,15 @@
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 { createTranslator, type TranslateFn } from "@livepeer-frameworks/player-core";
5
6
  import { SeekToLiveIcon } from "../icons";
6
7
 
7
8
  let pc: any = getContext("fw-player-controller");
8
- const translatorCtx = getContext<Readable<TranslateFn> | undefined>("fw-translator");
9
- const fallbackT = createTranslator({ locale: "en" });
10
- let t: TranslateFn = $derived(translatorCtx ? $translatorCtx : fallbackT);
9
+ const translatorStore: Readable<TranslateFn> =
10
+ getContext<Readable<TranslateFn> | undefined>("fw-translator") ??
11
+ readable(createTranslator({ locale: "en" }));
12
+ let t: TranslateFn = $derived($translatorStore);
11
13
  </script>
12
14
 
13
15
  {#if pc?.isEffectivelyLive}
@@ -1,13 +1,15 @@
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 { createTranslator, type TranslateFn } from "@livepeer-frameworks/player-core";
5
6
  import { PlayIcon, PauseIcon } from "../icons";
6
7
 
7
8
  let pc: any = getContext("fw-player-controller");
8
- const translatorCtx = getContext<Readable<TranslateFn> | undefined>("fw-translator");
9
- const fallbackT = createTranslator({ locale: "en" });
10
- let t: TranslateFn = $derived(translatorCtx ? $translatorCtx : fallbackT);
9
+ const translatorStore: Readable<TranslateFn> =
10
+ getContext<Readable<TranslateFn> | undefined>("fw-translator") ??
11
+ readable(createTranslator({ locale: "en" }));
12
+ let t: TranslateFn = $derived($translatorStore);
11
13
  </script>
12
14
 
13
15
  <button
@@ -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 { SettingsIcon } from "../icons";
5
6
  import {
@@ -48,9 +49,10 @@
48
49
  let availableLocales = getAvailableLocales();
49
50
 
50
51
  let controller: any = getContext("fw-player-controller");
51
- const translatorCtx = getContext<Readable<TranslateFn> | undefined>("fw-translator");
52
- const fallbackT = createTranslator({ locale: "en" });
53
- let t: TranslateFn = $derived(translatorCtx ? $translatorCtx : fallbackT);
52
+ const translatorStore: Readable<TranslateFn> =
53
+ getContext<Readable<TranslateFn> | undefined>("fw-translator") ??
54
+ readable(createTranslator({ locale: "en" }));
55
+ let t: TranslateFn = $derived($translatorStore);
54
56
  let isOpen = $state(false);
55
57
 
56
58
  let qualities = $derived(propQualities ?? controller?.getQualities?.() ?? []);
@@ -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 { createTranslator, type TranslateFn } from "@livepeer-frameworks/player-core";
5
6
  import { SkipBackIcon, SkipForwardIcon } from "../icons";
@@ -11,9 +12,10 @@
11
12
 
12
13
  let { direction, seconds = 10 }: Props = $props();
13
14
  let pc: any = getContext("fw-player-controller");
14
- const translatorCtx = getContext<Readable<TranslateFn> | undefined>("fw-translator");
15
- const fallbackT = createTranslator({ locale: "en" });
16
- let t: TranslateFn = $derived(translatorCtx ? $translatorCtx : fallbackT);
15
+ const translatorStore: Readable<TranslateFn> =
16
+ getContext<Readable<TranslateFn> | undefined>("fw-translator") ??
17
+ readable(createTranslator({ locale: "en" }));
18
+ let t: TranslateFn = $derived($translatorStore);
17
19
 
18
20
  let label = $derived(direction === "back" ? t("skipBackward") : t("skipForward"));
19
21
  </script>
@@ -23,7 +25,8 @@
23
25
  class="fw-btn-flush"
24
26
  aria-label={label}
25
27
  title={label}
26
- onclick={() => pc?.seek((pc?.currentTime ?? 0) + (direction === "back" ? -seconds : seconds))}
28
+ onclick={() =>
29
+ pc?.seek((pc?.currentTime ?? 0) + (direction === "back" ? -seconds * 1000 : seconds * 1000))}
27
30
  >
28
31
  {#if direction === "back"}
29
32
  <SkipBackIcon size={16} />