@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
|
@@ -52,7 +52,7 @@ const initialState = {
|
|
|
52
52
|
* let containerEl: HTMLElement;
|
|
53
53
|
*
|
|
54
54
|
* const playerStore = createPlayerControllerStore({
|
|
55
|
-
* contentId: '
|
|
55
|
+
* contentId: 'pk_...',
|
|
56
56
|
* contentType: 'live',
|
|
57
57
|
* gatewayUrl: 'https://gateway.example.com/graphql',
|
|
58
58
|
* });
|
|
@@ -122,6 +122,7 @@ export function createPlayerControllerStore(config) {
|
|
|
122
122
|
store.update(prev => ({
|
|
123
123
|
...prev,
|
|
124
124
|
streamState,
|
|
125
|
+
metadata: controller.getMetadata(),
|
|
125
126
|
isEffectivelyLive: controller.isEffectivelyLive(),
|
|
126
127
|
shouldShowIdleScreen: controller.shouldShowIdleScreen(),
|
|
127
128
|
}));
|
|
@@ -152,7 +153,11 @@ export function createPlayerControllerStore(config) {
|
|
|
152
153
|
}));
|
|
153
154
|
// Add video event listeners for state sync
|
|
154
155
|
const video = videoElement;
|
|
155
|
-
const handleVideoEvent = () =>
|
|
156
|
+
const handleVideoEvent = () => {
|
|
157
|
+
if (controller?.shouldSuppressVideoEvents?.())
|
|
158
|
+
return;
|
|
159
|
+
syncState();
|
|
160
|
+
};
|
|
156
161
|
video.addEventListener('play', handleVideoEvent);
|
|
157
162
|
video.addEventListener('pause', handleVideoEvent);
|
|
158
163
|
video.addEventListener('waiting', handleVideoEvent);
|
|
@@ -164,7 +169,7 @@ export function createPlayerControllerStore(config) {
|
|
|
164
169
|
video.removeEventListener('playing', handleVideoEvent);
|
|
165
170
|
});
|
|
166
171
|
}));
|
|
167
|
-
unsubscribers.push(controller.on('playerSelected', ({ player, source }) => {
|
|
172
|
+
unsubscribers.push(controller.on('playerSelected', ({ player: _player, source }) => {
|
|
168
173
|
store.update(prev => ({
|
|
169
174
|
...prev,
|
|
170
175
|
currentPlayerInfo: controller.getCurrentPlayerInfo(),
|
|
@@ -28,7 +28,7 @@ export interface StreamStateStore extends Readable<StreamState> {
|
|
|
28
28
|
*
|
|
29
29
|
* const streamState = createStreamStateManager({
|
|
30
30
|
* mistBaseUrl: 'https://mist.example.com',
|
|
31
|
-
* streamName: '
|
|
31
|
+
* streamName: 'pk_...', // playbackId (view key)
|
|
32
32
|
* });
|
|
33
33
|
*
|
|
34
34
|
* // Access values
|
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
* Port of useViewerEndpoints.ts React hook to Svelte 5 stores.
|
|
5
5
|
*/
|
|
6
6
|
import { type Readable } from 'svelte/store';
|
|
7
|
-
import type { ContentEndpoints,
|
|
7
|
+
import type { ContentEndpoints, ContentType } from '@livepeer-frameworks/player-core';
|
|
8
8
|
export interface ViewerEndpointsOptions {
|
|
9
9
|
gatewayUrl: string;
|
|
10
|
-
contentType: ContentType;
|
|
11
10
|
contentId: string;
|
|
11
|
+
contentType?: ContentType;
|
|
12
12
|
authToken?: string;
|
|
13
13
|
}
|
|
14
14
|
export type EndpointStatus = 'idle' | 'loading' | 'ready' | 'error';
|
|
@@ -32,7 +32,7 @@ export interface ViewerEndpointsStore extends Readable<ViewerEndpointsState> {
|
|
|
32
32
|
* const resolver = createEndpointResolver({
|
|
33
33
|
* gatewayUrl: 'https://gateway.example.com/graphql',
|
|
34
34
|
* contentType: 'live',
|
|
35
|
-
* contentId: '
|
|
35
|
+
* contentId: 'pk_...',
|
|
36
36
|
* });
|
|
37
37
|
*
|
|
38
38
|
* $: endpoints = $resolver.endpoints;
|
|
@@ -42,7 +42,7 @@ export interface ViewerEndpointsStore extends Readable<ViewerEndpointsState> {
|
|
|
42
42
|
*/
|
|
43
43
|
export declare function createEndpointResolver(options: ViewerEndpointsOptions): ViewerEndpointsStore;
|
|
44
44
|
export declare function createDerivedEndpoints(store: ViewerEndpointsStore): Readable<ContentEndpoints | null>;
|
|
45
|
-
export declare function createDerivedPrimaryEndpoint(store: ViewerEndpointsStore): Readable<EndpointInfo | null>;
|
|
46
|
-
export declare function createDerivedMetadata(store: ViewerEndpointsStore): Readable<ContentMetadata | null>;
|
|
45
|
+
export declare function createDerivedPrimaryEndpoint(store: ViewerEndpointsStore): Readable<import("@livepeer-frameworks/player-core").EndpointInfo | null>;
|
|
46
|
+
export declare function createDerivedMetadata(store: ViewerEndpointsStore): Readable<import("@livepeer-frameworks/player-core").ContentMetadata | null>;
|
|
47
47
|
export declare function createDerivedStatus(store: ViewerEndpointsStore): Readable<EndpointStatus>;
|
|
48
48
|
export default createEndpointResolver;
|
|
@@ -48,7 +48,7 @@ const initialState = {
|
|
|
48
48
|
* const resolver = createEndpointResolver({
|
|
49
49
|
* gatewayUrl: 'https://gateway.example.com/graphql',
|
|
50
50
|
* contentType: 'live',
|
|
51
|
-
* contentId: '
|
|
51
|
+
* contentId: 'pk_...',
|
|
52
52
|
* });
|
|
53
53
|
*
|
|
54
54
|
* $: endpoints = $resolver.endpoints;
|
|
@@ -65,7 +65,7 @@ export function createEndpointResolver(options) {
|
|
|
65
65
|
* Fetch endpoints from Gateway
|
|
66
66
|
*/
|
|
67
67
|
async function fetchEndpoints() {
|
|
68
|
-
if (!gatewayUrl || !
|
|
68
|
+
if (!gatewayUrl || !contentId || !mounted)
|
|
69
69
|
return;
|
|
70
70
|
// Abort previous request
|
|
71
71
|
abortController?.abort();
|
|
@@ -74,8 +74,8 @@ export function createEndpointResolver(options) {
|
|
|
74
74
|
try {
|
|
75
75
|
const graphqlEndpoint = gatewayUrl.replace(/\/$/, '');
|
|
76
76
|
const query = `
|
|
77
|
-
query ResolveViewer($
|
|
78
|
-
resolveViewerEndpoint(
|
|
77
|
+
query ResolveViewer($contentId: String!) {
|
|
78
|
+
resolveViewerEndpoint(contentId: $contentId) {
|
|
79
79
|
primary { nodeId baseUrl protocol url geoDistance loadScore outputs }
|
|
80
80
|
fallbacks { nodeId baseUrl protocol url geoDistance loadScore outputs }
|
|
81
81
|
metadata { contentType contentId title description durationSeconds status isLive viewers recordingSizeBytes clipSource createdAt }
|
|
@@ -88,7 +88,7 @@ export function createEndpointResolver(options) {
|
|
|
88
88
|
'Content-Type': 'application/json',
|
|
89
89
|
...(authToken ? { Authorization: `Bearer ${authToken}` } : {}),
|
|
90
90
|
},
|
|
91
|
-
body: JSON.stringify({ query, variables: {
|
|
91
|
+
body: JSON.stringify({ query, variables: { contentId } }),
|
|
92
92
|
signal: abortController.signal,
|
|
93
93
|
});
|
|
94
94
|
if (!res.ok)
|
package/dist/ui/Slider.svelte
CHANGED
package/package.json
CHANGED
package/src/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/src/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);
|
|
@@ -633,7 +633,7 @@
|
|
|
633
633
|
{/each}
|
|
634
634
|
|
|
635
635
|
<!-- Floating particles -->
|
|
636
|
-
{#each particles as particle,
|
|
636
|
+
{#each particles as particle, _i}
|
|
637
637
|
<div
|
|
638
638
|
class="particle"
|
|
639
639
|
style="
|
|
@@ -648,7 +648,7 @@
|
|
|
648
648
|
{/each}
|
|
649
649
|
|
|
650
650
|
<!-- Animated bubbles -->
|
|
651
|
-
{#each bubbles as bubble,
|
|
651
|
+
{#each bubbles as bubble, _i}
|
|
652
652
|
<div
|
|
653
653
|
class="bubble"
|
|
654
654
|
style="
|
package/src/LoadingScreen.svelte
CHANGED
|
@@ -608,7 +608,7 @@
|
|
|
608
608
|
{/each}
|
|
609
609
|
|
|
610
610
|
<!-- Floating particles -->
|
|
611
|
-
{#each particles as particle,
|
|
611
|
+
{#each particles as particle, _i}
|
|
612
612
|
<div
|
|
613
613
|
class="particle"
|
|
614
614
|
style="
|
|
@@ -623,7 +623,7 @@
|
|
|
623
623
|
{/each}
|
|
624
624
|
|
|
625
625
|
<!-- Animated bubbles -->
|
|
626
|
-
{#each bubbles as bubble,
|
|
626
|
+
{#each bubbles as bubble, _i}
|
|
627
627
|
<div
|
|
628
628
|
class="bubble"
|
|
629
629
|
style="
|
package/src/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}
|
|
@@ -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';
|
|
@@ -119,7 +117,7 @@
|
|
|
119
117
|
let showSettingsMenu = $state(false);
|
|
120
118
|
let isNearLiveState = $state(true);
|
|
121
119
|
let buffered: TimeRanges | undefined = $state(undefined);
|
|
122
|
-
let
|
|
120
|
+
let _hasSeekToLive = false; // Track if we've auto-seeked to live
|
|
123
121
|
let qualityValue = $state('auto');
|
|
124
122
|
let captionValue = $state('none');
|
|
125
123
|
|
|
@@ -287,7 +285,7 @@
|
|
|
287
285
|
// Reset seek-to-live flag when video element changes
|
|
288
286
|
$effect(() => {
|
|
289
287
|
if (video) {
|
|
290
|
-
|
|
288
|
+
_hasSeekToLive = false;
|
|
291
289
|
}
|
|
292
290
|
});
|
|
293
291
|
|
|
@@ -311,7 +309,7 @@
|
|
|
311
309
|
}));
|
|
312
310
|
|
|
313
311
|
// Seek value for slider
|
|
314
|
-
let
|
|
312
|
+
let _seekValue = $derived.by(() => {
|
|
315
313
|
if (isLive) {
|
|
316
314
|
const window = liveEdge - seekableStart;
|
|
317
315
|
if (window <= 0) return 1000;
|
|
@@ -387,26 +385,6 @@
|
|
|
387
385
|
isMuted = next === 0;
|
|
388
386
|
}
|
|
389
387
|
|
|
390
|
-
function handleSeekChange(val: number) {
|
|
391
|
-
if (disabled || !video) return;
|
|
392
|
-
if (isLive) {
|
|
393
|
-
const window = liveEdge - seekableStart;
|
|
394
|
-
const newTime = seekableStart + (val / 1000) * window;
|
|
395
|
-
if (onseek) {
|
|
396
|
-
onseek(newTime);
|
|
397
|
-
} else {
|
|
398
|
-
video.currentTime = newTime;
|
|
399
|
-
}
|
|
400
|
-
} else if (Number.isFinite(duration)) {
|
|
401
|
-
const newTime = (val / 1000) * duration;
|
|
402
|
-
if (onseek) {
|
|
403
|
-
onseek(newTime);
|
|
404
|
-
} else {
|
|
405
|
-
video.currentTime = newTime;
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
|
|
410
388
|
function handleFullscreen() {
|
|
411
389
|
if (disabled) return;
|
|
412
390
|
const container = document.querySelector('[data-player-container="true"]') as HTMLElement | null;
|
package/src/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}%;"
|
package/src/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) {
|
|
@@ -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/src/index.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
|
|
@@ -19,16 +19,6 @@ export interface PlaybackQualityStore extends Readable<PlaybackQuality | null> {
|
|
|
19
19
|
destroy: () => void;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
const initialQuality: PlaybackQuality = {
|
|
23
|
-
score: 100,
|
|
24
|
-
bitrate: 0,
|
|
25
|
-
bufferedAhead: 0,
|
|
26
|
-
frameDropRate: 0,
|
|
27
|
-
stallCount: 0,
|
|
28
|
-
latency: 0,
|
|
29
|
-
timestamp: Date.now(),
|
|
30
|
-
};
|
|
31
|
-
|
|
32
22
|
/**
|
|
33
23
|
* Create a playback quality monitoring store.
|
|
34
24
|
*
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Port of PlayerContext.tsx React context to Svelte 5 stores.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { writable, derived, type Readable
|
|
7
|
+
import { writable, derived, type Readable } from 'svelte/store';
|
|
8
8
|
import { getContext, setContext } from 'svelte';
|
|
9
9
|
import { globalPlayerManager, type StreamInfo, type IPlayer } from '@livepeer-frameworks/player-core';
|
|
10
10
|
|
|
@@ -3,13 +3,12 @@
|
|
|
3
3
|
* for declarative usage in Svelte 5 components.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { writable, derived, type Readable
|
|
6
|
+
import { writable, derived, type Readable } from 'svelte/store';
|
|
7
7
|
import {
|
|
8
8
|
PlayerController,
|
|
9
9
|
type PlayerControllerConfig,
|
|
10
10
|
type PlayerState,
|
|
11
11
|
type StreamState,
|
|
12
|
-
type StreamSource,
|
|
13
12
|
type PlaybackQuality,
|
|
14
13
|
type ContentEndpoints,
|
|
15
14
|
type ContentMetadata,
|
|
@@ -192,7 +191,7 @@ const initialState: PlayerControllerState = {
|
|
|
192
191
|
* let containerEl: HTMLElement;
|
|
193
192
|
*
|
|
194
193
|
* const playerStore = createPlayerControllerStore({
|
|
195
|
-
* contentId: '
|
|
194
|
+
* contentId: 'pk_...',
|
|
196
195
|
* contentType: 'live',
|
|
197
196
|
* gatewayUrl: 'https://gateway.example.com/graphql',
|
|
198
197
|
* });
|
|
@@ -270,6 +269,7 @@ export function createPlayerControllerStore(
|
|
|
270
269
|
store.update(prev => ({
|
|
271
270
|
...prev,
|
|
272
271
|
streamState,
|
|
272
|
+
metadata: controller!.getMetadata(),
|
|
273
273
|
isEffectivelyLive: controller!.isEffectivelyLive(),
|
|
274
274
|
shouldShowIdleScreen: controller!.shouldShowIdleScreen(),
|
|
275
275
|
}));
|
|
@@ -321,7 +321,7 @@ export function createPlayerControllerStore(
|
|
|
321
321
|
});
|
|
322
322
|
}));
|
|
323
323
|
|
|
324
|
-
unsubscribers.push(controller.on('playerSelected', ({ player, source }) => {
|
|
324
|
+
unsubscribers.push(controller.on('playerSelected', ({ player: _player, source }) => {
|
|
325
325
|
store.update(prev => ({
|
|
326
326
|
...prev,
|
|
327
327
|
currentPlayerInfo: controller!.getCurrentPlayerInfo(),
|
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { writable, derived, type Readable } from 'svelte/store';
|
|
8
|
-
import type { ContentEndpoints,
|
|
8
|
+
import type { ContentEndpoints, ContentType } from '@livepeer-frameworks/player-core';
|
|
9
9
|
|
|
10
10
|
export interface ViewerEndpointsOptions {
|
|
11
11
|
gatewayUrl: string;
|
|
12
|
-
contentType: ContentType;
|
|
13
12
|
contentId: string;
|
|
13
|
+
contentType?: ContentType;
|
|
14
14
|
authToken?: string;
|
|
15
15
|
}
|
|
16
16
|
|
|
@@ -82,7 +82,7 @@ const initialState: ViewerEndpointsState = {
|
|
|
82
82
|
* const resolver = createEndpointResolver({
|
|
83
83
|
* gatewayUrl: 'https://gateway.example.com/graphql',
|
|
84
84
|
* contentType: 'live',
|
|
85
|
-
* contentId: '
|
|
85
|
+
* contentId: 'pk_...',
|
|
86
86
|
* });
|
|
87
87
|
*
|
|
88
88
|
* $: endpoints = $resolver.endpoints;
|
|
@@ -101,7 +101,7 @@ export function createEndpointResolver(options: ViewerEndpointsOptions): ViewerE
|
|
|
101
101
|
* Fetch endpoints from Gateway
|
|
102
102
|
*/
|
|
103
103
|
async function fetchEndpoints() {
|
|
104
|
-
if (!gatewayUrl || !
|
|
104
|
+
if (!gatewayUrl || !contentId || !mounted) return;
|
|
105
105
|
|
|
106
106
|
// Abort previous request
|
|
107
107
|
abortController?.abort();
|
|
@@ -112,8 +112,8 @@ export function createEndpointResolver(options: ViewerEndpointsOptions): ViewerE
|
|
|
112
112
|
try {
|
|
113
113
|
const graphqlEndpoint = gatewayUrl.replace(/\/$/, '');
|
|
114
114
|
const query = `
|
|
115
|
-
query ResolveViewer($
|
|
116
|
-
resolveViewerEndpoint(
|
|
115
|
+
query ResolveViewer($contentId: String!) {
|
|
116
|
+
resolveViewerEndpoint(contentId: $contentId) {
|
|
117
117
|
primary { nodeId baseUrl protocol url geoDistance loadScore outputs }
|
|
118
118
|
fallbacks { nodeId baseUrl protocol url geoDistance loadScore outputs }
|
|
119
119
|
metadata { contentType contentId title description durationSeconds status isLive viewers recordingSizeBytes clipSource createdAt }
|
|
@@ -127,7 +127,7 @@ export function createEndpointResolver(options: ViewerEndpointsOptions): ViewerE
|
|
|
127
127
|
'Content-Type': 'application/json',
|
|
128
128
|
...(authToken ? { Authorization: `Bearer ${authToken}` } : {}),
|
|
129
129
|
},
|
|
130
|
-
body: JSON.stringify({ query, variables: {
|
|
130
|
+
body: JSON.stringify({ query, variables: { contentId } }),
|
|
131
131
|
signal: abortController.signal,
|
|
132
132
|
});
|
|
133
133
|
|