@livepeer-frameworks/player-svelte 0.0.3

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 (169) hide show
  1. package/dist/DevModePanel.svelte +650 -0
  2. package/dist/DevModePanel.svelte.d.ts +31 -0
  3. package/dist/DvdLogo.svelte +213 -0
  4. package/dist/DvdLogo.svelte.d.ts +7 -0
  5. package/dist/Icons.svelte +27 -0
  6. package/dist/Icons.svelte.d.ts +25 -0
  7. package/dist/IdleScreen.svelte +752 -0
  8. package/dist/IdleScreen.svelte.d.ts +11 -0
  9. package/dist/LoadingScreen.svelte +689 -0
  10. package/dist/LoadingScreen.svelte.d.ts +7 -0
  11. package/dist/Player.svelte +482 -0
  12. package/dist/Player.svelte.d.ts +26 -0
  13. package/dist/PlayerControls.svelte +739 -0
  14. package/dist/PlayerControls.svelte.d.ts +20 -0
  15. package/dist/SeekBar.svelte +274 -0
  16. package/dist/SeekBar.svelte.d.ts +25 -0
  17. package/dist/SkipIndicator.svelte +95 -0
  18. package/dist/SkipIndicator.svelte.d.ts +14 -0
  19. package/dist/SpeedIndicator.svelte +38 -0
  20. package/dist/SpeedIndicator.svelte.d.ts +8 -0
  21. package/dist/StatsPanel.svelte +155 -0
  22. package/dist/StatsPanel.svelte.d.ts +27 -0
  23. package/dist/StreamStateOverlay.svelte +266 -0
  24. package/dist/StreamStateOverlay.svelte.d.ts +18 -0
  25. package/dist/SubtitleRenderer.svelte +234 -0
  26. package/dist/SubtitleRenderer.svelte.d.ts +41 -0
  27. package/dist/ThumbnailOverlay.svelte +96 -0
  28. package/dist/ThumbnailOverlay.svelte.d.ts +11 -0
  29. package/dist/TitleOverlay.svelte +47 -0
  30. package/dist/TitleOverlay.svelte.d.ts +9 -0
  31. package/dist/assets/logomark.svg +56 -0
  32. package/dist/components/VolumeIcons.svelte +53 -0
  33. package/dist/components/VolumeIcons.svelte.d.ts +10 -0
  34. package/dist/global.d.ts +15 -0
  35. package/dist/icons/FullscreenExitIcon.svelte +33 -0
  36. package/dist/icons/FullscreenExitIcon.svelte.d.ts +8 -0
  37. package/dist/icons/FullscreenIcon.svelte +33 -0
  38. package/dist/icons/FullscreenIcon.svelte.d.ts +8 -0
  39. package/dist/icons/PauseIcon.svelte +28 -0
  40. package/dist/icons/PauseIcon.svelte.d.ts +8 -0
  41. package/dist/icons/PictureInPictureIcon.svelte +28 -0
  42. package/dist/icons/PictureInPictureIcon.svelte.d.ts +8 -0
  43. package/dist/icons/PlayIcon.svelte +27 -0
  44. package/dist/icons/PlayIcon.svelte.d.ts +8 -0
  45. package/dist/icons/SeekToLiveIcon.svelte +30 -0
  46. package/dist/icons/SeekToLiveIcon.svelte.d.ts +8 -0
  47. package/dist/icons/SettingsIcon.svelte +40 -0
  48. package/dist/icons/SettingsIcon.svelte.d.ts +8 -0
  49. package/dist/icons/SkipBackIcon.svelte +32 -0
  50. package/dist/icons/SkipBackIcon.svelte.d.ts +8 -0
  51. package/dist/icons/SkipForwardIcon.svelte +32 -0
  52. package/dist/icons/SkipForwardIcon.svelte.d.ts +8 -0
  53. package/dist/icons/StatsIcon.svelte +29 -0
  54. package/dist/icons/StatsIcon.svelte.d.ts +8 -0
  55. package/dist/icons/VolumeOffIcon.svelte +29 -0
  56. package/dist/icons/VolumeOffIcon.svelte.d.ts +8 -0
  57. package/dist/icons/VolumeUpIcon.svelte +34 -0
  58. package/dist/icons/VolumeUpIcon.svelte.d.ts +8 -0
  59. package/dist/icons/index.d.ts +17 -0
  60. package/dist/icons/index.js +17 -0
  61. package/dist/index.d.ts +50 -0
  62. package/dist/index.js +54 -0
  63. package/dist/player.css +2 -0
  64. package/dist/stores/index.d.ts +15 -0
  65. package/dist/stores/index.js +21 -0
  66. package/dist/stores/playbackQuality.d.ts +43 -0
  67. package/dist/stores/playbackQuality.js +107 -0
  68. package/dist/stores/playerContext.d.ts +73 -0
  69. package/dist/stores/playerContext.js +166 -0
  70. package/dist/stores/playerController.d.ts +178 -0
  71. package/dist/stores/playerController.js +358 -0
  72. package/dist/stores/playerSelection.d.ts +84 -0
  73. package/dist/stores/playerSelection.js +159 -0
  74. package/dist/stores/streamState.d.ts +44 -0
  75. package/dist/stores/streamState.js +314 -0
  76. package/dist/stores/viewerEndpoints.d.ts +48 -0
  77. package/dist/stores/viewerEndpoints.js +178 -0
  78. package/dist/types.d.ts +4 -0
  79. package/dist/types.js +4 -0
  80. package/dist/ui/Badge.svelte +21 -0
  81. package/dist/ui/Badge.svelte.d.ts +32 -0
  82. package/dist/ui/Button.svelte +42 -0
  83. package/dist/ui/Button.svelte.d.ts +35 -0
  84. package/dist/ui/Slider.svelte +100 -0
  85. package/dist/ui/Slider.svelte.d.ts +17 -0
  86. package/dist/ui/badge.d.ts +6 -0
  87. package/dist/ui/badge.js +10 -0
  88. package/dist/ui/button.d.ts +8 -0
  89. package/dist/ui/button.js +21 -0
  90. package/dist/ui/context-menu/ContextMenuCheckboxItem.svelte +34 -0
  91. package/dist/ui/context-menu/ContextMenuCheckboxItem.svelte.d.ts +31 -0
  92. package/dist/ui/context-menu/ContextMenuContent.svelte +17 -0
  93. package/dist/ui/context-menu/ContextMenuContent.svelte.d.ts +7 -0
  94. package/dist/ui/context-menu/ContextMenuItem.svelte +22 -0
  95. package/dist/ui/context-menu/ContextMenuItem.svelte.d.ts +8 -0
  96. package/dist/ui/context-menu/ContextMenuLabel.svelte +22 -0
  97. package/dist/ui/context-menu/ContextMenuLabel.svelte.d.ts +8 -0
  98. package/dist/ui/context-menu/ContextMenuPortal.svelte +11 -0
  99. package/dist/ui/context-menu/ContextMenuPortal.svelte.d.ts +6 -0
  100. package/dist/ui/context-menu/ContextMenuRadioItem.svelte +21 -0
  101. package/dist/ui/context-menu/ContextMenuRadioItem.svelte.d.ts +31 -0
  102. package/dist/ui/context-menu/ContextMenuSeparator.svelte +14 -0
  103. package/dist/ui/context-menu/ContextMenuSeparator.svelte.d.ts +6 -0
  104. package/dist/ui/context-menu/ContextMenuShortcut.svelte +19 -0
  105. package/dist/ui/context-menu/ContextMenuShortcut.svelte.d.ts +7 -0
  106. package/dist/ui/context-menu/ContextMenuSubContent.svelte +20 -0
  107. package/dist/ui/context-menu/ContextMenuSubContent.svelte.d.ts +7 -0
  108. package/dist/ui/context-menu/ContextMenuSubTrigger.svelte +34 -0
  109. package/dist/ui/context-menu/ContextMenuSubTrigger.svelte.d.ts +8 -0
  110. package/dist/ui/context-menu/index.d.ts +17 -0
  111. package/dist/ui/context-menu/index.js +17 -0
  112. package/package.json +51 -0
  113. package/src/DevModePanel.svelte +650 -0
  114. package/src/DvdLogo.svelte +213 -0
  115. package/src/Icons.svelte +27 -0
  116. package/src/IdleScreen.svelte +739 -0
  117. package/src/LoadingScreen.svelte +674 -0
  118. package/src/Player.svelte +483 -0
  119. package/src/PlayerControls.svelte +752 -0
  120. package/src/SeekBar.svelte +274 -0
  121. package/src/SkipIndicator.svelte +95 -0
  122. package/src/SpeedIndicator.svelte +37 -0
  123. package/src/StatsPanel.svelte +155 -0
  124. package/src/StreamStateOverlay.svelte +266 -0
  125. package/src/SubtitleRenderer.svelte +234 -0
  126. package/src/ThumbnailOverlay.svelte +96 -0
  127. package/src/TitleOverlay.svelte +47 -0
  128. package/src/assets/logomark.svg +56 -0
  129. package/src/components/VolumeIcons.svelte +53 -0
  130. package/src/global.d.ts +15 -0
  131. package/src/icons/FullscreenExitIcon.svelte +33 -0
  132. package/src/icons/FullscreenIcon.svelte +33 -0
  133. package/src/icons/PauseIcon.svelte +28 -0
  134. package/src/icons/PictureInPictureIcon.svelte +28 -0
  135. package/src/icons/PlayIcon.svelte +27 -0
  136. package/src/icons/SeekToLiveIcon.svelte +30 -0
  137. package/src/icons/SettingsIcon.svelte +40 -0
  138. package/src/icons/SkipBackIcon.svelte +32 -0
  139. package/src/icons/SkipForwardIcon.svelte +32 -0
  140. package/src/icons/StatsIcon.svelte +29 -0
  141. package/src/icons/VolumeOffIcon.svelte +29 -0
  142. package/src/icons/VolumeUpIcon.svelte +34 -0
  143. package/src/icons/index.ts +18 -0
  144. package/src/index.ts +84 -0
  145. package/src/player.css +2 -0
  146. package/src/stores/index.ts +88 -0
  147. package/src/stores/playbackQuality.ts +137 -0
  148. package/src/stores/playerContext.ts +221 -0
  149. package/src/stores/playerController.ts +568 -0
  150. package/src/stores/playerSelection.ts +216 -0
  151. package/src/stores/streamState.ts +367 -0
  152. package/src/stores/viewerEndpoints.ts +224 -0
  153. package/src/types.ts +6 -0
  154. package/src/ui/Badge.svelte +21 -0
  155. package/src/ui/Button.svelte +42 -0
  156. package/src/ui/Slider.svelte +100 -0
  157. package/src/ui/badge.ts +20 -0
  158. package/src/ui/button.ts +35 -0
  159. package/src/ui/context-menu/ContextMenuCheckboxItem.svelte +34 -0
  160. package/src/ui/context-menu/ContextMenuContent.svelte +17 -0
  161. package/src/ui/context-menu/ContextMenuItem.svelte +22 -0
  162. package/src/ui/context-menu/ContextMenuLabel.svelte +22 -0
  163. package/src/ui/context-menu/ContextMenuPortal.svelte +11 -0
  164. package/src/ui/context-menu/ContextMenuRadioItem.svelte +21 -0
  165. package/src/ui/context-menu/ContextMenuSeparator.svelte +14 -0
  166. package/src/ui/context-menu/ContextMenuShortcut.svelte +19 -0
  167. package/src/ui/context-menu/ContextMenuSubContent.svelte +20 -0
  168. package/src/ui/context-menu/ContextMenuSubTrigger.svelte +34 -0
  169. package/src/ui/context-menu/index.ts +36 -0
