@livepeer-frameworks/player-wc 0.1.1 → 0.1.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 (116) hide show
  1. package/dist/cjs/components/fw-dev-mode-panel.js +845 -212
  2. package/dist/cjs/components/fw-dev-mode-panel.js.map +1 -1
  3. package/dist/cjs/components/fw-dvd-logo.js +211 -0
  4. package/dist/cjs/components/fw-dvd-logo.js.map +1 -0
  5. package/dist/cjs/components/fw-idle-screen.js +641 -97
  6. package/dist/cjs/components/fw-idle-screen.js.map +1 -1
  7. package/dist/cjs/components/fw-loading-screen.js +513 -0
  8. package/dist/cjs/components/fw-loading-screen.js.map +1 -0
  9. package/dist/cjs/components/fw-player-controls.js +347 -173
  10. package/dist/cjs/components/fw-player-controls.js.map +1 -1
  11. package/dist/cjs/components/fw-player.js +460 -60
  12. package/dist/cjs/components/fw-player.js.map +1 -1
  13. package/dist/cjs/components/fw-seek-bar.js +292 -142
  14. package/dist/cjs/components/fw-seek-bar.js.map +1 -1
  15. package/dist/cjs/components/fw-settings-menu.js +191 -81
  16. package/dist/cjs/components/fw-settings-menu.js.map +1 -1
  17. package/dist/cjs/components/fw-stats-panel.js +134 -70
  18. package/dist/cjs/components/fw-stats-panel.js.map +1 -1
  19. package/dist/cjs/components/fw-stream-state-overlay.js +338 -0
  20. package/dist/cjs/components/fw-stream-state-overlay.js.map +1 -0
  21. package/dist/cjs/components/fw-subtitle-renderer.js +174 -27
  22. package/dist/cjs/components/fw-subtitle-renderer.js.map +1 -1
  23. package/dist/cjs/components/fw-thumbnail-overlay.js +161 -0
  24. package/dist/cjs/components/fw-thumbnail-overlay.js.map +1 -0
  25. package/dist/cjs/components/fw-volume-control.js +150 -69
  26. package/dist/cjs/components/fw-volume-control.js.map +1 -1
  27. package/dist/cjs/components/shared/hitmarker-audio.js +76 -0
  28. package/dist/cjs/components/shared/hitmarker-audio.js.map +1 -0
  29. package/dist/cjs/constants/media-assets.js +11 -0
  30. package/dist/cjs/constants/media-assets.js.map +1 -0
  31. package/dist/cjs/controllers/player-controller-host.js +28 -1
  32. package/dist/cjs/controllers/player-controller-host.js.map +1 -1
  33. package/dist/cjs/define.js +8 -0
  34. package/dist/cjs/define.js.map +1 -1
  35. package/dist/cjs/icons/index.js +27 -0
  36. package/dist/cjs/icons/index.js.map +1 -1
  37. package/dist/cjs/index.js +20 -0
  38. package/dist/cjs/index.js.map +1 -1
  39. package/dist/esm/components/fw-dev-mode-panel.js +846 -213
  40. package/dist/esm/components/fw-dev-mode-panel.js.map +1 -1
  41. package/dist/esm/components/fw-dvd-logo.js +211 -0
  42. package/dist/esm/components/fw-dvd-logo.js.map +1 -0
  43. package/dist/esm/components/fw-idle-screen.js +643 -99
  44. package/dist/esm/components/fw-idle-screen.js.map +1 -1
  45. package/dist/esm/components/fw-loading-screen.js +513 -0
  46. package/dist/esm/components/fw-loading-screen.js.map +1 -0
  47. package/dist/esm/components/fw-player-controls.js +348 -174
  48. package/dist/esm/components/fw-player-controls.js.map +1 -1
  49. package/dist/esm/components/fw-player.js +460 -60
  50. package/dist/esm/components/fw-player.js.map +1 -1
  51. package/dist/esm/components/fw-seek-bar.js +293 -143
  52. package/dist/esm/components/fw-seek-bar.js.map +1 -1
  53. package/dist/esm/components/fw-settings-menu.js +192 -82
  54. package/dist/esm/components/fw-settings-menu.js.map +1 -1
  55. package/dist/esm/components/fw-stats-panel.js +135 -71
  56. package/dist/esm/components/fw-stats-panel.js.map +1 -1
  57. package/dist/esm/components/fw-stream-state-overlay.js +338 -0
  58. package/dist/esm/components/fw-stream-state-overlay.js.map +1 -0
  59. package/dist/esm/components/fw-subtitle-renderer.js +175 -28
  60. package/dist/esm/components/fw-subtitle-renderer.js.map +1 -1
  61. package/dist/esm/components/fw-thumbnail-overlay.js +161 -0
  62. package/dist/esm/components/fw-thumbnail-overlay.js.map +1 -0
  63. package/dist/esm/components/fw-volume-control.js +150 -69
  64. package/dist/esm/components/fw-volume-control.js.map +1 -1
  65. package/dist/esm/components/shared/hitmarker-audio.js +74 -0
  66. package/dist/esm/components/shared/hitmarker-audio.js.map +1 -0
  67. package/dist/esm/constants/media-assets.js +8 -0
  68. package/dist/esm/constants/media-assets.js.map +1 -0
  69. package/dist/esm/controllers/player-controller-host.js +28 -1
  70. package/dist/esm/controllers/player-controller-host.js.map +1 -1
  71. package/dist/esm/define.js +8 -0
  72. package/dist/esm/define.js.map +1 -1
  73. package/dist/esm/icons/index.js +26 -2
  74. package/dist/esm/icons/index.js.map +1 -1
  75. package/dist/esm/index.js +4 -0
  76. package/dist/esm/index.js.map +1 -1
  77. package/dist/fw-player.iife.js +2072 -880
  78. package/dist/types/components/fw-dev-mode-panel.d.ts +36 -9
  79. package/dist/types/components/fw-dvd-logo.d.ts +29 -0
  80. package/dist/types/components/fw-idle-screen.d.ts +36 -0
  81. package/dist/types/components/fw-loading-screen.d.ts +36 -0
  82. package/dist/types/components/fw-player-controls.d.ts +21 -6
  83. package/dist/types/components/fw-player.d.ts +28 -1
  84. package/dist/types/components/fw-seek-bar.d.ts +31 -14
  85. package/dist/types/components/fw-settings-menu.d.ts +15 -1
  86. package/dist/types/components/fw-stats-panel.d.ts +4 -4
  87. package/dist/types/components/fw-stream-state-overlay.d.ts +20 -0
  88. package/dist/types/components/fw-subtitle-renderer.d.ts +33 -2
  89. package/dist/types/components/fw-thumbnail-overlay.d.ts +17 -0
  90. package/dist/types/components/fw-volume-control.d.ts +11 -4
  91. package/dist/types/components/shared/hitmarker-audio.d.ts +1 -0
  92. package/dist/types/constants/media-assets.d.ts +5 -0
  93. package/dist/types/controllers/player-controller-host.d.ts +14 -1
  94. package/dist/types/iife-entry.d.ts +4 -0
  95. package/dist/types/index.d.ts +4 -0
  96. package/package.json +3 -3
  97. package/src/components/fw-dev-mode-panel.ts +929 -228
  98. package/src/components/fw-dvd-logo.ts +233 -0
  99. package/src/components/fw-idle-screen.ts +680 -100
  100. package/src/components/fw-loading-screen.ts +540 -0
  101. package/src/components/fw-player-controls.ts +435 -176
  102. package/src/components/fw-player.ts +505 -57
  103. package/src/components/fw-seek-bar.ts +336 -143
  104. package/src/components/fw-settings-menu.ts +208 -85
  105. package/src/components/fw-stats-panel.ts +150 -77
  106. package/src/components/fw-stream-state-overlay.ts +331 -0
  107. package/src/components/fw-subtitle-renderer.ts +216 -28
  108. package/src/components/fw-thumbnail-overlay.ts +148 -0
  109. package/src/components/fw-volume-control.ts +166 -66
  110. package/src/components/shared/hitmarker-audio.ts +92 -0
  111. package/src/constants/media-assets.ts +7 -0
  112. package/src/controllers/player-controller-host.ts +29 -2
  113. package/src/define.ts +8 -0
  114. package/src/iife-entry.ts +4 -0
  115. package/src/index.ts +4 -0
  116. package/dist/fw-player.iife.js.map +0 -1
