@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.
Files changed (41) hide show
  1. package/README.md +78 -0
  2. package/dist/DevModePanel.svelte +2 -19
  3. package/dist/IdleScreen.svelte +3 -3
  4. package/dist/LoadingScreen.svelte +2 -2
  5. package/dist/Player.svelte +13 -17
  6. package/dist/Player.svelte.d.ts +2 -1
  7. package/dist/PlayerControls.svelte +24 -33
  8. package/dist/PlayerControls.svelte.d.ts +2 -0
  9. package/dist/SeekBar.svelte +1 -1
  10. package/dist/SpeedIndicator.svelte +5 -6
  11. package/dist/StatsPanel.svelte +37 -20
  12. package/dist/StatsPanel.svelte.d.ts +2 -14
  13. package/dist/SubtitleRenderer.svelte +3 -3
  14. package/dist/components/VolumeIcons.svelte +1 -1
  15. package/dist/index.d.ts +2 -2
  16. package/dist/index.js +2 -2
  17. package/dist/stores/playbackQuality.js +0 -9
  18. package/dist/stores/playerController.d.ts +1 -1
  19. package/dist/stores/playerController.js +8 -3
  20. package/dist/stores/streamState.d.ts +1 -1
  21. package/dist/stores/streamState.js +1 -1
  22. package/dist/stores/viewerEndpoints.d.ts +5 -5
  23. package/dist/stores/viewerEndpoints.js +5 -5
  24. package/dist/ui/Slider.svelte +1 -1
  25. package/package.json +1 -1
  26. package/src/DevModePanel.svelte +2 -19
  27. package/src/IdleScreen.svelte +3 -3
  28. package/src/LoadingScreen.svelte +2 -2
  29. package/src/Player.svelte +12 -17
  30. package/src/PlayerControls.svelte +3 -25
  31. package/src/SeekBar.svelte +1 -1
  32. package/src/StatsPanel.svelte +37 -20
  33. package/src/SubtitleRenderer.svelte +3 -3
  34. package/src/components/VolumeIcons.svelte +1 -1
  35. package/src/index.ts +2 -2
  36. package/src/stores/playbackQuality.ts +0 -10
  37. package/src/stores/playerContext.ts +1 -1
  38. package/src/stores/playerController.ts +4 -4
  39. package/src/stores/streamState.ts +1 -1
  40. package/src/stores/viewerEndpoints.ts +7 -7
  41. 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.
@@ -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 !isOpen}
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">
@@ -299,7 +299,7 @@
299
299
  }
300
300
  }
301
301
 
302
- let statusLabel = $derived(getStatusLabel(status));
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, i}
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, i}
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, i}
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, i}
641
+ {#each bubbles as bubble, _i}
642
642
  <div
643
643
  class="bubble"
644
644
  style="
@@ -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, onDestroy } from 'svelte';
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 ContentMetadata, type PlayerState, type PlayerStateContext, type ContentType, type EndpointInfo } from '@livepeer-frameworks/player-core';
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 = 'live',
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?.isOnline ? {
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>
@@ -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 hasSeekToLive = false; // Track if we've auto-seeked to live
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 canSeek = $derived.by(() => {
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() { isPlaying = !video!.paused; }
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
- hasSeekToLive = false;
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 seekValue = $derived.by(() => {
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 (DVR disabled)' : (isNearLiveState ? 'At live edge' : 'Jump to live')}
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>;
@@ -240,7 +240,7 @@
240
240
  isDragging && 'fw-seek-track--active'
241
241
  )}>
242
242
  <!-- Buffered segments -->
243
- {#each bufferedSegments as segment, index}
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 inset-0 z-30 pointer-events-none
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/80 text-white px-4 py-2 rounded-full
26
- text-lg font-semibold tabular-nums
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
- backdrop-blur-sm border border-white/20
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-5 h-5" aria-hidden="true">
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>
@@ -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?: StreamStateInfo | null;
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(streamState?.viewers ?? metadata?.viewers ?? '—');
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
- if (!streamState?.tracks?.length) return '—';
68
- return streamState.tracks.map(t => {
71
+ const tracks = metadata?.tracks ?? deriveTracksFromMist();
72
+ if (!tracks?.length) return '—';
73
+ return tracks.map(t => {
69
74
  if (t.type === 'video') {
70
- return `${t.codec} ${t.width}x${t.height}@${t.bps ? Math.round(t.bps / 1000) + 'kbps' : '?'}`;
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
- return `${t.codec} ${t.channels}ch`;
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?: StreamStateInfo | null;
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 lastCueId: string | null = null;
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
- lastCueId = activeCue.id;
169
+ _lastCueId = activeCue.id;
170
170
  } else {
171
171
  displayedText = '';
172
- lastCueId = null;
172
+ _lastCueId = null;
173
173
  }
174
174
  });
175
175
 
@@ -1,7 +1,7 @@
1
1
  <script lang="ts">
2
2
  let {
3
3
  size = 16,
4
- color = 'currentColor',
4
+ color: _color = 'currentColor',
5
5
  className = '',
6
6
  isMuted = false,
7
7
  volume = 1 // 0-1 range
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/svelte';
15
+ * import { Player } from '@livepeer-frameworks/player-svelte';
16
16
  * </script>
17
17
  *
18
18
  * <Player
19
- * contentId="my-stream"
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/svelte';
15
+ * import { Player } from '@livepeer-frameworks/player-svelte';
16
16
  * </script>
17
17
  *
18
18
  * <Player
19
- * contentId="my-stream"
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: 'my-stream',
148
+ * contentId: 'pk_...',
149
149
  * contentType: 'live',
150
150
  * gatewayUrl: 'https://gateway.example.com/graphql',
151
151
  * });