@livepeer-frameworks/player-svelte 0.1.2 → 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.
- package/LICENSE.md +24 -0
- package/README.md +6 -2
- package/dist/DevModePanel.svelte +53 -16
- package/dist/IdleScreen.svelte +36 -28
- package/dist/LoadingScreen.svelte +107 -67
- package/dist/LoadingScreen.svelte.bak +702 -0
- package/dist/Player.svelte +200 -53
- package/dist/Player.svelte.d.ts +6 -1
- package/dist/PlayerControls.svelte +114 -32
- package/dist/PlayerControls.svelte.d.ts +3 -0
- package/dist/StreamStateOverlay.svelte +33 -21
- package/dist/SubtitleRenderer.svelte +2 -2
- package/dist/controls/FullscreenButton.svelte +26 -0
- package/dist/controls/FullscreenButton.svelte.d.ts +3 -0
- package/dist/controls/LiveBadge.svelte +23 -0
- package/dist/controls/LiveBadge.svelte.d.ts +3 -0
- package/dist/controls/PlayButton.svelte +26 -0
- package/dist/controls/PlayButton.svelte.d.ts +3 -0
- package/dist/controls/SettingsMenu.svelte +208 -0
- package/dist/controls/SettingsMenu.svelte.d.ts +28 -0
- package/dist/controls/SkipButton.svelte +33 -0
- package/dist/controls/SkipButton.svelte.d.ts +7 -0
- package/dist/controls/TimeDisplay.svelte +18 -0
- package/dist/controls/TimeDisplay.svelte.d.ts +3 -0
- package/dist/controls/VolumeControl.svelte +26 -0
- package/dist/controls/VolumeControl.svelte.d.ts +3 -0
- package/dist/controls/index.d.ts +7 -0
- package/dist/controls/index.js +7 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -1
- package/dist/stores/i18n.d.ts +3 -0
- package/dist/stores/i18n.js +4 -0
- package/dist/stores/index.d.ts +1 -0
- package/dist/stores/index.js +2 -0
- package/dist/stores/playerController.d.ts +2 -0
- package/dist/stores/playerController.js +4 -0
- package/package.json +19 -19
- package/src/DevModePanel.svelte +53 -16
- package/src/IdleScreen.svelte +12 -4
- package/src/LoadingScreen.svelte +90 -50
- package/src/LoadingScreen.svelte.bak +702 -0
- package/src/Player.svelte +200 -53
- package/src/PlayerControls.svelte +114 -32
- package/src/StreamStateOverlay.svelte +17 -5
- package/src/controls/FullscreenButton.svelte +26 -0
- package/src/controls/LiveBadge.svelte +23 -0
- package/src/controls/PlayButton.svelte +26 -0
- package/src/controls/SettingsMenu.svelte +208 -0
- package/src/controls/SkipButton.svelte +33 -0
- package/src/controls/TimeDisplay.svelte +18 -0
- package/src/controls/VolumeControl.svelte +26 -0
- package/src/controls/index.ts +7 -0
- package/src/index.ts +10 -0
- package/src/stores/i18n.ts +7 -0
- package/src/stores/index.ts +3 -0
- package/src/stores/playerController.ts +7 -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-
|
|
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 ? "
|
|
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="
|
|
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="
|
|
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="
|
|
651
|
+
aria-label={t("volume")}
|
|
596
652
|
onmouseenter={() => hasAudio && (isVolumeHovered = true)}
|
|
597
653
|
onmouseleave={() => {
|
|
598
654
|
isVolumeHovered = false;
|
|
@@ -602,12 +658,17 @@
|
|
|
602
658
|
onfocusout={(e) => {
|
|
603
659
|
if (!e.currentTarget.contains(e.relatedTarget as Node)) isVolumeFocused = false;
|
|
604
660
|
}}
|
|
661
|
+
onpointerup={(e) => {
|
|
662
|
+
if (hasAudio && e.target === e.currentTarget) {
|
|
663
|
+
handleMute();
|
|
664
|
+
}
|
|
665
|
+
}}
|
|
605
666
|
>
|
|
606
667
|
<button
|
|
607
668
|
type="button"
|
|
608
669
|
class="fw-volume-btn"
|
|
609
|
-
aria-label={!hasAudio ? "
|
|
610
|
-
title={!hasAudio ? "
|
|
670
|
+
aria-label={!hasAudio ? t("muted") : isMuted ? t("unmute") : t("mute")}
|
|
671
|
+
title={!hasAudio ? t("muted") : isMuted ? t("unmute") : t("mute")}
|
|
611
672
|
onclick={handleMute}
|
|
612
673
|
disabled={!hasAudio}
|
|
613
674
|
>
|
|
@@ -629,7 +690,7 @@
|
|
|
629
690
|
oninput={handleVolumeChange}
|
|
630
691
|
orientation="horizontal"
|
|
631
692
|
className="w-full"
|
|
632
|
-
aria-label="
|
|
693
|
+
aria-label={t("volume")}
|
|
633
694
|
disabled={!hasAudio}
|
|
634
695
|
/>
|
|
635
696
|
</div>
|
|
@@ -651,13 +712,9 @@
|
|
|
651
712
|
"fw-live-badge",
|
|
652
713
|
!hasDvrWindow || isNearLiveState ? "fw-live-badge--active" : "fw-live-badge--behind"
|
|
653
714
|
)}
|
|
654
|
-
title={
|
|
655
|
-
? "Live only"
|
|
656
|
-
: isNearLiveState
|
|
657
|
-
? "At live edge"
|
|
658
|
-
: "Jump to live"}
|
|
715
|
+
title={t("live")}
|
|
659
716
|
>
|
|
660
|
-
|
|
717
|
+
{t("live").toUpperCase()}
|
|
661
718
|
{#if !isNearLiveState && hasDvrWindow}
|
|
662
719
|
<SeekToLiveIcon size={10} />
|
|
663
720
|
{/if}
|
|
@@ -673,8 +730,8 @@
|
|
|
673
730
|
<button
|
|
674
731
|
type="button"
|
|
675
732
|
class={cn("fw-btn-flush", isStatsOpen && "fw-btn-flush--active")}
|
|
676
|
-
aria-label="
|
|
677
|
-
title="
|
|
733
|
+
aria-label={t("showStats")}
|
|
734
|
+
title={t("showStats")}
|
|
678
735
|
onclick={onStatsToggle}
|
|
679
736
|
{disabled}
|
|
680
737
|
>
|
|
@@ -686,8 +743,8 @@
|
|
|
686
743
|
<button
|
|
687
744
|
type="button"
|
|
688
745
|
class={cn("fw-btn-flush group", showSettingsMenu && "fw-btn-flush--active")}
|
|
689
|
-
aria-label="
|
|
690
|
-
title="
|
|
746
|
+
aria-label={t("settings")}
|
|
747
|
+
title={t("settings")}
|
|
691
748
|
onclick={() => (showSettingsMenu = !showSettingsMenu)}
|
|
692
749
|
{disabled}
|
|
693
750
|
>
|
|
@@ -695,11 +752,11 @@
|
|
|
695
752
|
</button>
|
|
696
753
|
|
|
697
754
|
{#if showSettingsMenu}
|
|
698
|
-
<div class="fw-
|
|
755
|
+
<div class="fw-settings-menu">
|
|
699
756
|
<!-- Playback Mode - only show for live content (not VOD/clips) -->
|
|
700
757
|
{#if onModeChange && isContentLive !== false}
|
|
701
758
|
<div class="fw-settings-section">
|
|
702
|
-
<div class="fw-settings-label">
|
|
759
|
+
<div class="fw-settings-label">{t("mode")}</div>
|
|
703
760
|
<div class="fw-settings-options">
|
|
704
761
|
{#each ["auto", "low-latency", "quality"] as mode}
|
|
705
762
|
<button
|
|
@@ -713,7 +770,11 @@
|
|
|
713
770
|
showSettingsMenu = false;
|
|
714
771
|
}}
|
|
715
772
|
>
|
|
716
|
-
{mode === "low-latency"
|
|
773
|
+
{mode === "low-latency"
|
|
774
|
+
? t("fast")
|
|
775
|
+
: mode === "quality"
|
|
776
|
+
? t("stable")
|
|
777
|
+
: t("auto")}
|
|
717
778
|
</button>
|
|
718
779
|
{/each}
|
|
719
780
|
</div>
|
|
@@ -723,7 +784,7 @@
|
|
|
723
784
|
<!-- Speed (hidden for WebRTC MediaStream) -->
|
|
724
785
|
{#if supportsPlaybackRate}
|
|
725
786
|
<div class="fw-settings-section">
|
|
726
|
-
<div class="fw-settings-label">
|
|
787
|
+
<div class="fw-settings-label">{t("speed")}</div>
|
|
727
788
|
<div class="fw-settings-options fw-settings-options--wrap">
|
|
728
789
|
{#each SPEED_PRESETS as rate}
|
|
729
790
|
<button
|
|
@@ -745,7 +806,7 @@
|
|
|
745
806
|
<!-- Quality -->
|
|
746
807
|
{#if qualities.length > 0}
|
|
747
808
|
<div class="fw-settings-section">
|
|
748
|
-
<div class="fw-settings-label">
|
|
809
|
+
<div class="fw-settings-label">{t("quality")}</div>
|
|
749
810
|
<div class="fw-settings-list">
|
|
750
811
|
<button
|
|
751
812
|
class={cn(
|
|
@@ -754,7 +815,7 @@
|
|
|
754
815
|
)}
|
|
755
816
|
onclick={() => handleQualityChange("auto")}
|
|
756
817
|
>
|
|
757
|
-
|
|
818
|
+
{t("auto")}
|
|
758
819
|
</button>
|
|
759
820
|
{#each qualities as q}
|
|
760
821
|
<button
|
|
@@ -774,7 +835,7 @@
|
|
|
774
835
|
<!-- Captions -->
|
|
775
836
|
{#if textTracks.length > 0}
|
|
776
837
|
<div class="fw-settings-section">
|
|
777
|
-
<div class="fw-settings-label">
|
|
838
|
+
<div class="fw-settings-label">{t("captions")}</div>
|
|
778
839
|
<div class="fw-settings-list">
|
|
779
840
|
<button
|
|
780
841
|
class={cn(
|
|
@@ -783,17 +844,38 @@
|
|
|
783
844
|
)}
|
|
784
845
|
onclick={() => handleCaptionChange("none")}
|
|
785
846
|
>
|
|
786
|
-
|
|
847
|
+
{t("captionsOff")}
|
|
787
848
|
</button>
|
|
788
|
-
{#each textTracks as
|
|
849
|
+
{#each textTracks as tt}
|
|
789
850
|
<button
|
|
790
851
|
class={cn(
|
|
791
852
|
"fw-settings-list-item",
|
|
792
|
-
captionValue ===
|
|
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}
|
|
870
|
+
<button
|
|
871
|
+
type="button"
|
|
872
|
+
class={cn(
|
|
873
|
+
"fw-settings-list-item",
|
|
874
|
+
activeLocale === loc && "fw-settings-list-item--active"
|
|
793
875
|
)}
|
|
794
|
-
onclick={() =>
|
|
876
|
+
onclick={() => onLocaleChange(loc)}
|
|
795
877
|
>
|
|
796
|
-
{
|
|
878
|
+
{getLocaleDisplayName(loc)}
|
|
797
879
|
</button>
|
|
798
880
|
{/each}
|
|
799
881
|
</div>
|
|
@@ -807,8 +889,8 @@
|
|
|
807
889
|
<button
|
|
808
890
|
type="button"
|
|
809
891
|
class="fw-btn-flush"
|
|
810
|
-
aria-label="
|
|
811
|
-
title="
|
|
892
|
+
aria-label={isFullscreen ? t("exitFullscreen") : t("fullscreen")}
|
|
893
|
+
title={t("fullscreen")}
|
|
812
894
|
onclick={handleFullscreen}
|
|
813
895
|
{disabled}
|
|
814
896
|
>
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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>
|
|
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
|
|
171
|
-
|
|
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,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,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,208 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { getContext } from "svelte";
|
|
3
|
+
import type { Readable } from "svelte/store";
|
|
4
|
+
import { SettingsIcon } from "../icons";
|
|
5
|
+
import {
|
|
6
|
+
SPEED_PRESETS,
|
|
7
|
+
getAvailableLocales,
|
|
8
|
+
getLocaleDisplayName,
|
|
9
|
+
createTranslator,
|
|
10
|
+
type TranslateFn,
|
|
11
|
+
} from "@livepeer-frameworks/player-core";
|
|
12
|
+
import type { FwLocale } from "@livepeer-frameworks/player-core";
|
|
13
|
+
|
|
14
|
+
interface Props {
|
|
15
|
+
qualities?: Array<{ id: string; label: string; active?: boolean }>;
|
|
16
|
+
activeQuality?: string;
|
|
17
|
+
onSelectQuality?: (id: string) => void;
|
|
18
|
+
textTracks?: Array<{ id: string; label: string; active?: boolean }>;
|
|
19
|
+
activeCaption?: string;
|
|
20
|
+
onSelectCaption?: (id: string) => void;
|
|
21
|
+
playbackRate?: number;
|
|
22
|
+
onSpeedChange?: (rate: number) => void;
|
|
23
|
+
supportsSpeed?: boolean;
|
|
24
|
+
playbackMode?: "auto" | "low-latency" | "quality";
|
|
25
|
+
onModeChange?: (mode: "auto" | "low-latency" | "quality") => void;
|
|
26
|
+
showModeSelector?: boolean;
|
|
27
|
+
activeLocale?: FwLocale;
|
|
28
|
+
onLocaleChange?: (locale: FwLocale) => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let {
|
|
32
|
+
qualities: propQualities,
|
|
33
|
+
activeQuality,
|
|
34
|
+
onSelectQuality,
|
|
35
|
+
textTracks: propTextTracks,
|
|
36
|
+
activeCaption,
|
|
37
|
+
onSelectCaption,
|
|
38
|
+
playbackRate = 1,
|
|
39
|
+
onSpeedChange,
|
|
40
|
+
supportsSpeed = true,
|
|
41
|
+
playbackMode,
|
|
42
|
+
onModeChange,
|
|
43
|
+
showModeSelector = false,
|
|
44
|
+
activeLocale = undefined,
|
|
45
|
+
onLocaleChange = undefined,
|
|
46
|
+
}: Props = $props();
|
|
47
|
+
|
|
48
|
+
let availableLocales = getAvailableLocales();
|
|
49
|
+
|
|
50
|
+
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);
|
|
54
|
+
let isOpen = $state(false);
|
|
55
|
+
|
|
56
|
+
let qualities = $derived(propQualities ?? controller?.getQualities?.() ?? []);
|
|
57
|
+
let qualityValue = $derived(activeQuality ?? qualities.find((q: any) => q.active)?.id ?? "auto");
|
|
58
|
+
let textTracks = $derived(propTextTracks ?? []);
|
|
59
|
+
let captionValue = $derived(activeCaption ?? textTracks.find((t: any) => t.active)?.id ?? "none");
|
|
60
|
+
|
|
61
|
+
function selectQuality(id: string) {
|
|
62
|
+
if (onSelectQuality) onSelectQuality(id);
|
|
63
|
+
else controller?.selectQuality?.(id);
|
|
64
|
+
isOpen = false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function handleKeyDown(e: KeyboardEvent) {
|
|
68
|
+
if (e.key === "Escape") {
|
|
69
|
+
isOpen = false;
|
|
70
|
+
e.preventDefault();
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
|
|
74
|
+
e.preventDefault();
|
|
75
|
+
const menu = e.currentTarget as HTMLElement;
|
|
76
|
+
const items = menu.querySelectorAll<HTMLButtonElement>("button");
|
|
77
|
+
if (!items.length) return;
|
|
78
|
+
const current = Array.from(items).indexOf(document.activeElement as HTMLButtonElement);
|
|
79
|
+
const next =
|
|
80
|
+
e.key === "ArrowDown"
|
|
81
|
+
? (current + 1) % items.length
|
|
82
|
+
: (current - 1 + items.length) % items.length;
|
|
83
|
+
items[next]?.focus();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
</script>
|
|
87
|
+
|
|
88
|
+
<div class="fw-control-group" style="position: relative">
|
|
89
|
+
<button
|
|
90
|
+
type="button"
|
|
91
|
+
class="fw-btn-flush group"
|
|
92
|
+
class:fw-btn-flush--active={isOpen}
|
|
93
|
+
aria-label={t("settings")}
|
|
94
|
+
title={t("settings")}
|
|
95
|
+
onclick={() => (isOpen = !isOpen)}
|
|
96
|
+
>
|
|
97
|
+
<SettingsIcon size={16} />
|
|
98
|
+
</button>
|
|
99
|
+
|
|
100
|
+
{#if isOpen}
|
|
101
|
+
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
102
|
+
<div class="fw-settings-menu" role="menu" aria-label={t("settings")} onkeydown={handleKeyDown}>
|
|
103
|
+
{#if showModeSelector && onModeChange}
|
|
104
|
+
<div class="fw-settings-section">
|
|
105
|
+
<div class="fw-settings-label">{t("mode")}</div>
|
|
106
|
+
<div class="fw-settings-options">
|
|
107
|
+
{#each ["auto", "low-latency", "quality"] as mode}
|
|
108
|
+
<button
|
|
109
|
+
class="fw-settings-btn"
|
|
110
|
+
class:fw-settings-btn--active={playbackMode === mode}
|
|
111
|
+
onclick={() => {
|
|
112
|
+
onModeChange?.(mode as any);
|
|
113
|
+
isOpen = false;
|
|
114
|
+
}}
|
|
115
|
+
>
|
|
116
|
+
{mode === "low-latency" ? t("fast") : mode === "quality" ? t("stable") : t("auto")}
|
|
117
|
+
</button>
|
|
118
|
+
{/each}
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
{/if}
|
|
122
|
+
|
|
123
|
+
{#if supportsSpeed}
|
|
124
|
+
<div class="fw-settings-section">
|
|
125
|
+
<div class="fw-settings-label">{t("speed")}</div>
|
|
126
|
+
<div class="fw-settings-options fw-settings-options--wrap">
|
|
127
|
+
{#each SPEED_PRESETS as rate}
|
|
128
|
+
<button
|
|
129
|
+
class="fw-settings-btn"
|
|
130
|
+
class:fw-settings-btn--active={playbackRate === rate}
|
|
131
|
+
onclick={() => {
|
|
132
|
+
onSpeedChange?.(rate);
|
|
133
|
+
isOpen = false;
|
|
134
|
+
}}
|
|
135
|
+
>
|
|
136
|
+
{rate}x
|
|
137
|
+
</button>
|
|
138
|
+
{/each}
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
{/if}
|
|
142
|
+
|
|
143
|
+
{#if qualities.length > 0}
|
|
144
|
+
<div class="fw-settings-section">
|
|
145
|
+
<div class="fw-settings-label">{t("quality")}</div>
|
|
146
|
+
<div class="fw-settings-list">
|
|
147
|
+
<button
|
|
148
|
+
class="fw-settings-list-item"
|
|
149
|
+
class:fw-settings-list-item--active={qualityValue === "auto"}
|
|
150
|
+
onclick={() => selectQuality("auto")}>{t("auto")}</button
|
|
151
|
+
>
|
|
152
|
+
{#each qualities as q}
|
|
153
|
+
<button
|
|
154
|
+
class="fw-settings-list-item"
|
|
155
|
+
class:fw-settings-list-item--active={qualityValue === q.id}
|
|
156
|
+
onclick={() => selectQuality(q.id)}>{q.label}</button
|
|
157
|
+
>
|
|
158
|
+
{/each}
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
{/if}
|
|
162
|
+
|
|
163
|
+
{#if textTracks.length > 0}
|
|
164
|
+
<div class="fw-settings-section">
|
|
165
|
+
<div class="fw-settings-label">{t("captions")}</div>
|
|
166
|
+
<div class="fw-settings-list">
|
|
167
|
+
<button
|
|
168
|
+
class="fw-settings-list-item"
|
|
169
|
+
class:fw-settings-list-item--active={captionValue === "none"}
|
|
170
|
+
onclick={() => {
|
|
171
|
+
onSelectCaption?.("none");
|
|
172
|
+
isOpen = false;
|
|
173
|
+
}}>{t("captionsOff")}</button
|
|
174
|
+
>
|
|
175
|
+
{#each textTracks as tt}
|
|
176
|
+
<button
|
|
177
|
+
class="fw-settings-list-item"
|
|
178
|
+
class:fw-settings-list-item--active={captionValue === tt.id}
|
|
179
|
+
onclick={() => {
|
|
180
|
+
onSelectCaption?.(tt.id);
|
|
181
|
+
isOpen = false;
|
|
182
|
+
}}>{tt.label || tt.id}</button
|
|
183
|
+
>
|
|
184
|
+
{/each}
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
{/if}
|
|
188
|
+
|
|
189
|
+
{#if onLocaleChange}
|
|
190
|
+
<div class="fw-settings-section">
|
|
191
|
+
<div class="fw-settings-label">{t("language")}</div>
|
|
192
|
+
<div class="fw-settings-list">
|
|
193
|
+
{#each availableLocales as l}
|
|
194
|
+
<button
|
|
195
|
+
class="fw-settings-list-item"
|
|
196
|
+
class:fw-settings-list-item--active={activeLocale === l}
|
|
197
|
+
onclick={() => {
|
|
198
|
+
onLocaleChange?.(l);
|
|
199
|
+
isOpen = false;
|
|
200
|
+
}}>{getLocaleDisplayName(l)}</button
|
|
201
|
+
>
|
|
202
|
+
{/each}
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
{/if}
|
|
206
|
+
</div>
|
|
207
|
+
{/if}
|
|
208
|
+
</div>
|