@@ -1,14 +1,75 @@
1
1
  import { LitElement, html, css, nothing } from "lit";
2
- import { customElement, property } from "lit/decorators.js";
3
- import { classMap } from "lit/directives/class-map.js";
2
+ import { customElement, property, query, state } from "lit/decorators.js";
4
3
  import { sharedStyles } from "../styles/shared-styles.js";
5
4
  import { utilityStyles } from "../styles/utility-styles.js";
5
+ import { LOGOMARK_DATA_URL } from "../constants/media-assets.js";
6
+ import { playHitmarkerSound } from "./shared/hitmarker-audio.js";
7
+ import "./fw-dvd-logo.js";
8
+
9
+ interface ParticleState {
10
+ left: number;
11
+ size: number;
12
+ color: string;
13
+ duration: number;
14
+ delay: number;
15
+ }
16
+
17
+ interface BubbleState {
18
+ top: number;
19
+ left: number;
20
+ size: number;
21
+ opacity: number;
22
+ color: string;
23
+ }
24
+
25
+ interface Hitmarker {
26
+ id: number;
27
+ x: number;
28
+ y: number;
29
+ }
30
+
31
+ const BUBBLE_COLORS = [
32
+ "rgba(122, 162, 247, 0.2)",
33
+ "rgba(187, 154, 247, 0.2)",
34
+ "rgba(158, 206, 106, 0.2)",
35
+ "rgba(115, 218, 202, 0.2)",
36
+ "rgba(125, 207, 255, 0.2)",
37
+ "rgba(247, 118, 142, 0.2)",
38
+ "rgba(224, 175, 104, 0.2)",
39
+ "rgba(42, 195, 222, 0.2)",
40
+ ];
41
+
42
+ const PARTICLE_COLORS = [
43
+ "#7aa2f7",
44
+ "#bb9af7",
45
+ "#9ece6a",
46
+ "#73daca",
47
+ "#7dcfff",
48
+ "#f7768e",
49
+ "#e0af68",
50
+ "#2ac3de",
51
+ ];
6
52
 
