@livepeer-frameworks/player-svelte 0.1.3 → 0.2.1

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 (50) hide show
  1. package/dist/DevModePanel.svelte +14 -15
  2. package/dist/IdleScreen.svelte +12 -4
  3. package/dist/LoadingScreen.svelte +90 -50
  4. package/dist/LoadingScreen.svelte.bak +702 -0
  5. package/dist/Player.svelte +200 -53
  6. package/dist/Player.svelte.d.ts +6 -1
  7. package/dist/PlayerControls.svelte +109 -32
  8. package/dist/PlayerControls.svelte.d.ts +3 -0
  9. package/dist/StreamStateOverlay.svelte +17 -5
  10. package/dist/controls/FullscreenButton.svelte +26 -0
  11. package/dist/controls/FullscreenButton.svelte.d.ts +3 -0
  12. package/dist/controls/LiveBadge.svelte +23 -0
  13. package/dist/controls/LiveBadge.svelte.d.ts +3 -0
  14. package/dist/controls/PlayButton.svelte +26 -0
  15. package/dist/controls/PlayButton.svelte.d.ts +3 -0
  16. package/dist/controls/SettingsMenu.svelte +208 -0
  17. package/dist/controls/SettingsMenu.svelte.d.ts +28 -0
  18. package/dist/controls/SkipButton.svelte +33 -0
  19. package/dist/controls/SkipButton.svelte.d.ts +7 -0
  20. package/dist/controls/TimeDisplay.svelte +18 -0
  21. package/dist/controls/TimeDisplay.svelte.d.ts +3 -0
  22. package/dist/controls/VolumeControl.svelte +26 -0
  23. package/dist/controls/VolumeControl.svelte.d.ts +3 -0
  24. package/dist/controls/index.d.ts +7 -0
  25. package/dist/controls/index.js +7 -0
  26. package/dist/index.d.ts +3 -2
  27. package/dist/index.js +3 -1
  28. package/dist/stores/i18n.d.ts +3 -0
  29. package/dist/stores/i18n.js +4 -0
  30. package/dist/stores/index.d.ts +1 -0
  31. package/dist/stores/index.js +2 -0
  32. package/package.json +8 -8
  33. package/src/DevModePanel.svelte +14 -15
  34. package/src/IdleScreen.svelte +12 -4
  35. package/src/LoadingScreen.svelte +90 -50
  36. package/src/LoadingScreen.svelte.bak +702 -0
  37. package/src/Player.svelte +200 -53
  38. package/src/PlayerControls.svelte +109 -32
  39. package/src/StreamStateOverlay.svelte +17 -5
  40. package/src/controls/FullscreenButton.svelte +26 -0
  41. package/src/controls/LiveBadge.svelte +23 -0
  42. package/src/controls/PlayButton.svelte +26 -0
  43. package/src/controls/SettingsMenu.svelte +208 -0
  44. package/src/controls/SkipButton.svelte +33 -0
  45. package/src/controls/TimeDisplay.svelte +18 -0
  46. package/src/controls/VolumeControl.svelte +26 -0
  47. package/src/controls/index.ts +7 -0
  48. package/src/index.ts +10 -0
  49. package/src/stores/i18n.ts +7 -0
  50. package/src/stores/index.ts +3 -0
@@ -1,9 +1,13 @@
1
1
  <script lang="ts">
2
+ import { getContext } from "svelte";
3
+ import type { Readable } from "svelte/store";
2
4
  import {
3
5
  cn,
4
6
  globalPlayerManager,
7
+ createTranslator,
5
8
  type MistStreamInfo,
6
9
  type PlaybackMode,
10
+ type TranslateFn,
7
11
  // Seeking utilities from core
8
12
  SPEED_PRESETS,
9
13
  isMediaStreamSource,
@@ -15,7 +19,10 @@
15
19
  isLiveContent,
16
20
  // Time formatting from core
17
21
  formatTimeDisplay,
22
+ getAvailableLocales,
23
+ getLocaleDisplayName,
18
24
  } from "@livepeer-frameworks/player-core";
25
+ import type { FwLocale } from "@livepeer-frameworks/player-core";
19
26
  import SeekBar from "./SeekBar.svelte";
20
27
  import Slider from "./ui/Slider.svelte";
21
28
  import VolumeIcons from "./components/VolumeIcons.svelte";
@@ -31,6 +38,11 @@
31
38
  SeekToLiveIcon,
32
39
  } from "./icons";
33
40
 
41
+ // 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);
45
+
34
46
  // Props - aligned with React PlayerControls
