@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
|
>
|
|
@@ -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
|
-
|
|
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}
|
|
@@ -177,7 +189,7 @@
|
|
|
177
189
|
{/if}
|
|
178
190
|
|
|
179
191
|
<style>
|
|
180
|
-
.
|
|
192
|
+
.overlay-backdrop {
|
|
181
193
|
position: absolute;
|
|
182
194
|
inset: 0;
|
|
183
195
|
z-index: 20;
|
|
@@ -188,14 +200,14 @@
|
|
|
188
200
|
backdrop-filter: blur(4px);
|
|
189
201
|
}
|
|
190
202
|
|
|
191
|
-
.
|
|
203
|
+
.slab {
|
|
192
204
|
width: 280px;
|
|
193
205
|
max-width: 90%;
|
|
194
206
|
background-color: hsl(var(--tn-bg, 233 23% 17%) / 0.95);
|
|
195
207
|
border: 1px solid hsl(var(--tn-border, 233 23% 25%) / 0.3);
|
|
196
208
|
}
|
|
197
209
|
|
|
198
|
-
.
|
|
210
|
+
.slab-header {
|
|
199
211
|
display: flex;
|
|
200
212
|
align-items: center;
|
|
201
213
|
gap: 0.5rem;
|
|
@@ -208,15 +220,15 @@
|
|
|
208
220
|
color: hsl(var(--tn-fg-dark, 233 23% 60%));
|
|
209
221
|
}
|
|
210
222
|
|
|
211
|
-
.
|
|
223
|
+
.slab-body {
|
|
212
224
|
padding: 1rem;
|
|
213
225
|
}
|
|
214
226
|
|
|
215
|
-
.
|
|
227
|
+
.slab-actions {
|
|
216
228
|
border-top: 1px solid hsl(var(--tn-border, 233 23% 25%) / 0.3);
|
|
217
229
|
}
|
|
218
230
|
|
|
219
|
-
.
|
|
231
|
+
.btn-flush {
|
|
220
232
|
width: 100%;
|
|
221
233
|
padding: 0.625rem 1rem;
|
|
222
234
|
background: none;
|
|
@@ -230,41 +242,41 @@
|
|
|
230
242
|
transition: background-color 0.15s;
|
|
231
243
|
}
|
|
232
244
|
|
|
233
|
-
.
|
|
245
|
+
.btn-flush:hover {
|
|
234
246
|
background-color: hsl(var(--tn-bg-visual, 233 23% 20%) / 0.5);
|
|
235
247
|
}
|
|
236
248
|
|
|
237
|
-
.
|
|
249
|
+
.progress-bar {
|
|
238
250
|
height: 0.375rem;
|
|
239
251
|
width: 100%;
|
|
240
252
|
overflow: hidden;
|
|
241
253
|
background-color: hsl(var(--tn-bg-visual, 233 23% 20%));
|
|
242
254
|
}
|
|
243
255
|
|
|
244
|
-
.
|
|
256
|
+
.progress-fill {
|
|
245
257
|
height: 100%;
|
|
246
258
|
transition: width 0.3s ease;
|
|
247
259
|
background-color: hsl(var(--tn-yellow, 40 70% 64%));
|
|
248
260
|
}
|
|
249
261
|
|
|
250
|
-
.
|
|
262
|
+
.icon {
|
|
251
263
|
width: 1.25rem;
|
|
252
264
|
height: 1.25rem;
|
|
253
265
|
}
|
|
254
266
|
|
|
255
|
-
.
|
|
267
|
+
.icon-online {
|
|
256
268
|
color: hsl(var(--tn-green, 115 54% 57%));
|
|
257
269
|
}
|
|
258
270
|
|
|
259
|
-
.
|
|
271
|
+
.icon-offline {
|
|
260
272
|
color: hsl(var(--tn-red, 355 68% 65%));
|
|
261
273
|
}
|
|
262
274
|
|
|
263
|
-
.
|
|
275
|
+
.icon-warning {
|
|
264
276
|
color: hsl(var(--tn-yellow, 40 70% 64%));
|
|
265
277
|
}
|
|
266
278
|
|
|
267
|
-
.
|
|
279
|
+
.polling-indicator {
|
|
268
280
|
display: flex;
|
|
269
281
|
align-items: center;
|
|
270
282
|
gap: 0.5rem;
|
|
@@ -273,7 +285,7 @@
|
|
|
273
285
|
color: hsl(var(--tn-fg-dark, 233 23% 60%));
|
|
274
286
|
}
|
|
275
287
|
|
|
276
|
-
.
|
|
288
|
+
.polling-dot {
|
|
277
289
|
width: 0.375rem;
|
|
278
290
|
height: 0.375rem;
|
|
279
291
|
background-color: hsl(var(--tn-cyan, 192 78% 73%));
|
|
@@ -296,7 +308,7 @@
|
|
|
296
308
|
}
|
|
297
309
|
}
|
|
298
310
|
|
|
299
|
-
.
|
|
311
|
+
.animate-spin {
|
|
300
312
|
animation: spin 1s linear infinite;
|
|
301
313
|
}
|
|
302
314
|
</style>
|
|
@@ -221,7 +221,7 @@
|
|
|
221
221
|
{/if}
|
|
222
222
|
|
|
223
223
|
<style>
|
|
224
|
-
.
|
|
224
|
+
.subtitle-container {
|
|
225
225
|
position: absolute;
|
|
226
226
|
left: 50%;
|
|
227
227
|
transform: translateX(-50%);
|
|
@@ -230,7 +230,7 @@
|
|
|
230
230
|
pointer-events: none;
|
|
231
231
|
}
|
|
232
232
|
|
|
233
|
-
.
|
|
233
|
+
.subtitle-text {
|
|
234
234
|
display: inline-block;
|
|
235
235
|
white-space: pre-wrap;
|
|
236
236
|
}
|
|
@@ -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>
|