@livepeer-frameworks/player-react 0.1.0 → 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 (41) hide show
  1. package/README.md +7 -9
  2. package/package.json +1 -1
  3. package/src/components/DevModePanel.tsx +244 -143
  4. package/src/components/Icons.tsx +105 -25
  5. package/src/components/IdleScreen.tsx +262 -128
  6. package/src/components/LoadingScreen.tsx +169 -151
  7. package/src/components/LogoOverlay.tsx +3 -6
  8. package/src/components/Player.tsx +84 -56
  9. package/src/components/PlayerControls.tsx +349 -256
  10. package/src/components/PlayerErrorBoundary.tsx +6 -13
  11. package/src/components/SeekBar.tsx +96 -88
  12. package/src/components/SkipIndicator.tsx +2 -12
  13. package/src/components/SpeedIndicator.tsx +2 -11
  14. package/src/components/StatsPanel.tsx +31 -22
  15. package/src/components/StreamStateOverlay.tsx +105 -49
  16. package/src/components/SubtitleRenderer.tsx +29 -29
  17. package/src/components/ThumbnailOverlay.tsx +5 -6
  18. package/src/components/TitleOverlay.tsx +2 -8
  19. package/src/components/players/DashJsPlayer.tsx +13 -11
  20. package/src/components/players/HlsJsPlayer.tsx +13 -11
  21. package/src/components/players/MewsWsPlayer/index.tsx +13 -11
  22. package/src/components/players/MistPlayer.tsx +13 -11
  23. package/src/components/players/MistWebRTCPlayer/index.tsx +19 -10
  24. package/src/components/players/NativePlayer.tsx +10 -12
  25. package/src/components/players/VideoJsPlayer.tsx +13 -11
  26. package/src/context/PlayerContext.tsx +4 -8
  27. package/src/context/index.ts +3 -3
  28. package/src/hooks/useMetaTrack.ts +27 -27
  29. package/src/hooks/usePlaybackQuality.ts +3 -3
  30. package/src/hooks/usePlayerController.ts +186 -138
  31. package/src/hooks/usePlayerSelection.ts +6 -6
  32. package/src/hooks/useStreamState.ts +51 -56
  33. package/src/hooks/useTelemetry.ts +18 -3
  34. package/src/hooks/useViewerEndpoints.ts +34 -23
  35. package/src/index.tsx +36 -28
  36. package/src/types.ts +8 -8
  37. package/src/ui/badge.tsx +6 -5
  38. package/src/ui/button.tsx +9 -8
  39. package/src/ui/context-menu.tsx +42 -61
  40. package/src/ui/select.tsx +13 -7
  41. 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
  );
@@ -214,45 +223,103 @@ function StatusIcon({ status }: { status?: StreamStatus }) {
214
223
  const iconClass = "w-5 h-5";
215
224
 
216
225
  // Spinner for loading states
217
- 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
+ ) {
218
232
  return (
219
- <svg className={`${iconClass} animate-spin`} fill="none" viewBox="0 0 24 24" style={{ color: 'hsl(var(--tn-yellow, 40 95% 64%))' }}>
220
- <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
221
- <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
+ />
222
252
  </svg>
223
253
  );
224
254
  }
225
255
 
226
256
  // Offline icon