35
47
  interface Props {
36
48
  currentTime: number;
@@ -49,6 +61,8 @@
49
61
  isContentLive?: boolean;
50
62
  /** Jump to live edge callback */
51
63
  onJumpToLive?: () => void;
64
+ activeLocale?: FwLocale;
65
+ onLocaleChange?: (locale: FwLocale) => void;
52
66
  }
53
67
 
54
68
  let {
@@ -66,6 +80,8 @@
66
80
  onStatsToggle = undefined,
67
81
  isContentLive = undefined,
68
82
  onJumpToLive = undefined,
83
+ activeLocale = undefined,
84
+ onLocaleChange = undefined,
69
85
  }: Props = $props();
70
86
 
71
87
  // Video element discovery
@@ -123,6 +139,46 @@
123
139
  let qualityValue = $state("auto");
124
140
  let captionValue = $state("none");
125
141
 
142
+ // Audio detection: trust MistServer metadata first, then DOM fallback
143
+ $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
+ if (!video) {
151
+ hasAudio = true;
152
+ return;
153
+ }
154
+
155
+ const checkAudio = () => {
156
+ if (video!.srcObject instanceof MediaStream) {
157
+ hasAudio = video!.srcObject.getAudioTracks().length > 0;
158
+ return;
159
+ }
160
+ const videoAny = video as any;
161
+ if (videoAny.audioTracks && videoAny.audioTracks.length !== undefined) {
162
+ hasAudio = videoAny.audioTracks.length > 0;
163
+ return;
164
+ }
165
+ hasAudio = true;
166
+ };
167
+ checkAudio();
168
+ video.addEventListener("loadedmetadata", checkAudio);
169
+ // Safari: audioTracks may be populated after loadedmetadata for HLS streams
170
+ const audioTracks = (video as any).audioTracks;
171
+ if (audioTracks?.addEventListener) {
172
+ audioTracks.addEventListener("addtrack", checkAudio);
173
+ }
174
+ return () => {
175
+ video!.removeEventListener("loadedmetadata", checkAudio);
176
+ if (audioTracks?.removeEventListener) {
177
+ audioTracks.removeEventListener("addtrack", checkAudio);
178
+ }
179
+ };
180
+ });
181
+
126
182
  // Text tracks from player
127
183
  let textTracks = $derived.by(() => {
128
184
  return globalPlayerManager.getCurrentPlayer()?.getTextTracks?.() ?? [];
@@ -507,7 +563,7 @@
507
563
 
508
564
  <div
509
565
  class={cn(
510
- "fw-player-surface fw-controls-wrapper",
566
+ "fw-controls-wrapper",
511
567
  isVisible ? "fw-controls-wrapper--visible" : "fw-controls-wrapper--hidden"
512
568
  )}
513
569
  >
@@ -551,7 +607,7 @@
551
607
  <button
552
608
  type="button"
553
609
  class="fw-btn-flush"
554
- aria-label={isPlaying ? "Pause" : "Play"}
610
+ aria-label={isPlaying ? t("pause") : t("play")}
555
611
  onclick={handlePlayPause}
556
612
  {disabled}
557
613
  >
@@ -565,7 +621,7 @@
565
621
  <button
566
622
  type="button"
567
623
  class="fw-btn-flush hidden sm:flex"
568
- aria-label="Skip back 10s"
624
+ aria-label={t("skipBackward")}
569
625
  onclick={handleSkipBack}
570
626
  {disabled}
571
627
  >
@@ -574,7 +630,7 @@
574
630
  <button
575
631
  type="button"
576
632
  class="fw-btn-flush hidden sm:flex"
577
- aria-label="Skip forward 10s"
633
+ aria-label={t("skipForward")}
578
634
  onclick={handleSkipForward}
579
635
  {disabled}
580
636
  >
@@ -592,7 +648,7 @@
592
648
  !hasAudio && "fw-volume-group--disabled"
593
649
  )}
594
650
  role="group"
595
- aria-label="Volume controls"
651
+ aria-label={t("volume")}
596
652
  onmouseenter={() => hasAudio && (isVolumeHovered = true)}
