@livepeer-frameworks/player-react 0.0.4 → 0.1.1

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 (54) hide show
  1. package/README.md +16 -5
  2. package/dist/cjs/index.js +1 -1
  3. package/dist/cjs/index.js.map +1 -1
  4. package/dist/esm/index.js +1 -1
  5. package/dist/esm/index.js.map +1 -1
  6. package/dist/types/components/PlayerControls.d.ts +2 -0
  7. package/dist/types/components/StatsPanel.d.ts +2 -14
  8. package/dist/types/hooks/useMetaTrack.d.ts +1 -1
  9. package/dist/types/hooks/usePlayerController.d.ts +2 -0
  10. package/dist/types/hooks/useStreamState.d.ts +1 -1
  11. package/dist/types/hooks/useTelemetry.d.ts +1 -1
  12. package/dist/types/hooks/useViewerEndpoints.d.ts +2 -2
  13. package/dist/types/types.d.ts +1 -1
  14. package/dist/types/ui/button.d.ts +1 -1
  15. package/package.json +1 -1
  16. package/src/components/DevModePanel.tsx +249 -170
  17. package/src/components/Icons.tsx +105 -25
  18. package/src/components/IdleScreen.tsx +262 -142
  19. package/src/components/LoadingScreen.tsx +171 -153
  20. package/src/components/LogoOverlay.tsx +3 -6
  21. package/src/components/Player.tsx +86 -74
  22. package/src/components/PlayerControls.tsx +351 -263
  23. package/src/components/PlayerErrorBoundary.tsx +6 -13
  24. package/src/components/SeekBar.tsx +96 -88
  25. package/src/components/SkipIndicator.tsx +2 -12
  26. package/src/components/SpeedIndicator.tsx +2 -11
  27. package/src/components/StatsPanel.tsx +65 -34
  28. package/src/components/StreamStateOverlay.tsx +105 -49
  29. package/src/components/SubtitleRenderer.tsx +29 -29
  30. package/src/components/ThumbnailOverlay.tsx +5 -6
  31. package/src/components/TitleOverlay.tsx +2 -8
  32. package/src/components/players/DashJsPlayer.tsx +13 -11
  33. package/src/components/players/HlsJsPlayer.tsx +13 -11
  34. package/src/components/players/MewsWsPlayer/index.tsx +13 -11
  35. package/src/components/players/MistPlayer.tsx +13 -11
  36. package/src/components/players/MistWebRTCPlayer/index.tsx +19 -10
  37. package/src/components/players/NativePlayer.tsx +10 -12
  38. package/src/components/players/VideoJsPlayer.tsx +13 -11
  39. package/src/context/PlayerContext.tsx +4 -8
  40. package/src/context/index.ts +3 -3
  41. package/src/hooks/useMetaTrack.ts +28 -28
  42. package/src/hooks/usePlaybackQuality.ts +3 -3
  43. package/src/hooks/usePlayerController.ts +186 -140
  44. package/src/hooks/usePlayerSelection.ts +6 -6
  45. package/src/hooks/useStreamState.ts +53 -58
  46. package/src/hooks/useTelemetry.ts +19 -4
  47. package/src/hooks/useViewerEndpoints.ts +40 -30
  48. package/src/index.tsx +36 -28
  49. package/src/types.ts +9 -9
  50. package/src/ui/badge.tsx +6 -5
  51. package/src/ui/button.tsx +9 -8
  52. package/src/ui/context-menu.tsx +42 -61
  53. package/src/ui/select.tsx +13 -7
  54. package/src/ui/slider.tsx +18 -29
@@ -29,16 +29,19 @@ const AnimatedBubble: React.FC<AnimatedBubbleProps> = ({ index }) => {
29
29
 
30
30
  const animationCycle = () => {
31
31
  setOpacity(0.15);
32
- setTimeout(() => {
33
- setOpacity(0);
34
- setTimeout(() => {
35
- setPosition(getRandomPosition());
36
- setSize(getRandomSize());
32
+ setTimeout(
33
+ () => {
34
+ setOpacity(0);
37
35
  setTimeout(() => {
38
- animationCycle();
39
- }, 200);
40
- }, 1500);
41
- }, 4000 + Math.random() * 3000);
36
+ setPosition(getRandomPosition());
37
+ setSize(getRandomSize());
38
+ setTimeout(() => {
39
+ animationCycle();
40
+ }, 200);
41
+ }, 1500);
42
+ },
43
+ 4000 + Math.random() * 3000
44
+ );
42
45
  };
43
46
 
44
47
  const timeout = setTimeout(animationCycle, index * 500);
@@ -58,19 +61,21 @@ const AnimatedBubble: React.FC<AnimatedBubbleProps> = ({ index }) => {
58
61
 
59
62
  return (
60
63
  <div
61
- style={{
62
- position: "absolute",
63
- top: `${position.top}%`,
64
- left: `${position.left}%`,
65
- width: `${size}px`,
66
- height: `${size}px`,
67
- borderRadius: "50%",
68
- background: bubbleColors[index % bubbleColors.length],
69
- opacity,
70
- transition: "opacity 1s ease-in-out",
71
- pointerEvents: "none",
72
- userSelect: "none",
73
- } as React.CSSProperties}
64
+ style={
65
+ {
66
+ position: "absolute",
67
+ top: `${position.top}%`,
68
+ left: `${position.left}%`,
69
+ width: `${size}px`,
70
+ height: `${size}px`,
71
+ borderRadius: "50%",
72
+ background: bubbleColors[index % bubbleColors.length],
73
+ opacity,
74
+ transition: "opacity 1s ease-in-out",
75
+ pointerEvents: "none",
76
+ userSelect: "none",
77
+ } as React.CSSProperties
78
+ }
74
79
  />
75
80
  );
76
81
  };
