@livepeer-frameworks/player-svelte 0.0.3 → 0.1.0
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/README.md +78 -0
- package/dist/DevModePanel.svelte +2 -19
- package/dist/IdleScreen.svelte +3 -3
- package/dist/LoadingScreen.svelte +2 -2
- package/dist/Player.svelte +13 -17
- package/dist/Player.svelte.d.ts +2 -1
- package/dist/PlayerControls.svelte +24 -33
- package/dist/PlayerControls.svelte.d.ts +2 -0
- package/dist/SeekBar.svelte +1 -1
- package/dist/SpeedIndicator.svelte +5 -6
- package/dist/StatsPanel.svelte +37 -20
- package/dist/StatsPanel.svelte.d.ts +2 -14
- package/dist/SubtitleRenderer.svelte +3 -3
- package/dist/components/VolumeIcons.svelte +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/stores/playbackQuality.js +0 -9
- package/dist/stores/playerController.d.ts +1 -1
- package/dist/stores/playerController.js +8 -3
- package/dist/stores/streamState.d.ts +1 -1
- package/dist/stores/streamState.js +1 -1
- package/dist/stores/viewerEndpoints.d.ts +5 -5
- package/dist/stores/viewerEndpoints.js +5 -5
- package/dist/ui/Slider.svelte +1 -1
- package/package.json +1 -1
- package/src/DevModePanel.svelte +2 -19
- package/src/IdleScreen.svelte +3 -3
- package/src/LoadingScreen.svelte +2 -2
- package/src/Player.svelte +12 -17
- package/src/PlayerControls.svelte +3 -25
- package/src/SeekBar.svelte +1 -1
- package/src/StatsPanel.svelte +37 -20
- package/src/SubtitleRenderer.svelte +3 -3
- package/src/components/VolumeIcons.svelte +1 -1
- package/src/index.ts +2 -2
- package/src/stores/playbackQuality.ts +0 -10
- package/src/stores/playerContext.ts +1 -1
- package/src/stores/playerController.ts +4 -4
- package/src/stores/streamState.ts +1 -1
- package/src/stores/viewerEndpoints.ts +7 -7
- package/src/ui/Slider.svelte +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# @livepeer-frameworks/player-svelte
|
|
2
|
+
|
|
3
|
+
Svelte wrapper for the FrameWorks player. Resolves endpoints via Gateway or Mist and renders the best available player (WebCodecs, HLS, etc).
|
|
4
|
+
|
|
5
|
+
**Docs:** `docs.frameworks.network`
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm add @livepeer-frameworks/player-svelte
|
|
11
|
+
# or
|
|
12
|
+
npm i @livepeer-frameworks/player-svelte
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
```svelte
|
|
18
|
+
<script lang="ts">
|
|
19
|
+
import { Player } from '@livepeer-frameworks/player-svelte';
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<div style="width: 100%; height: 500px;">
|
|
23
|
+
<Player
|
|
24
|
+
contentType="live"
|
|
25
|
+
contentId="pk_..." // playbackId
|
|
26
|
+
options={{ gatewayUrl: 'https://your-bridge/graphql' }}
|
|
27
|
+
/>
|
|
28
|
+
</div>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Notes:
|
|
32
|
+
- There is **no default gateway**; provide `gatewayUrl` unless you pass `endpoints` or `mistUrl`.
|
|
33
|
+
|
|
34
|
+
### Direct MistServer Node (mistUrl)
|
|
35
|
+
|
|
36
|
+
```svelte
|
|
37
|
+
<Player
|
|
38
|
+
contentType="live"
|
|
39
|
+
contentId="pk_..."
|
|
40
|
+
options={{ mistUrl: 'https://edge.example.com' }}
|
|
41
|
+
/>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Styles
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
import '@livepeer-frameworks/player-svelte/player.css';
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Controls & Shortcuts
|
|
51
|
+
|
|
52
|
+
The player ships with keyboard/mouse shortcuts when the player is focused (click/tap once).
|
|
53
|
+
|
|
54
|
+
**Keyboard**
|
|
55
|
+
| Shortcut | Action | Notes |
|
|
56
|
+
|---|---|---|
|
|
57
|
+
| Space | Play/Pause | Hold = 2x speed (when seekable) |
|
|
58
|
+
| K | Play/Pause | YouTube-style |
|
|
59
|
+
| J / Left | Skip back 10s | Disabled on live-only |
|
|
60
|
+
| L / Right | Skip forward 10s | Disabled on live-only |
|
|
61
|
+
| Up / Down | Volume +/-10% | - |
|
|
62
|
+
| M | Mute/Unmute | - |
|
|
63
|
+
| F | Fullscreen toggle | - |
|
|
64
|
+
| C | Captions toggle | - |
|
|
65
|
+
| 0-9 | Seek to 0-90% | Disabled on live-only |
|
|
66
|
+
| , / . | Prev/Next frame (paused) | WebCodecs = true step; others = buffered-only |
|
|
67
|
+
|
|
68
|
+
**Mouse / Touch**
|
|
69
|
+
| Gesture | Action | Notes |
|
|
70
|
+
|---|---|---|
|
|
71
|
+
| Double-click | Fullscreen toggle | Desktop |
|
|
72
|
+
| Double-tap (left/right) | Skip +/-10s | Touch only, disabled on live-only |
|
|
73
|
+
| Click/Tap and hold | 2x speed | Disabled on live-only |
|
|
74
|
+
|
|
75
|
+
**Constraints**
|
|
76
|
+
- Live-only streams disable seeking/skip/2x hold and frame-step.
|
|
77
|
+
- Live with DVR buffer enables the same shortcuts as VOD.
|
|
78
|
+
- Frame stepping only moves within already buffered ranges (no network seek). WebCodecs supports true frame stepping when paused.
|
package/dist/DevModePanel.svelte
CHANGED
|
@@ -11,8 +11,6 @@
|
|
|
11
11
|
type MistStreamInfo,
|
|
12
12
|
type PlaybackMode,
|
|
13
13
|
} from '@livepeer-frameworks/player-core';
|
|
14
|
-
import Button from './ui/Button.svelte';
|
|
15
|
-
import Badge from './ui/Badge.svelte';
|
|
16
14
|
|
|
17
15
|
/** Short labels for source types */
|
|
18
16
|
const SOURCE_TYPE_LABELS: Record<string, string> = {
|
|
@@ -56,7 +54,7 @@
|
|
|
56
54
|
videoElement = null,
|
|
57
55
|
protocol = undefined,
|
|
58
56
|
nodeId = undefined,
|
|
59
|
-
isVisible = true,
|
|
57
|
+
isVisible: _isVisible = true,
|
|
60
58
|
isOpen: controlledIsOpen = undefined,
|
|
61
59
|
onOpenChange = undefined,
|
|
62
60
|
}: Props = $props();
|
|
@@ -258,22 +256,7 @@
|
|
|
258
256
|
});
|
|
259
257
|
</script>
|
|
260
258
|
|
|
261
|
-
{#if
|
|
262
|
-
<button
|
|
263
|
-
type="button"
|
|
264
|
-
onclick={() => setIsOpen(true)}
|
|
265
|
-
class={cn(
|
|
266
|
-
'fw-dev-toggle',
|
|
267
|
-
isVisible ? '' : 'fw-dev-toggle--hidden'
|
|
268
|
-
)}
|
|
269
|
-
title="Advanced Settings"
|
|
270
|
-
aria-label="Open advanced settings panel"
|
|
271
|
-
>
|
|
272
|
-
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
273
|
-
<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z" />
|
|
274
|
-
</svg>
|
|
275
|
-
</button>
|
|
276
|
-
{:else}
|
|
259
|
+
{#if isOpen}
|
|
277
260
|
<div class="fw-dev-panel">
|
|
278
261
|
<!-- Header with tabs -->
|
|
279
262
|
<div class="fw-dev-header">
|
package/dist/IdleScreen.svelte
CHANGED
|
@@ -299,7 +299,7 @@
|
|
|
299
299
|
}
|
|
300
300
|
}
|
|
301
301
|
|
|
302
|
-
let
|
|
302
|
+
let _statusLabel = $derived(getStatusLabel(status));
|
|
303
303
|
let showRetry = $derived((status === 'ERROR' || status === 'INVALID') && onRetry);
|
|
304
304
|
let showProgress = $derived(status === 'INITIALIZING' && percentage !== undefined);
|
|
305
305
|
let displayMessage = $derived(error || message);
|
|
@@ -646,7 +646,7 @@
|
|
|
646
646
|
{/each}
|
|
647
647
|
|
|
648
648
|
<!-- Floating particles -->
|
|
649
|
-
{#each particles as particle,
|
|
649
|
+
{#each particles as particle, _i}
|
|
650
650
|
<div
|
|
651
651
|
class="particle"
|
|
652
652
|
style="
|
|
@@ -661,7 +661,7 @@
|
|
|
661
661
|
{/each}
|
|
662
662
|
|
|
663
663
|
<!-- Animated bubbles -->
|
|
664
|
-
{#each bubbles as bubble,
|
|
664
|
+
{#each bubbles as bubble, _i}
|
|
665
665
|
<div
|
|
666
666
|
class="bubble"
|
|
667
667
|
style="
|
|
@@ -623,7 +623,7 @@
|
|
|
623
623
|
{/each}
|
|
624
624
|
|
|
625
625
|
<!-- Floating particles -->
|
|
626
|
-
{#each particles as particle,
|
|
626
|
+
{#each particles as particle, _i}
|
|
627
627
|
<div
|
|
628
628
|
class="particle"
|
|
629
629
|
style="
|
|
@@ -638,7 +638,7 @@
|
|
|
638
638
|
{/each}
|
|
639
639
|
|
|
640
640
|
<!-- Animated bubbles -->
|
|
641
|
-
{#each bubbles as bubble,
|
|
641
|
+
{#each bubbles as bubble, _i}
|
|
642
642
|
<div
|
|
643
643
|
class="bubble"
|
|
644
644
|
style="
|
package/dist/Player.svelte
CHANGED
|
@@ -3,9 +3,8 @@
|
|
|
3
3
|
Thin wrapper over PlayerController from @livepeer-frameworks/player-core
|
|
4
4
|
-->
|
|
5
5
|
<script lang="ts">
|
|
6
|
-
import { onMount
|
|
6
|
+
import { onMount } from 'svelte';
|
|
7
7
|
import IdleScreen from './IdleScreen.svelte';
|
|
8
|
-
import LoadingScreen from './LoadingScreen.svelte';
|
|
9
8
|
import SubtitleRenderer from './SubtitleRenderer.svelte';
|
|
10
9
|
import PlayerControls from './PlayerControls.svelte';
|
|
11
10
|
import SpeedIndicator from './SpeedIndicator.svelte';
|
|
@@ -22,7 +21,7 @@
|
|
|
22
21
|
ContextMenuSeparator,
|
|
23
22
|
} from './ui/context-menu';
|
|
24
23
|
import { StatsIcon, SettingsIcon, PictureInPictureIcon } from './icons';
|
|
25
|
-
import { cn, type PlaybackMode, type ContentEndpoints, type
|
|
24
|
+
import { cn, type PlaybackMode, type ContentEndpoints, type PlayerState, type PlayerStateContext, type ContentType, type EndpointInfo, type PlayerMetadata } from '@livepeer-frameworks/player-core';
|
|
26
25
|
import { createPlayerControllerStore, type PlayerControllerStore } from './stores/playerController';
|
|
27
26
|
import type { SkipDirection } from './SkipIndicator.svelte';
|
|
28
27
|
|
|
@@ -48,15 +47,17 @@
|
|
|
48
47
|
playbackMode?: PlaybackMode;
|
|
49
48
|
};
|
|
50
49
|
onStateChange?: (state: PlayerState, context?: PlayerStateContext) => void;
|
|
50
|
+
onMetadata?: (metadata: PlayerMetadata) => void;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
let {
|
|
54
54
|
contentId,
|
|
55
|
-
contentType
|
|
55
|
+
contentType,
|
|
56
56
|
thumbnailUrl = null,
|
|
57
57
|
endpoints = undefined,
|
|
58
58
|
options = {},
|
|
59
59
|
onStateChange = undefined,
|
|
60
|
+
onMetadata = undefined,
|
|
60
61
|
}: Props = $props();
|
|
61
62
|
|
|
62
63
|
// ============================================================================
|
|
@@ -141,12 +142,18 @@
|
|
|
141
142
|
debug('playerStore created');
|
|
142
143
|
|
|
143
144
|
// Subscribe to store state
|
|
145
|
+
let prevMetadata: PlayerMetadata | null = null;
|
|
144
146
|
const unsubscribe = playerStore.subscribe(state => {
|
|
145
147
|
storeState = state;
|
|
146
148
|
// Forward state changes to prop callback
|
|
147
149
|
if (onStateChange && state.state) {
|
|
148
150
|
onStateChange(state.state);
|
|
149
151
|
}
|
|
152
|
+
// Forward metadata changes to prop callback
|
|
153
|
+
if (onMetadata && state.metadata && state.metadata !== prevMetadata) {
|
|
154
|
+
prevMetadata = state.metadata;
|
|
155
|
+
onMetadata(state.metadata);
|
|
156
|
+
}
|
|
150
157
|
});
|
|
151
158
|
|
|
152
159
|
return () => {
|
|
@@ -277,19 +284,7 @@
|
|
|
277
284
|
isOpen={isStatsOpen}
|
|
278
285
|
onClose={() => isStatsOpen = false}
|
|
279
286
|
{metadata}
|
|
280
|
-
streamState={storeState.streamState
|
|
281
|
-
status: storeState.streamState.status,
|
|
282
|
-
viewers: metadata?.viewers,
|
|
283
|
-
tracks: storeState.streamState.streamInfo?.meta?.tracks
|
|
284
|
-
? Object.values(storeState.streamState.streamInfo.meta.tracks).map((t: any) => ({
|
|
285
|
-
type: t.type,
|
|
286
|
-
codec: t.codec,
|
|
287
|
-
width: t.width,
|
|
288
|
-
height: t.height,
|
|
289
|
-
bps: t.bps,
|
|
290
|
-
}))
|
|
291
|
-
: [],
|
|
292
|
-
} : null}
|
|
287
|
+
streamState={storeState.streamState}
|
|
293
288
|
quality={storeState.playbackQuality}
|
|
294
289
|
videoElement={storeState.videoElement}
|
|
295
290
|
protocol={primaryEndpoint?.protocol}
|
|
@@ -420,6 +415,7 @@
|
|
|
420
415
|
isStatsOpen={isStatsOpen}
|
|
421
416
|
onStatsToggle={() => isStatsOpen = !isStatsOpen}
|
|
422
417
|
isContentLive={storeState.isEffectivelyLive}
|
|
418
|
+
onJumpToLive={() => playerStore?.getController()?.jumpToLive()}
|
|
423
419
|
/>
|
|
424
420
|
{/if}
|
|
425
421
|
</div>
|
package/dist/Player.svelte.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type PlaybackMode, type ContentEndpoints, type PlayerState, type PlayerStateContext, type ContentType } from '@livepeer-frameworks/player-core';
|
|
1
|
+
import { type PlaybackMode, type ContentEndpoints, type PlayerState, type PlayerStateContext, type ContentType, type PlayerMetadata } from '@livepeer-frameworks/player-core';
|
|
2
2
|
interface Props {
|
|
3
3
|
contentId: string;
|
|
4
4
|
contentType?: ContentType;
|
|
@@ -20,6 +20,7 @@ interface Props {
|
|
|
20
20
|
playbackMode?: PlaybackMode;
|
|
21
21
|
};
|
|
22
22
|
onStateChange?: (state: PlayerState, context?: PlayerStateContext) => void;
|
|
23
|
+
onMetadata?: (metadata: PlayerMetadata) => void;
|
|
23
24
|
}
|
|
24
25
|
declare const Player: import("svelte").Component<Props, {}, "">;
|
|
25
26
|
type Player = ReturnType<typeof Player>;
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
type PlaybackMode,
|
|
7
7
|
// Seeking utilities from core
|
|
8
8
|
SPEED_PRESETS,
|
|
9
|
-
getLatencyTier,
|
|
10
9
|
isMediaStreamSource,
|
|
11
10
|
supportsPlaybackRate as coreSupportsPlaybackRate,
|
|
12
11
|
calculateSeekableRange,
|
|
@@ -15,7 +14,6 @@
|
|
|
15
14
|
calculateIsNearLive,
|
|
16
15
|
isLiveContent,
|
|
17
16
|
// Time formatting from core
|
|
18
|
-
formatTime,
|
|
19
17
|
formatTimeDisplay,
|
|
20
18
|
} from '@livepeer-frameworks/player-core';
|
|
21
19
|
import SeekBar from './SeekBar.svelte';
|
|
@@ -49,6 +47,8 @@
|
|
|
49
47
|
onStatsToggle?: () => void;
|
|
50
48
|
/** Content-type based live flag (for mode selector visibility, separate from seek bar isLive) */
|
|
51
49
|
isContentLive?: boolean;
|
|
50
|
+
/** Jump to live edge callback */
|
|
51
|
+
onJumpToLive?: () => void;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
let {
|
|
@@ -65,6 +65,7 @@
|
|
|
65
65
|
isStatsOpen = false,
|
|
66
66
|
onStatsToggle = undefined,
|
|
67
67
|
isContentLive = undefined,
|
|
68
|
+
onJumpToLive = undefined,
|
|
68
69
|
}: Props = $props();
|
|
69
70
|
|
|
70
71
|
// Video element discovery
|
|
@@ -116,7 +117,7 @@
|
|
|
116
117
|
let showSettingsMenu = $state(false);
|
|
117
118
|
let isNearLiveState = $state(true);
|
|
118
119
|
let buffered: TimeRanges | undefined = $state(undefined);
|
|
119
|
-
let
|
|
120
|
+
let _hasSeekToLive = false; // Track if we've auto-seeked to live
|
|
120
121
|
let qualityValue = $state('auto');
|
|
121
122
|
let captionValue = $state('none');
|
|
122
123
|
|
|
@@ -193,6 +194,10 @@
|
|
|
193
194
|
return null;
|
|
194
195
|
}
|
|
195
196
|
|
|
197
|
+
const allowMediaStreamDvr = isMediaStreamSource(video)
|
|
198
|
+
&& (bufferWindowMs !== undefined && bufferWindowMs > 0)
|
|
199
|
+
&& (sourceType !== 'whep' && sourceType !== 'webrtc');
|
|
200
|
+
|
|
196
201
|
// Seekable range using core calculation (allow player override)
|
|
197
202
|
let seekableRange = $derived.by(() => getPlayerSeekableRange() ?? calculateSeekableRange({
|
|
198
203
|
isLive,
|
|
@@ -200,6 +205,7 @@
|
|
|
200
205
|
mistStreamInfo,
|
|
201
206
|
currentTime,
|
|
202
207
|
duration,
|
|
208
|
+
allowMediaStreamDvr,
|
|
203
209
|
}));
|
|
204
210
|
let seekableStart = $derived(seekableRange.seekableStart);
|
|
205
211
|
let liveEdge = $derived(seekableRange.liveEdge);
|
|
@@ -210,7 +216,7 @@
|
|
|
210
216
|
let liveThresholds = $derived(calculateLiveThresholds(sourceType, isWebRTC, bufferWindowMs));
|
|
211
217
|
|
|
212
218
|
// Can seek - check player's canSeek method first (for WebCodecs, MEWS server-side seeking)
|
|
213
|
-
let
|
|
219
|
+
let baseCanSeek = $derived.by(() => {
|
|
214
220
|
// Check if current player has canSeek method
|
|
215
221
|
const player = globalPlayerManager.getCurrentPlayer();
|
|
216
222
|
if (player && typeof (player as any).canSeek === 'function') {
|
|
@@ -224,12 +230,17 @@
|
|
|
224
230
|
bufferWindowMs,
|
|
225
231
|
});
|
|
226
232
|
});
|
|
233
|
+
let canSeek = $derived(baseCanSeek && (!isLive || hasDvrWindow));
|
|
227
234
|
|
|
228
235
|
// Update state from video events
|
|
229
236
|
$effect(() => {
|
|
230
237
|
if (!video) return;
|
|
231
238
|
|
|
232
|
-
function updatePlayingState() {
|
|
239
|
+
function updatePlayingState() {
|
|
240
|
+
const player = globalPlayerManager.getCurrentPlayer();
|
|
241
|
+
const paused = player?.isPaused?.() ?? video!.paused;
|
|
242
|
+
isPlaying = !paused;
|
|
243
|
+
}
|
|
233
244
|
function updateMutedState() {
|
|
234
245
|
isMuted = video!.muted || video!.volume === 0;
|
|
235
246
|
const vol = video!.volume;
|
|
@@ -274,7 +285,7 @@
|
|
|
274
285
|
// Reset seek-to-live flag when video element changes
|
|
275
286
|
$effect(() => {
|
|
276
287
|
if (video) {
|
|
277
|
-
|
|
288
|
+
_hasSeekToLive = false;
|
|
278
289
|
}
|
|
279
290
|
});
|
|
280
291
|
|
|
@@ -298,7 +309,7 @@
|
|
|
298
309
|
}));
|
|
299
310
|
|
|
300
311
|
// Seek value for slider
|
|
301
|
-
let
|
|
312
|
+
let _seekValue = $derived.by(() => {
|
|
302
313
|
if (isLive) {
|
|
303
314
|
const window = liveEdge - seekableStart;
|
|
304
315
|
if (window <= 0) return 1000;
|
|
@@ -374,26 +385,6 @@
|
|
|
374
385
|
isMuted = next === 0;
|
|
375
386
|
}
|
|
376
387
|
|
|
377
|
-
function handleSeekChange(val: number) {
|
|
378
|
-
if (disabled || !video) return;
|
|
379
|
-
if (isLive) {
|
|
380
|
-
const window = liveEdge - seekableStart;
|
|
381
|
-
const newTime = seekableStart + (val / 1000) * window;
|
|
382
|
-
if (onseek) {
|
|
383
|
-
onseek(newTime);
|
|
384
|
-
} else {
|
|
385
|
-
video.currentTime = newTime;
|
|
386
|
-
}
|
|
387
|
-
} else if (Number.isFinite(duration)) {
|
|
388
|
-
const newTime = (val / 1000) * duration;
|
|
389
|
-
if (onseek) {
|
|
390
|
-
onseek(newTime);
|
|
391
|
-
} else {
|
|
392
|
-
video.currentTime = newTime;
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
|
|
397
388
|
function handleFullscreen() {
|
|
398
389
|
if (disabled) return;
|
|
399
390
|
const container = document.querySelector('[data-player-container="true"]') as HTMLElement | null;
|
|
@@ -407,6 +398,10 @@
|
|
|
407
398
|
|
|
408
399
|
function handleGoLive() {
|
|
409
400
|
if (disabled || !video) return;
|
|
401
|
+
if (onJumpToLive) {
|
|
402
|
+
onJumpToLive();
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
410
405
|
globalPlayerManager.getCurrentPlayer()?.jumpToLive?.();
|
|
411
406
|
}
|
|
412
407
|
|
|
@@ -573,15 +568,11 @@
|
|
|
573
568
|
disabled={!hasDvrWindow || isNearLiveState}
|
|
574
569
|
class={cn(
|
|
575
570
|
'fw-live-badge',
|
|
576
|
-
(!hasDvrWindow || isNearLiveState) ? 'fw-live-badge--active' : 'fw-live-badge--behind'
|
|
577
|
-
!hasDvrWindow && 'fw-live-badge--nodvr'
|
|
571
|
+
(!hasDvrWindow || isNearLiveState) ? 'fw-live-badge--active' : 'fw-live-badge--behind'
|
|
578
572
|
)}
|
|
579
|
-
title={!hasDvrWindow ? 'Live only
|
|
573
|
+
title={!hasDvrWindow ? 'Live only' : (isNearLiveState ? 'At live edge' : 'Jump to live')}
|
|
580
574
|
>
|
|
581
575
|
LIVE
|
|
582
|
-
{#if !hasDvrWindow}
|
|
583
|
-
<span class="fw-live-badge__nodvr">NO DVR</span>
|
|
584
|
-
{/if}
|
|
585
576
|
{#if !isNearLiveState && hasDvrWindow}
|
|
586
577
|
<SeekToLiveIcon size={10} />
|
|
587
578
|
{/if}
|
|
@@ -14,6 +14,8 @@ interface Props {
|
|
|
14
14
|
onStatsToggle?: () => void;
|
|
15
15
|
/** Content-type based live flag (for mode selector visibility, separate from seek bar isLive) */
|
|
16
16
|
isContentLive?: boolean;
|
|
17
|
+
/** Jump to live edge callback */
|
|
18
|
+
onJumpToLive?: () => void;
|
|
17
19
|
}
|
|
18
20
|
declare const PlayerControls: import("svelte").Component<Props, {}, "">;
|
|
19
21
|
type PlayerControls = ReturnType<typeof PlayerControls>;
|
package/dist/SeekBar.svelte
CHANGED
|
@@ -240,7 +240,7 @@
|
|
|
240
240
|
isDragging && 'fw-seek-track--active'
|
|
241
241
|
)}>
|
|
242
242
|
<!-- Buffered segments -->
|
|
243
|
-
{#each bufferedSegments as segment,
|
|
243
|
+
{#each bufferedSegments as segment, _index}
|
|
244
244
|
<div
|
|
245
245
|
class="fw-seek-buffered"
|
|
246
246
|
style="left: {segment.startPercent}%; width: {segment.endPercent - segment.startPercent}%;"
|
|
@@ -15,22 +15,21 @@
|
|
|
15
15
|
</script>
|
|
16
16
|
|
|
17
17
|
<div
|
|
18
|
-
class="fw-speed-indicator absolute
|
|
19
|
-
flex items-center justify-center
|
|
18
|
+
class="fw-speed-indicator absolute top-3 right-3 z-30 pointer-events-none
|
|
20
19
|
transition-opacity duration-150
|
|
21
20
|
{isVisible ? 'opacity-100' : 'opacity-0'}
|
|
22
21
|
{className}"
|
|
23
22
|
>
|
|
24
23
|
<div
|
|
25
|
-
class="bg-black/
|
|
26
|
-
text-
|
|
24
|
+
class="bg-black/60 text-white px-2.5 py-1 rounded-md
|
|
25
|
+
text-xs font-semibold tabular-nums
|
|
27
26
|
flex items-center gap-2
|
|
28
|
-
|
|
27
|
+
border border-white/15
|
|
29
28
|
transform transition-transform duration-150
|
|
30
29
|
{isVisible ? 'scale-100' : 'scale-90'}"
|
|
31
30
|
>
|
|
32
31
|
<!-- Fast-forward icon -->
|
|
33
|
-
<svg viewBox="0 0 24 24" fill="currentColor" class="w-
|
|
32
|
+
<svg viewBox="0 0 24 24" fill="currentColor" class="w-4 h-4" aria-hidden="true">
|
|
34
33
|
<path d="M4 18l8.5-6L4 6v12zm9-12v12l8.5-6L13 6z" />
|
|
35
34
|
</svg>
|
|
36
35
|
<span>{speed}x</span>
|
package/dist/StatsPanel.svelte
CHANGED
|
@@ -3,27 +3,14 @@
|
|
|
3
3
|
Port of src/components/StatsPanel.tsx
|
|
4
4
|
-->
|
|
5
5
|
<script lang="ts">
|
|
6
|
-
import { cn, type ContentMetadata, type PlaybackQuality } from '@livepeer-frameworks/player-core';
|
|
6
|
+
import { cn, type ContentMetadata, type PlaybackQuality, type StreamState } from '@livepeer-frameworks/player-core';
|
|
7
7
|
import Button from './ui/Button.svelte';
|
|
8
8
|
|
|
9
|
-
interface StreamStateInfo {
|
|
10
|
-
status?: string;
|
|
11
|
-
viewers?: number;
|
|
12
|
-
tracks?: Array<{
|
|
13
|
-
type: string;
|
|
14
|
-
codec: string;
|
|
15
|
-
width?: number;
|
|
16
|
-
height?: number;
|
|
17
|
-
bps?: number;
|
|
18
|
-
channels?: number;
|
|
19
|
-
}>;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
9
|
interface Props {
|
|
23
10
|
isOpen: boolean;
|
|
24
11
|
onClose: () => void;
|
|
25
12
|
metadata?: ContentMetadata | null;
|
|
26
|
-
streamState?:
|
|
13
|
+
streamState?: StreamState | null;
|
|
27
14
|
quality?: PlaybackQuality | null;
|
|
28
15
|
videoElement?: HTMLVideoElement | null;
|
|
29
16
|
protocol?: string;
|
|
@@ -59,17 +46,38 @@
|
|
|
59
46
|
let latency = $derived(quality?.latency ? `${Math.round(quality.latency)} ms` : '—');
|
|
60
47
|
|
|
61
48
|
// Stream state stats
|
|
62
|
-
let viewers = $derived(
|
|
49
|
+
let viewers = $derived(metadata?.viewers ?? '—');
|
|
63
50
|
let streamStatus = $derived(streamState?.status ?? metadata?.status ?? '—');
|
|
64
51
|
|
|
52
|
+
const mistInfo = $derived(metadata?.mist ?? streamState?.streamInfo);
|
|
53
|
+
|
|
54
|
+
function deriveTracksFromMist() {
|
|
55
|
+
const mistTracks = mistInfo?.meta?.tracks;
|
|
56
|
+
if (!mistTracks) return undefined;
|
|
57
|
+
return Object.values(mistTracks).map((t: any) => ({
|
|
58
|
+
type: t.type,
|
|
59
|
+
codec: t.codec,
|
|
60
|
+
width: t.width,
|
|
61
|
+
height: t.height,
|
|
62
|
+
bitrate: typeof t.bps === 'number' ? Math.round(t.bps) : undefined,
|
|
63
|
+
fps: typeof t.fpks === 'number' ? t.fpks / 1000 : undefined,
|
|
64
|
+
channels: t.channels,
|
|
65
|
+
sampleRate: t.rate,
|
|
66
|
+
}));
|
|
67
|
+
}
|
|
68
|
+
|
|
65
69
|
// Format track info
|
|
66
70
|
function formatTracks(): string {
|
|
67
|
-
|
|
68
|
-
|
|
71
|
+
const tracks = metadata?.tracks ?? deriveTracksFromMist();
|
|
72
|
+
if (!tracks?.length) return '—';
|
|
73
|
+
return tracks.map(t => {
|
|
69
74
|
if (t.type === 'video') {
|
|
70
|
-
|
|
75
|
+
const resolution = t.width && t.height ? `${t.width}x${t.height}` : '?';
|
|
76
|
+
const bitrate = t.bitrate ? `${Math.round(t.bitrate / 1000)}kbps` : '?';
|
|
77
|
+
return `${t.codec ?? '?'} ${resolution}@${bitrate}`;
|
|
71
78
|
}
|
|
72
|
-
|
|
79
|
+
const channels = t.channels ? `${t.channels}ch` : '?';
|
|
80
|
+
return `${t.codec ?? '?'} ${channels}`;
|
|
73
81
|
}).join(', ');
|
|
74
82
|
}
|
|
75
83
|
|
|
@@ -96,6 +104,15 @@
|
|
|
96
104
|
{ label: 'Viewers', value: String(viewers) },
|
|
97
105
|
{ label: 'Status', value: streamStatus },
|
|
98
106
|
{ label: 'Tracks', value: formatTracks() },
|
|
107
|
+
{ label: 'Mist Type', value: mistInfo?.type ?? '—' },
|
|
108
|
+
{
|
|
109
|
+
label: 'Mist Buffer Window',
|
|
110
|
+
value: mistInfo?.meta?.buffer_window != null
|
|
111
|
+
? String(mistInfo.meta.buffer_window)
|
|
112
|
+
: '—',
|
|
113
|
+
},
|
|
114
|
+
{ label: 'Mist Lastms', value: mistInfo?.lastms != null ? String(mistInfo.lastms) : '—' },
|
|
115
|
+
{ label: 'Mist Unixoffset', value: mistInfo?.unixoffset != null ? String(mistInfo.unixoffset) : '—' },
|
|
99
116
|
);
|
|
100
117
|
|
|
101
118
|
if (metadata?.durationSeconds) {
|
|
@@ -1,21 +1,9 @@
|
|
|
1
|
-
import { type ContentMetadata, type PlaybackQuality } from '@livepeer-frameworks/player-core';
|
|
2
|
-
interface StreamStateInfo {
|
|
3
|
-
status?: string;
|
|
4
|
-
viewers?: number;
|
|
5
|
-
tracks?: Array<{
|
|
6
|
-
type: string;
|
|
7
|
-
codec: string;
|
|
8
|
-
width?: number;
|
|
9
|
-
height?: number;
|
|
10
|
-
bps?: number;
|
|
11
|
-
channels?: number;
|
|
12
|
-
}>;
|
|
13
|
-
}
|
|
1
|
+
import { type ContentMetadata, type PlaybackQuality, type StreamState } from '@livepeer-frameworks/player-core';
|
|
14
2
|
interface Props {
|
|
15
3
|
isOpen: boolean;
|
|
16
4
|
onClose: () => void;
|
|
17
5
|
metadata?: ContentMetadata | null;
|
|
18
|
-
streamState?:
|
|
6
|
+
streamState?: StreamState | null;
|
|
19
7
|
quality?: PlaybackQuality | null;
|
|
20
8
|
videoElement?: HTMLVideoElement | null;
|
|
21
9
|
protocol?: string;
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
// State
|
|
79
79
|
let liveCues = $state<SubtitleCue[]>([]);
|
|
80
80
|
let displayedText = $state<string>('');
|
|
81
|
-
let
|
|
81
|
+
let _lastCueId: string | null = null;
|
|
82
82
|
let unsubscribe: (() => void) | null = null;
|
|
83
83
|
|
|
84
84
|
// Merged style
|
|
@@ -166,10 +166,10 @@
|
|
|
166
166
|
|
|
167
167
|
if (activeCue) {
|
|
168
168
|
displayedText = activeCue.text;
|
|
169
|
-
|
|
169
|
+
_lastCueId = activeCue.id;
|
|
170
170
|
} else {
|
|
171
171
|
displayedText = '';
|
|
172
|
-
|
|
172
|
+
_lastCueId = null;
|
|
173
173
|
}
|
|
174
174
|
});
|
|
175
175
|
|
package/dist/index.d.ts
CHANGED
|
@@ -12,11 +12,11 @@
|
|
|
12
12
|
* @example
|
|
13
13
|
* ```svelte
|
|
14
14
|
* <script>
|
|
15
|
-
* import { Player } from '@livepeer-frameworks/player
|
|
15
|
+
* import { Player } from '@livepeer-frameworks/player-svelte';
|
|
16
16
|
* </script>
|
|
17
17
|
*
|
|
18
18
|
* <Player
|
|
19
|
-
* contentId="
|
|
19
|
+
* contentId="pk_..."
|
|
20
20
|
* contentType="live"
|
|
21
21
|
* options={{ gatewayUrl: "https://gateway.example.com/graphql", devMode: true }}
|
|
22
22
|
* autoplay
|
package/dist/index.js
CHANGED
|
@@ -12,11 +12,11 @@
|
|
|
12
12
|
* @example
|
|
13
13
|
* ```svelte
|
|
14
14
|
* <script>
|
|
15
|
-
* import { Player } from '@livepeer-frameworks/player
|
|
15
|
+
* import { Player } from '@livepeer-frameworks/player-svelte';
|
|
16
16
|
* </script>
|
|
17
17
|
*
|
|
18
18
|
* <Player
|
|
19
|
-
* contentId="
|
|
19
|
+
* contentId="pk_..."
|
|
20
20
|
* contentType="live"
|
|
21
21
|
* options={{ gatewayUrl: "https://gateway.example.com/graphql", devMode: true }}
|
|
22
22
|
* autoplay
|
|
@@ -5,15 +5,6 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { writable, derived } from 'svelte/store';
|
|
7
7
|
import { QualityMonitor } from '@livepeer-frameworks/player-core';
|
|
8
|
-
const initialQuality = {
|
|
9
|
-
score: 100,
|
|
10
|
-
bitrate: 0,
|
|
11
|
-
bufferedAhead: 0,
|
|
12
|
-
frameDropRate: 0,
|
|
13
|
-
stallCount: 0,
|
|
14
|
-
latency: 0,
|
|
15
|
-
timestamp: Date.now(),
|
|
16
|
-
};
|
|
17
8
|
/**
|
|
18
9
|
* Create a playback quality monitoring store.
|
|
19
10
|
*
|
|
@@ -145,7 +145,7 @@ export interface PlayerControllerStore extends Readable<PlayerControllerState> {
|
|
|
145
145
|
* let containerEl: HTMLElement;
|
|
146
146
|
*
|
|
147
147
|
* const playerStore = createPlayerControllerStore({
|
|
148
|
-
* contentId: '
|
|
148
|
+
* contentId: 'pk_...',
|
|
149
149
|
* contentType: 'live',
|
|
150
150
|
* gatewayUrl: 'https://gateway.example.com/graphql',
|
|
151
151
|
* });
|