597
653
  onmouseleave={() => {
598
654
  isVolumeHovered = false;
@@ -611,8 +667,8 @@
611
667
  <button
612
668
  type="button"
613
669
  class="fw-volume-btn"
614
- aria-label={!hasAudio ? "No audio" : isMuted ? "Unmute" : "Mute"}
615
- title={!hasAudio ? "No audio" : isMuted ? "Unmute" : "Mute"}
670
+ aria-label={!hasAudio ? t("muted") : isMuted ? t("unmute") : t("mute")}
671
+ title={!hasAudio ? t("muted") : isMuted ? t("unmute") : t("mute")}
616
672
  onclick={handleMute}
617
673
  disabled={!hasAudio}
618
674
  >
@@ -634,7 +690,7 @@
634
690
  oninput={handleVolumeChange}
635
691
  orientation="horizontal"
636
692
  className="w-full"
637
- aria-label="Volume"
693
+ aria-label={t("volume")}
638
694
  disabled={!hasAudio}
639
695
  />
640
696
  </div>
@@ -656,13 +712,9 @@
656
712
  "fw-live-badge",
657
713
  !hasDvrWindow || isNearLiveState ? "fw-live-badge--active" : "fw-live-badge--behind"
658
714
  )}
659
- title={!hasDvrWindow
660
- ? "Live only"
661
- : isNearLiveState
662
- ? "At live edge"
663
- : "Jump to live"}
715
+ title={t("live")}
664
716
  >