227
- if (status === 'OFFLINE') {
257
+ if (status === "OFFLINE") {
228
258
  return (
229
- <svg className={iconClass} fill="none" viewBox="0 0 24 24" stroke="currentColor" style={{ color: 'hsl(var(--tn-red, 348 100% 72%))' }}>
230
- <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
+ />
231
272
  </svg>
232
273
  );
233
274
  }
234
275
 
235
276
  // Error icon
236
- if (status === 'ERROR' || status === 'INVALID') {
277
+ if (status === "ERROR" || status === "INVALID") {
237
278
  return (
238
- <svg className={iconClass} fill="none" viewBox="0 0 24 24" stroke="currentColor" style={{ color: 'hsl(var(--tn-red, 348 100% 72%))' }}>
239
- <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
+ />
240
292
  </svg>
241
293
  );
242
294
  }
243
295
 
244
296
  // Default spinner
245
297
  return (
246
- <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
+ >
247
304
  <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
248
- <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
+ />
249
310
  </svg>
250
311
  );
251
312
  }
252
313
 
253
- const StatusOverlay: React.FC<StatusOverlayProps> = ({ status, message, percentage, error, onRetry }) => {
254
- const showRetry = (status === 'ERROR' || status === 'INVALID') && onRetry;
255
- 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;
256
323
  const displayMessage = error || message;
257
324
 
258
325
  return (
@@ -352,50 +419,51 @@ interface Hitmarker {
352
419
  const playHitmarkerSound = () => {
353
420
  try {
354
421
  // Embedded hitmarker sound as base64 data URL
355
- const hitmarkerDataUrl = 'data:audio/mpeg;base64,SUQzBAAAAAAANFRDT04AAAAHAAADT3RoZXIAVFNTRQAAAA8AAANMYXZmNTcuODMuMTAwAAAAAAAAAAAA' +
356
- 'AAD/+1QAAAAAAAAAAAAAAAAAAAAA' +
357
- 'AAAAAAAAAAAAAAAAAAAAAABJbmZvAAAADwAAAAYAAAnAADs7Ozs7Ozs7Ozs7Ozs7OztiYmJiYmJiYmJi' +
358
- 'YmJiYmJiYomJiYmJiYmJiYmJiYmJiYmxsbGxsbGxsbGxsbGxsbGxsdjY2NjY2NjY2NjY2NjY2NjY////' +
359
- '/////////////////wAAAABMYXZjNTcuMTAAAAAAAAAAAAAAAAAkAkAAAAAAAAAJwOuMZun/+5RkAA8S' +
360
- '/F23AGAaAi0AF0AAAAAInXsEAIRXyQ8D4OQgjEhE3cO7ujuHF0XCOu4G7xKbi3Funu7u7p9dw7unu7u7' +
361
- 'p7u7u6fXcW7om7u7uiU3dxdT67u7p7uHdxelN3cW6fXcW7oXXd3eJTd3d0+u4t3iXdw4up70W4uiPruL' +
362
- 'DzMw8Pz79Y99JfkyfPv5/h9uTJoy79Y99Y97q3vyZPJk0ZfrL6x73Vn+J35dKKS/STQyQ8CAiCPNuRAO' +
363
- 'OqquAx+fzJeBKDAsgAMBuWcBsHKhjJTcCwIALyAvABbI0ZIcCmP8jHJe8gZAdVRp2TpnU/kUXV4iQuBA' +
364
- 'AkAQgisLPvwQ2Jz7wIkIpQ8QOl/KFy75w+2HpTFnRqXLQo0fzlSYRe5Ce9yZMEzRM4xesu95Mo8QQsoM' +
365
- 'H4gLg+fJqkmY3GZJE2kwGfMECJiAdIttoEa2yotfC7jsS2mjKgbzAfEMeiwZpGSUFCQwPKQiWXh0TnkN' +
366
- 'or5SmrKvwHlX2zFxKxPCzRL/+5RkIwADvUxLawwb0GdF6Y1hJlgNNJk+DSRwyQwI6AD2JCiBmhaff0dz' +
367
- 'CEBjgFABAcDNFc3YAEV4hQn0L/QvQnevom+n13eIjoTvABLrHg/L9RzdWXYonHbbbE2K0pX+gkL2g56R' +
368
- 'iwrbuWwhoABzQoMKOAIGAfE4UKk6BhSIJpECBq0CEYmZKYIiAJt72H24dNou7y/Ee7a/3v+MgySemSTY' +
369
- 'mnBAFwIAAGfCJ8/D9YfkwQEBcP38uA1d/EB1T5dZKEsgnuhwZirY5fIMRMdRn7U4OcN2m5NWeYdcPBwX' +
370
- 'DBOsJF1DBYks62pAURqz1hGoGHH/QIoRC80tYAJ8g4f3MPD51sywAbhAn/X9P/75tvZww3gZ3pYPDx/+' +
371
- 'ACO/7//ffHj/D/AAfATC4DYGFA3MRABo0lqWjBOl2yAda1C1BdhduXgm8FGnAQB/lDiEi6j9qw9EHigI' +
372
- 'IOLB6F1eIPd+T6Agc4//lMo6+k3tdttJY2gArU7cN07m2FLSm4gCjyz/+5RECwACwSRZawkdLFGi2mVh' +
373
- '5h4LfFdPVPGACViTavaeMAAV0UkkEsDhxxJwqF04on002mZah8w9+5ItfSAoyZa1dchnPpLmAEKrVMRA' +
374
- '//sD8w0WsB4xiw4JqaZMB45TdpIuXXUPf8Bpa35p/jQIAOAuZkmUeJoM5W6L2gqqO6rTuHjUTDnhy4Qi' +
375
- 'K348vtFysOizShoHbBpsPRYcSINCbiN4XOLPPAgq3dW2Ga7SlyiKXBV7W1RQl5BiiVGkwayJfEnPxgXk' +
376
- 'QeZxxzyhTuLO2XFUDDstoc6CkM1J8QZAjUN3bM8580cRygNfmPAELGjIH0Z/0A+8csyH/4eHvgAf8APg' +
377
- 'ABmZ98AARAADP////Dw8PHEmIpgGttpJQJsmZjq5nPQ8j5VqWW1evqdjP182PA6tHJZgkC5iSbEQkyJS' +
378
- 'z/BvP3eucLKN0+Wiza4feKKFBqiAEBAMXyYni5NZc16CDl/QY9j6BAcWSmQYcIcoMHYoQNBiIBgIBUAz' +
379
- 'QUMSnjj/+5RkCwADsFLffjEAAjrJe63JHACO6WtlnPMACKaCK1uMMADU5dI6JhW2cam98UlRmY4ihyKF' +
380
- 'rNsgpZd5PYgBALnYofKEt82De0GbW1DLibvFDK+bSeOm8qKdqUFZ7uiK8XMPHyqm3pTxUvcunUfxXEo9' +
381
- 'RNe5b/8vfCD3kzDN7vTtHyaIcntVDAYBAUBAAAAQBI2vguYNsHWm5AR3mZtZib8WAHFvz2Kf9//iYvlR' +
382
- 'B/+n///////////+UH7XoIDMoJAEAMtj8JshJPRwklVqNSpYnalfE+VzNCAISCoxVHEpIo/WrTiMvP7V' +
383
- 'TujOPnOglLbMLN/pq/d2Y4lRJIkSnPlUSJEjSKJqM41d88zWtMzP+fCOORmc9NeM+f1nnO//efM52/fG' +
384
- '/ef385+5u+u1bRJkwU8FAkEItZpkRYeQYcAgZTEYlaZa2yROLeC0qdX73rZJJ/d2f6v6Or0u/+5FBYcn' +
385
- 'g0MlCiQTR9GUU5LScmSuSlH00IWqXA6jlw4BEcD/+5REEAAi3RtU+eYbGF1E+lk9g0YJzLUgh7BlQVGT' +
386
- 'ZJD0jKhhTNVilqrMzFRK+x/szcMKBWKep4NP1A0DR6RESkTp5Z1Q9Y8REgqMg1DpUBPleeqlRQcerBpM' +
387
- 'jiURHVD4XwAALhAgbxxlxYD5OFkG8oQRPB2EpsxSCNVlgcYUqoAyiVJmaARlkwplICfPoUy/zWEzM2pc' +
388
- 'NYzAQNJDSniEYecSEqxFEzQqEvUFGnvzwUfcRlpZ9T2LCR5QdDQDDhKICAjpJCagpRo9UQRPClZZlg6E' +
389
- 'p9DMTkTl+okuhRIVIzAQEf9L+Mx/DUjqmqN6kX7M36lS4zgLyJV3iV6j3xF8kJduJawVw1nndAlBaLLg' +
390
- 'JupwsTcLkxmJgFLgSzoCmHjSNGSqkGPCpnNqTXIwolf6qlVWN+q/su37HzgrES1pWGg3KnWh0FXCVniJ' +
391
- '9K5b4iCrpLEuIcFTqwkVLFiqgaDqCCSMVWqxBAVCFOLVrVahm2ahUThUKJnmFCw15hD0Qhb/+5REEAhC' +
392
- 'YSRCSQEb4FOGaBUMI6JIRYC0QIB2SQsgGpgwDghgIlS6FU8VBXDoiBp5Y9gtkVnhEhYBdJFQ7kQ3w1yp' +
393
- '0NB2CoNPEttZ1/aeDUAAA26FEghWgEKNVAVWkFAQEmMK2Uwk/qI0hqUb/4epVIZH1ai6szf6kzH1f2ar' +
394
- 'xYGS9FcOsN5UlJLQt///+oo0FRDTUQ0FBQr9f5LxXP+mEUfk0AIrf/5GRmQ0//mX//ZbLP5b5GrWSz+W' +
395
- 'SkZMrWyyyy2GRqyggVRyMv////////st//sn/yyVDI1l8mVgoYGDCOqiqIQBxmvxWCggTpZZZD//aWfy' +
396
- 'yWf/y/7KGDA0ssBggTof9k/+WS/8slQyMp/5Nfln8WAqGcUbULCrKxT9ISF+kKsxQWpMQU1FMy4xMDCq' +
397
- 'qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq' +
398
- '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=";
399
467
 
400
468
  const audio = new Audio(hitmarkerDataUrl);
401
469
  audio.volume = 0.3;
@@ -425,8 +493,8 @@ const createSyntheticHitmarkerSound = () => {
425
493
  oscillator2.frequency.setValueAtTime(3600, audioContext.currentTime);
426
494
  oscillator2.frequency.exponentialRampToValueAtTime(1800, audioContext.currentTime + 0.04);
427
495
 
428
- oscillator1.type = 'triangle';
429
- oscillator2.type = 'sine';
496
+ oscillator1.type = "triangle";
497
+ oscillator2.type = "sine";
430
498
 
431
499
  gainNode1.gain.setValueAtTime(0, audioContext.currentTime);
432
500
  gainNode1.gain.linearRampToValueAtTime(0.4, audioContext.currentTime + 0.002);
@@ -490,19 +558,19 @@ export const IdleScreen: React.FC<IdleScreenProps> = ({
490
558
  y,
491
559
  };
492
560
 
493
- setHitmarkers(prev => [...prev, newHitmarker]);
561
+ setHitmarkers((prev) => [...prev, newHitmarker]);
494
562
  playHitmarkerSound();
495
563
 
496
564
  setTimeout(() => {
497
- setHitmarkers(prev => prev.filter(h => h.id !== newHitmarker.id));
565
+ setHitmarkers((prev) => prev.filter((h) => h.id !== newHitmarker.id));
498
566
  }, 600);
499
567
  };
500
568
 
501
569
  // Inject CSS animations
502
570
  useEffect(() => {
503
- const styleId = 'idle-screen-animations';
571
+ const styleId = "idle-screen-animations";
504
572
  if (!document.getElementById(styleId)) {
505
- const style = document.createElement('style');
573
+ const style = document.createElement("style");
506
574
  style.id = styleId;
507
575
  style.textContent = `
508
576
  @keyframes fadeInOut {
@@ -542,11 +610,12 @@ export const IdleScreen: React.FC<IdleScreenProps> = ({
542
610
  <div
543
611
  ref={containerRef}
544
612
  className="fw-player-root"
545
- style={{
546
- position: "absolute",
547
- inset: 0,
548
- zIndex: 5,
549
- background: `
613
+ style={
614
+ {
615
+ position: "absolute",
616
+ inset: 0,
617
+ zIndex: 5,
618
+ background: `
550
619
  linear-gradient(135deg,
551
620
  hsl(var(--tn-bg-dark, 235 21% 11%)) 0%,
552
621
  hsl(var(--tn-bg, 233 23% 17%)) 25%,
@@ -555,19 +624,20 @@ export const IdleScreen: React.FC<IdleScreenProps> = ({
555
624
  hsl(var(--tn-bg-dark, 235 21% 11%)) 100%
556
625
  )
557
626
  `,
558
- backgroundSize: "400% 400%",
559
- animation: "gradientShift 16s ease-in-out infinite",
560
- display: "flex",
561
- flexDirection: "column",
562
- alignItems: "center",
563
- justifyContent: "center",
564
- overflow: "hidden",
565
- borderRadius: "0",
566
- userSelect: "none",
567
- } 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
+ }
568
638
  >
569
639
  {/* Hitmarkers */}
570
- {hitmarkers.map(hitmarker => (
640
+ {hitmarkers.map((hitmarker) => (
571
641
  <div
572
642
  key={hitmarker.id}
573
643
  style={{
@@ -581,10 +651,62 @@ export const IdleScreen: React.FC<IdleScreenProps> = ({
581
651
  height: "40px",
582
652
  }}
583
653
  >
584
- <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" }} />
585
- <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" }} />
586
- <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" }} />
587
- <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
+ />
588
710
  </div>
589
711
  ))}
590
712
 
@@ -598,7 +720,16 @@ export const IdleScreen: React.FC<IdleScreenProps> = ({
598
720
  width: `${Math.random() * 4 + 2}px`,
599
721
  height: `${Math.random() * 4 + 2}px`,
600
722
  borderRadius: "50%",
601
- 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],
602
733
  opacity: 0,
603
734
  animation: `floatUp ${8 + Math.random() * 4}s linear infinite`,
604
735
  animationDelay: `${Math.random() * 8}s`,
@@ -613,7 +744,10 @@ export const IdleScreen: React.FC<IdleScreenProps> = ({
613
744
  ))}
614
745
 
615
746
  {/* Center logo */}
616
- <CenterLogo containerRef={containerRef as React.RefObject<HTMLDivElement>} onHitmarker={createHitmarker} />
747
+ <CenterLogo
748
+ containerRef={containerRef as React.RefObject<HTMLDivElement>}
749
+ onHitmarker={createHitmarker}
750
+ />
617
751
 
618
752
  {/* DVD Logo */}
619
753
  <DvdLogo parentRef={containerRef as React.RefObject<HTMLDivElement>} scale={0.08} />