@livepeer-frameworks/player-svelte 0.1.1 → 0.1.2
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/dist/DevModePanel.svelte +266 -127
- package/dist/DevModePanel.svelte.d.ts +1 -1
- package/dist/DvdLogo.svelte +17 -21
- package/dist/Icons.svelte +5 -3
- package/dist/Icons.svelte.d.ts +6 -19
- package/dist/IdleScreen.svelte +277 -186
- package/dist/IdleScreen.svelte.d.ts +1 -1
- package/dist/LoadingScreen.svelte +190 -162
- package/dist/Player.svelte +244 -111
- package/dist/Player.svelte.d.ts +1 -1
- package/dist/PlayerControls.svelte +263 -168
- package/dist/PlayerControls.svelte.d.ts +1 -1
- package/dist/SeekBar.svelte +61 -35
- package/dist/SkipIndicator.svelte +4 -4
- package/dist/SkipIndicator.svelte.d.ts +1 -1
- package/dist/SpeedIndicator.svelte +1 -1
- package/dist/StatsPanel.svelte +76 -57
- package/dist/StatsPanel.svelte.d.ts +1 -1
- package/dist/StreamStateOverlay.svelte +143 -107
- package/dist/StreamStateOverlay.svelte.d.ts +1 -1
- package/dist/SubtitleRenderer.svelte +46 -43
- package/dist/ThumbnailOverlay.svelte +22 -19
- package/dist/TitleOverlay.svelte +6 -11
- package/dist/components/VolumeIcons.svelte +12 -6
- package/dist/global.d.ts +3 -3
- package/dist/icons/FullscreenExitIcon.svelte +1 -5
- package/dist/icons/FullscreenIcon.svelte +1 -5
- package/dist/icons/PauseIcon.svelte +1 -5
- package/dist/icons/PictureInPictureIcon.svelte +12 -6
- package/dist/icons/PlayIcon.svelte +1 -5
- package/dist/icons/SeekToLiveIcon.svelte +1 -5
- package/dist/icons/SettingsIcon.svelte +1 -5
- package/dist/icons/SkipBackIcon.svelte +1 -5
- package/dist/icons/SkipForwardIcon.svelte +1 -5
- package/dist/icons/StatsIcon.svelte +1 -5
- package/dist/icons/VolumeOffIcon.svelte +1 -5
- package/dist/icons/VolumeUpIcon.svelte +1 -5
- package/dist/icons/index.d.ts +12 -12
- package/dist/icons/index.js +12 -12
- package/dist/index.d.ts +24 -24
- package/dist/index.js +21 -21
- package/dist/stores/index.d.ts +6 -6
- package/dist/stores/index.js +6 -6
- package/dist/stores/playbackQuality.d.ts +2 -2
- package/dist/stores/playbackQuality.js +7 -7
- package/dist/stores/playerContext.d.ts +2 -2
- package/dist/stores/playerContext.js +17 -17
- package/dist/stores/playerController.d.ts +13 -4
- package/dist/stores/playerController.js +80 -56
- package/dist/stores/playerSelection.d.ts +2 -2
- package/dist/stores/playerSelection.js +7 -7
- package/dist/stores/streamState.d.ts +2 -2
- package/dist/stores/streamState.js +56 -56
- package/dist/stores/viewerEndpoints.d.ts +3 -3
- package/dist/stores/viewerEndpoints.js +21 -21
- package/dist/types.d.ts +1 -1
- package/dist/ui/Badge.svelte +9 -10
- package/dist/ui/Badge.svelte.d.ts +8 -29
- package/dist/ui/Button.svelte +16 -16
- package/dist/ui/Button.svelte.d.ts +8 -29
- package/dist/ui/Slider.svelte +21 -55
- package/dist/ui/badge.js +1 -1
- package/dist/ui/button.js +2 -2
- package/dist/ui/context-menu/ContextMenuCheckboxItem.svelte +5 -7
- package/dist/ui/context-menu/ContextMenuCheckboxItem.svelte.d.ts +6 -27
- package/dist/ui/context-menu/ContextMenuContent.svelte +2 -9
- package/dist/ui/context-menu/ContextMenuItem.svelte +1 -5
- package/dist/ui/context-menu/ContextMenuLabel.svelte +1 -5
- package/dist/ui/context-menu/ContextMenuRadioItem.svelte +5 -7
- package/dist/ui/context-menu/ContextMenuRadioItem.svelte.d.ts +6 -27
- package/dist/ui/context-menu/ContextMenuSeparator.svelte +2 -8
- package/dist/ui/context-menu/ContextMenuShortcut.svelte +2 -12
- package/dist/ui/context-menu/ContextMenuSubContent.svelte +1 -5
- package/package.json +15 -7
- package/src/DevModePanel.svelte +1 -0
- package/src/Icons.svelte +5 -3
- package/src/IdleScreen.svelte +21 -14
- package/src/LoadingScreen.svelte +20 -13
- package/src/Player.svelte +48 -2
- package/src/PlayerControls.svelte +36 -17
- package/src/SeekBar.svelte +33 -0
- package/src/StreamStateOverlay.svelte +2 -2
- package/src/stores/playerController.ts +39 -1
- package/src/stores/viewerEndpoints.ts +1 -1
- package/src/ui/Badge.svelte +7 -4
- package/src/ui/Button.svelte +13 -13
- package/src/ui/context-menu/ContextMenuCheckboxItem.svelte +4 -2
- package/src/ui/context-menu/ContextMenuRadioItem.svelte +4 -2
package/dist/SeekBar.svelte
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Port of src/components/SeekBar.tsx
|
|
4
4
|
-->
|
|
5
5
|
<script lang="ts">
|
|
6
|
-
import { cn } from
|
|
6
|
+
import { cn } from "@livepeer-frameworks/player-core";
|
|
7
7
|
|
|
8
8
|
interface Props {
|
|
9
9
|
/** Current playback time in seconds */
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
buffered = undefined,
|
|
35
35
|
disabled = false,
|
|
36
36
|
onseek = undefined,
|
|
37
|
-
class: className =
|
|
37
|
+
class: className = "",
|
|
38
38
|
isLive = false,
|
|
39
39
|
seekableStart = 0,
|
|
40
40
|
liveEdge = undefined,
|
|
@@ -96,29 +96,29 @@
|
|
|
96
96
|
|
|
97
97
|
// Format time as MM:SS or HH:MM:SS
|
|
98
98
|
function formatTime(seconds: number): string {
|
|
99
|
-
if (!Number.isFinite(seconds) || seconds < 0) return
|
|
99
|
+
if (!Number.isFinite(seconds) || seconds < 0) return "0:00";
|
|
100
100
|
const total = Math.floor(seconds);
|
|
101
101
|
const hours = Math.floor(total / 3600);
|
|
102
102
|
const minutes = Math.floor((total % 3600) / 60);
|
|
103
103
|
const secs = total % 60;
|
|
104
104
|
if (hours > 0) {
|
|
105
|
-
return `${hours}:${String(minutes).padStart(2,
|
|
105
|
+
return `${hours}:${String(minutes).padStart(2, "0")}:${String(secs).padStart(2, "0")}`;
|
|
106
106
|
}
|
|
107
|
-
return `${minutes}:${String(secs).padStart(2,
|
|
107
|
+
return `${minutes}:${String(secs).padStart(2, "0")}`;
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
// Format relative time for live streams
|
|
111
111
|
function formatLiveTime(seconds: number, edge: number): string {
|
|
112
112
|
const behindSeconds = edge - seconds;
|
|
113
|
-
if (behindSeconds < 1) return
|
|
113
|
+
if (behindSeconds < 1) return "LIVE";
|
|
114
114
|
const total = Math.floor(behindSeconds);
|
|
115
115
|
const hours = Math.floor(total / 3600);
|
|
116
116
|
const minutes = Math.floor((total % 3600) / 60);
|
|
117
117
|
const secs = total % 60;
|
|
118
118
|
if (hours > 0) {
|
|
119
|
-
return `-${hours}:${String(minutes).padStart(2,
|
|
119
|
+
return `-${hours}:${String(minutes).padStart(2, "0")}:${String(secs).padStart(2, "0")}`;
|
|
120
120
|
}
|
|
121
|
-
return `-${minutes}:${String(secs).padStart(2,
|
|
121
|
+
return `-${minutes}:${String(secs).padStart(2, "0")}`;
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
// Calculate time from mouse position
|
|
@@ -130,7 +130,7 @@
|
|
|
130
130
|
|
|
131
131
|
// Live with valid seekable window
|
|
132
132
|
if (isLive && Number.isFinite(seekableWindow) && seekableWindow > 0) {
|
|
133
|
-
return seekableStart +
|
|
133
|
+
return seekableStart + percent * seekableWindow;
|
|
134
134
|
}
|
|
135
135
|
|
|
136
136
|
// VOD with finite duration
|
|
@@ -144,7 +144,7 @@
|
|
|
144
144
|
const start = Number.isFinite(seekableStart) ? seekableStart : 0;
|
|
145
145
|
const window = liveEdge - start;
|
|
146
146
|
if (window > 0) {
|
|
147
|
-
return start +
|
|
147
|
+
return start + percent * window;
|
|
148
148
|
}
|
|
149
149
|
}
|
|
150
150
|
|
|
@@ -189,16 +189,16 @@
|
|
|
189
189
|
|
|
190
190
|
const handleDragEnd = () => {
|
|
191
191
|
isDragging = false;
|
|
192
|
-
document.removeEventListener(
|
|
193
|
-
document.removeEventListener(
|
|
192
|
+
document.removeEventListener("mousemove", handleDragMove);
|
|
193
|
+
document.removeEventListener("mouseup", handleDragEnd);
|
|
194
194
|
if (commitOnRelease && dragTime !== null) {
|
|
195
195
|
onseek?.(dragTime);
|
|
196
196
|
dragTime = null;
|
|
197
197
|
}
|
|
198
198
|
};
|
|
199
199
|
|
|
200
|
-
document.addEventListener(
|
|
201
|
-
document.addEventListener(
|
|
200
|
+
document.addEventListener("mousemove", handleDragMove);
|
|
201
|
+
document.addEventListener("mouseup", handleDragEnd);
|
|
202
202
|
|
|
203
203
|
// Initial seek
|
|
204
204
|
const time = getTimeFromPosition(e.clientX);
|
|
@@ -211,34 +211,69 @@
|
|
|
211
211
|
|
|
212
212
|
let showThumb = $derived(isHovering || isDragging);
|
|
213
213
|
let canShowTooltip = $derived(isLive ? seekableWindow > 0 : Number.isFinite(duration));
|
|
214
|
-
let ariaValueText = $derived(
|
|
214
|
+
let ariaValueText = $derived(
|
|
215
|
+
isLive ? formatLiveTime(displayTime, effectiveLiveEdge) : formatTime(displayTime)
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
// Handle keyboard navigation for accessibility
|
|
219
|
+
function handleKeyDown(e: KeyboardEvent) {
|
|
220
|
+
if (disabled) return;
|
|
221
|
+
const step = e.shiftKey ? 10 : 5; // 5s default, 10s with shift
|
|
222
|
+
const rangeEnd = isLive ? effectiveLiveEdge : duration;
|
|
223
|
+
const rangeStart = isLive ? seekableStart : 0;
|
|
224
|
+
|
|
225
|
+
let newTime: number | null = null;
|
|
226
|
+
switch (e.key) {
|
|
227
|
+
case "ArrowLeft":
|
|
228
|
+
case "ArrowDown":
|
|
229
|
+
newTime = Math.max(rangeStart, currentTime - step);
|
|
230
|
+
break;
|
|
231
|
+
case "ArrowRight":
|
|
232
|
+
case "ArrowUp":
|
|
233
|
+
newTime = Math.min(rangeEnd, currentTime + step);
|
|
234
|
+
break;
|
|
235
|
+
case "Home":
|
|
236
|
+
newTime = rangeStart;
|
|
237
|
+
break;
|
|
238
|
+
case "End":
|
|
239
|
+
newTime = rangeEnd;
|
|
240
|
+
break;
|
|
241
|
+
default:
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
if (newTime !== null) {
|
|
245
|
+
e.preventDefault();
|
|
246
|
+
onseek?.(newTime);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
215
249
|
</script>
|
|
216
250
|
|
|
217
251
|
<div
|
|
218
252
|
bind:this={trackRef}
|
|
219
253
|
class={cn(
|
|
220
|
-
|
|
221
|
-
disabled &&
|
|
254
|
+
"group relative w-full h-6 flex items-center cursor-pointer",
|
|
255
|
+
disabled && "opacity-50 cursor-not-allowed",
|
|
222
256
|
className
|
|
223
257
|
)}
|
|
224
258
|
onmouseenter={() => !disabled && (isHovering = true)}
|
|
225
|
-
onmouseleave={() => {
|
|
259
|
+
onmouseleave={() => {
|
|
260
|
+
isHovering = false;
|
|
261
|
+
isDragging = false;
|
|
262
|
+
}}
|
|
226
263
|
onmousemove={handleMouseMove}
|
|
227
264
|
onclick={handleClick}
|
|
228
265
|
onmousedown={handleMouseDown}
|
|
266
|
+
onkeydown={handleKeyDown}
|
|
229
267
|
role="slider"
|
|
230
268
|
aria-label="Seek"
|
|
231
269
|
aria-valuemin={isLive ? seekableStart : 0}
|
|
232
|
-
aria-valuemax={isLive ? effectiveLiveEdge :
|
|
270
|
+
aria-valuemax={isLive ? effectiveLiveEdge : duration || 100}
|
|
233
271
|
aria-valuenow={displayTime}
|
|
234
272
|
aria-valuetext={ariaValueText}
|
|
235
273
|
tabindex={disabled ? -1 : 0}
|
|
236
274
|
>
|
|
237
275
|
<!-- Track background -->
|
|
238
|
-
<div class={cn(
|
|
239
|
-
'fw-seek-track',
|
|
240
|
-
isDragging && 'fw-seek-track--active'
|
|
241
|
-
)}>
|
|
276
|
+
<div class={cn("fw-seek-track", isDragging && "fw-seek-track--active")}>
|
|
242
277
|
<!-- Buffered segments -->
|
|
243
278
|
{#each bufferedSegments as segment, _index}
|
|
244
279
|
<div
|
|
@@ -247,27 +282,18 @@
|
|
|
247
282
|
></div>
|
|
248
283
|
{/each}
|
|
249
284
|
<!-- Playback progress -->
|
|
250
|
-
<div
|
|
251
|
-
class="fw-seek-progress"
|
|
252
|
-
style="width: {progressPercent}%;"
|
|
253
|
-
></div>
|
|
285
|
+
<div class="fw-seek-progress" style="width: {progressPercent}%;"></div>
|
|
254
286
|
</div>
|
|
255
287
|
|
|
256
288
|
<!-- Thumb -->
|
|
257
289
|
<div
|
|
258
|
-
class={cn(
|
|
259
|
-
'fw-seek-thumb',
|
|
260
|
-
showThumb ? 'fw-seek-thumb--active' : 'fw-seek-thumb--hidden'
|
|
261
|
-
)}
|
|
290
|
+
class={cn("fw-seek-thumb", showThumb ? "fw-seek-thumb--active" : "fw-seek-thumb--hidden")}
|
|
262
291
|
style="left: {progressPercent}%;"
|
|
263
292
|
></div>
|
|
264
293
|
|
|
265
294
|
<!-- Hover time tooltip -->
|
|
266
295
|
{#if isHovering && !isDragging && canShowTooltip}
|
|
267
|
-
<div
|
|
268
|
-
class="fw-seek-tooltip"
|
|
269
|
-
style="left: {hoverPosition}%;"
|
|
270
|
-
>
|
|
296
|
+
<div class="fw-seek-tooltip" style="left: {hoverPosition}%;">
|
|
271
297
|
{isLive ? formatLiveTime(hoverTime, effectiveLiveEdge) : formatTime(hoverTime)}
|
|
272
298
|
</div>
|
|
273
299
|
{/if}
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { onMount } from
|
|
2
|
+
import { onMount } from "svelte";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Skip indicator overlay that appears when double-tapping to skip.
|
|
6
6
|
* Shows the skip direction and amount (e.g., "-10s" or "+10s") with a ripple effect.
|
|
7
7
|
*/
|
|
8
|
-
export type SkipDirection =
|
|
8
|
+
export type SkipDirection = "back" | "forward" | null;
|
|
9
9
|
|
|
10
10
|
let {
|
|
11
11
|
direction = null as SkipDirection,
|
|
12
12
|
seconds = 10,
|
|
13
|
-
class: className =
|
|
13
|
+
class: className = "",
|
|
14
14
|
onhide = undefined as (() => void) | undefined,
|
|
15
15
|
}: {
|
|
16
16
|
direction: SkipDirection;
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
};
|
|
47
47
|
});
|
|
48
48
|
|
|
49
|
-
let isBack = $derived(direction ===
|
|
49
|
+
let isBack = $derived(direction === "back");
|
|
50
50
|
</script>
|
|
51
51
|
|
|
52
52
|
{#if direction}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Skip indicator overlay that appears when double-tapping to skip.
|
|
3
3
|
* Shows the skip direction and amount (e.g., "-10s" or "+10s") with a ripple effect.
|
|
4
4
|
*/
|
|
5
|
-
export type SkipDirection =
|
|
5
|
+
export type SkipDirection = "back" | "forward" | null;
|
|
6
6
|
type $$ComponentProps = {
|
|
7
7
|
direction: SkipDirection;
|
|
8
8
|
seconds?: number;
|
package/dist/StatsPanel.svelte
CHANGED
|
@@ -3,8 +3,13 @@
|
|
|
3
3
|
Port of src/components/StatsPanel.tsx
|
|
4
4
|
-->
|
|
5
5
|
<script lang="ts">
|
|
6
|
-
import {
|
|
7
|
-
|
|
6
|
+
import {
|
|
7
|
+
cn,
|
|
8
|
+
type ContentMetadata,
|
|
9
|
+
type PlaybackQuality,
|
|
10
|
+
type StreamState,
|
|
11
|
+
} from "@livepeer-frameworks/player-core";
|
|
12
|
+
import Button from "./ui/Button.svelte";
|
|
8
13
|
|
|
9
14
|
interface Props {
|
|
10
15
|
isOpen: boolean;
|
|
@@ -31,23 +36,29 @@
|
|
|
31
36
|
}: Props = $props();
|
|
32
37
|
|
|
33
38
|
// Video element stats (reactive)
|
|
34
|
-
let currentRes = $derived(
|
|
39
|
+
let currentRes = $derived(
|
|
40
|
+
videoElement ? `${videoElement.videoWidth}x${videoElement.videoHeight}` : "—"
|
|
41
|
+
);
|
|
35
42
|
let buffered = $derived.by(() => {
|
|
36
|
-
if (!videoElement || videoElement.buffered.length === 0) return
|
|
37
|
-
return (
|
|
43
|
+
if (!videoElement || videoElement.buffered.length === 0) return "—";
|
|
44
|
+
return (
|
|
45
|
+
videoElement.buffered.end(videoElement.buffered.length - 1) - videoElement.currentTime
|
|
46
|
+
).toFixed(1);
|
|
38
47
|
});
|
|
39
|
-
let playbackRate = $derived(videoElement?.playbackRate?.toFixed(2) ??
|
|
48
|
+
let playbackRate = $derived(videoElement?.playbackRate?.toFixed(2) ?? "1.00");
|
|
40
49
|
|
|
41
50
|
// Quality monitor stats
|
|
42
|
-
let qualityScore = $derived(quality?.score?.toFixed(0) ??
|
|
43
|
-
let bitrateKbps = $derived(
|
|
44
|
-
|
|
51
|
+
let qualityScore = $derived(quality?.score?.toFixed(0) ?? "—");
|
|
52
|
+
let bitrateKbps = $derived(
|
|
53
|
+
quality?.bitrate ? `${(quality.bitrate / 1000).toFixed(0)} kbps` : "—"
|
|
54
|
+
);
|
|
55
|
+
let frameDropRate = $derived(quality?.frameDropRate?.toFixed(1) ?? "—");
|
|
45
56
|
let stallCount = $derived(quality?.stallCount ?? 0);
|
|
46
|
-
let latency = $derived(quality?.latency ? `${Math.round(quality.latency)} ms` :
|
|
57
|
+
let latency = $derived(quality?.latency ? `${Math.round(quality.latency)} ms` : "—");
|
|
47
58
|
|
|
48
59
|
// Stream state stats
|
|
49
|
-
let viewers = $derived(metadata?.viewers ??
|
|
50
|
-
let streamStatus = $derived(streamState?.status ?? metadata?.status ??
|
|
60
|
+
let viewers = $derived(metadata?.viewers ?? "—");
|
|
61
|
+
let streamStatus = $derived(streamState?.status ?? metadata?.status ?? "—");
|
|
51
62
|
|
|
52
63
|
const mistInfo = $derived(metadata?.mist ?? streamState?.streamInfo);
|
|
53
64
|
|
|
@@ -59,8 +70,8 @@
|
|
|
59
70
|
codec: t.codec,
|
|
60
71
|
width: t.width,
|
|
61
72
|
height: t.height,
|
|
62
|
-
bitrate: typeof t.bps ===
|
|
63
|
-
fps: typeof t.fpks ===
|
|
73
|
+
bitrate: typeof t.bps === "number" ? Math.round(t.bps) : undefined,
|
|
74
|
+
fps: typeof t.fpks === "number" ? t.fpks / 1000 : undefined,
|
|
64
75
|
channels: t.channels,
|
|
65
76
|
sampleRate: t.rate,
|
|
66
77
|
}));
|
|
@@ -69,16 +80,18 @@
|
|
|
69
80
|
// Format track info
|
|
70
81
|
function formatTracks(): string {
|
|
71
82
|
const tracks = metadata?.tracks ?? deriveTracksFromMist();
|
|
72
|
-
if (!tracks?.length) return
|
|
73
|
-
return tracks
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
83
|
+
if (!tracks?.length) return "—";
|
|
84
|
+
return tracks
|
|
85
|
+
.map((t) => {
|
|
86
|
+
if (t.type === "video") {
|
|
87
|
+
const resolution = t.width && t.height ? `${t.width}x${t.height}` : "?";
|
|
88
|
+
const bitrate = t.bitrate ? `${Math.round(t.bitrate / 1000)}kbps` : "?";
|
|
89
|
+
return `${t.codec ?? "?"} ${resolution}@${bitrate}`;
|
|
90
|
+
}
|
|
91
|
+
const channels = t.channels ? `${t.channels}ch` : "?";
|
|
92
|
+
return `${t.codec ?? "?"} ${channels}`;
|
|
93
|
+
})
|
|
94
|
+
.join(", ");
|
|
82
95
|
}
|
|
83
96
|
|
|
84
97
|
// Build stats array
|
|
@@ -86,44 +99,45 @@
|
|
|
86
99
|
const result: Array<{ label: string; value: string }> = [];
|
|
87
100
|
|
|
88
101
|
if (metadata?.title) {
|
|
89
|
-
result.push({ label:
|
|
102
|
+
result.push({ label: "Title", value: metadata.title });
|
|
90
103
|
}
|
|
91
104
|
|
|
92
105
|
result.push(
|
|
93
|
-
{ label:
|
|
94
|
-
{ label:
|
|
95
|
-
{ label:
|
|
96
|
-
{ label:
|
|
97
|
-
{ label:
|
|
98
|
-
{ label:
|
|
99
|
-
{ label:
|
|
100
|
-
{ label:
|
|
101
|
-
{ label:
|
|
102
|
-
{ label:
|
|
103
|
-
{ label:
|
|
104
|
-
{ label:
|
|
105
|
-
{ label:
|
|
106
|
-
{ label:
|
|
107
|
-
{ label:
|
|
106
|
+
{ label: "Resolution", value: currentRes },
|
|
107
|
+
{ label: "Buffer", value: `${buffered}s` },
|
|
108
|
+
{ label: "Latency", value: latency },
|
|
109
|
+
{ label: "Bitrate", value: bitrateKbps },
|
|
110
|
+
{ label: "Quality Score", value: `${qualityScore}/100` },
|
|
111
|
+
{ label: "Frame Drop Rate", value: `${frameDropRate}%` },
|
|
112
|
+
{ label: "Stalls", value: String(stallCount) },
|
|
113
|
+
{ label: "Playback Rate", value: `${playbackRate}x` },
|
|
114
|
+
{ label: "Protocol", value: protocol ?? "—" },
|
|
115
|
+
{ label: "Node", value: nodeId ?? "—" },
|
|
116
|
+
{ label: "Geo Distance", value: geoDistance ? `${geoDistance.toFixed(0)} km` : "—" },
|
|
117
|
+
{ label: "Viewers", value: String(viewers) },
|
|
118
|
+
{ label: "Status", value: streamStatus },
|
|
119
|
+
{ label: "Tracks", value: formatTracks() },
|
|
120
|
+
{ label: "Mist Type", value: mistInfo?.type ?? "—" },
|
|
108
121
|
{
|
|
109
|
-
label:
|
|
110
|
-
value: mistInfo?.meta?.buffer_window != null
|
|
111
|
-
? String(mistInfo.meta.buffer_window)
|
|
112
|
-
: '—',
|
|
122
|
+
label: "Mist Buffer Window",
|
|
123
|
+
value: mistInfo?.meta?.buffer_window != null ? String(mistInfo.meta.buffer_window) : "—",
|
|
113
124
|
},
|
|
114
|
-
{ label:
|
|
115
|
-
{
|
|
125
|
+
{ label: "Mist Lastms", value: mistInfo?.lastms != null ? String(mistInfo.lastms) : "—" },
|
|
126
|
+
{
|
|
127
|
+
label: "Mist Unixoffset",
|
|
128
|
+
value: mistInfo?.unixoffset != null ? String(mistInfo.unixoffset) : "—",
|
|
129
|
+
}
|
|
116
130
|
);
|
|
117
131
|
|
|
118
132
|
if (metadata?.durationSeconds) {
|
|
119
133
|
const mins = Math.floor(metadata.durationSeconds / 60);
|
|
120
134
|
const secs = metadata.durationSeconds % 60;
|
|
121
|
-
result.push({ label:
|
|
135
|
+
result.push({ label: "Duration", value: `${mins}:${String(secs).padStart(2, "0")}` });
|
|
122
136
|
}
|
|
123
137
|
|
|
124
138
|
if (metadata?.recordingSizeBytes) {
|
|
125
139
|
const mb = (metadata.recordingSizeBytes / (1024 * 1024)).toFixed(1);
|
|
126
|
-
result.push({ label:
|
|
140
|
+
result.push({ label: "Size", value: `${mb} MB` });
|
|
127
141
|
}
|
|
128
142
|
|
|
129
143
|
return result;
|
|
@@ -133,19 +147,17 @@
|
|
|
133
147
|
{#if isOpen}
|
|
134
148
|
<div
|
|
135
149
|
class={cn(
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
150
|
+
"fw-stats-panel absolute top-2 right-2 z-30",
|
|
151
|
+
"bg-black border border-white/10 rounded",
|
|
152
|
+
"text-white text-xs font-mono",
|
|
153
|
+
"max-w-[320px] max-h-[80%] overflow-auto",
|
|
154
|
+
"shadow-lg"
|
|
141
155
|
)}
|
|
142
156
|
style="background-color: #000000;"
|
|
143
157
|
>
|
|
144
158
|
<!-- Header -->
|
|
145
159
|
<div class="flex items-center justify-between px-3 py-2 border-b border-white/10">
|
|
146
|
-
<span class="text-white/70 text-[10px] uppercase tracking-wider">
|
|
147
|
-
Stats Overlay
|
|
148
|
-
</span>
|
|
160
|
+
<span class="text-white/70 text-[10px] uppercase tracking-wider"> Stats Overlay </span>
|
|
149
161
|
<Button
|
|
150
162
|
type="button"
|
|
151
163
|
variant="ghost"
|
|
@@ -153,7 +165,14 @@
|
|
|
153
165
|
class="text-white/50 hover:text-white transition-colors p-1 -mr-1 h-auto w-auto min-w-0"
|
|
154
166
|
aria-label="Close stats panel"
|
|
155
167
|
>
|
|
156
|
-
<svg
|
|
168
|
+
<svg
|
|
169
|
+
width="12"
|
|
170
|
+
height="12"
|
|
171
|
+
viewBox="0 0 12 12"
|
|
172
|
+
fill="none"
|
|
173
|
+
stroke="currentColor"
|
|
174
|
+
stroke-width="1.5"
|
|
175
|
+
>
|
|
157
176
|
<path d="M2 2l8 8M10 2l-8 8" />
|
|
158
177
|
</svg>
|
|
159
178
|
</Button>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ContentMetadata, type PlaybackQuality, type StreamState } from
|
|
1
|
+
import { type ContentMetadata, type PlaybackQuality, type StreamState } from "@livepeer-frameworks/player-core";
|
|
2
2
|
interface Props {
|
|
3
3
|
isOpen: boolean;
|
|
4
4
|
onClose: () => void;
|