@@ -0,0 +1,650 @@
1
+ <!--
2
+ DevModePanel.svelte - Advanced settings overlay for testing player configurations
3
+ Port of src/components/DevModePanel.tsx
4
+ -->
5
+ <script lang="ts">
6
+ import {
7
+ cn,
8
+ globalPlayerManager,
9
+ QualityMonitor,
10
+ type StreamInfo,
11
+ type MistStreamInfo,
12
+ type PlaybackMode,
13
+ } from '@livepeer-frameworks/player-core';
14
+ import Button from './ui/Button.svelte';
15
+ import Badge from './ui/Badge.svelte';
16
+
17
+ /** Short labels for source types */
18
+ const SOURCE_TYPE_LABELS: Record<string, string> = {
19
+ 'html5/application/vnd.apple.mpegurl': 'HLS',
20
+ 'dash/video/mp4': 'DASH',
21
+ 'html5/video/mp4': 'MP4',
22
+ 'html5/video/webm': 'WebM',
23
+ 'whep': 'WHEP',
24
+ 'mist/html': 'Mist',
25
+ 'mist/legacy': 'Auto',
26
+ 'ws/video/mp4': 'MEWS',
27
+ };
28
+
29
+ interface Props {
30
+ /** Callback when user selects a combo (one-shot selection) */
31
+ onSettingsChange: (settings: { forcePlayer?: string; forceType?: string; forceSource?: number }) => void;
32
+ playbackMode?: PlaybackMode;
33
+ onModeChange?: (mode: PlaybackMode) => void;
34
+ onReload?: () => void;
35
+ streamInfo?: StreamInfo | null;
36
+ mistStreamInfo?: MistStreamInfo | null;
37
+ currentPlayer?: { name: string; shortname: string } | null;
38
+ currentSource?: { url: string; type: string } | null;
39
+ videoElement?: HTMLVideoElement | null;
40
+ protocol?: string;
41
+ nodeId?: string;
42
+ isVisible?: boolean;
43
+ isOpen?: boolean;
44
+ onOpenChange?: (isOpen: boolean) => void;
45
+ }
46
+
47
+ let {
48
+ onSettingsChange,
49
+ playbackMode = 'auto',
50
+ onModeChange = undefined,
51
+ onReload = undefined,
52
+ streamInfo = null,
53
+ mistStreamInfo = null,
54
+ currentPlayer = null,
55
+ currentSource = null,
56
+ videoElement = null,
57
+ protocol = undefined,
58
+ nodeId = undefined,
59
+ isVisible = true,
60
+ isOpen: controlledIsOpen = undefined,
61
+ onOpenChange = undefined,
62
+ }: Props = $props();
63
+
64
+ // Internal state
65
+ let internalIsOpen = $state(false);
66
+ let activeTab = $state<'config' | 'stats'>('config');
67
+ let hoveredComboIndex = $state<number | null>(null);
68
+ let tooltipAbove = $state(false);
69
+ let showDisabledPlayers = $state(false);
70
+ let comboListRef: HTMLDivElement | undefined = $state();
71
+
72
+ // Quality monitoring state
73
+ let playbackScore = $state(1.0);
74
+ let qualityScore = $state(100);
75
+ let stallCount = $state(0);
76
+ let frameDropRate = $state(0);
77
+ let qualityMonitor: QualityMonitor | null = null;
78
+
79
+ // Video stats
80
+ let stats = $state<{
81
+ resolution: string;
82
+ buffered: string;
83
+ playbackRate: string;
84
+ currentTime: string;
85
+ duration: string;
86
+ readyState: number;
87
+ networkState: number;
88
+ } | null>(null);
89
+
90
+ // Player-specific stats (HLS.js / WebRTC)
91
+ let playerStats = $state<any>(null);
92
+ let statsIntervalRef: ReturnType<typeof setInterval> | null = null;
93
+
94
+ // Controlled/uncontrolled state
95
+ let isOpen = $derived(controlledIsOpen !== undefined ? controlledIsOpen : internalIsOpen);
96
+
97
+ function setIsOpen(value: boolean) {
98
+ if (onOpenChange) {
99
+ onOpenChange(value);
100
+ } else {
101
+ internalIsOpen = value;
102
+ }
103
+ }
104
+
105
+ // Get all player-source combinations with scores
106
+ // getAllCombinations now includes all combos (compatible + incompatible)
107
+ // and uses content-based caching - won't spam on every MistServer update
108
+ let allCombinations = $derived.by(() => {
109
+ if (!streamInfo) return [];
110
+ try {
111
+ return globalPlayerManager.getAllCombinations(streamInfo, playbackMode);
112
+ } catch {
113
+ return [];
114
+ }
115
+ });
116
+
117
+ let combinations = $derived(allCombinations.filter(c => c.compatible));
118
+
119
+ // Find active combo index
120
+ let activeComboIndex = $derived.by(() => {
121
+ if (!currentPlayer || !currentSource || allCombinations.length === 0) return -1;
122
+ return allCombinations.findIndex(
123
+ c => c.player === currentPlayer.shortname && c.sourceType === currentSource.type
124
+ );
125
+ });
126
+
127
+ let activeCompatibleIndex = $derived.by(() => {
128
+ if (!currentPlayer || !currentSource || combinations.length === 0) return -1;
129
+ return combinations.findIndex(
130
+ c => c.player === currentPlayer.shortname && c.sourceType === currentSource.type
131
+ );
132
+ });
133
+
134
+ // Handlers
135
+ function handleReload() {
136
+ // Just trigger reload - controller manages the state
137
+ onReload?.();
138
+ }
139
+
140
+ function handleNextCombo() {
141
+ if (combinations.length === 0) return;
142
+ const startIdx = activeCompatibleIndex >= 0 ? activeCompatibleIndex : -1;
143
+ const nextIdx = (startIdx + 1) % combinations.length;
144
+ const combo = combinations[nextIdx];
145
+ onSettingsChange({
146
+ forcePlayer: combo.player,
147
+ forceType: combo.sourceType,
148
+ forceSource: combo.sourceIndex,
149
+ });
150
+ }
151
+
152
+ function handleSelectCombo(index: number) {
153
+ const combo = allCombinations[index];
154
+ if (!combo) return;
155
+ onSettingsChange({
156
+ forcePlayer: combo.player,
157
+ forceType: combo.sourceType,
158
+ forceSource: combo.sourceIndex,
159
+ });
160
+ }
161
+
162
+ function handleComboHover(index: number, e: MouseEvent) {
163
+ hoveredComboIndex = index;
164
+ if (comboListRef) {
165
+ const container = comboListRef;
166
+ const row = e.currentTarget as HTMLElement;
167
+ const containerRect = container.getBoundingClientRect();
168
+ const rowRect = row.getBoundingClientRect();
169
+ const relativePosition = (rowRect.top - containerRect.top) / containerRect.height;
170
+ tooltipAbove = relativePosition > 0.6;
171
+ }
172
+ }
173
+
174
+ // Quality monitoring
175
+ $effect(() => {
176
+ if (videoElement && isOpen) {
177
+ if (!qualityMonitor) {
178
+ qualityMonitor = new QualityMonitor({
179
+ sampleInterval: 500,
180
+ onSample: (quality) => {
181
+ qualityScore = quality.score;
182
+ stallCount = quality.stallCount;
183
+ frameDropRate = quality.frameDropRate;
184
+ if (qualityMonitor) {
185
+ playbackScore = qualityMonitor.getPlaybackScore();
186
+ }
187
+ },
188
+ });
189
+ }
190
+ qualityMonitor.start(videoElement);
191
+ }
192
+
193
+ return () => {
194
+ qualityMonitor?.stop();
195
+ };
196
+ });
197
+
198
+ // Video stats polling
199
+ $effect(() => {
200
+ if (!isOpen || activeTab !== 'stats') return;
201
+
202
+ function updateStats() {
203
+ const player = globalPlayerManager.getCurrentPlayer();
204
+ const v = player?.getVideoElement() || videoElement;
205
+ if (!v) {
206
+ stats = null;
207
+ return;
208
+ }
209
+ stats = {
210
+ resolution: `${v.videoWidth}x${v.videoHeight}`,
211
+ buffered: v.buffered.length > 0
212
+ ? (v.buffered.end(v.buffered.length - 1) - v.currentTime).toFixed(1)
213
+ : '0',
214
+ playbackRate: v.playbackRate.toFixed(2),
215
+ currentTime: v.currentTime.toFixed(1),
216
+ duration: isFinite(v.duration) ? v.duration.toFixed(1) : 'live',
217
+ readyState: v.readyState,
218
+ networkState: v.networkState,
219
+ };
220
+ }
221
+
222
+ updateStats();
223
+ const interval = setInterval(updateStats, 500);
224
+ return () => clearInterval(interval);
225
+ });
226
+
227
+ // Poll player-specific stats when stats tab is open
228
+ $effect(() => {
229
+ if (!isOpen || activeTab !== 'stats') {
230
+ playerStats = null;
231
+ return;
232
+ }
233
+
234
+ async function pollStats() {
235
+ try {
236
+ const player = globalPlayerManager.getCurrentPlayer();
237
+ if (player && typeof player.getStats === 'function') {
238
+ const stats = await player.getStats();
239
+ if (stats) {
240
+ playerStats = stats;
241
+ }
242
+ }
243
+ } catch {
244
+ // Ignore errors
245
+ }
246
+ }
247
+
248
+ // Poll immediately and then every 500ms
249
+ pollStats();
250
+ statsIntervalRef = setInterval(pollStats, 500);
251
+
252
+ return () => {
253
+ if (statsIntervalRef) {
254
+ clearInterval(statsIntervalRef);
255
+ statsIntervalRef = null;
256
+ }
257
+ };
258
+ });
259
+ </script>
260
+
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}
277
+ <div class="fw-dev-panel">
278
+ <!-- Header with tabs -->
279
+ <div class="fw-dev-header">
280
+ <button
281
+ type="button"
282
+ onclick={() => activeTab = 'config'}
283
+ class={cn('fw-dev-tab', activeTab === 'config' && 'fw-dev-tab--active')}
284
+ >
285
+ Config
286
+ </button>
287
+ <button
288
+ type="button"
289
+ onclick={() => activeTab = 'stats'}
290
+ class={cn('fw-dev-tab', activeTab === 'stats' && 'fw-dev-tab--active')}
291
+ >
292
+ Stats
293
+ </button>
294
+ <div class="fw-dev-spacer"></div>
295
+ <button
296
+ type="button"
297
+ onclick={() => setIsOpen(false)}
298
+ class="fw-dev-close"
299
+ aria-label="Close dev mode panel"
300
+ >
301
+ <svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.5">
302
+ <path d="M2 2l8 8M10 2l-8 8" />
303
+ </svg>
304
+ </button>
305
+ </div>
306
+
307
+ {#if activeTab === 'config'}
308
+ <div bind:this={comboListRef} class="fw-dev-body">
309
+ <!-- Current State -->
310
+ <div class="fw-dev-section">
311
+ <div class="fw-dev-label">Active</div>
312
+ <div class="fw-dev-value">
313
+ {currentPlayer?.name || 'None'}{' '}
314
+ <span class="fw-dev-value-arrow">→</span>{' '}
315
+ {SOURCE_TYPE_LABELS[currentSource?.type || ''] || currentSource?.type || '—'}
316
+ </div>
317
+ {#if nodeId}
318
+ <div class="fw-dev-value-muted">Node: {nodeId}</div>
319
+ {/if}
320
+ </div>
321
+
322
+ <!-- Playback Mode Selector -->
323
+ <div class="fw-dev-section">
324
+ <div class="fw-dev-label">Playback Mode</div>
325
+ <div class="fw-dev-mode-group">
326
+ {#each ['auto', 'low-latency', 'quality'] as mode}
327
+ <button
328
+ type="button"
329
+ onclick={() => onModeChange?.(mode as PlaybackMode)}
330
+ class={cn('fw-dev-mode-btn', playbackMode === mode && 'fw-dev-mode-btn--active')}
331
+ >
332
+ {mode === 'low-latency' ? 'Low Lat' : mode.charAt(0).toUpperCase() + mode.slice(1)}
333
+ </button>
334
+ {/each}
335
+ </div>
336
+ <div class="fw-dev-mode-desc">
337
+ {#if playbackMode === 'auto'}Balanced: MP4/WS → WHEP → HLS{/if}
338
+ {#if playbackMode === 'low-latency'}WHEP/WebRTC first (sub-1s delay){/if}
339
+ {#if playbackMode === 'quality'}MP4/WS first, HLS fallback{/if}
340
+ </div>
341
+ </div>
342
+
343
+ <!-- Action buttons -->
344
+ <div class="fw-dev-actions">
345
+ <button type="button" onclick={handleReload} class="fw-dev-action-btn">
346
+ Reload
347
+ </button>
348
+ <button type="button" onclick={handleNextCombo} class="fw-dev-action-btn">
349
+ Next Option
350
+ </button>
351
+ </div>
352
+
353
+ <!-- Combo list -->
354
+ <div class="fw-dev-section fw-dev-section-header">
355
+ <div class="fw-dev-list-header">
356
+ <span class="fw-dev-list-title">
357
+ Player Options ({combinations.length})
358
+ </span>
359
+ {#if allCombinations.length > combinations.length}
360
+ <button
361
+ type="button"
362
+ onclick={() => showDisabledPlayers = !showDisabledPlayers}
363
+ class="fw-dev-list-toggle"
364
+ >
365
+ <svg
366
+ width="10"
367
+ height="10"
368
+ viewBox="0 0 24 24"
369
+ fill="none"
370
+ stroke="currentColor"
371
+ stroke-width="2"
372
+ class={cn('fw-dev-chevron', showDisabledPlayers && 'fw-dev-chevron--open')}
373
+ >
374
+ <path d="M6 9l6 6 6-6" />
375
+ </svg>
376
+ {showDisabledPlayers ? 'Hide' : 'Show'} disabled ({allCombinations.length - combinations.length})
377
+ </button>
378
+ {/if}
379
+ </div>
380
+
381
+ {#if allCombinations.length === 0}
382
+ <div class="fw-dev-list-empty">No stream info available</div>
383
+ {:else}
384
+ {#each allCombinations as combo, index}
385
+ {@const isCodecIncompat = (combo as any).codecIncompatible === true}
386
+ {@const shouldShow = combo.compatible || isCodecIncompat || showDisabledPlayers}
387
+ {@const isActive = activeComboIndex === index}
388
+ {@const typeLabel = SOURCE_TYPE_LABELS[combo.sourceType] || combo.sourceType.split('/').pop()}
389
+
390
+ {#if shouldShow}
391
+ <div
392
+ class="fw-dev-combo"
393
+ onmouseenter={(e) => handleComboHover(index, e)}
394
+ onmouseleave={() => hoveredComboIndex = null}
395
+ >
396
+ <button
397
+ type="button"
398
+ onclick={() => handleSelectCombo(index)}
399
+ class={cn(
400
+ 'fw-dev-combo-btn',
401
+ isActive && 'fw-dev-combo-btn--active',
402
+ !combo.compatible && !isCodecIncompat && 'fw-dev-combo-btn--disabled',
403
+ isCodecIncompat && 'fw-dev-combo-btn--codec-warn'
404
+ )}
405
+ >
406
+ <!-- Rank -->
407
+ <span class={cn(
408
+ 'fw-dev-combo-rank',
409
+ isActive ? 'fw-dev-combo-rank--active' :
410
+ !combo.compatible && !isCodecIncompat ? 'fw-dev-combo-rank--disabled' :
411
+ isCodecIncompat ? 'fw-dev-combo-rank--warn' : ''
412
+ )}>
413
+ {combo.compatible ? index + 1 : isCodecIncompat ? '⚠' : '—'}
414
+ </span>
415
+ <!-- Player + Protocol -->
416
+ <span class="fw-dev-combo-name">
417
+ {combo.playerName}{' '}
418
+ <span class="fw-dev-combo-arrow">→</span>{' '}
419
+ <span class={cn(
420
+ 'fw-dev-combo-type',
421
+ isCodecIncompat && 'fw-dev-combo-type--warn',
422
+ !combo.compatible && !isCodecIncompat && 'fw-dev-combo-type--disabled'
423
+ )}>{typeLabel}</span>
424
+ </span>
425
+ <!-- Score -->
426
+ <span class={cn(
427
+ 'fw-dev-combo-score',
428
+ !combo.compatible && !isCodecIncompat ? 'fw-dev-combo-score--disabled' :
429
+ isCodecIncompat ? 'fw-dev-combo-score--low' :
430
+ combo.score >= 2 ? 'fw-dev-combo-score--high' :
431
+ combo.score >= 1.5 ? 'fw-dev-combo-score--mid' :
432
+ 'fw-dev-combo-score--low'
433
+ )}>
434
+ {combo.score.toFixed(2)}
435
+ </span>
436
+ </button>
437
+
438
+ <!-- Tooltip -->
439
+ {#if hoveredComboIndex === index}
440
+ <div class={cn(
441
+ 'fw-dev-tooltip',
442
+ tooltipAbove ? 'fw-dev-tooltip--above' : 'fw-dev-tooltip--below'
443
+ )}>
444
+ <div class="fw-dev-tooltip-header">
445
+ <div class="fw-dev-tooltip-title">{combo.playerName}</div>
446
+ <div class="fw-dev-tooltip-subtitle">{combo.sourceType}</div>
447
+ {#if combo.scoreBreakdown?.trackTypes?.length}
448
+ <div class="fw-dev-tooltip-tracks">
449
+ Tracks: <span class="fw-dev-tooltip-value">{combo.scoreBreakdown.trackTypes.join(', ')}</span>
450
+ </div>
451
+ {/if}
452
+ </div>
453
+ {#if combo.compatible && combo.scoreBreakdown}
454
+ <div class="fw-dev-tooltip-score">Score: {combo.score.toFixed(2)}</div>
455
+ <div class="fw-dev-tooltip-row">Tracks: <span class="fw-dev-tooltip-value">{combo.scoreBreakdown.trackScore.toFixed(2)}</span> <span class="fw-dev-tooltip-weight">x{combo.scoreBreakdown.weights.tracks}</span></div>
456
+ <div class="fw-dev-tooltip-row">Priority: <span class="fw-dev-tooltip-value">{combo.scoreBreakdown.priorityScore.toFixed(2)}</span> <span class="fw-dev-tooltip-weight">x{combo.scoreBreakdown.weights.priority}</span></div>
457
+ <div class="fw-dev-tooltip-row">Source: <span class="fw-dev-tooltip-value">{combo.scoreBreakdown.sourceScore.toFixed(2)}</span> <span class="fw-dev-tooltip-weight">x{combo.scoreBreakdown.weights.source}</span></div>
458
+ {:else}
459
+ <div class="fw-dev-tooltip-error">{combo.incompatibleReason || 'Incompatible'}</div>
460
+ {/if}
461
+ </div>
462
+ {/if}
463
+ </div>
464
+ {/if}
465
+ {/each}
466
+ {/if}
467
+ </div>
468
+ </div>
469
+ {:else if activeTab === 'stats'}
470
+ <div class="fw-dev-body">
471
+ <!-- Playback Rate -->
472
+ <div class="fw-dev-section">
473
+ <div class="fw-dev-label">Playback Rate</div>
474
+ <div class="fw-dev-rate">
475
+ <div class={cn(
476
+ 'fw-dev-rate-value',
477
+ playbackScore >= 0.95 && playbackScore <= 1.05 ? 'fw-dev-stat-value--good' :
478
+ playbackScore > 1.05 ? 'fw-dev-stat-value--accent' :
479
+ playbackScore >= 0.75 ? 'fw-dev-stat-value--warn' :
480
+ 'fw-dev-stat-value--bad'
481
+ )}>
482
+ {playbackScore.toFixed(2)}×
483
+ </div>
484
+ <div class="fw-dev-rate-status">
485
+ {playbackScore >= 0.95 && playbackScore <= 1.05 ? 'realtime' :
486
+ playbackScore > 1.05 ? 'catching up' :
487
+ playbackScore >= 0.75 ? 'slightly slow' : 'stalling'}
488
+ </div>
489
+ </div>
490
+ <div class="fw-dev-rate-stats">
491
+ <span class={qualityScore >= 75 ? 'fw-dev-stat-value--good' : 'fw-dev-stat-value--bad'}>Quality: {qualityScore}/100</span>
492
+ <span class={stallCount === 0 ? 'fw-dev-stat-value--good' : 'fw-dev-stat-value--warn'}>Stalls: {stallCount}</span>
493
+ <span class={frameDropRate < 1 ? 'fw-dev-stat-value--good' : 'fw-dev-stat-value--bad'}>Drops: {frameDropRate.toFixed(1)}%</span>
494
+ </div>
495
+ </div>
496
+
497
+ <!-- Video Stats -->
498
+ {#if stats}
499
+ <div class="fw-dev-stat"><span class="fw-dev-stat-label">Resolution</span><span class="fw-dev-stat-value">{stats.resolution}</span></div>
500
+ <div class="fw-dev-stat"><span class="fw-dev-stat-label">Buffer</span><span class="fw-dev-stat-value">{stats.buffered}s</span></div>
501
+ <div class="fw-dev-stat"><span class="fw-dev-stat-label">Playback Rate</span><span class="fw-dev-stat-value">{stats.playbackRate}x</span></div>
502
+ <div class="fw-dev-stat"><span class="fw-dev-stat-label">Time</span><span class="fw-dev-stat-value">{stats.currentTime} / {stats.duration}</span></div>
503
+ <div class="fw-dev-stat"><span class="fw-dev-stat-label">Ready State</span><span class="fw-dev-stat-value">{stats.readyState}</span></div>
504
+ <div class="fw-dev-stat"><span class="fw-dev-stat-label">Network State</span><span class="fw-dev-stat-value">{stats.networkState}</span></div>
505
+ {#if protocol}
506
+ <div class="fw-dev-stat"><span class="fw-dev-stat-label">Protocol</span><span class="fw-dev-stat-value">{protocol}</span></div>
507
+ {/if}
508
+ {#if nodeId}
509
+ <div class="fw-dev-stat"><span class="fw-dev-stat-label">Node ID</span><span class="fw-dev-stat-value">{nodeId}</span></div>
510
+ {/if}
511
+ {:else}
512
+ <div class="fw-dev-list-empty">No video element available</div>
513
+ {/if}
514
+
515
+ <!-- Player-specific Stats (HLS.js / WebRTC) -->
516
+ {#if playerStats}
517
+ <div class="fw-dev-section fw-dev-section-header">
518
+ <div class="fw-dev-label">
519
+ {playerStats.type === 'hls' ? 'HLS.js Stats' :
520
+ playerStats.type === 'webrtc' ? 'WebRTC Stats' : 'Player Stats'}
521
+ </div>
522
+ </div>
523
+ <!-- HLS-specific stats -->
524
+ {#if playerStats.type === 'hls'}
525
+ <div class="fw-dev-stat">
526
+ <span class="fw-dev-stat-label">Bitrate</span>
527
+ <span class="fw-dev-stat-value fw-dev-stat-value--accent">
528
+ {playerStats.currentBitrate > 0 ? `${Math.round(playerStats.currentBitrate / 1000)} kbps` : 'N/A'}
529
+ </span>
530
+ </div>
531
+ <div class="fw-dev-stat">
532
+ <span class="fw-dev-stat-label">Bandwidth Est.</span>
533
+ <span class="fw-dev-stat-value">
534
+ {playerStats.bandwidthEstimate > 0 ? `${Math.round(playerStats.bandwidthEstimate / 1000)} kbps` : 'N/A'}
535
+ </span>
536
+ </div>
537
+ <div class="fw-dev-stat">
538
+ <span class="fw-dev-stat-label">Level</span>
539
+ <span class="fw-dev-stat-value">
540
+ {playerStats.currentLevel >= 0 ? playerStats.currentLevel : 'Auto'} / {playerStats.levels?.length || 0}
541
+ </span>
542
+ </div>
543
+ {#if playerStats.latency !== undefined}
544
+ <div class="fw-dev-stat">
545
+ <span class="fw-dev-stat-label">Latency</span>
546
+ <span class={playerStats.latency > 5000 ? 'fw-dev-stat-value fw-dev-stat-value--warn' : 'fw-dev-stat-value'}>
547
+ {Math.round(playerStats.latency)} ms
548
+ </span>
549
+ </div>
550
+ {/if}
551
+ {/if}
552
+
553
+ <!-- WebRTC-specific stats -->
554
+ {#if playerStats.type === 'webrtc'}
555
+ {#if playerStats.video}
556
+ <div class="fw-dev-stat">
557
+ <span class="fw-dev-stat-label">Video Bitrate</span>
558
+ <span class="fw-dev-stat-value fw-dev-stat-value--accent">
559
+ {playerStats.video.bitrate > 0 ? `${Math.round(playerStats.video.bitrate / 1000)} kbps` : 'N/A'}
560
+ </span>
561
+ </div>
562
+ <div class="fw-dev-stat">
563
+ <span class="fw-dev-stat-label">FPS</span>
564
+ <span class="fw-dev-stat-value">{Math.round(playerStats.video.framesPerSecond || 0)}</span>
565
+ </div>
566
+ <div class="fw-dev-stat">
567
+ <span class="fw-dev-stat-label">Frames</span>
568
+ <span class="fw-dev-stat-value">
569
+ {playerStats.video.framesDecoded} decoded,{' '}
570
+ <span class={playerStats.video.frameDropRate > 1 ? 'fw-dev-stat-value--bad' : 'fw-dev-stat-value--good'}>
571
+ {playerStats.video.framesDropped} dropped
572
+ </span>
573
+ </span>
574
+ </div>
575
+ <div class="fw-dev-stat">
576
+ <span class="fw-dev-stat-label">Packet Loss</span>
577
+ <span class={playerStats.video.packetLossRate > 1 ? 'fw-dev-stat-value fw-dev-stat-value--bad' : 'fw-dev-stat-value fw-dev-stat-value--good'}>
578
+ {playerStats.video.packetLossRate?.toFixed(2) || 0}%
579
+ </span>
580
+ </div>
581
+ <div class="fw-dev-stat">
582
+ <span class="fw-dev-stat-label">Jitter</span>
583
+ <span class={playerStats.video.jitter > 30 ? 'fw-dev-stat-value fw-dev-stat-value--warn' : 'fw-dev-stat-value'}>
584
+ {playerStats.video.jitter?.toFixed(1) || 0} ms
585
+ </span>
586
+ </div>
587
+ <div class="fw-dev-stat">
588
+ <span class="fw-dev-stat-label">Jitter Buffer</span>
589
+ <span class="fw-dev-stat-value">{playerStats.video.jitterBufferDelay?.toFixed(1) || 0} ms</span>
590
+ </div>
591
+ {/if}
592
+ {#if playerStats.network}
593
+ <div class="fw-dev-stat">
594
+ <span class="fw-dev-stat-label">RTT</span>
595
+ <span class={playerStats.network.rtt > 200 ? 'fw-dev-stat-value fw-dev-stat-value--warn' : 'fw-dev-stat-value'}>
596
+ {Math.round(playerStats.network.rtt || 0)} ms
597
+ </span>
598
+ </div>
599
+ {/if}
600
+ {/if}
601
+ {/if}
602
+
603
+ <!-- MistServer Track Info -->
604
+ {#if mistStreamInfo?.meta?.tracks && Object.keys(mistStreamInfo.meta.tracks).length > 0}
605
+ <div class="fw-dev-section fw-dev-section-header">
606
+ <div class="fw-dev-label">
607
+ Tracks ({Object.keys(mistStreamInfo.meta.tracks).length})
608
+ </div>
609
+ </div>
610
+ {#each Object.entries(mistStreamInfo.meta.tracks) as [id, track]}
611
+ <div class="fw-dev-track">
612
+ <div class="fw-dev-track-header">
613
+ <span class={cn(
614
+ 'fw-dev-track-badge',
615
+ track.type === 'video' ? 'fw-dev-track-badge--video' :
616
+ track.type === 'audio' ? 'fw-dev-track-badge--audio' :
617
+ 'fw-dev-track-badge--other'
618
+ )}>
619
+ {track.type}
620
+ </span>
621
+ <span class="fw-dev-track-codec">{track.codec}</span>
622
+ <span class="fw-dev-track-id">#{id}</span>
623
+ </div>
624
+ <div class="fw-dev-track-meta">
625
+ {#if track.type === 'video' && track.width && track.height}
626
+ <span>{track.width}×{track.height}</span>
627
+ {/if}
628
+ {#if track.bps}
629
+ <span>{Math.round(track.bps / 1000)} kbps</span>
630
+ {/if}
631
+ {#if track.fpks}
632
+ <span>{Math.round(track.fpks / 1000)} fps</span>
633
+ {/if}
634
+ {#if track.type === 'audio' && track.channels}
635
+ <span>{track.channels}ch</span>
636
+ {/if}
637
+ {#if track.type === 'audio' && track.rate}
638
+ <span>{track.rate} Hz</span>
639
+ {/if}
640
+ {#if track.lang}
641
+ <span>{track.lang}</span>
642
+ {/if}
643
+ </div>
644
+ </div>
645
+ {/each}
646
+ {/if}
647
+ </div>
648
+ {/if}
649
+ </div>
650
+ {/if}
@@ -0,0 +1,31 @@
1
+ import { type StreamInfo, type MistStreamInfo, type PlaybackMode } from '@livepeer-frameworks/player-core';
2
+ interface Props {
3
+ /** Callback when user selects a combo (one-shot selection) */
4
+ onSettingsChange: (settings: {
5
+ forcePlayer?: string;
6
+ forceType?: string;
7
+ forceSource?: number;
8
+ }) => void;
9
+ playbackMode?: PlaybackMode;
10
+ onModeChange?: (mode: PlaybackMode) => void;
11
+ onReload?: () => void;
12
+ streamInfo?: StreamInfo | null;
13
+ mistStreamInfo?: MistStreamInfo | null;
14
+ currentPlayer?: {
15
+ name: string;
16
+ shortname: string;
17
+ } | null;
18
+ currentSource?: {
19
+ url: string;
20
+ type: string;
21
+ } | null;
22
+ videoElement?: HTMLVideoElement | null;
23
+ protocol?: string;
24
+ nodeId?: string;
25
+ isVisible?: boolean;
26
+ isOpen?: boolean;
27
+ onOpenChange?: (isOpen: boolean) => void;
28
+ }
29
+ declare const DevModePanel: import("svelte").Component<Props, {}, "">;
30
+ type DevModePanel = ReturnType<typeof DevModePanel>;
31
+ export default DevModePanel;