@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
|
@@ -2,11 +2,7 @@
|
|
|
2
2
|
import { ContextMenu } from "bits-ui";
|
|
3
3
|
import { cn } from "@livepeer-frameworks/player-core";
|
|
4
4
|
|
|
5
|
-
let {
|
|
6
|
-
children,
|
|
7
|
-
class: className,
|
|
8
|
-
...rest
|
|
9
|
-
}: { children?: any; class?: string } = $props();
|
|
5
|
+
let { children, class: className, ...rest }: { children?: any; class?: string } = $props();
|
|
10
6
|
</script>
|
|
11
7
|
|
|
12
8
|
<ContextMenu.SubContent
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@livepeer-frameworks/player-svelte",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Svelte 5 components for FrameWorks streaming player",
|
|
6
6
|
"svelte": "./dist/index.js",
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
],
|
|
14
14
|
"exports": {
|
|
15
15
|
".": {
|
|
16
|
+
"source": "./src/index.ts",
|
|
16
17
|
"svelte": "./dist/index.js",
|
|
17
18
|
"import": "./dist/index.js",
|
|
18
19
|
"types": "./dist/index.d.ts"
|
|
@@ -22,22 +23,29 @@
|
|
|
22
23
|
"scripts": {
|
|
23
24
|
"build": "svelte-package -i src -o dist",
|
|
24
25
|
"build:watch": "svelte-package -i src -o dist --watch",
|
|
25
|
-
"type-check": "svelte-check"
|
|
26
|
+
"type-check": "svelte-check",
|
|
27
|
+
"test": "vitest run",
|
|
28
|
+
"test:watch": "vitest",
|
|
29
|
+
"test:coverage": "vitest run --coverage"
|
|
26
30
|
},
|
|
27
31
|
"dependencies": {
|
|
28
32
|
"@livepeer-frameworks/player-core": "workspace:*",
|
|
29
|
-
"bits-ui": "^2.
|
|
33
|
+
"bits-ui": "^2.15.5"
|
|
30
34
|
},
|
|
31
35
|
"peerDependencies": {
|
|
32
36
|
"svelte": "^5.0.0"
|
|
33
37
|
},
|
|
34
38
|
"devDependencies": {
|
|
35
39
|
"@sveltejs/package": "^2.3.7",
|
|
36
|
-
"@sveltejs/vite-plugin-svelte": "^
|
|
37
|
-
"svelte": "^5.
|
|
38
|
-
"
|
|
40
|
+
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
|
41
|
+
"@testing-library/svelte": "^5.2.7",
|
|
42
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
43
|
+
"jsdom": "^28.0.0",
|
|
44
|
+
"svelte": "^5.50.0",
|
|
45
|
+
"svelte-check": "^4.3.6",
|
|
39
46
|
"typescript": "^5.9.2",
|
|
40
|
-
"vite": "^
|
|
47
|
+
"vite": "^7.3.1",
|
|
48
|
+
"vitest": "^4.0.18"
|
|
41
49
|
},
|
|
42
50
|
"keywords": [
|
|
43
51
|
"svelte",
|
package/src/DevModePanel.svelte
CHANGED
package/src/Icons.svelte
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
<PlayIcon size={24} />
|
|
11
11
|
<VolumeIcon isMuted={false} />
|
|
12
12
|
-->
|
|
13
|
-
<script
|
|
13
|
+
<script module lang="ts">
|
|
14
14
|
export interface IconProps {
|
|
15
15
|
size?: number;
|
|
16
16
|
color?: string;
|
|
@@ -19,9 +19,11 @@
|
|
|
19
19
|
</script>
|
|
20
20
|
|
|
21
21
|
<script lang="ts">
|
|
22
|
-
|
|
22
|
+
import type { Snippet } from "svelte";
|
|
23
|
+
// This file exports components via module script
|
|
23
24
|
// The default export is a placeholder - use the named exports
|
|
25
|
+
let { children }: { children?: Snippet } = $props();
|
|
24
26
|
</script>
|
|
25
27
|
|
|
26
28
|
<!-- This component itself is not rendered - use the exported icon components below -->
|
|
27
|
-
|
|
29
|
+
{@render children?.()}
|
package/src/IdleScreen.svelte
CHANGED
|
@@ -387,7 +387,7 @@
|
|
|
387
387
|
animation-duration: {particle.duration}s;
|
|
388
388
|
animation-delay: {particle.delay}s;
|
|
389
389
|
"
|
|
390
|
-
|
|
390
|
+
></div>
|
|
391
391
|
{/each}
|
|
392
392
|
|
|
393
393
|
<!-- Animated bubbles -->
|
|
@@ -402,7 +402,7 @@
|
|
|
402
402
|
background: {bubble.color};
|
|
403
403
|
opacity: {bubble.opacity};
|
|
404
404
|
"
|
|
405
|
-
|
|
405
|
+
></div>
|
|
406
406
|
{/each}
|
|
407
407
|
|
|
408
408
|
<!-- Center logo with push-away effect -->
|
|
@@ -415,18 +415,19 @@
|
|
|
415
415
|
class="logo-pulse"
|
|
416
416
|
class:hovered={isHovered}
|
|
417
417
|
style="width: {logoSize * 1.4}px; height: {logoSize * 1.4}px;"
|
|
418
|
-
|
|
418
|
+
></div>
|
|
419
419
|
|
|
420
420
|
<!-- Logo image -->
|
|
421
|
-
<
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
421
|
+
<button type="button" class="logo-button" onclick={handleLogoClick} aria-label="Logo">
|
|
422
|
+
<img
|
|
423
|
+
src={logomarkAsset}
|
|
424
|
+
alt=""
|
|
425
|
+
class="logo-image"
|
|
426
|
+
class:hovered={isHovered}
|
|
427
|
+
style="width: {logoSize}px; height: {logoSize}px;"
|
|
428
|
+
draggable="false"
|
|
429
|
+
/>
|
|
430
|
+
</button>
|
|
430
431
|
</div>
|
|
431
432
|
|
|
432
433
|
<!-- Bouncing DVD Logo -->
|
|
@@ -515,7 +516,7 @@
|
|
|
515
516
|
<!-- Progress bar -->
|
|
516
517
|
{#if showProgress}
|
|
517
518
|
<div class="progress-bar">
|
|
518
|
-
<div class="progress-fill" style="width: {Math.min(100, percentage ?? 0)}
|
|
519
|
+
<div class="progress-fill" style="width: {Math.min(100, percentage ?? 0)};"></div>
|
|
519
520
|
</div>
|
|
520
521
|
{/if}
|
|
521
522
|
|
|
@@ -526,7 +527,7 @@
|
|
|
526
527
|
</div>
|
|
527
528
|
|
|
528
529
|
<!-- Subtle overlay texture -->
|
|
529
|
-
<div class="overlay-texture"
|
|
530
|
+
<div class="overlay-texture"></div>
|
|
530
531
|
</div>
|
|
531
532
|
|
|
532
533
|
<style>
|
|
@@ -686,6 +687,12 @@
|
|
|
686
687
|
transform: scale(1.2);
|
|
687
688
|
}
|
|
688
689
|
|
|
690
|
+
.logo-button {
|
|
691
|
+
all: unset;
|
|
692
|
+
cursor: pointer;
|
|
693
|
+
display: block;
|
|
694
|
+
}
|
|
695
|
+
|
|
689
696
|
.logo-image {
|
|
690
697
|
position: relative;
|
|
691
698
|
z-index: 1;
|
package/src/LoadingScreen.svelte
CHANGED
|
@@ -387,7 +387,7 @@
|
|
|
387
387
|
animation-duration: {particle.duration}s;
|
|
388
388
|
animation-delay: {particle.delay}s;
|
|
389
389
|
"
|
|
390
|
-
|
|
390
|
+
></div>
|
|
391
391
|
{/each}
|
|
392
392
|
|
|
393
393
|
<!-- Animated bubbles -->
|
|
@@ -402,7 +402,7 @@
|
|
|
402
402
|
background: {bubble.color};
|
|
403
403
|
opacity: {bubble.opacity};
|
|
404
404
|
"
|
|
405
|
-
|
|
405
|
+
></div>
|
|
406
406
|
{/each}
|
|
407
407
|
|
|
408
408
|
<!-- Center logo with push-away effect -->
|
|
@@ -415,18 +415,19 @@
|
|
|
415
415
|
class="logo-pulse"
|
|
416
416
|
class:hovered={isHovered}
|
|
417
417
|
style="width: {logoSize * 1.4}px; height: {logoSize * 1.4}px;"
|
|
418
|
-
|
|
418
|
+
></div>
|
|
419
419
|
|
|
420
420
|
<!-- Logo image -->
|
|
421
|
-
<
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
421
|
+
<button type="button" class="logo-button" onclick={handleLogoClick} aria-label="Logo">
|
|
422
|
+
<img
|
|
423
|
+
src={effectiveLogoSrc}
|
|
424
|
+
alt=""
|
|
425
|
+
class="logo-image"
|
|
426
|
+
class:hovered={isHovered}
|
|
427
|
+
style="width: {logoSize}px; height: {logoSize}px;"
|
|
428
|
+
draggable="false"
|
|
429
|
+
/>
|
|
430
|
+
</button>
|
|
430
431
|
</div>
|
|
431
432
|
|
|
432
433
|
<!-- Bouncing DVD Logo -->
|
|
@@ -438,7 +439,7 @@
|
|
|
438
439
|
</div>
|
|
439
440
|
|
|
440
441
|
<!-- Subtle overlay texture -->
|
|
441
|
-
<div class="overlay-texture"
|
|
442
|
+
<div class="overlay-texture"></div>
|
|
442
443
|
</div>
|
|
443
444
|
|
|
444
445
|
<style>
|
|
@@ -605,6 +606,12 @@
|
|
|
605
606
|
transform: scale(1.2);
|
|
606
607
|
}
|
|
607
608
|
|
|
609
|
+
.logo-button {
|
|
610
|
+
all: unset;
|
|
611
|
+
cursor: pointer;
|
|
612
|
+
display: block;
|
|
613
|
+
}
|
|
614
|
+
|
|
608
615
|
.logo-image {
|
|
609
616
|
position: relative;
|
|
610
617
|
z-index: 1;
|
package/src/Player.svelte
CHANGED
|
@@ -80,7 +80,12 @@
|
|
|
80
80
|
let skipDirection: SkipDirection = $state(null);
|
|
81
81
|
|
|
82
82
|
// Playback mode preference (persistent)
|
|
83
|
-
let devPlaybackMode: PlaybackMode = $state(
|
|
83
|
+
let devPlaybackMode: PlaybackMode = $state("auto");
|
|
84
|
+
$effect(() => {
|
|
85
|
+
if (options?.playbackMode) {
|
|
86
|
+
devPlaybackMode = options.playbackMode;
|
|
87
|
+
}
|
|
88
|
+
});
|
|
84
89
|
|
|
85
90
|
// Container ref
|
|
86
91
|
let containerRef: HTMLElement | undefined = $state();
|
|
@@ -119,6 +124,7 @@
|
|
|
119
124
|
currentSourceInfo: null as { url: string; type: string } | null,
|
|
120
125
|
playbackQuality: null as any,
|
|
121
126
|
subtitlesEnabled: false,
|
|
127
|
+
toast: null as { message: string; timestamp: number } | null,
|
|
122
128
|
});
|
|
123
129
|
|
|
124
130
|
// Track if we've already attached to prevent double-attach race
|
|
@@ -197,6 +203,15 @@
|
|
|
197
203
|
}
|
|
198
204
|
});
|
|
199
205
|
|
|
206
|
+
// Auto-dismiss toast after 3 seconds
|
|
207
|
+
$effect(() => {
|
|
208
|
+
if (!storeState.toast) return;
|
|
209
|
+
const timer = setTimeout(() => {
|
|
210
|
+
playerStore?.dismissToast();
|
|
211
|
+
}, 3000);
|
|
212
|
+
return () => clearTimeout(timer);
|
|
213
|
+
});
|
|
214
|
+
|
|
200
215
|
// ============================================================================
|
|
201
216
|
// Dev Mode Callbacks
|
|
202
217
|
// ============================================================================
|
|
@@ -277,7 +292,8 @@
|
|
|
277
292
|
options?.devMode && "flex"
|
|
278
293
|
)}
|
|
279
294
|
data-player-container="true"
|
|
280
|
-
|
|
295
|
+
role="region"
|
|
296
|
+
aria-label="Video player"
|
|
281
297
|
onmouseenter={() => playerStore?.handleMouseEnter()}
|
|
282
298
|
onmouseleave={() => playerStore?.handleMouseLeave()}
|
|
283
299
|
onmousemove={() => playerStore?.handleMouseMove()}
|
|
@@ -461,6 +477,36 @@
|
|
|
461
477
|
</div>
|
|
462
478
|
{/if}
|
|
463
479
|
|
|
480
|
+
<!-- Toast notification -->
|
|
481
|
+
{#if storeState.toast}
|
|
482
|
+
<div
|
|
483
|
+
class="absolute bottom-20 left-1/2 -translate-x-1/2 z-30 animate-in fade-in slide-in-from-bottom-2 duration-200"
|
|
484
|
+
role="status"
|
|
485
|
+
aria-live="polite"
|
|
486
|
+
>
|
|
487
|
+
<div
|
|
488
|
+
class="flex items-center gap-2 rounded-lg border border-white/10 bg-black/80 px-4 py-2 text-sm text-white shadow-lg backdrop-blur-sm"
|
|
489
|
+
>
|
|
490
|
+
<span>{storeState.toast.message}</span>
|
|
491
|
+
<button
|
|
492
|
+
type="button"
|
|
493
|
+
onclick={() => playerStore?.dismissToast()}
|
|
494
|
+
class="ml-2 text-white/60 hover:text-white"
|
|
495
|
+
aria-label="Dismiss"
|
|
496
|
+
>
|
|
497
|
+
<svg width="12" height="12" viewBox="0 0 12 12" fill="none">
|
|
498
|
+
<path
|
|
499
|
+
d="M9 3L3 9M3 3L9 9"
|
|
500
|
+
stroke="currentColor"
|
|
501
|
+
stroke-width="1.5"
|
|
502
|
+
stroke-linecap="round"
|
|
503
|
+
/>
|
|
504
|
+
</svg>
|
|
505
|
+
</button>
|
|
506
|
+
</div>
|
|
507
|
+
</div>
|
|
508
|
+
{/if}
|
|
509
|
+
|
|
464
510
|
<!-- Player controls -->
|
|
465
511
|
{#if !useStockControls}
|
|
466
512
|
<PlayerControls
|
|
@@ -161,6 +161,7 @@
|
|
|
161
161
|
let isVolumeHovered = $state(false);
|
|
162
162
|
let isVolumeFocused = $state(false);
|
|
163
163
|
let isVolumeExpanded = $derived(isVolumeHovered || isVolumeFocused);
|
|
164
|
+
let volumeGroupRef: HTMLDivElement | null = $state(null);
|
|
164
165
|
|
|
165
166
|
// Derived values - using centralized core utilities
|
|
166
167
|
let isLive = $derived(isLiveContent(isContentLive, mistStreamInfo, duration));
|
|
@@ -207,12 +208,13 @@
|
|
|
207
208
|
return null;
|
|
208
209
|
}
|
|
209
210
|
|
|
210
|
-
|
|
211
|
+
let allowMediaStreamDvr = $derived(
|
|
211
212
|
isMediaStreamSource(video) &&
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
213
|
+
bufferWindowMs !== undefined &&
|
|
214
|
+
bufferWindowMs > 0 &&
|
|
215
|
+
sourceType !== "whep" &&
|
|
216
|
+
sourceType !== "webrtc"
|
|
217
|
+
);
|
|
216
218
|
|
|
217
219
|
// Seekable range using core calculation (allow player override)
|
|
218
220
|
let seekableRange = $derived.by(
|
|
@@ -387,9 +389,9 @@
|
|
|
387
389
|
function handleMute() {
|
|
388
390
|
if (disabled) return;
|
|
389
391
|
const player = globalPlayerManager.getCurrentPlayer();
|
|
390
|
-
if (player?.
|
|
391
|
-
|
|
392
|
-
player.
|
|
392
|
+
if (player?.setMuted) {
|
|
393
|
+
const currentlyMuted = player.isMuted?.() ?? video?.muted ?? false;
|
|
394
|
+
player.setMuted(!currentlyMuted);
|
|
393
395
|
} else {
|
|
394
396
|
// Fallback: direct video manipulation
|
|
395
397
|
const v = video;
|
|
@@ -467,6 +469,19 @@
|
|
|
467
469
|
showSettingsMenu = false;
|
|
468
470
|
}
|
|
469
471
|
|
|
472
|
+
// Non-passive wheel listener for volume control
|
|
473
|
+
$effect(() => {
|
|
474
|
+
if (!volumeGroupRef) return;
|
|
475
|
+
const handler = (e: WheelEvent) => {
|
|
476
|
+
if (disabled || !hasAudio) return;
|
|
477
|
+
e.preventDefault();
|
|
478
|
+
const delta = e.deltaY < 0 ? 5 : -5;
|
|
479
|
+
handleVolumeChange(volumeValue + delta);
|
|
480
|
+
};
|
|
481
|
+
volumeGroupRef.addEventListener("wheel", handler, { passive: false });
|
|
482
|
+
return () => volumeGroupRef?.removeEventListener("wheel", handler);
|
|
483
|
+
});
|
|
484
|
+
|
|
470
485
|
// Close menu when clicking outside - with debounce to prevent immediate close from same click
|
|
471
486
|
$effect(() => {
|
|
472
487
|
if (!showSettingsMenu) return;
|
|
@@ -497,7 +512,14 @@
|
|
|
497
512
|
)}
|
|
498
513
|
>
|
|
499
514
|
<!-- Control bar -->
|
|
500
|
-
<div
|
|
515
|
+
<div
|
|
516
|
+
class="fw-control-bar pointer-events-auto"
|
|
517
|
+
role="toolbar"
|
|
518
|
+
aria-label="Media controls"
|
|
519
|
+
tabindex="-1"
|
|
520
|
+
onclick={(e) => e.stopPropagation()}
|
|
521
|
+
onkeydown={(e) => e.stopPropagation()}
|
|
522
|
+
>
|
|
501
523
|
<!-- Seek bar -->
|
|
502
524
|
{#if canSeek}
|
|
503
525
|
<div class="fw-seek-wrapper">
|
|
@@ -563,26 +585,23 @@
|
|
|
563
585
|
|
|
564
586
|
<!-- Volume -->
|
|
565
587
|
<div
|
|
588
|
+
bind:this={volumeGroupRef}
|
|
566
589
|
class={cn(
|
|
567
590
|
"fw-volume-group",
|
|
568
591
|
isVolumeExpanded && "fw-volume-group--expanded",
|
|
569
592
|
!hasAudio && "fw-volume-group--disabled"
|
|
570
593
|
)}
|
|
594
|
+
role="group"
|
|
595
|
+
aria-label="Volume controls"
|
|
571
596
|
onmouseenter={() => hasAudio && (isVolumeHovered = true)}
|
|
572
597
|
onmouseleave={() => {
|
|
573
598
|
isVolumeHovered = false;
|
|
574
599
|
isVolumeFocused = false;
|
|
575
600
|
}}
|
|
576
|
-
|
|
577
|
-
|
|
601
|
+
onfocusin={() => hasAudio && (isVolumeFocused = true)}
|
|
602
|
+
onfocusout={(e) => {
|
|
578
603
|
if (!e.currentTarget.contains(e.relatedTarget as Node)) isVolumeFocused = false;
|
|
579
604
|
}}
|
|
580
|
-
onclick={(e) => {
|
|
581
|
-
if (disabled) return;
|
|
582
|
-
if (hasAudio && e.target === e.currentTarget) {
|
|
583
|
-
handleMute();
|
|
584
|
-
}
|
|
585
|
-
}}
|
|
586
605
|
>
|
|
587
606
|
<button
|
|
588
607
|
type="button"
|
package/src/SeekBar.svelte
CHANGED
|
@@ -214,6 +214,38 @@
|
|
|
214
214
|
let ariaValueText = $derived(
|
|
215
215
|
isLive ? formatLiveTime(displayTime, effectiveLiveEdge) : formatTime(displayTime)
|
|
216
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
|
+
}
|
|
217
249
|
</script>
|
|
218
250
|
|
|
219
251
|
<div
|
|
@@ -231,6 +263,7 @@
|
|
|
231
263
|
onmousemove={handleMouseMove}
|
|
232
264
|
onclick={handleClick}
|
|
233
265
|
onmousedown={handleMouseDown}
|
|
266
|
+
onkeydown={handleKeyDown}
|
|
234
267
|
role="slider"
|
|
235
268
|
aria-label="Seek"
|
|
236
269
|
aria-valuemin={isLive ? seekableStart : 0}
|
|
@@ -129,7 +129,7 @@
|
|
|
129
129
|
{#if showProgress && percentage !== undefined}
|
|
130
130
|
<div style="margin-top: 0.75rem;">
|
|
131
131
|
<div class="progress-bar">
|
|
132
|
-
<div class="progress-fill" style="width: {Math.min(100, percentage)}
|
|
132
|
+
<div class="progress-fill" style="width: {Math.min(100, percentage)};"></div>
|
|
133
133
|
</div>
|
|
134
134
|
<p
|
|
135
135
|
style="margin-top: 0.375rem; font-size: 0.75rem; font-family: monospace; color: hsl(var(--tn-fg-dark, 233 23% 60%));"
|
|
@@ -158,7 +158,7 @@
|
|
|
158
158
|
<!-- Polling indicator for non-error states -->
|
|
159
159
|
{#if !showRetry}
|
|
160
160
|
<div class="polling-indicator">
|
|
161
|
-
<span class="polling-dot"
|
|
161
|
+
<span class="polling-dot"></span>
|
|
162
162
|
<span>Checking stream status...</span>
|
|
163
163
|
</div>
|
|
164
164
|
{/if}
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
type PlaybackQuality,
|
|
13
13
|
type ContentEndpoints,
|
|
14
14
|
type ContentMetadata,
|
|
15
|
+
type ClassifiedError,
|
|
15
16
|
} from "@livepeer-frameworks/player-core";
|
|
16
17
|
|
|
17
18
|
// ============================================================================
|
|
@@ -50,6 +51,8 @@ export interface PlayerControllerState {
|
|
|
50
51
|
volume: number;
|
|
51
52
|
/** Error text */
|
|
52
53
|
error: string | null;
|
|
54
|
+
/** Error details for debugging */
|
|
55
|
+
errorDetails: ClassifiedError["details"] | null;
|
|
53
56
|
/** Is passive error */
|
|
54
57
|
isPassiveError: boolean;
|
|
55
58
|
/** Has playback ever started */
|
|
@@ -80,6 +83,8 @@ export interface PlayerControllerState {
|
|
|
80
83
|
playbackQuality: PlaybackQuality | null;
|
|
81
84
|
/** Subtitles enabled */
|
|
82
85
|
subtitlesEnabled: boolean;
|
|
86
|
+
/** Toast message to display (auto-dismisses) */
|
|
87
|
+
toast: { message: string; timestamp: number } | null;
|
|
83
88
|
}
|
|
84
89
|
|
|
85
90
|
export interface PlayerControllerStore extends Readable<PlayerControllerState> {
|
|
@@ -115,6 +120,8 @@ export interface PlayerControllerStore extends Readable<PlayerControllerState> {
|
|
|
115
120
|
toggleSubtitles: () => void;
|
|
116
121
|
/** Clear error */
|
|
117
122
|
clearError: () => void;
|
|
123
|
+
/** Dismiss toast notification */
|
|
124
|
+
dismissToast: () => void;
|
|
118
125
|
/** Retry playback */
|
|
119
126
|
retry: () => Promise<void>;
|
|
120
127
|
/** Reload player */
|
|
@@ -158,6 +165,7 @@ const initialState: PlayerControllerState = {
|
|
|
158
165
|
isMuted: true,
|
|
159
166
|
volume: 1,
|
|
160
167
|
error: null,
|
|
168
|
+
errorDetails: null,
|
|
161
169
|
isPassiveError: false,
|
|
162
170
|
hasPlaybackStarted: false,
|
|
163
171
|
isHoldingSpeed: false,
|
|
@@ -173,6 +181,7 @@ const initialState: PlayerControllerState = {
|
|
|
173
181
|
currentSourceInfo: null,
|
|
174
182
|
playbackQuality: null,
|
|
175
183
|
subtitlesEnabled: false,
|
|
184
|
+
toast: null,
|
|
176
185
|
};
|
|
177
186
|
|
|
178
187
|
// ============================================================================
|
|
@@ -401,6 +410,25 @@ export function createPlayerControllerStore(
|
|
|
401
410
|
})
|
|
402
411
|
);
|
|
403
412
|
|
|
413
|
+
// Error handling events - show toasts/modals
|
|
414
|
+
unsubscribers.push(
|
|
415
|
+
controller.on("protocolSwapped", (data) => {
|
|
416
|
+
const message = `Switched to ${data.toProtocol}`;
|
|
417
|
+
store.update((prev) => ({ ...prev, toast: { message, timestamp: Date.now() } }));
|
|
418
|
+
})
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
unsubscribers.push(
|
|
422
|
+
controller.on("playbackFailed", (data) => {
|
|
423
|
+
store.update((prev) => ({
|
|
424
|
+
...prev,
|
|
425
|
+
error: data.message,
|
|
426
|
+
errorDetails: data.details ?? null,
|
|
427
|
+
isPassiveError: false,
|
|
428
|
+
}));
|
|
429
|
+
})
|
|
430
|
+
);
|
|
431
|
+
|
|
404
432
|
// Set initial loop state
|
|
405
433
|
store.update((prev) => ({
|
|
406
434
|
...prev,
|
|
@@ -483,7 +511,16 @@ export function createPlayerControllerStore(
|
|
|
483
511
|
|
|
484
512
|
function clearError(): void {
|
|
485
513
|
controller?.clearError();
|
|
486
|
-
store.update((prev) => ({
|
|
514
|
+
store.update((prev) => ({
|
|
515
|
+
...prev,
|
|
516
|
+
error: null,
|
|
517
|
+
errorDetails: null,
|
|
518
|
+
isPassiveError: false,
|
|
519
|
+
}));
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function dismissToast(): void {
|
|
523
|
+
store.update((prev) => ({ ...prev, toast: null }));
|
|
487
524
|
}
|
|
488
525
|
|
|
489
526
|
async function retry(): Promise<void> {
|
|
@@ -549,6 +586,7 @@ export function createPlayerControllerStore(
|
|
|
549
586
|
togglePiP,
|
|
550
587
|
toggleSubtitles,
|
|
551
588
|
clearError,
|
|
589
|
+
dismissToast,
|
|
552
590
|
retry,
|
|
553
591
|
reload,
|
|
554
592
|
getQualities,
|
|
@@ -151,7 +151,7 @@ export function createEndpointResolver(options: ViewerEndpointsOptions): ViewerE
|
|
|
151
151
|
} catch {}
|
|
152
152
|
}
|
|
153
153
|
if (fallbacks) {
|
|
154
|
-
fallbacks.forEach((fb:
|
|
154
|
+
fallbacks.forEach((fb: typeof primary) => {
|
|
155
155
|
if (fb.outputs && typeof fb.outputs === "string") {
|
|
156
156
|
try {
|
|
157
157
|
fb.outputs = JSON.parse(fb.outputs);
|
package/src/ui/Badge.svelte
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import type { Snippet } from "svelte";
|
|
2
3
|
import { cn } from "@livepeer-frameworks/player-core";
|
|
3
4
|
import { badgeVariants, type BadgeVariant } from "./badge";
|
|
4
5
|
|
|
5
|
-
type
|
|
6
|
+
type Props = {
|
|
6
7
|
variant?: BadgeVariant;
|
|
7
8
|
class?: string;
|
|
8
|
-
|
|
9
|
+
children?: Snippet;
|
|
10
|
+
[key: string]: unknown;
|
|
11
|
+
};
|
|
9
12
|
|
|
10
|
-
let { variant = "default", class: className = "", ...rest }:
|
|
13
|
+
let { variant = "default", class: className = "", children, ...rest }: Props = $props();
|
|
11
14
|
|
|
12
15
|
let mergedClasses = $derived(cn(badgeVariants(variant, className)));
|
|
13
16
|
</script>
|
|
14
17
|
|
|
15
18
|
<div class={mergedClasses} {...rest}>
|
|
16
|
-
|
|
19
|
+
{@render children?.()}
|
|
17
20
|
</div>
|