@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,739 @@
1
+ <!--
2
+ IdleScreen.svelte - Idle/offline state screen with Tokyo Night theme
3
+ Port of src/components/IdleScreen.tsx
4
+
5
+ Features:
6
+ - Animated colored bubbles with Tokyo Night palette
7
+ - Center logo with mouse-tracking "push away" effect
8
+ - Bouncing DVD logo component
9
+ - Hitmarker sound effects (Web Audio API synthesis)
10
+ - Floating particles with gradients
11
+ - Pulsing circle around logo
12
+ - Animated background gradient shifts
13
+ - Status overlay at bottom
14
+ -->
15
+ <script lang="ts">
16
+ import { onMount, onDestroy } from 'svelte';
17
+ import type { StreamStatus } from '@livepeer-frameworks/player-core';
18
+ import DvdLogo from './DvdLogo.svelte';
19
+ import logomarkAsset from './assets/logomark.svg';
20
+
21
+ interface Props {
22
+ status?: StreamStatus;
23
+ message?: string;
24
+ percentage?: number;
25
+ error?: string;
26
+ onRetry?: () => void;
27
+ }
28
+
29
+ let {
30
+ status = 'OFFLINE',
31
+ message = 'Waiting for stream...',
32
+ percentage = undefined,
33
+ error = undefined,
34
+ onRetry = undefined,
35
+ }: Props = $props();
36
+
37
+ // Container ref for mouse tracking
38
+ let containerRef: HTMLDivElement | undefined = $state();
39
+
40
+ // Hitmarker state
41
+ interface Hitmarker {
42
+ id: number;
43
+ x: number;
44
+ y: number;
45
+ }
46
+ let hitmarkers = $state<Hitmarker[]>([]);
47
+
48
+ // Center logo state
49
+ let logoSize = $state(100);
50
+ let offset = $state({ x: 0, y: 0 });
51
+ let isHovered = $state(false);
52
+
53
+ // Tokyo Night inspired pastel colors for bubbles
54
+ const bubbleColors = [
55
+ 'rgba(122, 162, 247, 0.2)', // Terminal Blue
56
+ 'rgba(187, 154, 247, 0.2)', // Terminal Magenta
57
+ 'rgba(158, 206, 106, 0.2)', // Strings/CSS classes
58
+ 'rgba(115, 218, 202, 0.2)', // Terminal Green
59
+ 'rgba(125, 207, 255, 0.2)', // Terminal Cyan
60
+ 'rgba(247, 118, 142, 0.2)', // Keywords/Terminal Red
61
+ 'rgba(224, 175, 104, 0.2)', // Terminal Yellow
62
+ 'rgba(42, 195, 222, 0.2)', // Language functions
63
+ ];
64
+
65
+ // Particle colors
66
+ const particleColors = [
67
+ '#7aa2f7', // Terminal Blue
68
+ '#bb9af7', // Terminal Magenta
69
+ '#9ece6a', // Strings/CSS classes
70
+ '#73daca', // Terminal Green
71
+ '#7dcfff', // Terminal Cyan
72
+ '#f7768e', // Keywords/Terminal Red
73
+ '#e0af68', // Terminal Yellow
74
+ '#2ac3de', // Language functions
75
+ ];
76
+
77
+ // Generate random particles (matching React's 12 particles)
78
+ const particles = Array.from({ length: 12 }, (_, i) => ({
79
+ left: Math.random() * 100,
80
+ size: Math.random() * 4 + 2,
81
+ color: particleColors[i % 8],
82
+ duration: 8 + Math.random() * 4,
83
+ delay: Math.random() * 8,
84
+ }));
85
+
86
+ // Animated bubble state
87
+ interface BubbleState {
88
+ position: { top: number; left: number };
89
+ size: number;
90
+ opacity: number;
91
+ color: string;
92
+ timeoutId: ReturnType<typeof setTimeout> | null;
93
+ }
94
+
95
+ let bubbles = $state<BubbleState[]>(
96
+ Array.from({ length: 8 }, (_, i) => ({
97
+ position: { top: Math.random() * 80 + 10, left: Math.random() * 80 + 10 },
98
+ size: Math.random() * 60 + 30,
99
+ opacity: 0,
100
+ color: bubbleColors[i % bubbleColors.length],
101
+ timeoutId: null,
102
+ }))
103
+ );
104
+
105
+ // Bubble animation cycle
106
+ function animateBubble(index: number) {
107
+ bubbles[index].opacity = 0.15;
108
+
109
+ const visibleDuration = 4000 + Math.random() * 3000;
110
+
111
+ const timeout1 = setTimeout(() => {
112
+ bubbles[index].opacity = 0;
113
+
114
+ const timeout2 = setTimeout(() => {
115
+ bubbles[index].position = {
116
+ top: Math.random() * 80 + 10,
117
+ left: Math.random() * 80 + 10,
118
+ };
119
+ bubbles[index].size = Math.random() * 60 + 30;
120
+
121
+ const timeout3 = setTimeout(() => {
122
+ animateBubble(index);
123
+ }, 200);
124
+ bubbles[index].timeoutId = timeout3;
125
+ }, 1500);
126
+ bubbles[index].timeoutId = timeout2;
127
+ }, visibleDuration);
128
+ bubbles[index].timeoutId = timeout1;
129
+ }
130
+
131
+ // Hitmarker sound synthesis
132
+ function createSyntheticHitmarkerSound() {
133
+ try {
134
+ const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)();
135
+
136
+ const oscillator1 = audioContext.createOscillator();
137
+ const oscillator2 = audioContext.createOscillator();
138
+ const noiseBuffer = audioContext.createBuffer(1, audioContext.sampleRate * 0.1, audioContext.sampleRate);
139
+ const noiseSource = audioContext.createBufferSource();
140
+
141
+ // Generate white noise for the initial "crack"
142
+ const noiseData = noiseBuffer.getChannelData(0);
143
+ for (let i = 0; i < noiseData.length; i++) {
144
+ noiseData[i] = Math.random() * 2 - 1;
145
+ }
146
+ noiseSource.buffer = noiseBuffer;
147
+
148
+ const gainNode1 = audioContext.createGain();
149
+ const gainNode2 = audioContext.createGain();
150
+ const noiseGain = audioContext.createGain();
151
+ const masterGain = audioContext.createGain();
152
+
153
+ oscillator1.connect(gainNode1);
154
+ oscillator2.connect(gainNode2);
155
+ noiseSource.connect(noiseGain);
156
+
157
+ gainNode1.connect(masterGain);
158
+ gainNode2.connect(masterGain);
159
+ noiseGain.connect(masterGain);
160
+ masterGain.connect(audioContext.destination);
161
+
162
+ // Sharp metallic frequencies
163
+ oscillator1.frequency.setValueAtTime(1800, audioContext.currentTime);
164
+ oscillator1.frequency.exponentialRampToValueAtTime(900, audioContext.currentTime + 0.08);
165
+
166
+ oscillator2.frequency.setValueAtTime(3600, audioContext.currentTime);
167
+ oscillator2.frequency.exponentialRampToValueAtTime(1800, audioContext.currentTime + 0.04);
168
+
169
+ oscillator1.type = 'triangle';
170
+ oscillator2.type = 'sine';
171
+
172
+ // Sharp attack, quick decay
173
+ gainNode1.gain.setValueAtTime(0, audioContext.currentTime);
174
+ gainNode1.gain.linearRampToValueAtTime(0.4, audioContext.currentTime + 0.002);
175
+ gainNode1.gain.exponentialRampToValueAtTime(0.001, audioContext.currentTime + 0.12);
176
+
177
+ gainNode2.gain.setValueAtTime(0, audioContext.currentTime);
178
+ gainNode2.gain.linearRampToValueAtTime(0.3, audioContext.currentTime + 0.001);
179
+ gainNode2.gain.exponentialRampToValueAtTime(0.001, audioContext.currentTime + 0.06);
180
+
181
+ // Noise burst
182
+ noiseGain.gain.setValueAtTime(0, audioContext.currentTime);
183
+ noiseGain.gain.linearRampToValueAtTime(0.2, audioContext.currentTime + 0.001);
184
+ noiseGain.gain.exponentialRampToValueAtTime(0.001, audioContext.currentTime + 0.01);
185
+
186
+ masterGain.gain.setValueAtTime(0.5, audioContext.currentTime);
187
+
188
+ const startTime = audioContext.currentTime;
189
+ const stopTime = startTime + 0.15;
190
+
191
+ oscillator1.start(startTime);
192
+ oscillator2.start(startTime);
193
+ noiseSource.start(startTime);
194
+
195
+ oscillator1.stop(stopTime);
196
+ oscillator2.stop(stopTime);
197
+ noiseSource.stop(startTime + 0.02);
198
+ } catch {
199
+ // Audio context not available
200
+ }
201
+ }
202
+
203
+ function playHitmarkerSound() {
204
+ try {
205
+ const hitmarkerDataUrl = 'data:audio/mpeg;base64,SUQzBAAAAAAANFRDT04AAAAHAAADT3RoZXIAVFNTRQAAAA8AAANMYXZmNTcuODMuMTAwAAAAAAAAAAAA' +
206
+ 'AAD/+1QAAAAAAAAAAAAAAAAAAAAA' +
207
+ 'AAAAAAAAAAAAAAAAAAAAAABJbmZvAAAADwAAAAYAAAnAADs7Ozs7Ozs7Ozs7Ozs7OztiYmJiYmJiYmJi' +
208
+ 'YmJiYmJiYomJiYmJiYmJiYmJiYmJiYmxsbGxsbGxsbGxsbGxsbGxsdjY2NjY2NjY2NjY2NjY2NjY////' +
209
+ '/////////////////wAAAABMYXZjNTcuMTAAAAAAAAAAAAAAAAAkAkAAAAAAAAAJwOuMZun/+5RkAA8S' +
210
+ '/F23AGAaAi0AF0AAAAAInXsEAIRXyQ8D4OQgjEhE3cO7ujuHF0XCOu4G7xKbi3Funu7u7p9dw7unu7u7' +
211
+ 'p7u7u6fXcW7om7u7uiU3dxdT67u7p7uHdxelN3cW6fXcW7oXXd3eJTd3d0+u4t3iXdw4up70W4uiPruL' +
212
+ 'DzMw8Pz79Y99JfkyfPv5/h9uTJoy79Y99Y97q3vyZPJk0ZfrL6x73Vn+J35dKKS/STQyQ8CAiCPNuRAO' +
213
+ 'OqquAx+fzJeBKDAsgAMBuWcBsHKhjJTcCwIALyAvABbI0ZIcCmP8jHJe8gZAdVRp2TpnU/kUXV4iQuBA';
214
+
215
+ const audio = new Audio(hitmarkerDataUrl);
216
+ audio.volume = 0.3;
217
+ audio.play().catch(() => {
218
+ createSyntheticHitmarkerSound();
219
+ });
220
+ } catch {
221
+ createSyntheticHitmarkerSound();
222
+ }
223
+ }
224
+
225
+ function createHitmarker(e: { clientX: number; clientY: number }) {
226
+ if (!containerRef) return;
227
+
228
+ const rect = containerRef.getBoundingClientRect();
229
+ const x = e.clientX - rect.left;
230
+ const y = e.clientY - rect.top;
231
+
232
+ const newHitmarker: Hitmarker = {
233
+ id: Date.now() + Math.random(),
234
+ x,
235
+ y,
236
+ };
237
+
238
+ hitmarkers = [...hitmarkers, newHitmarker];
239
+ playHitmarkerSound();
240
+
241
+ setTimeout(() => {
242
+ hitmarkers = hitmarkers.filter(h => h.id !== newHitmarker.id);
243
+ }, 600);
244
+ }
245
+
246
+ // Mouse tracking for logo push-away effect
247
+ function handleMouseMove(e: MouseEvent) {
248
+ if (!containerRef) return;
249
+
250
+ const rect = containerRef.getBoundingClientRect();
251
+ const centerX = rect.left + rect.width / 2;
252
+ const centerY = rect.top + rect.height / 2;
253
+
254
+ const mouseX = e.clientX;
255
+ const mouseY = e.clientY;
256
+
257
+ const deltaX = mouseX - centerX;
258
+ const deltaY = mouseY - centerY;
259
+ const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
260
+
261
+ const maxDistance = logoSize * 1.5;
262
+ if (distance < maxDistance && distance > 0) {
263
+ const pushStrength = (maxDistance - distance) / maxDistance;
264
+ const pushDistance = 50 * pushStrength;
265
+
266
+ offset = {
267
+ x: -(deltaX / distance) * pushDistance,
268
+ y: -(deltaY / distance) * pushDistance,
269
+ };
270
+ isHovered = true;
271
+ } else {
272
+ offset = { x: 0, y: 0 };
273
+ isHovered = false;
274
+ }
275
+ }
276
+
277
+ function handleMouseLeave() {
278
+ offset = { x: 0, y: 0 };
279
+ isHovered = false;
280
+ }
281
+
282
+ function handleLogoClick(e: MouseEvent) {
283
+ e.stopPropagation();
284
+ createHitmarker({ clientX: e.clientX, clientY: e.clientY });
285
+ }
286
+
287
+ // Status helpers
288
+ function getStatusLabel(s?: StreamStatus): string {
289
+ switch (s) {
290
+ case 'ONLINE': return 'ONLINE';
291
+ case 'OFFLINE': return 'OFFLINE';
292
+ case 'INITIALIZING': return 'STARTING';
293
+ case 'BOOTING': return 'STARTING';
294
+ case 'WAITING_FOR_DATA': return 'WAITING';
295
+ case 'SHUTTING_DOWN': return 'ENDING';
296
+ case 'ERROR': return 'ERROR';
297
+ case 'INVALID': return 'ERROR';
298
+ default: return 'CONNECTING';
299
+ }
300
+ }
301
+
302
+ let statusLabel = $derived(getStatusLabel(status));
303
+ let showRetry = $derived((status === 'ERROR' || status === 'INVALID') && onRetry);
304
+ let showProgress = $derived(status === 'INITIALIZING' && percentage !== undefined);
305
+ let displayMessage = $derived(error || message);
306
+ let isLoading = $derived(status === 'INITIALIZING' || status === 'BOOTING' || status === 'WAITING_FOR_DATA' || !status);
307
+ let isError = $derived(status === 'ERROR' || status === 'INVALID');
308
+ let isOffline = $derived(status === 'OFFLINE');
309
+
310
+ // Update logo size on container resize
311
+ $effect(() => {
312
+ if (!containerRef) return;
313
+
314
+ const updateLogoSize = () => {
315
+ if (containerRef) {
316
+ const minDimension = Math.min(containerRef.clientWidth, containerRef.clientHeight);
317
+ logoSize = minDimension * 0.2;
318
+ }
319
+ };
320
+
321
+ updateLogoSize();
322
+
323
+ const resizeObserver = new ResizeObserver(updateLogoSize);
324
+ resizeObserver.observe(containerRef);
325
+
326
+ return () => resizeObserver.disconnect();
327
+ });
328
+
329
+ // Start bubble animations on mount
330
+ onMount(() => {
331
+ bubbles.forEach((_, index) => {
332
+ setTimeout(() => animateBubble(index), index * 500);
333
+ });
334
+ });
335
+
336
+ // Cleanup on destroy
337
+ onDestroy(() => {
338
+ bubbles.forEach(bubble => {
339
+ if (bubble.timeoutId) clearTimeout(bubble.timeoutId);
340
+ });
341
+ });
342
+ </script>
343
+
344
+ <style>
345
+ @keyframes fadeInOut {
346
+ 0%, 100% { opacity: 0.6; }
347
+ 50% { opacity: 0.9; }
348
+ }
349
+
350
+ @keyframes logoPulse {
351
+ 0%, 100% {
352
+ opacity: 0.15;
353
+ transform: scale(1);
354
+ }
355
+ 50% {
356
+ opacity: 0.25;
357
+ transform: scale(1.05);
358
+ }
359
+ }
360
+
361
+ @keyframes floatUp {
362
+ 0% {
363
+ transform: translateY(100vh) rotate(0deg);
364
+ opacity: 0;
365
+ }
366
+ 10% { opacity: 0.6; }
367
+ 90% { opacity: 0.6; }
368
+ 100% {
369
+ transform: translateY(-100px) rotate(360deg);
370
+ opacity: 0;
371
+ }
372
+ }
373
+
374
+ @keyframes gradientShift {
375
+ 0%, 100% { background-position: 0% 50%; }
376
+ 50% { background-position: 100% 50%; }
377
+ }
378
+
379
+ @keyframes hitmarkerFade45 {
380
+ 0% {
381
+ opacity: 1;
382
+ transform: translate(-50%, -50%) rotate(45deg) scale(0.5);
383
+ }
384
+ 20% {
385
+ opacity: 1;
386
+ transform: translate(-50%, -50%) rotate(45deg) scale(1.2);
387
+ }
388
+ 100% {
389
+ opacity: 0;
390
+ transform: translate(-50%, -50%) rotate(45deg) scale(1);
391
+ }
392
+ }
393
+
394
+ @keyframes hitmarkerFadeNeg45 {
395
+ 0% {
396
+ opacity: 1;
397
+ transform: translate(-50%, -50%) rotate(-45deg) scale(0.5);
398
+ }
399
+ 20% {
400
+ opacity: 1;
401
+ transform: translate(-50%, -50%) rotate(-45deg) scale(1.2);
402
+ }
403
+ 100% {
404
+ opacity: 0;
405
+ transform: translate(-50%, -50%) rotate(-45deg) scale(1);
406
+ }
407
+ }
408
+
409
+ @keyframes spin {
410
+ from { transform: rotate(0deg); }
411
+ to { transform: rotate(360deg); }
412
+ }
413
+
414
+ .idle-container {
415
+ position: absolute;
416
+ inset: 0;
417
+ z-index: 5;
418
+ background: linear-gradient(135deg,
419
+ hsl(var(--tn-bg-dark, 235 21% 11%)) 0%,
420
+ hsl(var(--tn-bg, 233 23% 17%)) 25%,
421
+ hsl(var(--tn-bg-dark, 235 21% 11%)) 50%,
422
+ hsl(var(--tn-bg, 233 23% 17%)) 75%,
423
+ hsl(var(--tn-bg-dark, 235 21% 11%)) 100%
424
+ );
425
+ background-size: 400% 400%;
426
+ animation: gradientShift 16s ease-in-out infinite;
427
+ display: flex;
428
+ flex-direction: column;
429
+ align-items: center;
430
+ justify-content: center;
431
+ overflow: hidden;
432
+ border-radius: 0;
433
+ user-select: none;
434
+ -webkit-user-select: none;
435
+ }
436
+
437
+ .bubble {
438
+ position: absolute;
439
+ border-radius: 50%;
440
+ transition: opacity 1s ease-in-out;
441
+ pointer-events: none;
442
+ user-select: none;
443
+ }
444
+
445
+ .particle {
446
+ position: absolute;
447
+ border-radius: 50%;
448
+ opacity: 0;
449
+ animation: floatUp linear infinite;
450
+ pointer-events: none;
451
+ user-select: none;
452
+ }
453
+
454
+ .center-logo {
455
+ position: absolute;
456
+ top: 50%;
457
+ left: 50%;
458
+ display: flex;
459
+ align-items: center;
460
+ justify-content: center;
461
+ z-index: 10;
462
+ transition: transform 0.3s ease-out;
463
+ user-select: none;
464
+ }
465
+
466
+ .logo-pulse {
467
+ position: absolute;
468
+ border-radius: 50%;
469
+ background: rgba(122, 162, 247, 0.15);
470
+ animation: logoPulse 3s ease-in-out infinite;
471
+ user-select: none;
472
+ pointer-events: none;
473
+ transition: transform 0.3s ease-out;
474
+ }
475
+
476
+ .logo-pulse.hovered {
477
+ animation: logoPulse 1s ease-in-out infinite;
478
+ transform: scale(1.2);
479
+ }
480
+
481
+ .logo-image {
482
+ position: relative;
483
+ z-index: 1;
484
+ filter: drop-shadow(0 4px 8px rgba(36, 40, 59, 0.3));
485
+ transition: all 0.3s ease-out;
486
+ user-select: none;
487
+ -webkit-user-drag: none;
488
+ -webkit-touch-callout: none;
489
+ }
490
+
491
+ .logo-image.hovered {
492
+ filter: drop-shadow(0 6px 12px rgba(36, 40, 59, 0.4)) brightness(1.1);
493
+ transform: scale(1.1);
494
+ cursor: pointer;
495
+ }
496
+
497
+ .overlay-texture {
498
+ position: absolute;
499
+ top: 0;
500
+ left: 0;
501
+ right: 0;
502
+ bottom: 0;
503
+ background:
504
+ radial-gradient(circle at 20% 80%, rgba(122, 162, 247, 0.03) 0%, transparent 50%),
505
+ radial-gradient(circle at 80% 20%, rgba(187, 154, 247, 0.03) 0%, transparent 50%),
506
+ radial-gradient(circle at 40% 40%, rgba(158, 206, 106, 0.02) 0%, transparent 50%);
507
+ pointer-events: none;
508
+ user-select: none;
509
+ }
510
+
511
+ .hitmarker {
512
+ position: absolute;
513
+ transform: translate(-50%, -50%);
514
+ pointer-events: none;
515
+ z-index: 100;
516
+ width: 40px;
517
+ height: 40px;
518
+ }
519
+
520
+ .hitmarker-line {
521
+ position: absolute;
522
+ width: 12px;
523
+ height: 3px;
524
+ background-color: #ffffff;
525
+ box-shadow: 0 0 8px rgba(255, 255, 255, 0.8);
526
+ border-radius: 1px;
527
+ }
528
+
529
+ .hitmarker-line.tl {
530
+ top: 25%;
531
+ left: 25%;
532
+ animation: hitmarkerFade45 0.6s ease-out forwards;
533
+ }
534
+
535
+ .hitmarker-line.tr {
536
+ top: 25%;
537
+ left: 75%;
538
+ animation: hitmarkerFadeNeg45 0.6s ease-out forwards;
539
+ }
540
+
541
+ .hitmarker-line.bl {
542
+ top: 75%;
543
+ left: 25%;
544
+ animation: hitmarkerFadeNeg45 0.6s ease-out forwards;
545
+ }
546
+
547
+ .hitmarker-line.br {
548
+ top: 75%;
549
+ left: 75%;
550
+ animation: hitmarkerFade45 0.6s ease-out forwards;
551
+ }
552
+
553
+ .status-overlay {
554
+ position: absolute;
555
+ bottom: 16px;
556
+ left: 50%;
557
+ transform: translateX(-50%);
558
+ z-index: 20;
559
+ display: flex;
560
+ flex-direction: column;
561
+ align-items: center;
562
+ gap: 8px;
563
+ max-width: 280px;
564
+ text-align: center;
565
+ }
566
+
567
+ .status-indicator {
568
+ display: flex;
569
+ align-items: center;
570
+ gap: 8px;
571
+ color: #787c99;
572
+ font-size: 13px;
573
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
574
+ }
575
+
576
+ .status-icon {
577
+ width: 20px;
578
+ height: 20px;
579
+ }
580
+
581
+ .status-icon.spinning {
582
+ animation: spin 1s linear infinite;
583
+ }
584
+
585
+ .progress-bar {
586
+ width: 160px;
587
+ height: 4px;
588
+ background: rgba(65, 72, 104, 0.4);
589
+ border-radius: 2px;
590
+ overflow: hidden;
591
+ }
592
+
593
+ .progress-fill {
594
+ height: 100%;
595
+ background: hsl(var(--tn-cyan, 193 100% 75%));
596
+ transition: width 0.3s ease-out;
597
+ }
598
+
599
+ .retry-button {
600
+ padding: 6px 16px;
601
+ background: transparent;
602
+ border: 1px solid rgba(122, 162, 247, 0.4);
603
+ border-radius: 4px;
604
+ color: #7aa2f7;
605
+ font-size: 11px;
606
+ font-weight: 500;
607
+ cursor: pointer;
608
+ transition: all 0.2s ease;
609
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
610
+ }
611
+
612
+ .retry-button:hover {
613
+ background: rgba(122, 162, 247, 0.1);
614
+ }
615
+ </style>
616
+
617
+ <div
618
+ bind:this={containerRef}
619
+ class="idle-container fw-player-root"
620
+ role="status"
621
+ aria-label="Stream status"
622
+ onmousemove={handleMouseMove}
623
+ onmouseleave={handleMouseLeave}
624
+ >
625
+ <!-- Hitmarkers -->
626
+ {#each hitmarkers as hitmarker (hitmarker.id)}
627
+ <div class="hitmarker" style="left: {hitmarker.x}px; top: {hitmarker.y}px;">
628
+ <div class="hitmarker-line tl"></div>
629
+ <div class="hitmarker-line tr"></div>
630
+ <div class="hitmarker-line bl"></div>
631
+ <div class="hitmarker-line br"></div>
632
+ </div>
633
+ {/each}
634
+
635
+ <!-- Floating particles -->
636
+ {#each particles as particle, i}
637
+ <div
638
+ class="particle"
639
+ style="
640
+ left: {particle.left}%;
641
+ width: {particle.size}px;
642
+ height: {particle.size}px;
643
+ background: {particle.color};
644
+ animation-duration: {particle.duration}s;
645
+ animation-delay: {particle.delay}s;
646
+ "
647
+ />
648
+ {/each}
649
+
650
+ <!-- Animated bubbles -->
651
+ {#each bubbles as bubble, i}
652
+ <div
653
+ class="bubble"
654
+ style="
655
+ top: {bubble.position.top}%;
656
+ left: {bubble.position.left}%;
657
+ width: {bubble.size}px;
658
+ height: {bubble.size}px;
659
+ background: {bubble.color};
660
+ opacity: {bubble.opacity};
661
+ "
662
+ />
663
+ {/each}
664
+
665
+ <!-- Center logo with push-away effect -->
666
+ <div
667
+ class="center-logo"
668
+ style="transform: translate(-50%, -50%) translate({offset.x}px, {offset.y}px);"
669
+ >
670
+ <!-- Pulsing circle background -->
671
+ <div
672
+ class="logo-pulse"
673
+ class:hovered={isHovered}
674
+ style="width: {logoSize * 1.4}px; height: {logoSize * 1.4}px;"
675
+ />
676
+
677
+ <!-- Logo image -->
678
+ <img
679
+ src={logomarkAsset}
680
+ alt="Logo"
681
+ class="logo-image"
682
+ class:hovered={isHovered}
683
+ style="width: {logoSize}px; height: {logoSize}px;"
684
+ onclick={handleLogoClick}
685
+ draggable="false"
686
+ />
687
+ </div>
688
+
689
+ <!-- Bouncing DVD Logo -->
690
+ <DvdLogo parentRef={containerRef} scale={0.08} />
691
+
692
+ <!-- Status overlay at bottom -->
693
+ <div class="status-overlay">
694
+ <div class="status-indicator">
695
+ <!-- Status icon -->
696
+ {#if isLoading}
697
+ <svg class="status-icon spinning" fill="none" viewBox="0 0 24 24" style="color: hsl(var(--tn-yellow, 40 95% 64%));">
698
+ <circle style="opacity: 0.25;" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
699
+ <path style="opacity: 0.75;" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
700
+ </svg>
701
+ {:else if isOffline}
702
+ <svg class="status-icon" fill="none" viewBox="0 0 24 24" stroke="currentColor" style="color: hsl(var(--tn-red, 348 100% 72%));">
703
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 5.636a9 9 0 010 12.728m0 0l-2.829-2.829m2.829 2.829L21 21M15.536 8.464a5 5 0 010 7.072m0 0l-2.829-2.829m-4.243 2.829a4.978 4.978 0 01-1.414-2.83m-1.414 5.658a9 9 0 01-2.167-9.238m7.824 2.167a1 1 0 111.414 1.414m-1.414-1.414L3 3m8.293 8.293l1.414 1.414" />
704
+ </svg>
705
+ {:else if isError}
706
+ <svg class="status-icon" fill="none" viewBox="0 0 24 24" stroke="currentColor" style="color: hsl(var(--tn-red, 348 100% 72%));">
707
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
708
+ </svg>
709
+ {:else}
710
+ <svg class="status-icon spinning" fill="none" viewBox="0 0 24 24" style="color: hsl(var(--tn-cyan, 193 100% 75%));">
711
+ <circle style="opacity: 0.25;" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
712
+ <path style="opacity: 0.75;" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
713
+ </svg>
714
+ {/if}
715
+ <span>{displayMessage}</span>
716
+ </div>
717
+
718
+ <!-- Progress bar -->
719
+ {#if showProgress}
720
+ <div class="progress-bar">
721
+ <div class="progress-fill" style="width: {Math.min(100, percentage ?? 0)}%;" />
722
+ </div>
723
+ {/if}
724
+
725
+ <!-- Retry button -->
726
+ {#if showRetry}
727
+ <button
728
+ type="button"
729
+ class="retry-button"
730
+ onclick={onRetry}
731
+ >
732
+ Retry
733
+ </button>
734
+ {/if}
735
+ </div>
736
+
737
+ <!-- Subtle overlay texture -->
738
+ <div class="overlay-texture" />
739
+ </div>