665
- LIVE
717
+ {t("live").toUpperCase()}
666
718
  {#if !isNearLiveState && hasDvrWindow}
667
719
  <SeekToLiveIcon size={10} />
668
720
  {/if}
@@ -678,8 +730,8 @@
678
730
  <button
679
731
  type="button"
680
732
  class={cn("fw-btn-flush", isStatsOpen && "fw-btn-flush--active")}
681
- aria-label="Toggle stats"
682
- title="Stats"
733
+ aria-label={t("showStats")}
734
+ title={t("showStats")}
683
735
  onclick={onStatsToggle}
684
736
  {disabled}
685
737
  >
@@ -691,8 +743,8 @@
691
743
  <button
692
744
  type="button"
693
745
  class={cn("fw-btn-flush group", showSettingsMenu && "fw-btn-flush--active")}
694
- aria-label="Settings"
695
- title="Settings"
746
+ aria-label={t("settings")}
747
+ title={t("settings")}
696
748
  onclick={() => (showSettingsMenu = !showSettingsMenu)}
697
749
  {disabled}
698
750
  >
@@ -700,11 +752,11 @@
700
752
  </button>
701
753
 
702
754
  {#if showSettingsMenu}
703
- <div class="fw-player-surface fw-settings-menu">
755
+ <div class="fw-settings-menu">
704
756
  <!-- Playback Mode - only show for live content (not VOD/clips) -->
705
757
  {#if onModeChange && isContentLive !== false}
706
758
  <div class="fw-settings-section">
707
- <div class="fw-settings-label">Mode</div>
759
+ <div class="fw-settings-label">{t("mode")}</div>
708
760
  <div class="fw-settings-options">
709
761
  {#each ["auto", "low-latency", "quality"] as mode}
710
762
  <button
@@ -718,7 +770,11 @@
718
770
  showSettingsMenu = false;
719
771
  }}
720
772
  >
721
- {mode === "low-latency" ? "Fast" : mode === "quality" ? "Stable" : "Auto"}
773
+ {mode === "low-latency"
774
+ ? t("fast")
775
+ : mode === "quality"
776
+ ? t("stable")
777
+ : t("auto")}
722
778
  </button>
723
779
  {/each}
724
780
  </div>
@@ -728,7 +784,7 @@
728
784
  <!-- Speed (hidden for WebRTC MediaStream) -->
729
785
  {#if supportsPlaybackRate}
730
786
  <div class="fw-settings-section">
731
- <div class="fw-settings-label">Speed</div>
787
+ <div class="fw-settings-label">{t("speed")}</div>
732
788
  <div class="fw-settings-options fw-settings-options--wrap">
733
789
  {#each SPEED_PRESETS as rate}
734
790
  <button
@@ -750,7 +806,7 @@
750
806
  <!-- Quality -->
751
807
  {#if qualities.length > 0}
752
808
  <div class="fw-settings-section">
753
- <div class="fw-settings-label">Quality</div>
809
+ <div class="fw-settings-label">{t("quality")}</div>
754
810
  <div class="fw-settings-list">
755
811
  <button
756
812
  class={cn(
@@ -759,7 +815,7 @@
759
815
  )}
760
816
  onclick={() => handleQualityChange("auto")}
761
817
  >
762
- Auto
818
+ {t("auto")}
763
819
  </button>
764
820
  {#each qualities as q}
765
821
  <button
@@ -779,7 +835,7 @@
779
835
  <!-- Captions -->
780
836
  {#if textTracks.length > 0}
781
837
  <div class="fw-settings-section">
782
- <div class="fw-settings-label">Captions</div>
838
+ <div class="fw-settings-label">{t("captions")}</div>
783
839
  <div class="fw-settings-list">
784
840
  <button
785
841
  class={cn(
@@ -788,17 +844,38 @@
788
844
  )}
789
845
  onclick={() => handleCaptionChange("none")}
790
846
  >
791
- Off
847
+ {t("captionsOff")}
792
848
  </button>
793
- {#each textTracks as t}
849
+ {#each textTracks as tt}
850
+ <button
851
+ class={cn(
852
+ "fw-settings-list-item",
853
+ captionValue === tt.id && "fw-settings-list-item--active"
854
+ )}
855
+ onclick={() => handleCaptionChange(tt.id)}
856
+ >
857
+ {tt.label || tt.id}
858
+ </button>
859
+ {/each}
860
+ </div>
861
+ </div>
862
+ {/if}
863
+
864
+ <!-- Locale -->
865
+ {#if onLocaleChange}
866
+ <div class="fw-settings-section">
867
+ <div class="fw-settings-label">{t("language")}</div>
868
+ <div class="fw-settings-list">
869
+ {#each getAvailableLocales() as loc}
794
870
  <button
871
+ type="button"
795
872
  class={cn(
796
873
  "fw-settings-list-item",
797
- captionValue === t.id && "fw-settings-list-item--active"
874
+ activeLocale === loc && "fw-settings-list-item--active"
798
875
  )}
799
- onclick={() => handleCaptionChange(t.id)}
876
+ onclick={() => onLocaleChange(loc)}
800
877
  >
801
- {t.label || t.id}
878
+ {getLocaleDisplayName(loc)}
802
879
  </button>
803
880
  {/each}
804
881
  </div>
@@ -812,8 +889,8 @@
812
889
  <button
813
890
  type="button"
814
891
  class="fw-btn-flush"
815
- aria-label="Toggle fullscreen"
816
- title="Fullscreen"
892
+ aria-label={isFullscreen ? t("exitFullscreen") : t("fullscreen")}
893
+ title={t("fullscreen")}
817
894
  onclick={handleFullscreen}
818
895
  {disabled}
819
896
  >
@@ -1,4 +1,5 @@
1
1
  import { type MistStreamInfo, type PlaybackMode } from "@livepeer-frameworks/player-core";
2
+ import type { FwLocale } from "@livepeer-frameworks/player-core";
2
3
  interface Props {
3
4
  currentTime: number;
4
5
  duration: number;
@@ -16,6 +17,8 @@ interface Props {
16
17
  isContentLive?: boolean;
17
18
  /** Jump to live edge callback */
18
19
  onJumpToLive?: () => void;
20
+ activeLocale?: FwLocale;
21
+ onLocaleChange?: (locale: FwLocale) => void;
19
22
  }
20
23
  declare const PlayerControls: import("svelte").Component<Props, {}, "">;
21
24
  type PlayerControls = ReturnType<typeof PlayerControls>;
@@ -10,6 +10,9 @@
10
10
  - Retry button for errors
11
11
  -->
12
12
  <script lang="ts">
13
+ import { getContext } from "svelte";
14
+ import type { Readable } from "svelte/store";
15
+ import { createTranslator, type TranslateFn } from "@livepeer-frameworks/player-core";
13
16
  import type { StreamStatus } from "@livepeer-frameworks/player-core";
14
17
 
15
18
  interface Props {
@@ -36,6 +39,10 @@
36
39
  class: className = "",
37
40
  }: Props = $props();
38
41
 
42
+ const translatorCtx = getContext<Readable<TranslateFn> | undefined>("fw-translator");
43
+ const fallbackT = createTranslator({ locale: "en" });
44
+ let t: TranslateFn = $derived(translatorCtx ? $translatorCtx : fallbackT);
45
+
39
46
  // Computed states
40
47
  let showRetry = $derived(status === "ERROR" || status === "INVALID" || status === "OFFLINE");
41
48
  let showProgress = $derived(status === "INITIALIZING" && percentage !== undefined);
@@ -143,7 +150,7 @@
143
150
  <p
144
151
  style="margin-top: 0.5rem; font-size: 0.75rem; color: hsl(var(--tn-fg-dark, 233 23% 60%));"
145
152
  >
146
- The stream will start when the broadcaster goes live
153
+ {t("broadcasterGoLive")}
147
154
  </p>
148
155
  {/if}
149
156
 
@@ -151,7 +158,7 @@
151
158
  <p
152
159
  style="margin-top: 0.5rem; font-size: 0.75rem; color: hsl(var(--tn-fg-dark, 233 23% 60%));"
153
160
  >
154
- Please wait while the stream prepares...
161
+ {t("streamPreparing")}
155
162
  </p>
156
163
  {/if}
157
164
 
@@ -159,7 +166,7 @@
159
166
  {#if !showRetry}
160
167
  <div class="polling-indicator">
161
168
  <span class="polling-dot"></span>
162
- <span>Checking stream status...</span>
169
+ <span>{t("checkingStatus")}</span>
163
170
  </div>
164
171
  {/if}
165
172
  </div>
@@ -167,8 +174,13 @@
167
174
  <!-- Slab actions - flush retry button -->
168
175
  {#if showRetry && onRetry}
169
176
  <div class="slab-actions">
170
- <button type="button" class="btn-flush" onclick={onRetry} aria-label="Retry connection">
171
- Retry Connection
177
+ <button
178
+ type="button"
179
+ class="btn-flush"
180
+ onclick={onRetry}
181
+ aria-label={t("retryConnection")}
182
+ >
183
+ {t("retryConnection")}
172
184
  </button>
173
185
  </div>
174
186
  {/if}
@@ -0,0 +1,26 @@
1
+ <script lang="ts">
2
+ import { getContext } from "svelte";
3
+ import type { Readable } from "svelte/store";
4
+ import { createTranslator, type TranslateFn } from "@livepeer-frameworks/player-core";
5
+ import { FullscreenIcon, FullscreenExitIcon } from "../icons";
6
+
7
+ 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);
11
+ </script>
12
+
13
+ <button
14
+ type="button"
15
+ class="fw-btn-flush"
16
+ aria-label={pc?.isFullscreen ? t("exitFullscreen") : t("fullscreen")}
17
+ aria-pressed={pc?.isFullscreen ?? false}
18
+ title={pc?.isFullscreen ? t("exitFullscreen") : t("fullscreen")}
19
+ onclick={() => pc?.toggleFullscreen()}
20
+ >
21
+ {#if pc?.isFullscreen}
22
+ <FullscreenExitIcon size={16} />
23
+ {:else}
24
+ <FullscreenIcon size={16} />
25
+ {/if}
26
+ </button>
@@ -0,0 +1,3 @@
1
+ declare const FullscreenButton: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type FullscreenButton = ReturnType<typeof FullscreenButton>;
3
+ export default FullscreenButton;
@@ -0,0 +1,23 @@
1
+ <script lang="ts">
2
+ import { getContext } from "svelte";
3
+ import type { Readable } from "svelte/store";
4
+ import { createTranslator, type TranslateFn } from "@livepeer-frameworks/player-core";
5
+ import { SeekToLiveIcon } from "../icons";
6
+
7
+ 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);
11
+ </script>
12
+
13
+ {#if pc?.isEffectivelyLive}
14
+ <button
15
+ type="button"
16
+ class="fw-live-badge fw-live-badge--active"
17
+ onclick={() => pc?.jumpToLive()}
18
+ aria-label={t("live")}
19
+ >
20
+ {t("live").toUpperCase()}
21
+ <SeekToLiveIcon size={10} />
22
+ </button>
23
+ {/if}
@@ -0,0 +1,3 @@
1
+ declare const LiveBadge: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type LiveBadge = ReturnType<typeof LiveBadge>;
3
+ export default LiveBadge;
@@ -0,0 +1,26 @@
1
+ <script lang="ts">
2
+ import { getContext } from "svelte";
3
+ import type { Readable } from "svelte/store";
4
+ import { createTranslator, type TranslateFn } from "@livepeer-frameworks/player-core";
5
+ import { PlayIcon, PauseIcon } from "../icons";
6
+
7
+ 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);
11
+ </script>
12
+
13
+ <button
14
+ type="button"
15
+ class="fw-btn-flush"
16
+ aria-label={pc?.isPlaying ? t("pause") : t("play")}
17
+ aria-pressed={pc?.isPlaying ?? false}
18
+ title={pc?.isPlaying ? t("pause") : t("play")}
19
+ onclick={() => pc?.togglePlay()}
20
+ >
21
+ {#if pc?.isPlaying}
22
+ <PauseIcon size={18} />
23
+ {:else}
24
+ <PlayIcon size={18} />
25
+ {/if}
26
+ </button>
@@ -0,0 +1,3 @@
1
+ declare const PlayButton: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type PlayButton = ReturnType<typeof PlayButton>;
3
+ export default PlayButton;