7
53
  @customElement("fw-idle-screen")
8
54
  export class FwIdleScreen extends LitElement {
9
55
  @property({ type: String }) status?: string;
10
56
  @property({ type: String }) message?: string;
11
57
  @property({ type: Number }) percentage?: number;
58
+ @property({ type: String }) error?: string;
59
+ @property({ type: String, attribute: "logo-src" }) logoSrc?: string;
60
+ @property({ type: Boolean, attribute: "retry-enabled" }) retryEnabled = false;
61
+ @property({ attribute: false }) onRetry?: () => void;
62
+ @query(".idle-container") private _containerEl?: HTMLDivElement;
63
+
64
+ @state() private _logoSize = 100;
65
+ @state() private _logoOffset = { x: 0, y: 0 };
66
+ @state() private _isLogoHovered = false;
67
+ @state() private _bubbles: BubbleState[] = this._createBubbles();
68
+ @state() private _hitmarkers: Hitmarker[] = [];
69
+
70
+ private readonly _particles: ParticleState[] = this._createParticles();
71
+ private _bubbleTimers = new Set<ReturnType<typeof setTimeout>>();
72
+ private _resizeObserver?: ResizeObserver;
12
73
 
13
74
  static styles = [
14
75
  sharedStyles,
@@ -17,159 +78,678 @@ export class FwIdleScreen extends LitElement {
17
78
  :host {
18
79
  display: contents;
19
80
  }
20
- .idle {
81
+ .idle-container {
21
82
  position: absolute;
22
83
  inset: 0;
84
+ z-index: 5;
85
+ background: linear-gradient(
86
+ 135deg,
87
+ hsl(var(--tn-bg-dark, 235 21% 11%)) 0%,
88
+ hsl(var(--tn-bg, 233 23% 17%)) 25%,
89
+ hsl(var(--tn-bg-dark, 235 21% 11%)) 50%,
90
+ hsl(var(--tn-bg, 233 23% 17%)) 75%,
91
+ hsl(var(--tn-bg-dark, 235 21% 11%)) 100%
92
+ );
93
+ background-size: 400% 400%;
94
+ animation: _fw-gradient-shift 16s ease-in-out infinite;
23
95
  display: flex;
96
+ flex-direction: column;
24
97
  align-items: center;
25
98
  justify-content: center;
26
- z-index: 15;
27
- background: linear-gradient(135deg, rgb(15 23 42), rgb(2 6 23), rgb(15 23 42));
28
99
  overflow: hidden;
100
+ user-select: none;
101
+ -webkit-user-select: none;
29
102
  }
30
- .card {
31
- position: relative;
32
- z-index: 10;
33
- max-width: 280px;
34
- width: 100%;
35
- text-align: center;
103
+
104
+ .particles,
105
+ .bubbles {
106
+ position: absolute;
107
+ inset: 0;
108
+ pointer-events: none;
36
109
  }
37
- .status-icon {
38
- margin: 0 auto 0.75rem;
39
- width: 2.5rem;
40
- height: 2.5rem;
110
+
111
+ .particle {
112
+ position: absolute;
113
+ border-radius: 50%;
114
+ opacity: 0;
115
+ animation: _fw-float-up linear infinite;
116
+ }
117
+
118
+ .bubble {
119
+ position: absolute;
120
+ border-radius: 50%;
121
+ transition: opacity 1s ease-in-out;
122
+ }
123
+
124
+ .center-logo {
125
+ position: absolute;
126
+ top: 50%;
127
+ left: 50%;
41
128
  display: flex;
42
129
  align-items: center;
43
130
  justify-content: center;
131
+ z-index: 10;
132
+ transition: transform 0.3s ease-out;
44
133
  }
45
- .spinner {
46
- width: 1.5rem;
47
- height: 1.5rem;
48
- border: 2px solid rgb(255 255 255 / 0.2);
49
- border-top-color: hsl(var(--tn-blue, 217 89% 61%));
134
+
135
+ .logo-pulse {
136
+ position: absolute;
50
137
  border-radius: 50%;
51
- animation: _fw-spin 1s linear infinite;
138
+ background: rgba(122, 162, 247, 0.15);
139
+ animation: _fw-logo-pulse 3s ease-in-out infinite;
140
+ pointer-events: none;
141
+ transition: transform 0.3s ease-out;
52
142
  }
53
- @keyframes _fw-spin {
54
- to {
55
- transform: rotate(360deg);
56
- }
143
+
144
+ .logo-pulse.hovered {
145
+ animation: _fw-logo-pulse 1s ease-in-out infinite;
146
+ transform: scale(1.2);
57
147
  }
58
- .offline-dot {
59
- width: 0.75rem;
60
- height: 0.75rem;
61
- border-radius: 50%;
62
- background: hsl(var(--tn-red, 348 74% 64%));
63
- animation: _fw-blink 2s ease-in-out infinite;
148
+
149
+ .logo-button {
150
+ all: unset;
151
+ cursor: pointer;
152
+ display: block;
64
153
  }
65
- @keyframes _fw-blink {
66
- 0%,
67
- 100% {
68
- opacity: 1;
69
- }
70
- 50% {
71
- opacity: 0.3;
72
- }
73
- }
74
- .label {
75
- font-size: 0.6875rem;
76
- font-weight: 600;
77
- text-transform: uppercase;
78
- letter-spacing: 0.05em;
79
- color: rgb(255 255 255 / 0.5);
80
- margin-bottom: 0.5rem;
81
- }
82
- .msg {
83
- font-size: 0.8125rem;
84
- color: rgb(255 255 255 / 0.7);
85
- margin-bottom: 0.75rem;
86
- }
87
- .progress-wrap {
88
- width: 100%;
89
- height: 0.25rem;
90
- background: rgb(255 255 255 / 0.1);
154
+
155
+ .logo-image {
156
+ position: relative;
157
+ z-index: 1;
158
+ display: block;
159
+ filter: drop-shadow(0 4px 8px rgb(36 40 59 / 0.3));
160
+ transition: all 0.3s ease-out;
161
+ cursor: default;
162
+ user-select: none;
163
+ -webkit-user-drag: none;
164
+ }
165
+
166
+ .logo-image.hovered {
167
+ transform: scale(1.1);
168
+ filter: drop-shadow(0 6px 12px rgb(36 40 59 / 0.4)) brightness(1.1);
169
+ }
170
+
171
+ .status-overlay {
172
+ position: absolute;
173
+ bottom: 16px;
174
+ left: 50%;
175
+ transform: translateX(-50%);
176
+ z-index: 20;
177
+ display: flex;
178
+ flex-direction: column;
179
+ align-items: center;
180
+ gap: 8px;
181
+ max-width: 280px;
182
+ text-align: center;
183
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
184
+ }
185
+
186
+ .status-indicator {
187
+ display: flex;
188
+ align-items: center;
189
+ gap: 8px;
190
+ color: #787c99;
191
+ font-size: 13px;
192
+ }
193
+
194
+ .status-icon {
195
+ width: 20px;
196
+ height: 20px;
197
+ flex: 0 0 auto;
198
+ }
199
+
200
+ .status-icon.spinning {
201
+ animation: _fw-spin 1s linear infinite;
202
+ }
203
+
204
+ .progress-bar {
205
+ width: 160px;
206
+ height: 4px;
207
+ background: rgb(65 72 104 / 0.4);
91
208
  border-radius: 2px;
92
209
  overflow: hidden;
93
- margin-bottom: 0.5rem;
94
210
  }
95
- .progress-bar {
211
+
212
+ .progress-fill {
96
213
  height: 100%;
97
- background: hsl(var(--tn-blue, 217 89% 61%));
98
- border-radius: 2px;
99
- transition: width 300ms ease;
214
+ background: hsl(var(--tn-cyan, 193 100% 75%));
215
+ transition: width 0.3s ease-out;
100
216
  }
101
- .pct {
102
- font-size: 0.6875rem;
103
- color: rgb(255 255 255 / 0.4);
104
- font-variant-numeric: tabular-nums;
217
+
218
+ .retry-btn {
219
+ padding: 6px 16px;
220
+ background: transparent;
221
+ border: 1px solid rgb(122 162 247 / 0.4);
222
+ border-radius: 4px;
223
+ color: #7aa2f7;
224
+ font-size: 11px;
225
+ font-weight: 500;
226
+ cursor: pointer;
227
+ transition: all 0.2s ease;
228
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
229
+ }
230
+
231
+ .retry-btn:hover {
232
+ background: rgb(122 162 247 / 0.1);
105
233
  }
106
- .particles {
234
+
235
+ .overlay-texture {
107
236
  position: absolute;
108
237
  inset: 0;
109
- overflow: hidden;
238
+ background:
239
+ radial-gradient(circle at 20% 80%, rgb(122 162 247 / 0.03) 0%, transparent 50%),
240
+ radial-gradient(circle at 80% 20%, rgb(187 154 247 / 0.03) 0%, transparent 50%),
241
+ radial-gradient(circle at 40% 40%, rgb(158 206 106 / 0.02) 0%, transparent 50%);
110
242
  pointer-events: none;
111
243
  }
112
- .particle {
244
+
245
+ .hitmarker {
113
246
  position: absolute;
114
- width: 4px;
115
- height: 4px;
116
- border-radius: 50%;
117
- opacity: 0.3;
118
- animation: _fw-float var(--dur, 8s) ease-in-out infinite;
247
+ transform: translate(-50%, -50%);
248
+ pointer-events: none;
249
+ z-index: 100;
250
+ width: 40px;
251
+ height: 40px;
252
+ }
253
+
254
+ .hitmarker-line {
255
+ position: absolute;
256
+ width: 12px;
257
+ height: 3px;
258
+ background-color: #fff;
259
+ box-shadow: 0 0 8px rgb(255 255 255 / 0.8);
260
+ border-radius: 1px;
261
+ }
262
+
263
+ .hitmarker-line.tl {
264
+ top: 25%;
265
+ left: 25%;
266
+ animation: _fw-hitmarker-fade-45 0.6s ease-out forwards;
267
+ }
268
+
269
+ .hitmarker-line.tr {
270
+ top: 25%;
271
+ left: 75%;
272
+ animation: _fw-hitmarker-fade-neg-45 0.6s ease-out forwards;
273
+ }
274
+
275
+ .hitmarker-line.bl {
276
+ top: 75%;
277
+ left: 25%;
278
+ animation: _fw-hitmarker-fade-neg-45 0.6s ease-out forwards;
279
+ }
280
+
281
+ .hitmarker-line.br {
282
+ top: 75%;
283
+ left: 75%;
284
+ animation: _fw-hitmarker-fade-45 0.6s ease-out forwards;
285
+ }
286
+
287
+ @keyframes _fw-spin {
288
+ from {
289
+ transform: rotate(0deg);
290
+ }
291
+ to {
292
+ transform: rotate(360deg);
293
+ }
119
294
  }
120
- @keyframes _fw-float {
295
+
296
+ @keyframes _fw-logo-pulse {
121
297
  0%,
122
298
  100% {
123
- transform: translateY(0) translateX(0);
299
+ opacity: 0.15;
300
+ transform: scale(1);
301
+ }
302
+ 50% {
303
+ opacity: 0.25;
304
+ transform: scale(1.05);
124
305
  }
125
- 25% {
126
- transform: translateY(-20px) translateX(10px);
306
+ }
307
+
308
+ @keyframes _fw-float-up {
309
+ 0% {
310
+ transform: translateY(100vh) rotate(0deg);
311
+ opacity: 0;
312
+ }
313
+ 10% {
314
+ opacity: 0.6;
315
+ }
316
+ 90% {
317
+ opacity: 0.6;
318
+ }
319
+ 100% {
320
+ transform: translateY(-100px) rotate(360deg);
321
+ opacity: 0;
322
+ }
323
+ }
324
+
325
+ @keyframes _fw-gradient-shift {
326
+ 0%,
327
+ 100% {
328
+ background-position: 0% 50%;
127
329
  }
128
330
  50% {
129
- transform: translateY(-10px) translateX(-5px);
331
+ background-position: 100% 50%;
332
+ }
333
+ }
334
+
335
+ @keyframes _fw-hitmarker-fade-45 {
336
+ 0% {
337
+ opacity: 1;
338
+ transform: translate(-50%, -50%) rotate(45deg) scale(0.5);
339
+ }
340
+ 20% {
341
+ opacity: 1;
342
+ transform: translate(-50%, -50%) rotate(45deg) scale(1.2);
343
+ }
344
+ 100% {
345
+ opacity: 0;
346
+ transform: translate(-50%, -50%) rotate(45deg) scale(1);
130
347
  }
131
- 75% {
132
- transform: translateY(-30px) translateX(15px);
348
+ }
349
+
350
+ @keyframes _fw-hitmarker-fade-neg-45 {
351
+ 0% {
352
+ opacity: 1;
353
+ transform: translate(-50%, -50%) rotate(-45deg) scale(0.5);
354
+ }
355
+ 20% {
356
+ opacity: 1;
357
+ transform: translate(-50%, -50%) rotate(-45deg) scale(1.2);
358
+ }
359
+ 100% {
360
+ opacity: 0;
361
+ transform: translate(-50%, -50%) rotate(-45deg) scale(1);
133
362
  }
134
363
  }
135
364
  `,
136
365
  ];
137
366
 
367
+ connectedCallback() {
368
+ super.connectedCallback();
369
+ this._clearBubbleTimers();
370
+ this._startBubbleAnimations();
371
+ }
372
+
373
+ disconnectedCallback() {
374
+ super.disconnectedCallback();
375
+ this._clearBubbleTimers();
376
+ this._resizeObserver?.disconnect();
377
+ this._resizeObserver = undefined;
378
+ }
379
+
380
+ protected firstUpdated() {
381
+ this._updateLogoSize();
382
+ if (typeof ResizeObserver !== "undefined") {
383
+ this._resizeObserver = new ResizeObserver(() => {
384
+ this._updateLogoSize();
385
+ });
386
+ if (this._containerEl) {
387
+ this._resizeObserver.observe(this._containerEl);
388
+ }
389
+ }
390
+ }
391
+
392
+ private _createParticles(): ParticleState[] {
393
+ return Array.from({ length: 12 }, (_, i) => ({
394
+ left: Math.random() * 100,
395
+ size: Math.random() * 4 + 2,
396
+ color: PARTICLE_COLORS[i % PARTICLE_COLORS.length],
397
+ duration: 8 + Math.random() * 4,
398
+ delay: Math.random() * 8,
399
+ }));
400
+ }
401
+
402
+ private _createBubbles(): BubbleState[] {
403
+ return Array.from({ length: 8 }, (_, i) => ({
404
+ top: Math.random() * 80 + 10,
405
+ left: Math.random() * 80 + 10,
406
+ size: Math.random() * 60 + 30,
407
+ opacity: 0,
408
+ color: BUBBLE_COLORS[i % BUBBLE_COLORS.length],
409
+ }));
410
+ }
411
+
412
+ private _setManagedTimer(callback: () => void, delayMs: number) {
413
+ const timer = setTimeout(() => {
414
+ this._bubbleTimers.delete(timer);
415
+ callback();
416
+ }, delayMs);
417
+ this._bubbleTimers.add(timer);
418
+ }
419
+
420
+ private _clearBubbleTimers() {
421
+ this._bubbleTimers.forEach((timer) => clearTimeout(timer));
422
+ this._bubbleTimers.clear();
423
+ }
424
+
425
+ private _updateBubble(index: number, nextState: Partial<BubbleState>) {
426
+ if (index < 0 || index >= this._bubbles.length) {
427
+ return;
428
+ }
429
+ const next = [...this._bubbles];
430
+ next[index] = { ...next[index], ...nextState };
431
+ this._bubbles = next;
432
+ }
433
+
434
+ private _animateBubble(index: number) {
435
+ this._updateBubble(index, { opacity: 0.15 });
436
+
437
+ const visibleDuration = 4000 + Math.random() * 3000;
438
+ this._setManagedTimer(() => {
439
+ this._updateBubble(index, { opacity: 0 });
440
+ this._setManagedTimer(() => {
441
+ this._updateBubble(index, {
442
+ top: Math.random() * 80 + 10,
443
+ left: Math.random() * 80 + 10,
444
+ size: Math.random() * 60 + 30,
445
+ });
446
+ this._setManagedTimer(() => this._animateBubble(index), 200);
447
+ }, 1500);
448
+ }, visibleDuration);
449
+ }
450
+
451
+ private _startBubbleAnimations() {
452
+ this._bubbles.forEach((_, index) => {
453
+ this._setManagedTimer(() => this._animateBubble(index), index * 500);
454
+ });
455
+ }
456
+
457
+ private _updateLogoSize() {
458
+ const rect = this._containerEl?.getBoundingClientRect() ?? this.getBoundingClientRect();
459
+ const minDimension = Math.min(rect.width, rect.height);
460
+ if (!Number.isFinite(minDimension) || minDimension <= 0) {
461
+ return;
462
+ }
463
+ this._logoSize = minDimension * 0.2;
464
+ }
465
+
466
+ private _handleMouseMove = (event: MouseEvent) => {
467
+ const rect = this._containerEl?.getBoundingClientRect() ?? this.getBoundingClientRect();
468
+ if (rect.width === 0 || rect.height === 0) {
469
+ return;
470
+ }
471
+
472
+ const centerX = rect.left + rect.width / 2;
473
+ const centerY = rect.top + rect.height / 2;
474
+ const deltaX = event.clientX - centerX;
475
+ const deltaY = event.clientY - centerY;
476
+ const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
477
+ const maxDistance = this._logoSize * 1.5;
478
+
479
+ if (distance < maxDistance && distance > 0) {
480
+ const pushStrength = (maxDistance - distance) / maxDistance;
481
+ const pushDistance = 50 * pushStrength;
482
+ this._logoOffset = {
483
+ x: -(deltaX / distance) * pushDistance,
484
+ y: -(deltaY / distance) * pushDistance,
485
+ };
486
+ this._isLogoHovered = true;
487
+ return;
488
+ }
489
+
490
+ this._logoOffset = { x: 0, y: 0 };
491
+ this._isLogoHovered = false;
492
+ };
493
+
494
+ private _handleMouseLeave = () => {
495
+ this._logoOffset = { x: 0, y: 0 };
496
+ this._isLogoHovered = false;
497
+ };
498
+
499
+ private _handleLogoClick = (event: MouseEvent) => {
500
+ event.stopPropagation();
501
+
502
+ const rect = this._containerEl?.getBoundingClientRect() ?? this.getBoundingClientRect();
503
+ const hitmarker = {
504
+ id: Date.now() + Math.random(),
505
+ x: event.clientX - rect.left,
506
+ y: event.clientY - rect.top,
507
+ };
508
+ this._hitmarkers = [...this._hitmarkers, hitmarker];
509
+ playHitmarkerSound();
510
+
511
+ this._setManagedTimer(() => {
512
+ this._hitmarkers = this._hitmarkers.filter((h) => h.id !== hitmarker.id);
513
+ }, 600);
514
+ };
515
+
516
+ private _handleRetry = () => {
517
+ if (this.onRetry) {
518
+ this.onRetry();
519
+ return;
520
+ }
521
+ this.dispatchEvent(
522
+ new CustomEvent("fw-retry", {
523
+ bubbles: true,
524
+ composed: true,
525
+ })
526
+ );
527
+ };
528
+
529
+ private get _isLoading() {
530
+ return (
531
+ this.status === "INITIALIZING" ||
532
+ this.status === "BOOTING" ||
533
+ this.status === "WAITING_FOR_DATA" ||
534
+ !this.status
535
+ );
536
+ }
537
+
538
+ private get _isOffline() {
539
+ return this.status === "OFFLINE";
540
+ }
541
+
542
+ private get _isError() {
543
+ return this.status === "ERROR" || this.status === "INVALID";
544
+ }
545
+
546
+ private get _showProgress() {
547
+ return this.status === "INITIALIZING" && this.percentage != null;
548
+ }
549
+
550
+ private get _showRetry() {
551
+ return this._isError && (this.retryEnabled || typeof this.onRetry === "function");
552
+ }
553
+
554
+ private get _displayMessage() {
555
+ return this.error || this.message || "Waiting for stream...";
556
+ }
557
+
558
+ private _renderStatusIcon() {
559
+ if (this._isLoading) {
560
+ return html`
561
+ <svg
562
+ class="status-icon spinning"
563
+ fill="none"
564
+ viewBox="0 0 24 24"
565
+ style="color: hsl(var(--tn-yellow, 40 95% 64%));"
566
+ >
567
+ <circle
568
+ style="opacity: 0.25;"
569
+ cx="12"
570
+ cy="12"
571
+ r="10"
572
+ stroke="currentColor"
573
+ stroke-width="4"
574
+ />
575
+ <path
576
+ style="opacity: 0.75;"
577
+ fill="currentColor"
578
+ 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"
579
+ ></path>
580
+ </svg>
581
+ `;
582
+ }
583
+
584
+ if (this._isOffline) {
585
+ return html`
586
+ <svg
587
+ class="status-icon"
588
+ fill="none"
589
+ viewBox="0 0 24 24"
590
+ stroke="currentColor"
591
+ style="color: hsl(var(--tn-red, 348 100% 72%));"
592
+ >
593
+ <path
594
+ stroke-linecap="round"
595
+ stroke-linejoin="round"
596
+ stroke-width="2"
597
+ 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"
598
+ ></path>
599
+ </svg>
600
+ `;
601
+ }
602
+
603
+ if (this._isError) {
604
+ return html`
605
+ <svg
606
+ class="status-icon"
607
+ fill="none"
608
+ viewBox="0 0 24 24"
609
+ stroke="currentColor"
610
+ style="color: hsl(var(--tn-red, 348 100% 72%));"
611
+ >
612
+ <path
613
+ stroke-linecap="round"
614
+ stroke-linejoin="round"
615
+ stroke-width="2"
616
+ 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"
617
+ ></path>
618
+ </svg>
619
+ `;
620
+ }
621
+
622
+ return html`
623
+ <svg
624
+ class="status-icon spinning"
625
+ fill="none"
626
+ viewBox="0 0 24 24"
627
+ style="color: hsl(var(--tn-cyan, 193 100% 75%));"
628
+ >
629
+ <circle
630
+ style="opacity: 0.25;"
631
+ cx="12"
632
+ cy="12"
633
+ r="10"
634
+ stroke="currentColor"
635
+ stroke-width="4"
636
+ />
637
+ <path
638
+ style="opacity: 0.75;"
639
+ fill="currentColor"
640
+ 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"
641
+ ></path>
642
+ </svg>
643
+ `;
644
+ }
645
+
138
646
  protected render() {
139
- const isOffline = this.status === "OFFLINE";
140
- const isInitializing = this.status === "INITIALIZING" || this.status === "STARTING";
647
+ const progress = Math.min(100, Math.max(0, this.percentage ?? 0));
648
+ const logoSrc = this.logoSrc || LOGOMARK_DATA_URL;
141
649
 
142
650
  return html`
143
- <div class="idle">
651
+ <div
652
+ class="idle-container fw-player-root"
653
+ role="status"
654
+ aria-label="Stream status"
655
+ @mousemove=${this._handleMouseMove}
656
+ @mouseleave=${this._handleMouseLeave}
657
+ >
658
+ ${this._hitmarkers.map(
659
+ (hitmarker) => html`
660
+ <div class="hitmarker" style="left: ${hitmarker.x}px; top: ${hitmarker.y}px;">
661
+ <div class="hitmarker-line tl"></div>
662
+ <div class="hitmarker-line tr"></div>
663
+ <div class="hitmarker-line bl"></div>
664
+ <div class="hitmarker-line br"></div>
665
+ </div>
666
+ `
667
+ )}
668
+
144
669
  <div class="particles">
145
- ${[0, 1, 2, 3, 4, 5, 6, 7].map(
146
- (i) => html`
670
+ ${this._particles.map(
671
+ (particle) => html`
147
672
  <div
148
673
  class="particle"
149
- style="left: ${10 + i * 11}%; top: ${20 + (i % 3) * 25}%; --dur: ${6 +
150
- i * 0.8}s; background: hsl(${217 + i * 15} 70% 60%); animation-delay: ${i * -1}s;"
674
+ style="
675
+ left: ${particle.left}%;
676
+ width: ${particle.size}px;
677
+ height: ${particle.size}px;
678
+ background: ${particle.color};
679
+ animation-duration: ${particle.duration}s;
680
+ animation-delay: ${particle.delay}s;
681
+ "
682
+ ></div>
683
+ `
684
+ )}
685
+ </div>
686
+
687
+ <div class="bubbles">
688
+ ${this._bubbles.map(
689
+ (bubble) => html`
690
+ <div
691
+ class="bubble"
692
+ style="
693
+ top: ${bubble.top}%;
694
+ left: ${bubble.left}%;
695
+ width: ${bubble.size}px;
696
+ height: ${bubble.size}px;
697
+ background: ${bubble.color};
698
+ opacity: ${bubble.opacity};
699
+ "
151
700
  ></div>
152
701
  `
153
702
  )}
154
703
  </div>
155
- <div class="card">
156
- <div class="status-icon">
157
- ${isOffline ? html`<div class="offline-dot"></div>` : html`<div class="spinner"></div>`}
704
+
705
+ <div
706
+ class="center-logo"
707
+ style="transform: translate(-50%, -50%) translate(${this._logoOffset.x}px, ${this
708
+ ._logoOffset.y}px);"
709
+ >
710
+ <div
711
+ class="logo-pulse ${this._isLogoHovered ? "hovered" : ""}"
712
+ style="width: ${this._logoSize * 1.4}px; height: ${this._logoSize * 1.4}px;"
713
+ ></div>
714
+ <button
715
+ type="button"
716
+ class="logo-button"
717
+ @click=${this._handleLogoClick}
718
+ aria-label="FrameWorks logo"
719
+ >
720
+ <img
721
+ src=${logoSrc}
722
+ alt=""
723
+ class="logo-image ${this._isLogoHovered ? "hovered" : ""}"
724
+ style="width: ${this._logoSize}px; height: ${this._logoSize}px;"
725
+ draggable="false"
726
+ />
727
+ </button>
728
+ </div>
729
+
730
+ <fw-dvd-logo .parentRef=${this._containerEl ?? null} .scale=${0.08}></fw-dvd-logo>
731
+
732
+ <div class="status-overlay">
733
+ <div class="status-indicator">
734
+ ${this._renderStatusIcon()}
735
+ <span>${this._displayMessage}</span>
158
736
  </div>
159
- ${this.status ? html`<div class="label">${this.status}</div>` : nothing}
160
- ${this.message ? html`<div class="msg">${this.message}</div>` : nothing}
161
- ${isInitializing && this.percentage != null
737
+
738
+ ${this._showProgress
162
739
  ? html`
163
- <div class="progress-wrap">
164
- <div
165
- class="progress-bar"
166
- style="width: ${Math.min(100, Math.max(0, this.percentage))}%"
167
- ></div>
740
+ <div class="progress-bar">
741
+ <div class="progress-fill" style="width: ${progress}%;"></div>
168
742
  </div>
169
- <div class="pct">${Math.round(this.percentage)}%</div>
170
743
  `
171
744
  : nothing}
745
+ ${this._showRetry
746
+ ? html`<button type="button" class="retry-btn" @click=${this._handleRetry}>
747
+ Retry
748
+ </button>`
749
+ : nothing}
172
750
  </div>
751
+
752
+ <div class="overlay-texture"></div>
173
753
  </div>
174
754
  `;
175
755
  }