@@ -138,11 +143,11 @@ const CenterLogo: React.FC<CenterLogoProps> = ({ containerRef, scale = 0.2, onHi
138
143
  useEffect(() => {
139
144
  if (containerRef.current) {
140
145
  const container = containerRef.current;
141
- container.addEventListener('mousemove', handleMouseMove);
142
- container.addEventListener('mouseleave', handleMouseLeave);
146
+ container.addEventListener("mousemove", handleMouseMove);
147
+ container.addEventListener("mouseleave", handleMouseLeave);
143
148
  return () => {
144
- container.removeEventListener('mousemove', handleMouseMove);
145
- container.removeEventListener('mouseleave', handleMouseLeave);
149
+ container.removeEventListener("mousemove", handleMouseMove);
150
+ container.removeEventListener("mouseleave", handleMouseLeave);
146
151
  };
147
152
  }
148
153
  }, [logoSize, containerRef]);
@@ -169,7 +174,9 @@ const CenterLogo: React.FC<CenterLogoProps> = ({ containerRef, scale = 0.2, onHi
169
174
  height: `${logoSize * 1.4}px`,
170
175
  borderRadius: "50%",
171
176
  background: "rgba(122, 162, 247, 0.15)",
172
- animation: isHovered ? "logoPulse 1s ease-in-out infinite" : "logoPulse 3s ease-in-out infinite",
177
+ animation: isHovered
178
+ ? "logoPulse 1s ease-in-out infinite"
179
+ : "logoPulse 3s ease-in-out infinite",
173
180
  transform: isHovered ? "scale(1.2)" : "scale(1)",
174
181
  transition: "transform 0.3s ease-out",
175
182
  pointerEvents: "none",
@@ -179,20 +186,22 @@ const CenterLogo: React.FC<CenterLogoProps> = ({ containerRef, scale = 0.2, onHi
179
186
  src={logomarkAsset}
180
187
  alt="FrameWorks Logo"
181
188
  onClick={handleLogoClick}
182
- style={{
183
- width: `${logoSize}px`,
184
- height: `${logoSize}px`,
185
- position: "relative",
186
- zIndex: 1,
187
- filter: isHovered
188
- ? "drop-shadow(0 6px 12px rgba(36, 40, 59, 0.4)) brightness(1.1)"
189
- : "drop-shadow(0 4px 8px rgba(36, 40, 59, 0.3))",
190
- transform: isHovered ? "scale(1.1)" : "scale(1)",
191
- transition: "all 0.3s ease-out",
192
- cursor: isHovered ? "pointer" : "default",
193
- userSelect: "none",
194
- WebkitUserDrag: "none",
195
- } as React.CSSProperties}
189
+ style={
190
+ {
191
+ width: `${logoSize}px`,
192
+ height: `${logoSize}px`,
193
+ position: "relative",
194
+ zIndex: 1,
195
+ filter: isHovered
196
+ ? "drop-shadow(0 6px 12px rgba(36, 40, 59, 0.4)) brightness(1.1)"
197
+ : "drop-shadow(0 4px 8px rgba(36, 40, 59, 0.3))",
198
+ transform: isHovered ? "scale(1.1)" : "scale(1)",
199
+ transition: "all 0.3s ease-out",
200
+ cursor: isHovered ? "pointer" : "default",
201
+ userSelect: "none",
202
+ WebkitUserDrag: "none",
203
+ } as React.CSSProperties
204
+ }
196
205
  />
197
206
  </div>
198
207
  );
@@ -210,63 +219,107 @@ interface StatusOverlayProps {
210
219
  onRetry?: () => void;
211
220
  }
212
221
 
213
- function getStatusLabel(status?: StreamStatus): string {
214
- switch (status) {
215
- case 'ONLINE': return 'ONLINE';
216
- case 'OFFLINE': return 'OFFLINE';
217
- case 'INITIALIZING': return 'STARTING';
218
- case 'BOOTING': return 'STARTING';
219
- case 'WAITING_FOR_DATA': return 'WAITING';
220
- case 'SHUTTING_DOWN': return 'ENDING';
221
- case 'ERROR': return 'ERROR';
222
- case 'INVALID': return 'ERROR';
223
- default: return 'CONNECTING';
224
- }
225
- }
226
-
227
222
  function StatusIcon({ status }: { status?: StreamStatus }) {
228
223
  const iconClass = "w-5 h-5";
229
224
 
230
225
  // Spinner for loading states
231
- if (status === 'INITIALIZING' || status === 'BOOTING' || status === 'WAITING_FOR_DATA' || !status) {
226
+ if (
227
+ status === "INITIALIZING" ||
228
+ status === "BOOTING" ||
229
+ status === "WAITING_FOR_DATA" ||
230
+ !status
231
+ ) {
232
232
  return (
233
- <svg className={`${iconClass} animate-spin`} fill="none" viewBox="0 0 24 24" style={{ color: 'hsl(var(--tn-yellow, 40 95% 64%))' }}>
234
- <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
235
- <path className="opacity-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" />
233
+ <svg
234
+ className={`${iconClass} animate-spin`}
235
+ fill="none"
236
+ viewBox="0 0 24 24"
237
+ style={{ color: "hsl(var(--tn-yellow, 40 95% 64%))" }}
238
+ >
239
+ <circle
240
+ className="opacity-25"
241
+ cx="12"
242
+ cy="12"
243
+ r="10"
244
+ stroke="currentColor"
245
+ strokeWidth="4"
246
+ />
247
+ <path
248
+ className="opacity-75"
249
+ fill="currentColor"
250
+ 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"
251
+ />
236
252
  </svg>
237
253
  );
238
254
  }
239
255
 
240
256
  // Offline icon
241
- if (status === 'OFFLINE') {
257
+ if (status === "OFFLINE") {
242
258
  return (
243
- <svg className={iconClass} fill="none" viewBox="0 0 24 24" stroke="currentColor" style={{ color: 'hsl(var(--tn-red, 348 100% 72%))' }}>
244
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={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" />
259
+ <svg
260
+ className={iconClass}
261
+ fill="none"
262
+ viewBox="0 0 24 24"
263
+ stroke="currentColor"
264
+ style={{ color: "hsl(var(--tn-red, 348 100% 72%))" }}
265
+ >
266
+ <path
267
+ strokeLinecap="round"
268
+ strokeLinejoin="round"
269
+ strokeWidth={2}
270
+ 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"
271
+ />
245
272
  </svg>
246
273
  );
247
274
  }
248
275
 
249
276
  // Error icon
250
- if (status === 'ERROR' || status === 'INVALID') {
277
+ if (status === "ERROR" || status === "INVALID") {
251
278
  return (
252
- <svg className={iconClass} fill="none" viewBox="0 0 24 24" stroke="currentColor" style={{ color: 'hsl(var(--tn-red, 348 100% 72%))' }}>
253
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={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" />
279
+ <svg
280
+ className={iconClass}
281
+ fill="none"
282
+ viewBox="0 0 24 24"
283
+ stroke="currentColor"
284
+ style={{ color: "hsl(var(--tn-red, 348 100% 72%))" }}
285
+ >
286
+ <path
287
+ strokeLinecap="round"
288
+ strokeLinejoin="round"
289
+ strokeWidth={2}
290
+ 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"
291
+ />
254
292
  </svg>
255
293
  );
256
294
  }
257
295
 
258
296
  // Default spinner
259
297
  return (
260
- <svg className={`${iconClass} animate-spin`} fill="none" viewBox="0 0 24 24" style={{ color: 'hsl(var(--tn-cyan, 193 100% 75%))' }}>
298
+ <svg
299
+ className={`${iconClass} animate-spin`}
300
+ fill="none"
301
+ viewBox="0 0 24 24"
302
+ style={{ color: "hsl(var(--tn-cyan, 193 100% 75%))" }}
303
+ >
261
304
  <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
262
- <path className="opacity-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" />
305
+ <path
306
+ className="opacity-75"
307
+ fill="currentColor"
308
+ 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"
309
+ />
263
310
  </svg>
264
311
  );
265
312
  }
266
313
 
267
- const StatusOverlay: React.FC<StatusOverlayProps> = ({ status, message, percentage, error, onRetry }) => {
268
- const showRetry = (status === 'ERROR' || status === 'INVALID') && onRetry;
269
- const showProgress = status === 'INITIALIZING' && percentage !== undefined;
314
+ const StatusOverlay: React.FC<StatusOverlayProps> = ({
315
+ status,
316
+ message,
317
+ percentage,
318
+ error,
319
+ onRetry,
320
+ }) => {
321
+ const showRetry = (status === "ERROR" || status === "INVALID") && onRetry;
322
+ const showProgress = status === "INITIALIZING" && percentage !== undefined;
270
323
  const displayMessage = error || message;
271
324
 
272
325
  return (
@@ -366,50 +419,51 @@ interface Hitmarker {
366
419
  const playHitmarkerSound = () => {
367
420
  try {
368
421
  // Embedded hitmarker sound as base64 data URL
369
- const hitmarkerDataUrl = 'data:audio/mpeg;base64,SUQzBAAAAAAANFRDT04AAAAHAAADT3RoZXIAVFNTRQAAAA8AAANMYXZmNTcuODMuMTAwAAAAAAAAAAAA' +
370
- 'AAD/+1QAAAAAAAAAAAAAAAAAAAAA' +
371
- 'AAAAAAAAAAAAAAAAAAAAAABJbmZvAAAADwAAAAYAAAnAADs7Ozs7Ozs7Ozs7Ozs7OztiYmJiYmJiYmJi' +
372
- 'YmJiYmJiYomJiYmJiYmJiYmJiYmJiYmxsbGxsbGxsbGxsbGxsbGxsdjY2NjY2NjY2NjY2NjY2NjY////' +
373
- '/////////////////wAAAABMYXZjNTcuMTAAAAAAAAAAAAAAAAAkAkAAAAAAAAAJwOuMZun/+5RkAA8S' +
374
- '/F23AGAaAi0AF0AAAAAInXsEAIRXyQ8D4OQgjEhE3cO7ujuHF0XCOu4G7xKbi3Funu7u7p9dw7unu7u7' +
375
- 'p7u7u6fXcW7om7u7uiU3dxdT67u7p7uHdxelN3cW6fXcW7oXXd3eJTd3d0+u4t3iXdw4up70W4uiPruL' +
376
- 'DzMw8Pz79Y99JfkyfPv5/h9uTJoy79Y99Y97q3vyZPJk0ZfrL6x73Vn+J35dKKS/STQyQ8CAiCPNuRAO' +
377
- 'OqquAx+fzJeBKDAsgAMBuWcBsHKhjJTcCwIALyAvABbI0ZIcCmP8jHJe8gZAdVRp2TpnU/kUXV4iQuBA' +
378
- 'AkAQgisLPvwQ2Jz7wIkIpQ8QOl/KFy75w+2HpTFnRqXLQo0fzlSYRe5Ce9yZMEzRM4xesu95Mo8QQsoM' +
379
- 'H4gLg+fJqkmY3GZJE2kwGfMECJiAdIttoEa2yotfC7jsS2mjKgbzAfEMeiwZpGSUFCQwPKQiWXh0TnkN' +
380
- 'or5SmrKvwHlX2zFxKxPCzRL/+5RkIwADvUxLawwb0GdF6Y1hJlgNNJk+DSRwyQwI6AD2JCiBmhaff0dz' +
381
- 'CEBjgFABAcDNFc3YAEV4hQn0L/QvQnevom+n13eIjoTvABLrHg/L9RzdWXYonHbbbE2K0pX+gkL2g56R' +
382
- 'iwrbuWwhoABzQoMKOAIGAfE4UKk6BhSIJpECBq0CEYmZKYIiAJt72H24dNou7y/Ee7a/3v+MgySemSTY' +
383
- 'mnBAFwIAAGfCJ8/D9YfkwQEBcP38uA1d/EB1T5dZKEsgnuhwZirY5fIMRMdRn7U4OcN2m5NWeYdcPBwX' +
384
- 'DBOsJF1DBYks62pAURqz1hGoGHH/QIoRC80tYAJ8g4f3MPD51sywAbhAn/X9P/75tvZww3gZ3pYPDx/+' +
385
- 'ACO/7//ffHj/D/AAfATC4DYGFA3MRABo0lqWjBOl2yAda1C1BdhduXgm8FGnAQB/lDiEi6j9qw9EHigI' +
386
- 'IOLB6F1eIPd+T6Agc4//lMo6+k3tdttJY2gArU7cN07m2FLSm4gCjyz/+5RECwACwSRZawkdLFGi2mVh' +
387
- '5h4LfFdPVPGACViTavaeMAAV0UkkEsDhxxJwqF04on002mZah8w9+5ItfSAoyZa1dchnPpLmAEKrVMRA' +
388
- '//sD8w0WsB4xiw4JqaZMB45TdpIuXXUPf8Bpa35p/jQIAOAuZkmUeJoM5W6L2gqqO6rTuHjUTDnhy4Qi' +
389
- 'K348vtFysOizShoHbBpsPRYcSINCbiN4XOLPPAgq3dW2Ga7SlyiKXBV7W1RQl5BiiVGkwayJfEnPxgXk' +
390
- 'QeZxxzyhTuLO2XFUDDstoc6CkM1J8QZAjUN3bM8580cRygNfmPAELGjIH0Z/0A+8csyH/4eHvgAf8APg' +
391
- 'ABmZ98AARAADP////Dw8PHEmIpgGttpJQJsmZjq5nPQ8j5VqWW1evqdjP182PA6tHJZgkC5iSbEQkyJS' +
392
- 'z/BvP3eucLKN0+Wiza4feKKFBqiAEBAMXyYni5NZc16CDl/QY9j6BAcWSmQYcIcoMHYoQNBiIBgIBUAz' +
393
- 'QUMSnjj/+5RkCwADsFLffjEAAjrJe63JHACO6WtlnPMACKaCK1uMMADU5dI6JhW2cam98UlRmY4ihyKF' +
394
- 'rNsgpZd5PYgBALnYofKEt82De0GbW1DLibvFDK+bSeOm8qKdqUFZ7uiK8XMPHyqm3pTxUvcunUfxXEo9' +
395
- 'RNe5b/8vfCD3kzDN7vTtHyaIcntVDAYBAUBAAAAQBI2vguYNsHWm5AR3mZtZib8WAHFvz2Kf9//iYvlR' +
396
- 'B/+n///////////+UH7XoIDMoJAEAMtj8JshJPRwklVqNSpYnalfE+VzNCAISCoxVHEpIo/WrTiMvP7V' +
397
- 'TujOPnOglLbMLN/pq/d2Y4lRJIkSnPlUSJEjSKJqM41d88zWtMzP+fCOORmc9NeM+f1nnO//efM52/fG' +
398
- '/ef385+5u+u1bRJkwU8FAkEItZpkRYeQYcAgZTEYlaZa2yROLeC0qdX73rZJJ/d2f6v6Or0u/+5FBYcn' +
399
- 'g0MlCiQTR9GUU5LScmSuSlH00IWqXA6jlw4BEcD/+5REEAAi3RtU+eYbGF1E+lk9g0YJzLUgh7BlQVGT' +
400
- 'ZJD0jKhhTNVilqrMzFRK+x/szcMKBWKep4NP1A0DR6RESkTp5Z1Q9Y8REgqMg1DpUBPleeqlRQcerBpM' +
401
- 'jiURHVD4XwAALhAgbxxlxYD5OFkG8oQRPB2EpsxSCNVlgcYUqoAyiVJmaARlkwplICfPoUy/zWEzM2pc' +
402
- 'NYzAQNJDSniEYecSEqxFEzQqEvUFGnvzwUfcRlpZ9T2LCR5QdDQDDhKICAjpJCagpRo9UQRPClZZlg6E' +
403
- 'p9DMTkTl+okuhRIVIzAQEf9L+Mx/DUjqmqN6kX7M36lS4zgLyJV3iV6j3xF8kJduJawVw1nndAlBaLLg' +
404
- 'JupwsTcLkxmJgFLgSzoCmHjSNGSqkGPCpnNqTXIwolf6qlVWN+q/su37HzgrES1pWGg3KnWh0FXCVniJ' +
405
- '9K5b4iCrpLEuIcFTqwkVLFiqgaDqCCSMVWqxBAVCFOLVrVahm2ahUThUKJnmFCw15hD0Qhb/+5REEAhC' +
406
- 'YSRCSQEb4FOGaBUMI6JIRYC0QIB2SQsgGpgwDghgIlS6FU8VBXDoiBp5Y9gtkVnhEhYBdJFQ7kQ3w1yp' +
407
- '0NB2CoNPEttZ1/aeDUAAA26FEghWgEKNVAVWkFAQEmMK2Uwk/qI0hqUb/4epVIZH1ai6szf6kzH1f2ar' +
408
- 'xYGS9FcOsN5UlJLQt///+oo0FRDTUQ0FBQr9f5LxXP+mEUfk0AIrf/5GRmQ0//mX//ZbLP5b5GrWSz+W' +
409
- 'SkZMrWyyyy2GRqyggVRyMv////////st//sn/yyVDI1l8mVgoYGDCOqiqIQBxmvxWCggTpZZZD//aWfy' +
410
- 'yWf/y/7KGDA0ssBggTof9k/+WS/8slQyMp/5Nfln8WAqGcUbULCrKxT9ISF+kKsxQWpMQU1FMy4xMDCq' +
411
- 'qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq' +
412
- 'qqqqqqqqqqqqqqqqqqqqqqqqqqo=';
422
+ const hitmarkerDataUrl =
423
+ "data:audio/mpeg;base64,SUQzBAAAAAAANFRDT04AAAAHAAADT3RoZXIAVFNTRQAAAA8AAANMYXZmNTcuODMuMTAwAAAAAAAAAAAA" +
424
+ "AAD/+1QAAAAAAAAAAAAAAAAAAAAA" +
425
+ "AAAAAAAAAAAAAAAAAAAAAABJbmZvAAAADwAAAAYAAAnAADs7Ozs7Ozs7Ozs7Ozs7OztiYmJiYmJiYmJi" +
426
+ "YmJiYmJiYomJiYmJiYmJiYmJiYmJiYmxsbGxsbGxsbGxsbGxsbGxsdjY2NjY2NjY2NjY2NjY2NjY////" +
427
+ "/////////////////wAAAABMYXZjNTcuMTAAAAAAAAAAAAAAAAAkAkAAAAAAAAAJwOuMZun/+5RkAA8S" +
428
+ "/F23AGAaAi0AF0AAAAAInXsEAIRXyQ8D4OQgjEhE3cO7ujuHF0XCOu4G7xKbi3Funu7u7p9dw7unu7u7" +
429
+ "p7u7u6fXcW7om7u7uiU3dxdT67u7p7uHdxelN3cW6fXcW7oXXd3eJTd3d0+u4t3iXdw4up70W4uiPruL" +
430
+ "DzMw8Pz79Y99JfkyfPv5/h9uTJoy79Y99Y97q3vyZPJk0ZfrL6x73Vn+J35dKKS/STQyQ8CAiCPNuRAO" +
431
+ "OqquAx+fzJeBKDAsgAMBuWcBsHKhjJTcCwIALyAvABbI0ZIcCmP8jHJe8gZAdVRp2TpnU/kUXV4iQuBA" +
432
+ "AkAQgisLPvwQ2Jz7wIkIpQ8QOl/KFy75w+2HpTFnRqXLQo0fzlSYRe5Ce9yZMEzRM4xesu95Mo8QQsoM" +
433
+ "H4gLg+fJqkmY3GZJE2kwGfMECJiAdIttoEa2yotfC7jsS2mjKgbzAfEMeiwZpGSUFCQwPKQiWXh0TnkN" +
434
+ "or5SmrKvwHlX2zFxKxPCzRL/+5RkIwADvUxLawwb0GdF6Y1hJlgNNJk+DSRwyQwI6AD2JCiBmhaff0dz" +
435
+ "CEBjgFABAcDNFc3YAEV4hQn0L/QvQnevom+n13eIjoTvABLrHg/L9RzdWXYonHbbbE2K0pX+gkL2g56R" +
436
+ "iwrbuWwhoABzQoMKOAIGAfE4UKk6BhSIJpECBq0CEYmZKYIiAJt72H24dNou7y/Ee7a/3v+MgySemSTY" +
437
+ "mnBAFwIAAGfCJ8/D9YfkwQEBcP38uA1d/EB1T5dZKEsgnuhwZirY5fIMRMdRn7U4OcN2m5NWeYdcPBwX" +
438
+ "DBOsJF1DBYks62pAURqz1hGoGHH/QIoRC80tYAJ8g4f3MPD51sywAbhAn/X9P/75tvZww3gZ3pYPDx/+" +
439
+ "ACO/7//ffHj/D/AAfATC4DYGFA3MRABo0lqWjBOl2yAda1C1BdhduXgm8FGnAQB/lDiEi6j9qw9EHigI" +
440
+ "IOLB6F1eIPd+T6Agc4//lMo6+k3tdttJY2gArU7cN07m2FLSm4gCjyz/+5RECwACwSRZawkdLFGi2mVh" +
441
+ "5h4LfFdPVPGACViTavaeMAAV0UkkEsDhxxJwqF04on002mZah8w9+5ItfSAoyZa1dchnPpLmAEKrVMRA" +
442
+ "//sD8w0WsB4xiw4JqaZMB45TdpIuXXUPf8Bpa35p/jQIAOAuZkmUeJoM5W6L2gqqO6rTuHjUTDnhy4Qi" +
443
+ "K348vtFysOizShoHbBpsPRYcSINCbiN4XOLPPAgq3dW2Ga7SlyiKXBV7W1RQl5BiiVGkwayJfEnPxgXk" +
444
+ "QeZxxzyhTuLO2XFUDDstoc6CkM1J8QZAjUN3bM8580cRygNfmPAELGjIH0Z/0A+8csyH/4eHvgAf8APg" +
445
+ "ABmZ98AARAADP////Dw8PHEmIpgGttpJQJsmZjq5nPQ8j5VqWW1evqdjP182PA6tHJZgkC5iSbEQkyJS" +
446
+ "z/BvP3eucLKN0+Wiza4feKKFBqiAEBAMXyYni5NZc16CDl/QY9j6BAcWSmQYcIcoMHYoQNBiIBgIBUAz" +
447
+ "QUMSnjj/+5RkCwADsFLffjEAAjrJe63JHACO6WtlnPMACKaCK1uMMADU5dI6JhW2cam98UlRmY4ihyKF" +
448
+ "rNsgpZd5PYgBALnYofKEt82De0GbW1DLibvFDK+bSeOm8qKdqUFZ7uiK8XMPHyqm3pTxUvcunUfxXEo9" +
449
+ "RNe5b/8vfCD3kzDN7vTtHyaIcntVDAYBAUBAAAAQBI2vguYNsHWm5AR3mZtZib8WAHFvz2Kf9//iYvlR" +
450
+ "B/+n///////////+UH7XoIDMoJAEAMtj8JshJPRwklVqNSpYnalfE+VzNCAISCoxVHEpIo/WrTiMvP7V" +
451
+ "TujOPnOglLbMLN/pq/d2Y4lRJIkSnPlUSJEjSKJqM41d88zWtMzP+fCOORmc9NeM+f1nnO//efM52/fG" +
452
+ "/ef385+5u+u1bRJkwU8FAkEItZpkRYeQYcAgZTEYlaZa2yROLeC0qdX73rZJJ/d2f6v6Or0u/+5FBYcn" +
453
+ "g0MlCiQTR9GUU5LScmSuSlH00IWqXA6jlw4BEcD/+5REEAAi3RtU+eYbGF1E+lk9g0YJzLUgh7BlQVGT" +
454
+ "ZJD0jKhhTNVilqrMzFRK+x/szcMKBWKep4NP1A0DR6RESkTp5Z1Q9Y8REgqMg1DpUBPleeqlRQcerBpM" +
455
+ "jiURHVD4XwAALhAgbxxlxYD5OFkG8oQRPB2EpsxSCNVlgcYUqoAyiVJmaARlkwplICfPoUy/zWEzM2pc" +
456
+ "NYzAQNJDSniEYecSEqxFEzQqEvUFGnvzwUfcRlpZ9T2LCR5QdDQDDhKICAjpJCagpRo9UQRPClZZlg6E" +
457
+ "p9DMTkTl+okuhRIVIzAQEf9L+Mx/DUjqmqN6kX7M36lS4zgLyJV3iV6j3xF8kJduJawVw1nndAlBaLLg" +
458
+ "JupwsTcLkxmJgFLgSzoCmHjSNGSqkGPCpnNqTXIwolf6qlVWN+q/su37HzgrES1pWGg3KnWh0FXCVniJ" +
459
+ "9K5b4iCrpLEuIcFTqwkVLFiqgaDqCCSMVWqxBAVCFOLVrVahm2ahUThUKJnmFCw15hD0Qhb/+5REEAhC" +
460
+ "YSRCSQEb4FOGaBUMI6JIRYC0QIB2SQsgGpgwDghgIlS6FU8VBXDoiBp5Y9gtkVnhEhYBdJFQ7kQ3w1yp" +
461
+ "0NB2CoNPEttZ1/aeDUAAA26FEghWgEKNVAVWkFAQEmMK2Uwk/qI0hqUb/4epVIZH1ai6szf6kzH1f2ar" +
462
+ "xYGS9FcOsN5UlJLQt///+oo0FRDTUQ0FBQr9f5LxXP+mEUfk0AIrf/5GRmQ0//mX//ZbLP5b5GrWSz+W" +
463
+ "SkZMrWyyyy2GRqyggVRyMv////////st//sn/yyVDI1l8mVgoYGDCOqiqIQBxmvxWCggTpZZZD//aWfy" +
464
+ "yWf/y/7KGDA0ssBggTof9k/+WS/8slQyMp/5Nfln8WAqGcUbULCrKxT9ISF+kKsxQWpMQU1FMy4xMDCq" +
465
+ "qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq" +
466
+ "qqqqqqqqqqqqqqqqqqqqqqqqqqo=";
413
467
 
414
468
  const audio = new Audio(hitmarkerDataUrl);
415
469
  audio.volume = 0.3;
@@ -439,8 +493,8 @@ const createSyntheticHitmarkerSound = () => {
439
493
  oscillator2.frequency.setValueAtTime(3600, audioContext.currentTime);
440
494
  oscillator2.frequency.exponentialRampToValueAtTime(1800, audioContext.currentTime + 0.04);
441
495
 
442
- oscillator1.type = 'triangle';
443
- oscillator2.type = 'sine';
496
+ oscillator1.type = "triangle";
497
+ oscillator2.type = "sine";
444
498
 
445
499
  gainNode1.gain.setValueAtTime(0, audioContext.currentTime);
446
500
  gainNode1.gain.linearRampToValueAtTime(0.4, audioContext.currentTime + 0.002);
@@ -504,19 +558,19 @@ export const IdleScreen: React.FC<IdleScreenProps> = ({
504
558
  y,
505
559
  };
506
560
 
507
- setHitmarkers(prev => [...prev, newHitmarker]);
561
+ setHitmarkers((prev) => [...prev, newHitmarker]);
508
562
  playHitmarkerSound();
509
563
 
510
564
  setTimeout(() => {
511
- setHitmarkers(prev => prev.filter(h => h.id !== newHitmarker.id));
565
+ setHitmarkers((prev) => prev.filter((h) => h.id !== newHitmarker.id));
512
566
  }, 600);
513
567
  };
514
568
 
515
569
  // Inject CSS animations
516
570
  useEffect(() => {
517
- const styleId = 'idle-screen-animations';
571
+ const styleId = "idle-screen-animations";
518
572
  if (!document.getElementById(styleId)) {
519
- const style = document.createElement('style');
573
+ const style = document.createElement("style");
520
574
  style.id = styleId;
521
575
  style.textContent = `
522
576
  @keyframes fadeInOut {
@@ -556,11 +610,12 @@ export const IdleScreen: React.FC<IdleScreenProps> = ({
556
610
  <div
557
611
  ref={containerRef}
558
612
  className="fw-player-root"
559
- style={{
560
- position: "absolute",
561
- inset: 0,
562
- zIndex: 5,
563
- background: `
613
+ style={
614
+ {
615
+ position: "absolute",
616
+ inset: 0,
617
+ zIndex: 5,
618
+ background: `
564
619
  linear-gradient(135deg,
565
620
  hsl(var(--tn-bg-dark, 235 21% 11%)) 0%,
566
621
  hsl(var(--tn-bg, 233 23% 17%)) 25%,
@@ -569,19 +624,20 @@ export const IdleScreen: React.FC<IdleScreenProps> = ({
569
624
  hsl(var(--tn-bg-dark, 235 21% 11%)) 100%
570
625
  )
571
626
  `,
572
- backgroundSize: "400% 400%",
573
- animation: "gradientShift 16s ease-in-out infinite",
574
- display: "flex",
575
- flexDirection: "column",
576
- alignItems: "center",
577
- justifyContent: "center",
578
- overflow: "hidden",
579
- borderRadius: "0",
580
- userSelect: "none",
581
- } as React.CSSProperties}
627
+ backgroundSize: "400% 400%",
628
+ animation: "gradientShift 16s ease-in-out infinite",
629
+ display: "flex",
630
+ flexDirection: "column",
631
+ alignItems: "center",
632
+ justifyContent: "center",
633
+ overflow: "hidden",
634
+ borderRadius: "0",
635
+ userSelect: "none",
636
+ } as React.CSSProperties
637
+ }
582
638
  >
583
639
  {/* Hitmarkers */}
584
- {hitmarkers.map(hitmarker => (
640
+ {hitmarkers.map((hitmarker) => (
585
641
  <div
586
642
  key={hitmarker.id}
587
643
  style={{
@@ -595,10 +651,62 @@ export const IdleScreen: React.FC<IdleScreenProps> = ({
595
651
  height: "40px",
596
652
  }}
597
653
  >
598
- <div style={{ position: "absolute", top: "25%", left: "25%", width: "12px", height: "3px", backgroundColor: "#ffffff", transform: "translate(-50%, -50%) rotate(45deg)", animation: "hitmarkerFade45 0.6s ease-out forwards", boxShadow: "0 0 8px rgba(255, 255, 255, 0.8)", borderRadius: "1px" }} />
599
- <div style={{ position: "absolute", top: "25%", left: "75%", width: "12px", height: "3px", backgroundColor: "#ffffff", transform: "translate(-50%, -50%) rotate(-45deg)", animation: "hitmarkerFadeNeg45 0.6s ease-out forwards", boxShadow: "0 0 8px rgba(255, 255, 255, 0.8)", borderRadius: "1px" }} />
600
- <div style={{ position: "absolute", top: "75%", left: "25%", width: "12px", height: "3px", backgroundColor: "#ffffff", transform: "translate(-50%, -50%) rotate(-45deg)", animation: "hitmarkerFadeNeg45 0.6s ease-out forwards", boxShadow: "0 0 8px rgba(255, 255, 255, 0.8)", borderRadius: "1px" }} />
601
- <div style={{ position: "absolute", top: "75%", left: "75%", width: "12px", height: "3px", backgroundColor: "#ffffff", transform: "translate(-50%, -50%) rotate(45deg)", animation: "hitmarkerFade45 0.6s ease-out forwards", boxShadow: "0 0 8px rgba(255, 255, 255, 0.8)", borderRadius: "1px" }} />
654
+ <div
655
+ style={{
656
+ position: "absolute",
657
+ top: "25%",
658
+ left: "25%",
659
+ width: "12px",
660
+ height: "3px",
661
+ backgroundColor: "#ffffff",
662
+ transform: "translate(-50%, -50%) rotate(45deg)",
663
+ animation: "hitmarkerFade45 0.6s ease-out forwards",
664
+ boxShadow: "0 0 8px rgba(255, 255, 255, 0.8)",
665
+ borderRadius: "1px",
666
+ }}
667
+ />
668
+ <div
669
+ style={{
670
+ position: "absolute",
671
+ top: "25%",
672
+ left: "75%",
673
+ width: "12px",
674
+ height: "3px",
675
+ backgroundColor: "#ffffff",
676
+ transform: "translate(-50%, -50%) rotate(-45deg)",
677
+ animation: "hitmarkerFadeNeg45 0.6s ease-out forwards",
678
+ boxShadow: "0 0 8px rgba(255, 255, 255, 0.8)",
679
+ borderRadius: "1px",
680
+ }}
681
+ />
682
+ <div
683
+ style={{
684
+ position: "absolute",
685
+ top: "75%",
686
+ left: "25%",
687
+ width: "12px",
688
+ height: "3px",
689
+ backgroundColor: "#ffffff",
690
+ transform: "translate(-50%, -50%) rotate(-45deg)",
691
+ animation: "hitmarkerFadeNeg45 0.6s ease-out forwards",
692
+ boxShadow: "0 0 8px rgba(255, 255, 255, 0.8)",
693
+ borderRadius: "1px",
694
+ }}
695
+ />
696
+ <div
697
+ style={{
698
+ position: "absolute",
699
+ top: "75%",
700
+ left: "75%",
701
+ width: "12px",
702
+ height: "3px",
703
+ backgroundColor: "#ffffff",
704
+ transform: "translate(-50%, -50%) rotate(45deg)",
705
+ animation: "hitmarkerFade45 0.6s ease-out forwards",
706
+ boxShadow: "0 0 8px rgba(255, 255, 255, 0.8)",
707
+ borderRadius: "1px",
708
+ }}
709
+ />
602
710
  </div>
603
711
  ))}
604
712
 
@@ -612,7 +720,16 @@ export const IdleScreen: React.FC<IdleScreenProps> = ({
612
720
  width: `${Math.random() * 4 + 2}px`,
613
721
  height: `${Math.random() * 4 + 2}px`,
614
722
  borderRadius: "50%",
615
- background: ["#7aa2f7", "#bb9af7", "#9ece6a", "#73daca", "#7dcfff", "#f7768e", "#e0af68", "#2ac3de"][index % 8],
723
+ background: [
724
+ "#7aa2f7",
725
+ "#bb9af7",
726
+ "#9ece6a",
727
+ "#73daca",
728
+ "#7dcfff",
729
+ "#f7768e",
730
+ "#e0af68",
731
+ "#2ac3de",
732
+ ][index % 8],
616
733
  opacity: 0,
617
734
  animation: `floatUp ${8 + Math.random() * 4}s linear infinite`,
618
735
  animationDelay: `${Math.random() * 8}s`,
@@ -627,7 +744,10 @@ export const IdleScreen: React.FC<IdleScreenProps> = ({
627
744
  ))}
628
745
 
629
746
  {/* Center logo */}
630
- <CenterLogo containerRef={containerRef as React.RefObject<HTMLDivElement>} onHitmarker={createHitmarker} />
747
+ <CenterLogo
748
+ containerRef={containerRef as React.RefObject<HTMLDivElement>}
749
+ onHitmarker={createHitmarker}
750
+ />
631
751
 
632
752
  {/* DVD Logo */}
633
753
  <DvdLogo parentRef={containerRef as React.RefObject<HTMLDivElement>} scale={0.08} />