@livepeer-frameworks/player-react 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 (88) hide show
  1. package/dist/cjs/index.js +2 -0
  2. package/dist/cjs/index.js.map +1 -0
  3. package/dist/esm/index.js +2 -0
  4. package/dist/esm/index.js.map +1 -0
  5. package/dist/types/components/DevModePanel.d.ts +47 -0
  6. package/dist/types/components/DvdLogo.d.ts +4 -0
  7. package/dist/types/components/Icons.d.ts +33 -0
  8. package/dist/types/components/IdleScreen.d.ts +16 -0
  9. package/dist/types/components/LoadingScreen.d.ts +6 -0
  10. package/dist/types/components/LogoOverlay.d.ts +11 -0
  11. package/dist/types/components/Player.d.ts +11 -0
  12. package/dist/types/components/PlayerControls.d.ts +60 -0
  13. package/dist/types/components/PlayerErrorBoundary.d.ts +23 -0
  14. package/dist/types/components/SeekBar.d.ts +33 -0
  15. package/dist/types/components/SkipIndicator.d.ts +14 -0
  16. package/dist/types/components/SpeedIndicator.d.ts +12 -0
  17. package/dist/types/components/StatsPanel.d.ts +31 -0
  18. package/dist/types/components/StreamStateOverlay.d.ts +24 -0
  19. package/dist/types/components/SubtitleRenderer.d.ts +69 -0
  20. package/dist/types/components/ThumbnailOverlay.d.ts +4 -0
  21. package/dist/types/components/TitleOverlay.d.ts +13 -0
  22. package/dist/types/components/players/DashJsPlayer.d.ts +18 -0
  23. package/dist/types/components/players/HlsJsPlayer.d.ts +18 -0
  24. package/dist/types/components/players/MewsWsPlayer/index.d.ts +18 -0
  25. package/dist/types/components/players/MistPlayer.d.ts +20 -0
  26. package/dist/types/components/players/MistWebRTCPlayer/index.d.ts +20 -0
  27. package/dist/types/components/players/NativePlayer.d.ts +19 -0
  28. package/dist/types/components/players/VideoJsPlayer.d.ts +18 -0
  29. package/dist/types/context/PlayerContext.d.ts +40 -0
  30. package/dist/types/context/index.d.ts +5 -0
  31. package/dist/types/hooks/useMetaTrack.d.ts +54 -0
  32. package/dist/types/hooks/usePlaybackQuality.d.ts +42 -0
  33. package/dist/types/hooks/usePlayerController.d.ts +163 -0
  34. package/dist/types/hooks/usePlayerSelection.d.ts +47 -0
  35. package/dist/types/hooks/useStreamState.d.ts +27 -0
  36. package/dist/types/hooks/useTelemetry.d.ts +57 -0
  37. package/dist/types/hooks/useViewerEndpoints.d.ts +14 -0
  38. package/dist/types/index.d.ts +33 -0
  39. package/dist/types/types.d.ts +94 -0
  40. package/dist/types/ui/badge.d.ts +9 -0
  41. package/dist/types/ui/button.d.ts +11 -0
  42. package/dist/types/ui/context-menu.d.ts +27 -0
  43. package/dist/types/ui/select.d.ts +10 -0
  44. package/dist/types/ui/slider.d.ts +13 -0
  45. package/package.json +71 -0
  46. package/src/assets/logomark.svg +56 -0
  47. package/src/components/DevModePanel.tsx +822 -0
  48. package/src/components/DvdLogo.tsx +201 -0
  49. package/src/components/Icons.tsx +282 -0
  50. package/src/components/IdleScreen.tsx +664 -0
  51. package/src/components/LoadingScreen.tsx +710 -0
  52. package/src/components/LogoOverlay.tsx +75 -0
  53. package/src/components/Player.tsx +419 -0
  54. package/src/components/PlayerControls.tsx +820 -0
  55. package/src/components/PlayerErrorBoundary.tsx +70 -0
  56. package/src/components/SeekBar.tsx +291 -0
  57. package/src/components/SkipIndicator.tsx +113 -0
  58. package/src/components/SpeedIndicator.tsx +57 -0
  59. package/src/components/StatsPanel.tsx +150 -0
  60. package/src/components/StreamStateOverlay.tsx +200 -0
  61. package/src/components/SubtitleRenderer.tsx +235 -0
  62. package/src/components/ThumbnailOverlay.tsx +90 -0
  63. package/src/components/TitleOverlay.tsx +48 -0
  64. package/src/components/players/DashJsPlayer.tsx +56 -0
  65. package/src/components/players/HlsJsPlayer.tsx +56 -0
  66. package/src/components/players/MewsWsPlayer/index.tsx +56 -0
  67. package/src/components/players/MistPlayer.tsx +60 -0
  68. package/src/components/players/MistWebRTCPlayer/index.tsx +59 -0
  69. package/src/components/players/NativePlayer.tsx +58 -0
  70. package/src/components/players/VideoJsPlayer.tsx +56 -0
  71. package/src/context/PlayerContext.tsx +71 -0
  72. package/src/context/index.ts +11 -0
  73. package/src/global.d.ts +4 -0
  74. package/src/hooks/useMetaTrack.ts +187 -0
  75. package/src/hooks/usePlaybackQuality.ts +126 -0
  76. package/src/hooks/usePlayerController.ts +525 -0
  77. package/src/hooks/usePlayerSelection.ts +117 -0
  78. package/src/hooks/useStreamState.ts +381 -0
  79. package/src/hooks/useTelemetry.ts +138 -0
  80. package/src/hooks/useViewerEndpoints.ts +120 -0
  81. package/src/index.tsx +75 -0
  82. package/src/player.css +2 -0
  83. package/src/types.ts +135 -0
  84. package/src/ui/badge.tsx +27 -0
  85. package/src/ui/button.tsx +47 -0
  86. package/src/ui/context-menu.tsx +193 -0
  87. package/src/ui/select.tsx +105 -0
  88. package/src/ui/slider.tsx +67 -0
@@ -0,0 +1,710 @@
1
+ import React, { useRef, useEffect, useState } from "react";
2
+ import DvdLogo from "./DvdLogo";
3
+ import logomarkAsset from "../assets/logomark.svg";
4
+
5
+ interface AnimatedBubbleProps {
6
+ index: number;
7
+ }
8
+
9
+ const AnimatedBubble: React.FC<AnimatedBubbleProps> = ({ index }) => {
10
+ const [position, setPosition] = useState({ top: 0, left: 0 });
11
+ const [size, setSize] = useState(40);
12
+ const [opacity, setOpacity] = useState(0);
13
+
14
+ const getRandomPosition = () => ({
15
+ top: Math.random() * 80 + 10, // 10% to 90%
16
+ left: Math.random() * 80 + 10, // 10% to 90%
17
+ });
18
+
19
+ const getRandomSize = () => Math.random() * 60 + 30; // 30px to 90px
20
+
21
+ useEffect(() => {
22
+ // Initial random position and size
23
+ setPosition(getRandomPosition());
24
+ setSize(getRandomSize());
25
+
26
+ const animationCycle = () => {
27
+ // Fade in
28
+ setOpacity(0.15);
29
+
30
+ setTimeout(() => {
31
+ // Fade out
32
+ setOpacity(0);
33
+
34
+ setTimeout(() => {
35
+ // Move to new random position with new size while invisible
36
+ setPosition(getRandomPosition());
37
+ setSize(getRandomSize());
38
+ // Start next cycle after a brief delay to ensure position change is complete
39
+ setTimeout(() => {
40
+ animationCycle();
41
+ }, 200);
42
+ }, 1500); // Wait for fade out to complete
43
+ }, 4000 + Math.random() * 3000); // Stay visible for 4-7 seconds (was 2-4)
44
+ };
45
+
46
+ // Start the animation cycle with staggered timing
47
+ const timeout = setTimeout(animationCycle, index * 500);
48
+
49
+ return () => {
50
+ clearTimeout(timeout);
51
+ };
52
+ }, [index]);
53
+
54
+ // Tokyo Night inspired pastel colors
55
+ const bubbleColors = [
56
+ "rgba(122, 162, 247, 0.2)", // Terminal Blue
57
+ "rgba(187, 154, 247, 0.2)", // Terminal Magenta
58
+ "rgba(158, 206, 106, 0.2)", // Strings/CSS classes
59
+ "rgba(115, 218, 202, 0.2)", // Terminal Green
60
+ "rgba(125, 207, 255, 0.2)", // Terminal Cyan
61
+ "rgba(247, 118, 142, 0.2)", // Keywords/Terminal Red
62
+ "rgba(224, 175, 104, 0.2)", // Terminal Yellow
63
+ "rgba(42, 195, 222, 0.2)", // Language functions
64
+ ];
65
+
66
+ return (
67
+ <div
68
+ style={{
69
+ position: "absolute",
70
+ top: `${position.top}%`,
71
+ left: `${position.left}%`,
72
+ width: `${size}px`,
73
+ height: `${size}px`,
74
+ borderRadius: "50%",
75
+ background: bubbleColors[index % bubbleColors.length],
76
+ opacity: opacity,
77
+ transition: "opacity 1s ease-in-out",
78
+ pointerEvents: "none",
79
+ userSelect: "none",
80
+ WebkitUserSelect: "none",
81
+ MozUserSelect: "none",
82
+ msUserSelect: "none",
83
+ } as React.CSSProperties}
84
+ />
85
+ );
86
+ };
87
+
88
+ interface CenterLogoProps {
89
+ containerRef: React.RefObject<HTMLDivElement>;
90
+ scale?: number;
91
+ onHitmarker?: (e: { clientX: number; clientY: number }) => void;
92
+ }
93
+
94
+ const CenterLogo: React.FC<CenterLogoProps> = ({ containerRef, scale = 0.2, onHitmarker }) => {
95
+ const [logoSize, setLogoSize] = useState(100);
96
+ const [offset, setOffset] = useState({ x: 0, y: 0 });
97
+ const [isHovered, setIsHovered] = useState(false);
98
+
99
+ useEffect(() => {
100
+ if (containerRef.current) {
101
+ const containerWidth = containerRef.current.clientWidth;
102
+ const containerHeight = containerRef.current.clientHeight;
103
+ const minDimension = Math.min(containerWidth, containerHeight);
104
+ setLogoSize(minDimension * scale);
105
+ }
106
+ }, [containerRef, scale]);
107
+
108
+ const handleLogoClick = (e: React.MouseEvent) => {
109
+ e.stopPropagation(); // Prevent event bubbling to container
110
+ if (onHitmarker) {
111
+ // Get the exact click position on the logo
112
+ onHitmarker({ clientX: e.clientX, clientY: e.clientY });
113
+ }
114
+ };
115
+
116
+ const handleMouseMove = (e: MouseEvent) => {
117
+ if (!containerRef.current) return;
118
+
119
+ const rect = containerRef.current.getBoundingClientRect();
120
+ const centerX = rect.left + rect.width / 2;
121
+ const centerY = rect.top + rect.height / 2;
122
+
123
+ const mouseX = e.clientX;
124
+ const mouseY = e.clientY;
125
+
126
+ // Calculate distance from center
127
+ const deltaX = mouseX - centerX;
128
+ const deltaY = mouseY - centerY;
129
+ const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
130
+
131
+ // If mouse is close to logo, move it away
132
+ const maxDistance = logoSize * 1.5; // Detection radius
133
+ if (distance < maxDistance && distance > 0) {
134
+ const pushStrength = (maxDistance - distance) / maxDistance;
135
+ const pushDistance = 50 * pushStrength; // Max push distance
136
+
137
+ // Normalize direction and apply push
138
+ const pushX = -(deltaX / distance) * pushDistance;
139
+ const pushY = -(deltaY / distance) * pushDistance;
140
+
141
+ setOffset({ x: pushX, y: pushY });
142
+ setIsHovered(true);
143
+ } else {
144
+ setOffset({ x: 0, y: 0 });
145
+ setIsHovered(false);
146
+ }
147
+ };
148
+
149
+ const handleMouseLeave = () => {
150
+ setOffset({ x: 0, y: 0 });
151
+ setIsHovered(false);
152
+ };
153
+
154
+ useEffect(() => {
155
+ if (containerRef.current) {
156
+ const container = containerRef.current;
157
+ container.addEventListener('mousemove', handleMouseMove);
158
+ container.addEventListener('mouseleave', handleMouseLeave);
159
+
160
+ return () => {
161
+ container.removeEventListener('mousemove', handleMouseMove);
162
+ container.removeEventListener('mouseleave', handleMouseLeave);
163
+ };
164
+ }
165
+ }, [logoSize, containerRef]);
166
+
167
+ return (
168
+ <div
169
+ style={{
170
+ position: "absolute",
171
+ top: "50%",
172
+ left: "50%",
173
+ transform: `translate(-50%, -50%) translate(${offset.x}px, ${offset.y}px)`,
174
+ display: "flex",
175
+ alignItems: "center",
176
+ justifyContent: "center",
177
+ zIndex: 10,
178
+ transition: "transform 0.3s ease-out",
179
+ userSelect: "none",
180
+ WebkitUserSelect: "none",
181
+ MozUserSelect: "none",
182
+ msUserSelect: "none",
183
+ }}
184
+ >
185
+ {/* Pulsing circle background */}
186
+ <div
187
+ style={{
188
+ position: "absolute",
189
+ width: `${logoSize * 1.4}px`,
190
+ height: `${logoSize * 1.4}px`,
191
+ borderRadius: "50%",
192
+ background: "rgba(122, 162, 247, 0.15)",
193
+ animation: isHovered ? "logoPulse 1s ease-in-out infinite" : "logoPulse 3s ease-in-out infinite",
194
+ transform: isHovered ? "scale(1.2)" : "scale(1)",
195
+ transition: "transform 0.3s ease-out",
196
+ userSelect: "none",
197
+ WebkitUserSelect: "none",
198
+ MozUserSelect: "none",
199
+ msUserSelect: "none",
200
+ pointerEvents: "none",
201
+ }}
202
+ />
203
+
204
+ {/* Logo - only the SVG is clickable */}
205
+ <img
206
+ src={logomarkAsset}
207
+ alt="FrameWorks Logo"
208
+ onClick={handleLogoClick}
209
+ style={{
210
+ width: `${logoSize}px`,
211
+ height: `${logoSize}px`,
212
+ position: "relative",
213
+ zIndex: 1,
214
+ filter: isHovered
215
+ ? "drop-shadow(0 6px 12px rgba(36, 40, 59, 0.4)) brightness(1.1)"
216
+ : "drop-shadow(0 4px 8px rgba(36, 40, 59, 0.3))",
217
+ transform: isHovered ? "scale(1.1)" : "scale(1)",
218
+ transition: "all 0.3s ease-out",
219
+ cursor: isHovered ? "pointer" : "default",
220
+ userSelect: "none",
221
+ WebkitUserSelect: "none",
222
+ MozUserSelect: "none",
223
+ msUserSelect: "none",
224
+ WebkitUserDrag: "none",
225
+ WebkitTouchCallout: "none",
226
+ } as React.CSSProperties}
227
+ />
228
+ </div>
229
+ );
230
+ };
231
+
232
+ interface LoadingScreenProps {
233
+ message?: string;
234
+ }
235
+
236
+ interface Hitmarker {
237
+ id: number;
238
+ x: number;
239
+ y: number;
240
+ }
241
+
242
+ const LoadingScreen: React.FC<LoadingScreenProps> = ({ message = "Waiting for source..." }) => {
243
+ const containerRef = useRef<HTMLDivElement>(null);
244
+ const [hitmarkers, setHitmarkers] = useState<Hitmarker[]>([]);
245
+
246
+ const playHitmarkerSound = () => {
247
+ try {
248
+ // Embedded hitmarker sound as base64 data URL
249
+ const hitmarkerDataUrl = 'data:audio/mpeg;base64,SUQzBAAAAAAANFRDT04AAAAHAAADT3RoZXIAVFNTRQAAAA8AAANMYXZmNTcuODMuMTAwAAAAAAAAAAAA' +
250
+ 'AAD/+1QAAAAAAAAAAAAAAAAAAAAA' +
251
+ 'AAAAAAAAAAAAAAAAAAAAAABJbmZvAAAADwAAAAYAAAnAADs7Ozs7Ozs7Ozs7Ozs7OztiYmJiYmJiYmJi' +
252
+ 'YmJiYmJiYomJiYmJiYmJiYmJiYmJiYmxsbGxsbGxsbGxsbGxsbGxsdjY2NjY2NjY2NjY2NjY2NjY////' +
253
+ '/////////////////wAAAABMYXZjNTcuMTAAAAAAAAAAAAAAAAAkAkAAAAAAAAAJwOuMZun/+5RkAA8S' +
254
+ '/F23AGAaAi0AF0AAAAAInXsEAIRXyQ8D4OQgjEhE3cO7ujuHF0XCOu4G7xKbi3Funu7u7p9dw7unu7u7' +
255
+ 'p7u7u6fXcW7om7u7uiU3dxdT67u7p7uHdxelN3cW6fXcW7oXXd3eJTd3d0+u4t3iXdw4up70W4uiPruL' +
256
+ 'DzMw8Pz79Y99JfkyfPv5/h9uTJoy79Y99Y97q3vyZPJk0ZfrL6x73Vn+J35dKKS/STQyQ8CAiCPNuRAO' +
257
+ 'OqquAx+fzJeBKDAsgAMBuWcBsHKhjJTcCwIALyAvABbI0ZIcCmP8jHJe8gZAdVRp2TpnU/kUXV4iQuBA' +
258
+ 'AkAQgisLPvwQ2Jz7wIkIpQ8QOl/KFy75w+2HpTFnRqXLQo0fzlSYRe5Ce9yZMEzRM4xesu95Mo8QQsoM' +
259
+ 'H4gLg+fJqkmY3GZJE2kwGfMECJiAdIttoEa2yotfC7jsS2mjKgbzAfEMeiwZpGSUFCQwPKQiWXh0TnkN' +
260
+ 'or5SmrKvwHlX2zFxKxPCzRL/+5RkIwADvUxLawwb0GdF6Y1hJlgNNJk+DSRwyQwI6AD2JCiBmhaff0dz' +
261
+ 'CEBjgFABAcDNFc3YAEV4hQn0L/QvQnevom+n13eIjoTvABLrHg/L9RzdWXYonHbbbE2K0pX+gkL2g56R' +
262
+ 'iwrbuWwhoABzQoMKOAIGAfE4UKk6BhSIJpECBq0CEYmZKYIiAJt72H24dNou7y/Ee7a/3v+MgySemSTY' +
263
+ 'mnBAFwIAAGfCJ8/D9YfkwQEBcP38uA1d/EB1T5dZKEsgnuhwZirY5fIMRMdRn7U4OcN2m5NWeYdcPBwX' +
264
+ 'DBOsJF1DBYks62pAURqz1hGoGHH/QIoRC80tYAJ8g4f3MPD51sywAbhAn/X9P/75tvZww3gZ3pYPDx/+' +
265
+ 'ACO/7//ffHj/D/AAfATC4DYGFA3MRABo0lqWjBOl2yAda1C1BdhduXgm8FGnAQB/lDiEi6j9qw9EHigI' +
266
+ 'IOLB6F1eIPd+T6Agc4//lMo6+k3tdttJY2gArU7cN07m2FLSm4gCjyz/+5RECwACwSRZawkdLFGi2mVh' +
267
+ '5h4LfFdPVPGACViTavaeMAAV0UkkEsDhxxJwqF04on002mZah8w9+5ItfSAoyZa1dchnPpLmAEKrVMRA' +
268
+ '//sD8w0WsB4xiw4JqaZMB45TdpIuXXUPf8Bpa35p/jQIAOAuZkmUeJoM5W6L2gqqO6rTuHjUTDnhy4Qi' +
269
+ 'K348vtFysOizShoHbBpsPRYcSINCbiN4XOLPPAgq3dW2Ga7SlyiKXBV7W1RQl5BiiVGkwayJfEnPxgXk' +
270
+ 'QeZxxzyhTuLO2XFUDDstoc6CkM1J8QZAjUN3bM8580cRygNfmPAELGjIH0Z/0A+8csyH/4eHvgAf8APg' +
271
+ 'ABmZ98AARAADP////Dw8PHEmIpgGttpJQJsmZjq5nPQ8j5VqWW1evqdjP182PA6tHJZgkC5iSbEQkyJS' +
272
+ 'z/BvP3eucLKN0+Wiza4feKKFBqiAEBAMXyYni5NZc16CDl/QY9j6BAcWSmQYcIcoMHYoQNBiIBgIBUAz' +
273
+ 'QUMSnjj/+5RkCwADsFLffjEAAjrJe63JHACO6WtlnPMACKaCK1uMMADU5dI6JhW2cam98UlRmY4ihyKF' +
274
+ 'rNsgpZd5PYgBALnYofKEt82De0GbW1DLibvFDK+bSeOm8qKdqUFZ7uiK8XMPHyqm3pTxUvcunUfxXEo9' +
275
+ 'RNe5b/8vfCD3kzDN7vTtHyaIcntVDAYBAUBAAAAQBI2vguYNsHWm5AR3mZtZib8WAHFvz2Kf9//iYvlR' +
276
+ 'B/+n///////////+UH7XoIDMoJAEAMtj8JshJPRwklVqNSpYnalfE+VzNCAISCoxVHEpIo/WrTiMvP7V' +
277
+ 'TujOPnOglLbMLN/pq/d2Y4lRJIkSnPlUSJEjSKJqM41d88zWtMzP+fCOORmc9NeM+f1nnO//efM52/fG' +
278
+ '/ef385+5u+u1bRJkwU8FAkEItZpkRYeQYcAgZTEYlaZa2yROLeC0qdX73rZJJ/d2f6v6Or0u/+5FBYcn' +
279
+ 'g0MlCiQTR9GUU5LScmSuSlH00IWqXA6jlw4BEcD/+5REEAAi3RtU+eYbGF1E+lk9g0YJzLUgh7BlQVGT' +
280
+ 'ZJD0jKhhTNVilqrMzFRK+x/szcMKBWKep4NP1A0DR6RESkTp5Z1Q9Y8REgqMg1DpUBPleeqlRQcerBpM' +
281
+ 'jiURHVD4XwAALhAgbxxlxYD5OFkG8oQRPB2EpsxSCNVlgcYUqoAyiVJmaARlkwplICfPoUy/zWEzM2pc' +
282
+ 'NYzAQNJDSniEYecSEqxFEzQqEvUFGnvzwUfcRlpZ9T2LCR5QdDQDDhKICAjpJCagpRo9UQRPClZZlg6E' +
283
+ 'p9DMTkTl+okuhRIVIzAQEf9L+Mx/DUjqmqN6kX7M36lS4zgLyJV3iV6j3xF8kJduJawVw1nndAlBaLLg' +
284
+ 'JupwsTcLkxmJgFLgSzoCmHjSNGSqkGPCpnNqTXIwolf6qlVWN+q/su37HzgrES1pWGg3KnWh0FXCVniJ' +
285
+ '9K5b4iCrpLEuIcFTqwkVLFiqgaDqCCSMVWqxBAVCFOLVrVahm2ahUThUKJnmFCw15hD0Qhb/+5REEAhC' +
286
+ 'YSRCSQEb4FOGaBUMI6JIRYC0QIB2SQsgGpgwDghgIlS6FU8VBXDoiBp5Y9gtkVnhEhYBdJFQ7kQ3w1yp' +
287
+ '0NB2CoNPEttZ1/aeDUAAA26FEghWgEKNVAVWkFAQEmMK2Uwk/qI0hqUb/4epVIZH1ai6szf6kzH1f2ar' +
288
+ 'xYGS9FcOsN5UlJLQt///+oo0FRDTUQ0FBQr9f5LxXP+mEUfk0AIrf/5GRmQ0//mX//ZbLP5b5GrWSz+W' +
289
+ 'SkZMrWyyyy2GRqyggVRyMv////////st//sn/yyVDI1l8mVgoYGDCOqiqIQBxmvxWCggTpZZZD//aWfy' +
290
+ 'yWf/y/7KGDA0ssBggTof9k/+WS/8slQyMp/5Nfln8WAqGcUbULCrKxT9ISF+kKsxQWpMQU1FMy4xMDCq' +
291
+ 'qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq' +
292
+ 'qqqqqqqqqqqqqqqqqqqqqqqqqqo=';
293
+
294
+ const audio = new Audio(hitmarkerDataUrl);
295
+ audio.volume = 0.3;
296
+ audio.play().catch(() => {
297
+ // Fallback to synthetic sound if data URL fails
298
+ createSyntheticHitmarkerSound();
299
+ });
300
+ } catch (error) {
301
+ // Fallback to synthetic sound
302
+ createSyntheticHitmarkerSound();
303
+ }
304
+ };
305
+
306
+ const createSyntheticHitmarkerSound = () => {
307
+ try {
308
+ const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)();
309
+
310
+ // Create a more realistic hitmarker sound with noise and metallic ring
311
+ const oscillator1 = audioContext.createOscillator();
312
+ const oscillator2 = audioContext.createOscillator();
313
+ const noiseBuffer = audioContext.createBuffer(1, audioContext.sampleRate * 0.1, audioContext.sampleRate);
314
+ const noiseSource = audioContext.createBufferSource();
315
+
316
+ // Generate white noise for the initial "crack"
317
+ const noiseData = noiseBuffer.getChannelData(0);
318
+ for (let i = 0; i < noiseData.length; i++) {
319
+ noiseData[i] = Math.random() * 2 - 1;
320
+ }
321
+ noiseSource.buffer = noiseBuffer;
322
+
323
+ const gainNode1 = audioContext.createGain();
324
+ const gainNode2 = audioContext.createGain();
325
+ const noiseGain = audioContext.createGain();
326
+ const masterGain = audioContext.createGain();
327
+
328
+ // Connect everything
329
+ oscillator1.connect(gainNode1);
330
+ oscillator2.connect(gainNode2);
331
+ noiseSource.connect(noiseGain);
332
+
333
+ gainNode1.connect(masterGain);
334
+ gainNode2.connect(masterGain);
335
+ noiseGain.connect(masterGain);
336
+ masterGain.connect(audioContext.destination);
337
+
338
+ // Sharp metallic frequencies
339
+ oscillator1.frequency.setValueAtTime(1800, audioContext.currentTime);
340
+ oscillator1.frequency.exponentialRampToValueAtTime(900, audioContext.currentTime + 0.08);
341
+
342
+ oscillator2.frequency.setValueAtTime(3600, audioContext.currentTime);
343
+ oscillator2.frequency.exponentialRampToValueAtTime(1800, audioContext.currentTime + 0.04);
344
+
345
+ oscillator1.type = 'triangle';
346
+ oscillator2.type = 'sine';
347
+
348
+ // Sharp attack, quick decay
349
+ gainNode1.gain.setValueAtTime(0, audioContext.currentTime);
350
+ gainNode1.gain.linearRampToValueAtTime(0.4, audioContext.currentTime + 0.002);
351
+ gainNode1.gain.exponentialRampToValueAtTime(0.001, audioContext.currentTime + 0.12);
352
+
353
+ gainNode2.gain.setValueAtTime(0, audioContext.currentTime);
354
+ gainNode2.gain.linearRampToValueAtTime(0.3, audioContext.currentTime + 0.001);
355
+ gainNode2.gain.exponentialRampToValueAtTime(0.001, audioContext.currentTime + 0.06);
356
+
357
+ // Noise burst for the initial crack
358
+ noiseGain.gain.setValueAtTime(0, audioContext.currentTime);
359
+ noiseGain.gain.linearRampToValueAtTime(0.2, audioContext.currentTime + 0.001);
360
+ noiseGain.gain.exponentialRampToValueAtTime(0.001, audioContext.currentTime + 0.01);
361
+
362
+ masterGain.gain.setValueAtTime(0.5, audioContext.currentTime);
363
+
364
+ const startTime = audioContext.currentTime;
365
+ const stopTime = startTime + 0.15;
366
+
367
+ oscillator1.start(startTime);
368
+ oscillator2.start(startTime);
369
+ noiseSource.start(startTime);
370
+
371
+ oscillator1.stop(stopTime);
372
+ oscillator2.stop(stopTime);
373
+ noiseSource.stop(startTime + 0.02);
374
+
375
+ } catch (error) {
376
+ console.log('Audio context not available');
377
+ }
378
+ };
379
+
380
+ const createHitmarker = (e: { clientX: number; clientY: number }) => {
381
+ if (!containerRef.current) return;
382
+
383
+ const rect = containerRef.current.getBoundingClientRect();
384
+ const x = e.clientX - rect.left;
385
+ const y = e.clientY - rect.top;
386
+
387
+ const newHitmarker: Hitmarker = {
388
+ id: Date.now() + Math.random(),
389
+ x,
390
+ y,
391
+ };
392
+
393
+ setHitmarkers(prev => [...prev, newHitmarker]);
394
+
395
+ // Play sound
396
+ playHitmarkerSound();
397
+
398
+ // Remove hitmarker after animation
399
+ setTimeout(() => {
400
+ setHitmarkers(prev => prev.filter(h => h.id !== newHitmarker.id));
401
+ }, 600);
402
+ };
403
+
404
+ // Inject CSS animations
405
+ useEffect(() => {
406
+ const styleId = 'loading-screen-animations';
407
+ if (!document.getElementById(styleId)) {
408
+ const style = document.createElement('style');
409
+ style.id = styleId;
410
+ style.textContent = `
411
+ @keyframes fadeInOut {
412
+ 0%, 100% {
413
+ opacity: 0.6;
414
+ }
415
+ 50% {
416
+ opacity: 0.9;
417
+ }
418
+ }
419
+
420
+ @keyframes logoPulse {
421
+ 0%, 100% {
422
+ opacity: 0.15;
423
+ transform: scale(1);
424
+ }
425
+ 50% {
426
+ opacity: 0.25;
427
+ transform: scale(1.05);
428
+ }
429
+ }
430
+
431
+ @keyframes shimmer {
432
+ 0% {
433
+ background-position: -200px 0;
434
+ }
435
+ 100% {
436
+ background-position: calc(200px + 100%) 0;
437
+ }
438
+ }
439
+
440
+ @keyframes floatUp {
441
+ 0% {
442
+ transform: translateY(100vh) rotate(0deg);
443
+ opacity: 0;
444
+ }
445
+ 10% {
446
+ opacity: 0.6;
447
+ }
448
+ 90% {
449
+ opacity: 0.6;
450
+ }
451
+ 100% {
452
+ transform: translateY(-100px) rotate(360deg);
453
+ opacity: 0;
454
+ }
455
+ }
456
+
457
+ @keyframes gradientShift {
458
+ 0%, 100% {
459
+ background-position: 0% 50%;
460
+ }
461
+ 50% {
462
+ background-position: 100% 50%;
463
+ }
464
+ }
465
+
466
+ @keyframes hitmarkerFade {
467
+ 0% {
468
+ opacity: 1;
469
+ transform: scale(0.5);
470
+ }
471
+ 20% {
472
+ opacity: 1;
473
+ transform: scale(1.2);
474
+ }
475
+ 100% {
476
+ opacity: 0;
477
+ transform: scale(1);
478
+ }
479
+ }
480
+
481
+ @keyframes hitmarkerFade45 {
482
+ 0% {
483
+ opacity: 1;
484
+ transform: translate(-50%, -50%) rotate(45deg) scale(0.5);
485
+ }
486
+ 20% {
487
+ opacity: 1;
488
+ transform: translate(-50%, -50%) rotate(45deg) scale(1.2);
489
+ }
490
+ 100% {
491
+ opacity: 0;
492
+ transform: translate(-50%, -50%) rotate(45deg) scale(1);
493
+ }
494
+ }
495
+
496
+ @keyframes hitmarkerFadeNeg45 {
497
+ 0% {
498
+ opacity: 1;
499
+ transform: translate(-50%, -50%) rotate(-45deg) scale(0.5);
500
+ }
501
+ 20% {
502
+ opacity: 1;
503
+ transform: translate(-50%, -50%) rotate(-45deg) scale(1.2);
504
+ }
505
+ 100% {
506
+ opacity: 0;
507
+ transform: translate(-50%, -50%) rotate(-45deg) scale(1);
508
+ }
509
+ }
510
+ `;
511
+ document.head.appendChild(style);
512
+ }
513
+ }, []);
514
+
515
+ return (
516
+ <div
517
+ ref={containerRef}
518
+ className="fw-player-root"
519
+ style={{
520
+ position: "relative",
521
+ width: "100%",
522
+ height: "100%",
523
+ minHeight: "300px",
524
+ background: `
525
+ linear-gradient(135deg,
526
+ hsl(var(--tn-bg-dark, 235 21% 11%)) 0%,
527
+ hsl(var(--tn-bg, 233 23% 17%)) 25%,
528
+ hsl(var(--tn-bg-dark, 235 21% 11%)) 50%,
529
+ hsl(var(--tn-bg, 233 23% 17%)) 75%,
530
+ hsl(var(--tn-bg-dark, 235 21% 11%)) 100%
531
+ )
532
+ `,
533
+ backgroundSize: "400% 400%",
534
+ animation: "gradientShift 16s ease-in-out infinite",
535
+ display: "flex",
536
+ flexDirection: "column",
537
+ alignItems: "center",
538
+ justifyContent: "center",
539
+ overflow: "hidden",
540
+ borderRadius: "0", /* Slab - no rounded corners */
541
+ userSelect: "none",
542
+ WebkitUserSelect: "none",
543
+ MozUserSelect: "none",
544
+ msUserSelect: "none",
545
+ } as React.CSSProperties}
546
+ >
547
+ {/* Hitmarkers */}
548
+ {hitmarkers.map(hitmarker => (
549
+ <div
550
+ key={hitmarker.id}
551
+ style={{
552
+ position: "absolute",
553
+ left: `${hitmarker.x}px`,
554
+ top: `${hitmarker.y}px`,
555
+ transform: "translate(-50%, -50%)",
556
+ pointerEvents: "none",
557
+ zIndex: 100,
558
+ width: "40px",
559
+ height: "40px",
560
+ }}
561
+ >
562
+ {/* Top-left diagonal line */}
563
+ <div
564
+ style={{
565
+ position: "absolute",
566
+ top: "25%",
567
+ left: "25%",
568
+ width: "12px",
569
+ height: "3px",
570
+ backgroundColor: "#ffffff",
571
+ transform: "translate(-50%, -50%) rotate(45deg)",
572
+ animation: "hitmarkerFade45 0.6s ease-out forwards",
573
+ boxShadow: "0 0 8px rgba(255, 255, 255, 0.8)",
574
+ borderRadius: "1px",
575
+ }}
576
+ />
577
+ {/* Top-right diagonal line */}
578
+ <div
579
+ style={{
580
+ position: "absolute",
581
+ top: "25%",
582
+ left: "75%",
583
+ width: "12px",
584
+ height: "3px",
585
+ backgroundColor: "#ffffff",
586
+ transform: "translate(-50%, -50%) rotate(-45deg)",
587
+ animation: "hitmarkerFadeNeg45 0.6s ease-out forwards",
588
+ boxShadow: "0 0 8px rgba(255, 255, 255, 0.8)",
589
+ borderRadius: "1px",
590
+ }}
591
+ />
592
+ {/* Bottom-left diagonal line */}
593
+ <div
594
+ style={{
595
+ position: "absolute",
596
+ top: "75%",
597
+ left: "25%",
598
+ width: "12px",
599
+ height: "3px",
600
+ backgroundColor: "#ffffff",
601
+ transform: "translate(-50%, -50%) rotate(-45deg)",
602
+ animation: "hitmarkerFadeNeg45 0.6s ease-out forwards",
603
+ boxShadow: "0 0 8px rgba(255, 255, 255, 0.8)",
604
+ borderRadius: "1px",
605
+ }}
606
+ />
607
+ {/* Bottom-right diagonal line */}
608
+ <div
609
+ style={{
610
+ position: "absolute",
611
+ top: "75%",
612
+ left: "75%",
613
+ width: "12px",
614
+ height: "3px",
615
+ backgroundColor: "#ffffff",
616
+ transform: "translate(-50%, -50%) rotate(45deg)",
617
+ animation: "hitmarkerFade45 0.6s ease-out forwards",
618
+ boxShadow: "0 0 8px rgba(255, 255, 255, 0.8)",
619
+ borderRadius: "1px",
620
+ }}
621
+ />
622
+ </div>
623
+ ))}
624
+
625
+ {/* Floating particles */}
626
+ {[...Array(12)].map((_, index) => (
627
+ <div
628
+ key={`particle-${index}`}
629
+ style={{
630
+ position: "absolute",
631
+ left: `${Math.random() * 100}%`,
632
+ width: `${Math.random() * 4 + 2}px`,
633
+ height: `${Math.random() * 4 + 2}px`,
634
+ borderRadius: "50%",
635
+ background: [
636
+ "#7aa2f7", // Terminal Blue
637
+ "#bb9af7", // Terminal Magenta
638
+ "#9ece6a", // Strings/CSS classes
639
+ "#73daca", // Terminal Green
640
+ "#7dcfff", // Terminal Cyan
641
+ "#f7768e", // Keywords/Terminal Red
642
+ "#e0af68", // Terminal Yellow
643
+ "#2ac3de", // Language functions
644
+ ][index % 8],
645
+ opacity: 0,
646
+ animation: `floatUp ${8 + Math.random() * 4}s linear infinite`,
647
+ animationDelay: `${Math.random() * 8}s`,
648
+ pointerEvents: "none",
649
+ userSelect: "none",
650
+ }}
651
+ />
652
+ ))}
653
+
654
+ {/* Animated bubbles with Tokyo Night colors */}
655
+ {[...Array(8)].map((_, index) => (
656
+ <AnimatedBubble key={index} index={index} />
657
+ ))}
658
+
659
+ {/* Center logo */}
660
+ <CenterLogo containerRef={containerRef as React.RefObject<HTMLDivElement>} onHitmarker={createHitmarker} />
661
+
662
+ {/* Bouncing DVD Logo */}
663
+ <DvdLogo parentRef={containerRef as React.RefObject<HTMLDivElement>} scale={0.08} />
664
+
665
+ {/* Message */}
666
+ <div
667
+ style={{
668
+ position: "absolute",
669
+ bottom: "20%",
670
+ left: "50%",
671
+ transform: "translateX(-50%)",
672
+ color: "#a9b1d6",
673
+ fontSize: "16px",
674
+ fontWeight: "500",
675
+ textAlign: "center",
676
+ animation: "fadeInOut 2s ease-in-out infinite",
677
+ textShadow: "0 2px 4px rgba(36, 40, 59, 0.5)",
678
+ fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
679
+ userSelect: "none",
680
+ WebkitUserSelect: "none",
681
+ MozUserSelect: "none",
682
+ msUserSelect: "none",
683
+ pointerEvents: "none",
684
+ }}
685
+ >
686
+ {message}
687
+ </div>
688
+
689
+ {/* Subtle overlay texture */}
690
+ <div
691
+ style={{
692
+ position: "absolute",
693
+ top: 0,
694
+ left: 0,
695
+ right: 0,
696
+ bottom: 0,
697
+ background: `
698
+ radial-gradient(circle at 20% 80%, rgba(122, 162, 247, 0.03) 0%, transparent 50%),
699
+ radial-gradient(circle at 80% 20%, rgba(187, 154, 247, 0.03) 0%, transparent 50%),
700
+ radial-gradient(circle at 40% 40%, rgba(158, 206, 106, 0.02) 0%, transparent 50%)
701
+ `,
702
+ pointerEvents: "none",
703
+ userSelect: "none",
704
+ }}
705
+ />
706
+ </div>
707
+ );
708
+ };
709
+
710
+ export default LoadingScreen;