@scarlett-player/ui 0.1.0

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.
package/dist/index.cjs ADDED
@@ -0,0 +1,1597 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ default: () => index_default,
24
+ formatLiveTime: () => formatLiveTime,
25
+ formatTime: () => formatTime,
26
+ icons: () => icons,
27
+ styles: () => styles,
28
+ uiPlugin: () => uiPlugin
29
+ });
30
+ module.exports = __toCommonJS(index_exports);
31
+
32
+ // src/styles.ts
33
+ var styles = `
34
+ /* ============================================
35
+ Container & Base
36
+ ============================================ */
37
+ .sp-container {
38
+ position: relative;
39
+ width: 100%;
40
+ height: 100%;
41
+ background: #000;
42
+ overflow: hidden;
43
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
44
+ }
45
+
46
+ .sp-container video {
47
+ width: 100%;
48
+ height: 100%;
49
+ display: block;
50
+ object-fit: contain;
51
+ }
52
+
53
+ .sp-container:focus {
54
+ outline: none;
55
+ }
56
+
57
+ /* ============================================
58
+ Gradient Overlay
59
+ ============================================ */
60
+ .sp-gradient {
61
+ position: absolute;
62
+ bottom: 0;
63
+ left: 0;
64
+ right: 0;
65
+ height: 160px;
66
+ background: linear-gradient(
67
+ to top,
68
+ rgba(0, 0, 0, 0.8) 0%,
69
+ rgba(0, 0, 0, 0.4) 50%,
70
+ transparent 100%
71
+ );
72
+ pointer-events: none;
73
+ opacity: 0;
74
+ transition: opacity 0.25s ease;
75
+ z-index: 5;
76
+ }
77
+
78
+ .sp-gradient--visible {
79
+ opacity: 1;
80
+ }
81
+
82
+ /* ============================================
83
+ Controls Container
84
+ ============================================ */
85
+ .sp-controls {
86
+ position: absolute;
87
+ bottom: 0;
88
+ left: 0;
89
+ right: 0;
90
+ display: flex;
91
+ align-items: center;
92
+ padding: 0 12px 12px;
93
+ gap: 4px;
94
+ opacity: 0;
95
+ transform: translateY(4px);
96
+ transition: opacity 0.25s ease, transform 0.25s ease;
97
+ z-index: 10;
98
+ }
99
+
100
+ .sp-controls--visible {
101
+ opacity: 1;
102
+ transform: translateY(0);
103
+ }
104
+
105
+ .sp-controls--hidden {
106
+ opacity: 0;
107
+ transform: translateY(4px);
108
+ pointer-events: none;
109
+ }
110
+
111
+ /* ============================================
112
+ Progress Bar (Above Controls)
113
+ ============================================ */
114
+ .sp-progress-wrapper {
115
+ position: absolute;
116
+ bottom: 48px;
117
+ left: 12px;
118
+ right: 12px;
119
+ height: 20px;
120
+ display: flex;
121
+ align-items: center;
122
+ cursor: pointer;
123
+ z-index: 10;
124
+ opacity: 0;
125
+ transition: opacity 0.25s ease;
126
+ }
127
+
128
+ .sp-progress-wrapper--visible {
129
+ opacity: 1;
130
+ }
131
+
132
+ .sp-progress {
133
+ position: relative;
134
+ width: 100%;
135
+ height: 3px;
136
+ background: rgba(255, 255, 255, 0.3);
137
+ border-radius: 1.5px;
138
+ transition: height 0.15s ease;
139
+ }
140
+
141
+ .sp-progress-wrapper:hover .sp-progress,
142
+ .sp-progress--dragging {
143
+ height: 5px;
144
+ }
145
+
146
+ .sp-progress__track {
147
+ position: absolute;
148
+ top: 0;
149
+ left: 0;
150
+ right: 0;
151
+ bottom: 0;
152
+ border-radius: inherit;
153
+ overflow: hidden;
154
+ }
155
+
156
+ .sp-progress__buffered {
157
+ position: absolute;
158
+ top: 0;
159
+ left: 0;
160
+ height: 100%;
161
+ background: rgba(255, 255, 255, 0.4);
162
+ border-radius: inherit;
163
+ transition: width 0.1s linear;
164
+ }
165
+
166
+ .sp-progress__filled {
167
+ position: absolute;
168
+ top: 0;
169
+ left: 0;
170
+ height: 100%;
171
+ background: var(--sp-accent, #e50914);
172
+ border-radius: inherit;
173
+ }
174
+
175
+ .sp-progress__handle {
176
+ position: absolute;
177
+ top: 50%;
178
+ width: 14px;
179
+ height: 14px;
180
+ background: var(--sp-accent, #e50914);
181
+ border-radius: 50%;
182
+ transform: translate(-50%, -50%) scale(0);
183
+ transition: transform 0.15s ease;
184
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
185
+ }
186
+
187
+ .sp-progress-wrapper:hover .sp-progress__handle,
188
+ .sp-progress--dragging .sp-progress__handle {
189
+ transform: translate(-50%, -50%) scale(1);
190
+ }
191
+
192
+ /* Progress Tooltip */
193
+ .sp-progress__tooltip {
194
+ position: absolute;
195
+ bottom: calc(100% + 8px);
196
+ padding: 6px 10px;
197
+ background: rgba(20, 20, 20, 0.95);
198
+ color: #fff;
199
+ font-size: 12px;
200
+ font-weight: 500;
201
+ font-variant-numeric: tabular-nums;
202
+ border-radius: 4px;
203
+ white-space: nowrap;
204
+ transform: translateX(-50%);
205
+ pointer-events: none;
206
+ opacity: 0;
207
+ transition: opacity 0.15s ease;
208
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
209
+ }
210
+
211
+ .sp-progress-wrapper:hover .sp-progress__tooltip {
212
+ opacity: 1;
213
+ }
214
+
215
+ /* ============================================
216
+ Control Buttons
217
+ ============================================ */
218
+ .sp-control {
219
+ background: none;
220
+ border: none;
221
+ color: rgba(255, 255, 255, 0.9);
222
+ cursor: pointer;
223
+ padding: 8px;
224
+ display: flex;
225
+ align-items: center;
226
+ justify-content: center;
227
+ border-radius: 4px;
228
+ transition: color 0.15s ease, transform 0.15s ease, background 0.15s ease;
229
+ flex-shrink: 0;
230
+ }
231
+
232
+ .sp-control:hover {
233
+ color: #fff;
234
+ background: rgba(255, 255, 255, 0.1);
235
+ }
236
+
237
+ .sp-control:active {
238
+ transform: scale(0.92);
239
+ }
240
+
241
+ .sp-control:focus-visible {
242
+ outline: 2px solid var(--sp-accent, #e50914);
243
+ outline-offset: 2px;
244
+ }
245
+
246
+ .sp-control:disabled {
247
+ opacity: 0.4;
248
+ cursor: not-allowed;
249
+ transform: none;
250
+ }
251
+
252
+ .sp-control:disabled:hover {
253
+ background: none;
254
+ }
255
+
256
+ .sp-control svg {
257
+ width: 24px;
258
+ height: 24px;
259
+ fill: currentColor;
260
+ display: block;
261
+ }
262
+
263
+ .sp-control--small svg {
264
+ width: 20px;
265
+ height: 20px;
266
+ }
267
+
268
+ /* ============================================
269
+ Spacer
270
+ ============================================ */
271
+ .sp-spacer {
272
+ flex: 1;
273
+ min-width: 0;
274
+ }
275
+
276
+ /* ============================================
277
+ Time Display
278
+ ============================================ */
279
+ .sp-time {
280
+ font-size: 13px;
281
+ font-variant-numeric: tabular-nums;
282
+ color: rgba(255, 255, 255, 0.9);
283
+ white-space: nowrap;
284
+ padding: 0 4px;
285
+ letter-spacing: 0.02em;
286
+ }
287
+
288
+ /* ============================================
289
+ Volume Control
290
+ ============================================ */
291
+ .sp-volume {
292
+ display: flex;
293
+ align-items: center;
294
+ position: relative;
295
+ }
296
+
297
+ .sp-volume__slider-wrap {
298
+ width: 0;
299
+ overflow: hidden;
300
+ transition: width 0.2s ease;
301
+ }
302
+
303
+ .sp-volume:hover .sp-volume__slider-wrap,
304
+ .sp-volume:focus-within .sp-volume__slider-wrap {
305
+ width: 64px;
306
+ }
307
+
308
+ .sp-volume__slider {
309
+ width: 64px;
310
+ height: 3px;
311
+ background: rgba(255, 255, 255, 0.3);
312
+ border-radius: 1.5px;
313
+ cursor: pointer;
314
+ position: relative;
315
+ margin: 0 8px 0 4px;
316
+ }
317
+
318
+ .sp-volume__level {
319
+ position: absolute;
320
+ top: 0;
321
+ left: 0;
322
+ height: 100%;
323
+ background: #fff;
324
+ border-radius: inherit;
325
+ transition: width 0.1s ease;
326
+ }
327
+
328
+ /* ============================================
329
+ Live Indicator
330
+ ============================================ */
331
+ .sp-live {
332
+ display: flex;
333
+ align-items: center;
334
+ gap: 6px;
335
+ font-size: 11px;
336
+ font-weight: 600;
337
+ text-transform: uppercase;
338
+ letter-spacing: 0.05em;
339
+ color: var(--sp-accent, #e50914);
340
+ cursor: pointer;
341
+ padding: 6px 10px;
342
+ border-radius: 4px;
343
+ transition: background 0.15s ease, opacity 0.15s ease;
344
+ }
345
+
346
+ .sp-live:hover {
347
+ background: rgba(255, 255, 255, 0.1);
348
+ }
349
+
350
+ .sp-live__dot {
351
+ width: 8px;
352
+ height: 8px;
353
+ background: currentColor;
354
+ border-radius: 50%;
355
+ animation: sp-pulse 2s ease-in-out infinite;
356
+ }
357
+
358
+ .sp-live--behind {
359
+ opacity: 0.6;
360
+ }
361
+
362
+ .sp-live--behind .sp-live__dot {
363
+ animation: none;
364
+ }
365
+
366
+ @keyframes sp-pulse {
367
+ 0%, 100% { opacity: 1; }
368
+ 50% { opacity: 0.4; }
369
+ }
370
+
371
+ /* ============================================
372
+ Quality / Settings Menu
373
+ ============================================ */
374
+ .sp-quality {
375
+ position: relative;
376
+ }
377
+
378
+ .sp-quality__btn {
379
+ display: flex;
380
+ align-items: center;
381
+ gap: 4px;
382
+ }
383
+
384
+ .sp-quality__label {
385
+ font-size: 12px;
386
+ font-weight: 500;
387
+ opacity: 0.9;
388
+ }
389
+
390
+ .sp-quality-menu {
391
+ position: absolute;
392
+ bottom: calc(100% + 8px);
393
+ right: 0;
394
+ background: rgba(20, 20, 20, 0.95);
395
+ backdrop-filter: blur(8px);
396
+ -webkit-backdrop-filter: blur(8px);
397
+ border-radius: 8px;
398
+ padding: 8px 0;
399
+ min-width: 150px;
400
+ box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4);
401
+ opacity: 0;
402
+ visibility: hidden;
403
+ transform: translateY(8px);
404
+ transition: opacity 0.15s ease, transform 0.15s ease, visibility 0.15s;
405
+ z-index: 20;
406
+ }
407
+
408
+ .sp-quality-menu--open {
409
+ opacity: 1;
410
+ visibility: visible;
411
+ transform: translateY(0);
412
+ }
413
+
414
+ .sp-quality-menu__item {
415
+ display: flex;
416
+ align-items: center;
417
+ justify-content: space-between;
418
+ padding: 10px 16px;
419
+ font-size: 13px;
420
+ color: rgba(255, 255, 255, 0.8);
421
+ cursor: pointer;
422
+ transition: background 0.1s ease, color 0.1s ease;
423
+ }
424
+
425
+ .sp-quality-menu__item:hover {
426
+ background: rgba(255, 255, 255, 0.1);
427
+ color: #fff;
428
+ }
429
+
430
+ .sp-quality-menu__item--active {
431
+ color: var(--sp-accent, #e50914);
432
+ }
433
+
434
+ .sp-quality-menu__check {
435
+ width: 16px;
436
+ height: 16px;
437
+ fill: currentColor;
438
+ margin-left: 8px;
439
+ opacity: 0;
440
+ }
441
+
442
+ .sp-quality-menu__item--active .sp-quality-menu__check {
443
+ opacity: 1;
444
+ }
445
+
446
+ /* ============================================
447
+ Cast Button States
448
+ ============================================ */
449
+ .sp-cast--active {
450
+ color: var(--sp-accent, #e50914);
451
+ }
452
+
453
+ .sp-cast--unavailable {
454
+ opacity: 0.4;
455
+ }
456
+
457
+ /* ============================================
458
+ Buffering Indicator
459
+ ============================================ */
460
+ .sp-buffering {
461
+ position: absolute;
462
+ top: 50%;
463
+ left: 50%;
464
+ transform: translate(-50%, -50%);
465
+ z-index: 15;
466
+ pointer-events: none;
467
+ opacity: 0;
468
+ transition: opacity 0.2s ease;
469
+ }
470
+
471
+ .sp-buffering--visible {
472
+ opacity: 1;
473
+ }
474
+
475
+ .sp-buffering svg {
476
+ width: 48px;
477
+ height: 48px;
478
+ fill: rgba(255, 255, 255, 0.9);
479
+ filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3));
480
+ }
481
+
482
+ @keyframes sp-spin {
483
+ from { transform: rotate(0deg); }
484
+ to { transform: rotate(360deg); }
485
+ }
486
+
487
+ .sp-spin {
488
+ animation: sp-spin 0.8s linear infinite;
489
+ }
490
+
491
+ /* ============================================
492
+ Reduced Motion
493
+ ============================================ */
494
+ @media (prefers-reduced-motion: reduce) {
495
+ .sp-gradient,
496
+ .sp-controls,
497
+ .sp-progress-wrapper,
498
+ .sp-progress,
499
+ .sp-progress__handle,
500
+ .sp-progress__tooltip,
501
+ .sp-control,
502
+ .sp-volume__slider-wrap,
503
+ .sp-quality-menu,
504
+ .sp-buffering {
505
+ transition: none;
506
+ }
507
+
508
+ .sp-live__dot,
509
+ .sp-spin {
510
+ animation: none;
511
+ }
512
+ }
513
+
514
+ /* ============================================
515
+ CSS Custom Properties (Theming)
516
+ ============================================ */
517
+ :root {
518
+ --sp-accent: #e50914;
519
+ --sp-color: #fff;
520
+ --sp-bg: rgba(0, 0, 0, 0.8);
521
+ --sp-control-height: 48px;
522
+ --sp-icon-size: 24px;
523
+ }
524
+ `;
525
+
526
+ // src/icons.ts
527
+ var icons = {
528
+ play: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>`,
529
+ pause: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M6 4h4v16H6V4zm8 0h4v16h-4V4z"/></svg>`,
530
+ replay: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z"/></svg>`,
531
+ volumeHigh: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/></svg>`,
532
+ volumeLow: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02z"/></svg>`,
533
+ volumeMute: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z"/></svg>`,
534
+ fullscreen: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/></svg>`,
535
+ exitFullscreen: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z"/></svg>`,
536
+ pip: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M19 7h-8v6h8V7zm2-4H3c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H3V5h18v14z"/></svg>`,
537
+ exitPip: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M21 3H3c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H3V5h18v14zM9 9h6v2H9z"/></svg>`,
538
+ settings: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M19.14 12.94c.04-.31.06-.63.06-.94 0-.31-.02-.63-.06-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.04.31-.06.63-.06.94s.02.63.06.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"/></svg>`,
539
+ chromecast: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M1 18v3h3c0-1.66-1.34-3-3-3zm0-4v2c2.76 0 5 2.24 5 5h2c0-3.87-3.13-7-7-7zm0-4v2c4.97 0 9 4.03 9 9h2c0-6.08-4.93-11-11-11zm20-7H3c-1.1 0-2 .9-2 2v3h2V5h18v14h-7v2h7c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z"/></svg>`,
540
+ chromecastConnected: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M1 18v3h3c0-1.66-1.34-3-3-3zm0-4v2c2.76 0 5 2.24 5 5h2c0-3.87-3.13-7-7-7zm18-7H5v1.63c3.96 1.28 7.09 4.41 8.37 8.37H19V7zM1 10v2c4.97 0 9 4.03 9 9h2c0-6.08-4.93-11-11-11zm20-7H3c-1.1 0-2 .9-2 2v3h2V5h18v14h-7v2h7c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z"/></svg>`,
541
+ airplay: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M6 22h12l-6-6-6 6zM21 3H3c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h4v-2H3V5h18v12h-4v2h4c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z"/></svg>`,
542
+ captions: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M19 4H5c-1.11 0-2 .9-2 2v12c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm-8 7H9.5v-.5h-2v3h2V13H11v1c0 .55-.45 1-1 1H7c-.55 0-1-.45-1-1v-4c0-.55.45-1 1-1h3c.55 0 1 .45 1 1v1zm7 0h-1.5v-.5h-2v3h2V13H18v1c0 .55-.45 1-1 1h-3c-.55 0-1-.45-1-1v-4c0-.55.45-1 1-1h3c.55 0 1 .45 1 1v1z"/></svg>`,
543
+ captionsOff: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M19.5 5.5v13h-15v-13h15zM19 4H5c-1.11 0-2 .9-2 2v12c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2z"/></svg>`,
544
+ checkmark: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>`,
545
+ chevronUp: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 8l-6 6 1.41 1.41L12 10.83l4.59 4.58L18 14z"/></svg>`,
546
+ chevronDown: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"/></svg>`,
547
+ spinner: `<svg viewBox="0 0 24 24" fill="currentColor" class="sp-spin"><path d="M12 4V2A10 10 0 0 0 2 12h2a8 8 0 0 1 8-8z"/></svg>`,
548
+ skipForward: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M4 18l8.5-6L4 6v12zm9-12v12l8.5-6L13 6z"/></svg>`,
549
+ skipBack: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M11 18V6l-8.5 6 8.5 6zm.5-6l8.5 6V6l-8.5 6z"/></svg>`,
550
+ forward10: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M18 13c0 3.31-2.69 6-6 6s-6-2.69-6-6 2.69-6 6-6v4l5-5-5-5v4c-4.42 0-8 3.58-8 8s3.58 8 8 8 8-3.58 8-8h-2z"/><text x="12" y="15" text-anchor="middle" font-size="7" font-weight="600">10</text></svg>`,
551
+ replay10: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z"/><text x="12" y="15" text-anchor="middle" font-size="7" font-weight="600">10</text></svg>`
552
+ };
553
+
554
+ // src/utils/dom.ts
555
+ function createElement(tag, attrs, children) {
556
+ const el = document.createElement(tag);
557
+ if (attrs) {
558
+ for (const [key, value] of Object.entries(attrs)) {
559
+ if (key === "className") {
560
+ el.className = value;
561
+ } else {
562
+ el.setAttribute(key, value);
563
+ }
564
+ }
565
+ }
566
+ if (children) {
567
+ for (const child of children) {
568
+ if (typeof child === "string") {
569
+ el.appendChild(document.createTextNode(child));
570
+ } else {
571
+ el.appendChild(child);
572
+ }
573
+ }
574
+ }
575
+ return el;
576
+ }
577
+ function createButton(className, label, icon) {
578
+ const btn = createElement("button", {
579
+ className: `sp-control ${className}`,
580
+ "aria-label": label,
581
+ type: "button"
582
+ });
583
+ btn.innerHTML = icon;
584
+ return btn;
585
+ }
586
+ function getVideo(container) {
587
+ return container.querySelector("video");
588
+ }
589
+
590
+ // src/utils/format.ts
591
+ function formatTime(seconds) {
592
+ if (!isFinite(seconds) || isNaN(seconds)) {
593
+ return "0:00";
594
+ }
595
+ const absSeconds = Math.abs(seconds);
596
+ const h = Math.floor(absSeconds / 3600);
597
+ const m = Math.floor(absSeconds % 3600 / 60);
598
+ const s = Math.floor(absSeconds % 60);
599
+ const sign = seconds < 0 ? "-" : "";
600
+ if (h > 0) {
601
+ return `${sign}${h}:${pad(m)}:${pad(s)}`;
602
+ }
603
+ return `${sign}${m}:${pad(s)}`;
604
+ }
605
+ function pad(n) {
606
+ return n < 10 ? `0${n}` : `${n}`;
607
+ }
608
+ function formatLiveTime(behindLive) {
609
+ if (behindLive <= 0) {
610
+ return "LIVE";
611
+ }
612
+ return `-${formatTime(behindLive)}`;
613
+ }
614
+
615
+ // src/controls/PlayButton.ts
616
+ var PlayButton = class {
617
+ constructor(api) {
618
+ this.clickHandler = () => {
619
+ this.toggle();
620
+ };
621
+ this.api = api;
622
+ this.el = createButton("sp-play", "Play", icons.play);
623
+ this.el.addEventListener("click", this.clickHandler);
624
+ }
625
+ render() {
626
+ return this.el;
627
+ }
628
+ update() {
629
+ const playing = this.api.getState("playing");
630
+ const ended = this.api.getState("ended");
631
+ let icon;
632
+ let label;
633
+ if (ended) {
634
+ icon = icons.replay;
635
+ label = "Replay";
636
+ } else if (playing) {
637
+ icon = icons.pause;
638
+ label = "Pause";
639
+ } else {
640
+ icon = icons.play;
641
+ label = "Play";
642
+ }
643
+ this.el.innerHTML = icon;
644
+ this.el.setAttribute("aria-label", label);
645
+ }
646
+ toggle() {
647
+ const video = getVideo(this.api.container);
648
+ if (!video) return;
649
+ const ended = this.api.getState("ended");
650
+ const playing = this.api.getState("playing");
651
+ if (ended) {
652
+ video.currentTime = 0;
653
+ video.play().catch(() => {
654
+ });
655
+ } else if (playing) {
656
+ video.pause();
657
+ } else {
658
+ video.play().catch(() => {
659
+ });
660
+ }
661
+ }
662
+ destroy() {
663
+ this.el.removeEventListener("click", this.clickHandler);
664
+ this.el.remove();
665
+ }
666
+ };
667
+
668
+ // src/controls/ProgressBar.ts
669
+ var ProgressBar = class {
670
+ constructor(api) {
671
+ this.isDragging = false;
672
+ this.lastSeekTime = 0;
673
+ this.seekThrottleMs = 100;
674
+ // Throttle seeks to max 10/sec
675
+ this.wasPlayingBeforeDrag = false;
676
+ this.onMouseDown = (e) => {
677
+ e.preventDefault();
678
+ const video = getVideo(this.api.container);
679
+ this.wasPlayingBeforeDrag = video ? !video.paused : false;
680
+ this.isDragging = true;
681
+ this.el.classList.add("sp-progress--dragging");
682
+ this.lastSeekTime = 0;
683
+ this.seek(e.clientX, true);
684
+ };
685
+ this.onDocMouseMove = (e) => {
686
+ if (this.isDragging) {
687
+ this.seek(e.clientX);
688
+ this.updateVisualPosition(e.clientX);
689
+ }
690
+ };
691
+ this.onMouseUp = (e) => {
692
+ if (this.isDragging) {
693
+ this.seek(e.clientX, true);
694
+ this.isDragging = false;
695
+ this.el.classList.remove("sp-progress--dragging");
696
+ if (this.wasPlayingBeforeDrag) {
697
+ const video = getVideo(this.api.container);
698
+ if (video && video.paused) {
699
+ video.play().catch(() => {
700
+ });
701
+ }
702
+ }
703
+ }
704
+ };
705
+ this.onMouseMove = (e) => {
706
+ this.updateTooltip(e.clientX);
707
+ };
708
+ this.onMouseLeave = () => {
709
+ if (!this.isDragging) {
710
+ this.tooltip.style.opacity = "0";
711
+ }
712
+ };
713
+ this.onKeyDown = (e) => {
714
+ const video = getVideo(this.api.container);
715
+ if (!video) return;
716
+ const step = 5;
717
+ const duration = this.api.getState("duration") || 0;
718
+ switch (e.key) {
719
+ case "ArrowLeft":
720
+ e.preventDefault();
721
+ video.currentTime = Math.max(0, video.currentTime - step);
722
+ break;
723
+ case "ArrowRight":
724
+ e.preventDefault();
725
+ video.currentTime = Math.min(duration, video.currentTime + step);
726
+ break;
727
+ case "Home":
728
+ e.preventDefault();
729
+ video.currentTime = 0;
730
+ break;
731
+ case "End":
732
+ e.preventDefault();
733
+ video.currentTime = duration;
734
+ break;
735
+ }
736
+ };
737
+ this.api = api;
738
+ this.wrapper = createElement("div", { className: "sp-progress-wrapper" });
739
+ this.el = createElement("div", { className: "sp-progress" });
740
+ const track = createElement("div", { className: "sp-progress__track" });
741
+ this.buffered = createElement("div", { className: "sp-progress__buffered" });
742
+ this.filled = createElement("div", { className: "sp-progress__filled" });
743
+ this.handle = createElement("div", { className: "sp-progress__handle" });
744
+ this.tooltip = createElement("div", { className: "sp-progress__tooltip" });
745
+ this.tooltip.textContent = "0:00";
746
+ track.appendChild(this.buffered);
747
+ track.appendChild(this.filled);
748
+ track.appendChild(this.handle);
749
+ this.el.appendChild(track);
750
+ this.el.appendChild(this.tooltip);
751
+ this.wrapper.appendChild(this.el);
752
+ this.el.setAttribute("role", "slider");
753
+ this.el.setAttribute("aria-label", "Seek");
754
+ this.el.setAttribute("aria-valuemin", "0");
755
+ this.el.setAttribute("tabindex", "0");
756
+ this.wrapper.addEventListener("mousedown", this.onMouseDown);
757
+ this.wrapper.addEventListener("mousemove", this.onMouseMove);
758
+ this.wrapper.addEventListener("mouseleave", this.onMouseLeave);
759
+ this.el.addEventListener("keydown", this.onKeyDown);
760
+ document.addEventListener("mousemove", this.onDocMouseMove);
761
+ document.addEventListener("mouseup", this.onMouseUp);
762
+ }
763
+ render() {
764
+ return this.wrapper;
765
+ }
766
+ /** Show the progress bar */
767
+ show() {
768
+ this.wrapper.classList.add("sp-progress-wrapper--visible");
769
+ }
770
+ /** Hide the progress bar */
771
+ hide() {
772
+ this.wrapper.classList.remove("sp-progress-wrapper--visible");
773
+ }
774
+ update() {
775
+ const currentTime = this.api.getState("currentTime") || 0;
776
+ const duration = this.api.getState("duration") || 0;
777
+ const bufferedRanges = this.api.getState("buffered");
778
+ if (duration > 0) {
779
+ const progress = currentTime / duration * 100;
780
+ this.filled.style.width = `${progress}%`;
781
+ this.handle.style.left = `${progress}%`;
782
+ if (bufferedRanges && bufferedRanges.length > 0) {
783
+ const bufferedEnd = bufferedRanges.end(bufferedRanges.length - 1);
784
+ const bufferedPercent = bufferedEnd / duration * 100;
785
+ this.buffered.style.width = `${bufferedPercent}%`;
786
+ }
787
+ this.el.setAttribute("aria-valuemax", String(Math.floor(duration)));
788
+ this.el.setAttribute("aria-valuenow", String(Math.floor(currentTime)));
789
+ this.el.setAttribute("aria-valuetext", formatTime(currentTime));
790
+ }
791
+ }
792
+ getTimeFromPosition(clientX) {
793
+ const rect = this.el.getBoundingClientRect();
794
+ const percent = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
795
+ const duration = this.api.getState("duration") || 0;
796
+ return percent * duration;
797
+ }
798
+ updateTooltip(clientX) {
799
+ const rect = this.el.getBoundingClientRect();
800
+ const percent = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
801
+ const time = this.getTimeFromPosition(clientX);
802
+ this.tooltip.textContent = formatTime(time);
803
+ this.tooltip.style.left = `${percent * 100}%`;
804
+ }
805
+ updateVisualPosition(clientX) {
806
+ const rect = this.el.getBoundingClientRect();
807
+ const percent = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
808
+ this.filled.style.width = `${percent * 100}%`;
809
+ this.handle.style.left = `${percent * 100}%`;
810
+ }
811
+ seek(clientX, force = false) {
812
+ const video = getVideo(this.api.container);
813
+ if (!video) return;
814
+ const now = Date.now();
815
+ if (!force && this.isDragging && now - this.lastSeekTime < this.seekThrottleMs) {
816
+ return;
817
+ }
818
+ this.lastSeekTime = now;
819
+ const time = this.getTimeFromPosition(clientX);
820
+ video.currentTime = time;
821
+ }
822
+ destroy() {
823
+ this.wrapper.removeEventListener("mousedown", this.onMouseDown);
824
+ this.wrapper.removeEventListener("mousemove", this.onMouseMove);
825
+ this.wrapper.removeEventListener("mouseleave", this.onMouseLeave);
826
+ document.removeEventListener("mousemove", this.onDocMouseMove);
827
+ document.removeEventListener("mouseup", this.onMouseUp);
828
+ this.wrapper.remove();
829
+ }
830
+ };
831
+
832
+ // src/controls/TimeDisplay.ts
833
+ var TimeDisplay = class {
834
+ constructor(api) {
835
+ this.api = api;
836
+ this.el = createElement("div", { className: "sp-time" });
837
+ this.el.setAttribute("aria-live", "off");
838
+ }
839
+ render() {
840
+ return this.el;
841
+ }
842
+ update() {
843
+ const live = this.api.getState("live");
844
+ const currentTime = this.api.getState("currentTime") || 0;
845
+ const duration = this.api.getState("duration") || 0;
846
+ if (live) {
847
+ const seekableRange = this.api.getState("seekableRange");
848
+ if (seekableRange) {
849
+ const behindLive = seekableRange.end - currentTime;
850
+ this.el.textContent = formatLiveTime(behindLive);
851
+ } else {
852
+ this.el.textContent = formatLiveTime(0);
853
+ }
854
+ } else {
855
+ this.el.textContent = `${formatTime(currentTime)} / ${formatTime(duration)}`;
856
+ }
857
+ }
858
+ destroy() {
859
+ this.el.remove();
860
+ }
861
+ };
862
+
863
+ // src/controls/VolumeControl.ts
864
+ var VolumeControl = class {
865
+ constructor(api) {
866
+ this.isDragging = false;
867
+ this.onMouseDown = (e) => {
868
+ e.preventDefault();
869
+ this.isDragging = true;
870
+ this.setVolume(this.getVolumeFromPosition(e.clientX));
871
+ };
872
+ this.onDocMouseMove = (e) => {
873
+ if (this.isDragging) {
874
+ this.setVolume(this.getVolumeFromPosition(e.clientX));
875
+ }
876
+ };
877
+ this.onMouseUp = () => {
878
+ this.isDragging = false;
879
+ };
880
+ this.onKeyDown = (e) => {
881
+ const video = getVideo(this.api.container);
882
+ if (!video) return;
883
+ const step = 0.1;
884
+ switch (e.key) {
885
+ case "ArrowUp":
886
+ case "ArrowRight":
887
+ e.preventDefault();
888
+ this.setVolume(video.volume + step);
889
+ break;
890
+ case "ArrowDown":
891
+ case "ArrowLeft":
892
+ e.preventDefault();
893
+ this.setVolume(video.volume - step);
894
+ break;
895
+ }
896
+ };
897
+ this.api = api;
898
+ this.el = createElement("div", { className: "sp-volume" });
899
+ this.btn = createElement("button", {
900
+ className: "sp-control sp-volume__btn",
901
+ "aria-label": "Mute",
902
+ type: "button"
903
+ });
904
+ this.btn.innerHTML = icons.volumeHigh;
905
+ this.btn.onclick = () => this.toggleMute();
906
+ const sliderWrap = createElement("div", { className: "sp-volume__slider-wrap" });
907
+ this.slider = createElement("div", { className: "sp-volume__slider" });
908
+ this.slider.setAttribute("role", "slider");
909
+ this.slider.setAttribute("aria-label", "Volume");
910
+ this.slider.setAttribute("aria-valuemin", "0");
911
+ this.slider.setAttribute("aria-valuemax", "100");
912
+ this.slider.setAttribute("tabindex", "0");
913
+ this.level = createElement("div", { className: "sp-volume__level" });
914
+ this.slider.appendChild(this.level);
915
+ sliderWrap.appendChild(this.slider);
916
+ this.el.appendChild(this.btn);
917
+ this.el.appendChild(sliderWrap);
918
+ this.slider.addEventListener("mousedown", this.onMouseDown);
919
+ this.slider.addEventListener("keydown", this.onKeyDown);
920
+ document.addEventListener("mousemove", this.onDocMouseMove);
921
+ document.addEventListener("mouseup", this.onMouseUp);
922
+ }
923
+ render() {
924
+ return this.el;
925
+ }
926
+ update() {
927
+ const volume = this.api.getState("volume") ?? 1;
928
+ const muted = this.api.getState("muted") ?? false;
929
+ let icon;
930
+ let label;
931
+ if (muted || volume === 0) {
932
+ icon = icons.volumeMute;
933
+ label = "Unmute";
934
+ } else if (volume < 0.5) {
935
+ icon = icons.volumeLow;
936
+ label = "Mute";
937
+ } else {
938
+ icon = icons.volumeHigh;
939
+ label = "Mute";
940
+ }
941
+ this.btn.innerHTML = icon;
942
+ this.btn.setAttribute("aria-label", label);
943
+ const displayVolume = muted ? 0 : volume;
944
+ this.level.style.width = `${displayVolume * 100}%`;
945
+ this.slider.setAttribute("aria-valuenow", String(Math.round(displayVolume * 100)));
946
+ }
947
+ toggleMute() {
948
+ const video = getVideo(this.api.container);
949
+ if (!video) return;
950
+ video.muted = !video.muted;
951
+ }
952
+ setVolume(percent) {
953
+ const video = getVideo(this.api.container);
954
+ if (!video) return;
955
+ const vol = Math.max(0, Math.min(1, percent));
956
+ video.volume = vol;
957
+ if (vol > 0 && video.muted) {
958
+ video.muted = false;
959
+ }
960
+ }
961
+ getVolumeFromPosition(clientX) {
962
+ const rect = this.slider.getBoundingClientRect();
963
+ return Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
964
+ }
965
+ destroy() {
966
+ document.removeEventListener("mousemove", this.onDocMouseMove);
967
+ document.removeEventListener("mouseup", this.onMouseUp);
968
+ this.el.remove();
969
+ }
970
+ };
971
+
972
+ // src/controls/LiveIndicator.ts
973
+ var LiveIndicator = class {
974
+ constructor(api) {
975
+ this.api = api;
976
+ this.el = createElement("div", { className: "sp-live" });
977
+ this.el.innerHTML = '<div class="sp-live__dot"></div><span>LIVE</span>';
978
+ this.el.setAttribute("role", "button");
979
+ this.el.setAttribute("aria-label", "Seek to live");
980
+ this.el.setAttribute("tabindex", "0");
981
+ this.el.onclick = () => this.seekToLive();
982
+ this.el.onkeydown = (e) => {
983
+ if (e.key === "Enter" || e.key === " ") {
984
+ e.preventDefault();
985
+ this.seekToLive();
986
+ }
987
+ };
988
+ }
989
+ render() {
990
+ return this.el;
991
+ }
992
+ update() {
993
+ const live = this.api.getState("live");
994
+ const liveEdge = this.api.getState("liveEdge");
995
+ this.el.style.display = live ? "" : "none";
996
+ if (liveEdge) {
997
+ this.el.classList.remove("sp-live--behind");
998
+ } else {
999
+ this.el.classList.add("sp-live--behind");
1000
+ }
1001
+ }
1002
+ seekToLive() {
1003
+ const video = getVideo(this.api.container);
1004
+ if (!video) return;
1005
+ const seekableRange = this.api.getState("seekableRange");
1006
+ if (seekableRange) {
1007
+ video.currentTime = seekableRange.end;
1008
+ }
1009
+ }
1010
+ destroy() {
1011
+ this.el.remove();
1012
+ }
1013
+ };
1014
+
1015
+ // src/controls/QualityMenu.ts
1016
+ var QualityMenu = class {
1017
+ constructor(api) {
1018
+ this.isOpen = false;
1019
+ this.lastQualitiesJson = "";
1020
+ this.api = api;
1021
+ this.el = createElement("div", { className: "sp-quality" });
1022
+ this.btn = createButton("sp-quality__btn", "Quality", icons.settings);
1023
+ this.btnLabel = createElement("span", { className: "sp-quality__label" });
1024
+ this.btnLabel.textContent = "Auto";
1025
+ this.btn.appendChild(this.btnLabel);
1026
+ this.btn.addEventListener("click", (e) => {
1027
+ e.stopPropagation();
1028
+ this.toggle();
1029
+ });
1030
+ this.menu = createElement("div", { className: "sp-quality-menu" });
1031
+ this.menu.setAttribute("role", "menu");
1032
+ this.menu.addEventListener("click", (e) => {
1033
+ e.stopPropagation();
1034
+ });
1035
+ this.el.appendChild(this.btn);
1036
+ this.el.appendChild(this.menu);
1037
+ this.closeHandler = (e) => {
1038
+ if (!this.el.contains(e.target)) {
1039
+ this.close();
1040
+ }
1041
+ };
1042
+ document.addEventListener("click", this.closeHandler);
1043
+ }
1044
+ render() {
1045
+ return this.el;
1046
+ }
1047
+ update() {
1048
+ const qualities = this.api.getState("qualities") || [];
1049
+ const currentQuality = this.api.getState("currentQuality");
1050
+ this.el.style.display = qualities.length > 0 ? "" : "none";
1051
+ this.btnLabel.textContent = currentQuality?.label || "Auto";
1052
+ const qualitiesJson = JSON.stringify(qualities.map((q) => q.id));
1053
+ const currentId = currentQuality?.id || "auto";
1054
+ if (qualitiesJson !== this.lastQualitiesJson) {
1055
+ this.lastQualitiesJson = qualitiesJson;
1056
+ this.rebuildMenu(qualities);
1057
+ }
1058
+ this.updateActiveStates(currentId);
1059
+ }
1060
+ rebuildMenu(qualities) {
1061
+ this.menu.innerHTML = "";
1062
+ const autoItem = this.createMenuItem("Auto", "auto");
1063
+ this.menu.appendChild(autoItem);
1064
+ const sorted = [...qualities].sort((a, b) => b.height - a.height);
1065
+ for (const q of sorted) {
1066
+ if (q.id === "auto") continue;
1067
+ const item = this.createMenuItem(q.label, q.id);
1068
+ this.menu.appendChild(item);
1069
+ }
1070
+ }
1071
+ updateActiveStates(activeId) {
1072
+ const items = this.menu.querySelectorAll(".sp-quality-menu__item");
1073
+ items.forEach((item) => {
1074
+ const id = item.getAttribute("data-quality-id");
1075
+ const isActive = id === activeId;
1076
+ item.classList.toggle("sp-quality-menu__item--active", isActive);
1077
+ });
1078
+ }
1079
+ createMenuItem(label, qualityId) {
1080
+ const item = createElement("div", {
1081
+ className: "sp-quality-menu__item"
1082
+ });
1083
+ item.setAttribute("role", "menuitem");
1084
+ item.setAttribute("data-quality-id", qualityId);
1085
+ const labelSpan = createElement("span", { className: "sp-quality-menu__label" });
1086
+ labelSpan.textContent = label;
1087
+ item.appendChild(labelSpan);
1088
+ item.addEventListener("click", (e) => {
1089
+ e.preventDefault();
1090
+ e.stopPropagation();
1091
+ this.selectQuality(qualityId);
1092
+ });
1093
+ return item;
1094
+ }
1095
+ selectQuality(qualityId) {
1096
+ this.api.emit("quality:select", {
1097
+ quality: qualityId,
1098
+ auto: qualityId === "auto"
1099
+ });
1100
+ this.close();
1101
+ }
1102
+ toggle() {
1103
+ this.isOpen ? this.close() : this.open();
1104
+ }
1105
+ open() {
1106
+ this.isOpen = true;
1107
+ this.menu.classList.add("sp-quality-menu--open");
1108
+ this.btn.setAttribute("aria-expanded", "true");
1109
+ }
1110
+ close() {
1111
+ this.isOpen = false;
1112
+ this.menu.classList.remove("sp-quality-menu--open");
1113
+ this.btn.setAttribute("aria-expanded", "false");
1114
+ }
1115
+ destroy() {
1116
+ document.removeEventListener("click", this.closeHandler);
1117
+ this.el.remove();
1118
+ }
1119
+ };
1120
+
1121
+ // src/controls/CastButton.ts
1122
+ function isChromecastSupported() {
1123
+ if (typeof navigator === "undefined") return false;
1124
+ const ua = navigator.userAgent;
1125
+ return /Chrome/.test(ua) && !/Edge|Edg/.test(ua);
1126
+ }
1127
+ function isAirPlaySupported() {
1128
+ if (typeof HTMLVideoElement === "undefined") return false;
1129
+ return typeof HTMLVideoElement.prototype.webkitShowPlaybackTargetPicker === "function";
1130
+ }
1131
+ var CastButton = class {
1132
+ constructor(api, type) {
1133
+ this.api = api;
1134
+ this.type = type;
1135
+ this.supported = type === "chromecast" ? isChromecastSupported() : isAirPlaySupported();
1136
+ const icon = type === "chromecast" ? icons.chromecast : icons.airplay;
1137
+ const label = type === "chromecast" ? "Cast" : "AirPlay";
1138
+ this.el = createButton(`sp-cast sp-cast--${type}`, label, icon);
1139
+ this.el.addEventListener("click", () => this.handleClick());
1140
+ if (!this.supported) {
1141
+ this.el.style.display = "none";
1142
+ }
1143
+ }
1144
+ render() {
1145
+ return this.el;
1146
+ }
1147
+ update() {
1148
+ if (!this.supported) {
1149
+ this.el.style.display = "none";
1150
+ return;
1151
+ }
1152
+ if (this.type === "chromecast") {
1153
+ const available = this.api.getState("chromecastAvailable");
1154
+ const active = this.api.getState("chromecastActive");
1155
+ this.el.style.display = "";
1156
+ this.el.disabled = !available && !active;
1157
+ this.el.classList.toggle("sp-cast--active", !!active);
1158
+ this.el.classList.toggle("sp-cast--unavailable", !available && !active);
1159
+ if (active) {
1160
+ this.el.innerHTML = icons.chromecastConnected;
1161
+ this.el.setAttribute("aria-label", "Stop casting");
1162
+ } else {
1163
+ this.el.innerHTML = icons.chromecast;
1164
+ this.el.setAttribute("aria-label", available ? "Cast" : "No Cast devices found");
1165
+ }
1166
+ } else {
1167
+ const active = this.api.getState("airplayActive");
1168
+ this.el.style.display = "";
1169
+ this.el.disabled = false;
1170
+ this.el.classList.toggle("sp-cast--active", !!active);
1171
+ this.el.classList.remove("sp-cast--unavailable");
1172
+ this.el.setAttribute("aria-label", active ? "Stop AirPlay" : "AirPlay");
1173
+ }
1174
+ }
1175
+ handleClick() {
1176
+ if (this.type === "chromecast") {
1177
+ this.handleChromecast();
1178
+ } else {
1179
+ this.handleAirPlay();
1180
+ }
1181
+ }
1182
+ handleChromecast() {
1183
+ const chromecast = this.api.getPlugin("chromecast");
1184
+ if (!chromecast) return;
1185
+ if (chromecast.isConnected()) {
1186
+ chromecast.endSession();
1187
+ } else {
1188
+ chromecast.requestSession().catch(() => {
1189
+ });
1190
+ }
1191
+ }
1192
+ async handleAirPlay() {
1193
+ const airplayPlugin = this.api.getPlugin("airplay");
1194
+ if (airplayPlugin) {
1195
+ await airplayPlugin.showPicker();
1196
+ } else {
1197
+ const video = getVideo(this.api.container);
1198
+ video?.webkitShowPlaybackTargetPicker?.();
1199
+ }
1200
+ }
1201
+ destroy() {
1202
+ this.el.remove();
1203
+ }
1204
+ };
1205
+
1206
+ // src/controls/PipButton.ts
1207
+ var PipButton = class {
1208
+ constructor(api) {
1209
+ this.clickHandler = () => {
1210
+ this.toggle();
1211
+ };
1212
+ this.api = api;
1213
+ const video = document.createElement("video");
1214
+ this.supported = "pictureInPictureEnabled" in document || "webkitSetPresentationMode" in video;
1215
+ this.el = createButton("sp-pip", "Picture-in-Picture", icons.pip);
1216
+ this.el.addEventListener("click", this.clickHandler);
1217
+ if (!this.supported) {
1218
+ this.el.style.display = "none";
1219
+ }
1220
+ }
1221
+ render() {
1222
+ return this.el;
1223
+ }
1224
+ update() {
1225
+ if (!this.supported) return;
1226
+ const pip = this.api.getState("pip");
1227
+ this.el.setAttribute("aria-label", pip ? "Exit Picture-in-Picture" : "Picture-in-Picture");
1228
+ this.el.classList.toggle("sp-pip--active", !!pip);
1229
+ }
1230
+ async toggle() {
1231
+ const video = getVideo(this.api.container);
1232
+ if (!video) {
1233
+ this.api.logger.warn("PiP: video element not found");
1234
+ return;
1235
+ }
1236
+ try {
1237
+ const isInPip = document.pictureInPictureElement === video || video.webkitPresentationMode === "picture-in-picture";
1238
+ if (isInPip) {
1239
+ if (document.pictureInPictureElement) {
1240
+ await document.exitPictureInPicture();
1241
+ } else if (video.webkitSetPresentationMode) {
1242
+ video.webkitSetPresentationMode("inline");
1243
+ }
1244
+ this.api.logger.debug("PiP: exited");
1245
+ } else {
1246
+ if (video.requestPictureInPicture) {
1247
+ await video.requestPictureInPicture();
1248
+ } else if (video.webkitSetPresentationMode) {
1249
+ video.webkitSetPresentationMode("picture-in-picture");
1250
+ }
1251
+ this.api.logger.debug("PiP: entered");
1252
+ }
1253
+ } catch (e) {
1254
+ this.api.logger.warn("PiP: failed", { error: e.message });
1255
+ }
1256
+ }
1257
+ destroy() {
1258
+ this.el.removeEventListener("click", this.clickHandler);
1259
+ this.el.remove();
1260
+ }
1261
+ };
1262
+
1263
+ // src/controls/FullscreenButton.ts
1264
+ var FullscreenButton = class {
1265
+ constructor(api) {
1266
+ this.clickHandler = () => {
1267
+ this.toggle();
1268
+ };
1269
+ this.api = api;
1270
+ this.el = createButton("sp-fullscreen", "Fullscreen", icons.fullscreen);
1271
+ this.el.addEventListener("click", this.clickHandler);
1272
+ }
1273
+ render() {
1274
+ return this.el;
1275
+ }
1276
+ update() {
1277
+ const fullscreen = this.api.getState("fullscreen");
1278
+ if (fullscreen) {
1279
+ this.el.innerHTML = icons.exitFullscreen;
1280
+ this.el.setAttribute("aria-label", "Exit fullscreen");
1281
+ } else {
1282
+ this.el.innerHTML = icons.fullscreen;
1283
+ this.el.setAttribute("aria-label", "Fullscreen");
1284
+ }
1285
+ }
1286
+ async toggle() {
1287
+ const container = this.api.container;
1288
+ const video = getVideo(container);
1289
+ try {
1290
+ if (document.fullscreenElement) {
1291
+ await document.exitFullscreen();
1292
+ } else if (container.requestFullscreen) {
1293
+ await container.requestFullscreen();
1294
+ } else if (video?.webkitEnterFullscreen) {
1295
+ video.webkitEnterFullscreen();
1296
+ }
1297
+ } catch {
1298
+ }
1299
+ }
1300
+ destroy() {
1301
+ this.el.removeEventListener("click", this.clickHandler);
1302
+ this.el.remove();
1303
+ }
1304
+ };
1305
+
1306
+ // src/controls/Spacer.ts
1307
+ var Spacer = class {
1308
+ constructor() {
1309
+ this.el = createElement("div", { className: "sp-spacer" });
1310
+ }
1311
+ render() {
1312
+ return this.el;
1313
+ }
1314
+ update() {
1315
+ }
1316
+ destroy() {
1317
+ this.el.remove();
1318
+ }
1319
+ };
1320
+
1321
+ // src/index.ts
1322
+ var DEFAULT_LAYOUT = [
1323
+ "play",
1324
+ "volume",
1325
+ "time",
1326
+ "live-indicator",
1327
+ "spacer",
1328
+ "quality",
1329
+ "chromecast",
1330
+ "airplay",
1331
+ "pip",
1332
+ "fullscreen"
1333
+ ];
1334
+ var DEFAULT_HIDE_DELAY = 3e3;
1335
+ function uiPlugin(config = {}) {
1336
+ let api;
1337
+ let controlBar = null;
1338
+ let gradient = null;
1339
+ let progressBar = null;
1340
+ let bufferingIndicator = null;
1341
+ let styleEl = null;
1342
+ let controls = [];
1343
+ let hideTimeout = null;
1344
+ let stateUnsubscribe = null;
1345
+ let controlsVisible = true;
1346
+ const layout = config.controls || DEFAULT_LAYOUT;
1347
+ const hideDelay = config.hideDelay ?? DEFAULT_HIDE_DELAY;
1348
+ const createControl = (slot) => {
1349
+ switch (slot) {
1350
+ case "play":
1351
+ return new PlayButton(api);
1352
+ case "volume":
1353
+ return new VolumeControl(api);
1354
+ case "progress":
1355
+ return null;
1356
+ case "time":
1357
+ return new TimeDisplay(api);
1358
+ case "live-indicator":
1359
+ return new LiveIndicator(api);
1360
+ case "quality":
1361
+ return new QualityMenu(api);
1362
+ case "chromecast":
1363
+ return new CastButton(api, "chromecast");
1364
+ case "airplay":
1365
+ return new CastButton(api, "airplay");
1366
+ case "pip":
1367
+ return new PipButton(api);
1368
+ case "fullscreen":
1369
+ return new FullscreenButton(api);
1370
+ case "spacer":
1371
+ return new Spacer();
1372
+ default:
1373
+ return null;
1374
+ }
1375
+ };
1376
+ const updateControls = () => {
1377
+ controls.forEach((c) => c.update());
1378
+ progressBar?.update();
1379
+ const waiting = api?.getState("waiting");
1380
+ const seeking = api?.getState("seeking");
1381
+ const playbackState = api?.getState("playbackState");
1382
+ const isLoading = playbackState === "loading";
1383
+ const showSpinner = waiting || seeking && !api?.getState("paused") || isLoading;
1384
+ bufferingIndicator?.classList.toggle("sp-buffering--visible", !!showSpinner);
1385
+ };
1386
+ const showControls = () => {
1387
+ if (controlsVisible) {
1388
+ resetHideTimer();
1389
+ return;
1390
+ }
1391
+ controlsVisible = true;
1392
+ controlBar?.classList.add("sp-controls--visible");
1393
+ controlBar?.classList.remove("sp-controls--hidden");
1394
+ gradient?.classList.add("sp-gradient--visible");
1395
+ progressBar?.show();
1396
+ api?.setState("controlsVisible", true);
1397
+ resetHideTimer();
1398
+ };
1399
+ const hideControls = () => {
1400
+ const paused = api?.getState("paused");
1401
+ if (paused) return;
1402
+ controlsVisible = false;
1403
+ controlBar?.classList.remove("sp-controls--visible");
1404
+ controlBar?.classList.add("sp-controls--hidden");
1405
+ gradient?.classList.remove("sp-gradient--visible");
1406
+ progressBar?.hide();
1407
+ api?.setState("controlsVisible", false);
1408
+ };
1409
+ const resetHideTimer = () => {
1410
+ if (hideTimeout) {
1411
+ clearTimeout(hideTimeout);
1412
+ }
1413
+ hideTimeout = setTimeout(hideControls, hideDelay);
1414
+ };
1415
+ const handleInteraction = () => {
1416
+ showControls();
1417
+ };
1418
+ const handleMouseLeave = () => {
1419
+ hideControls();
1420
+ };
1421
+ const handleKeyDown = (e) => {
1422
+ if (!api.container.contains(document.activeElement)) return;
1423
+ const video = api.container.querySelector("video");
1424
+ if (!video) return;
1425
+ switch (e.key) {
1426
+ case " ":
1427
+ case "k":
1428
+ e.preventDefault();
1429
+ video.paused ? video.play() : video.pause();
1430
+ break;
1431
+ case "m":
1432
+ e.preventDefault();
1433
+ video.muted = !video.muted;
1434
+ break;
1435
+ case "f":
1436
+ e.preventDefault();
1437
+ if (document.fullscreenElement) {
1438
+ document.exitFullscreen();
1439
+ } else {
1440
+ api.container.requestFullscreen?.();
1441
+ }
1442
+ break;
1443
+ case "ArrowLeft":
1444
+ e.preventDefault();
1445
+ video.currentTime = Math.max(0, video.currentTime - 5);
1446
+ showControls();
1447
+ break;
1448
+ case "ArrowRight":
1449
+ e.preventDefault();
1450
+ video.currentTime = Math.min(video.duration || 0, video.currentTime + 5);
1451
+ showControls();
1452
+ break;
1453
+ case "ArrowUp":
1454
+ e.preventDefault();
1455
+ video.volume = Math.min(1, video.volume + 0.1);
1456
+ showControls();
1457
+ break;
1458
+ case "ArrowDown":
1459
+ e.preventDefault();
1460
+ video.volume = Math.max(0, video.volume - 0.1);
1461
+ showControls();
1462
+ break;
1463
+ }
1464
+ };
1465
+ return {
1466
+ id: "ui-controls",
1467
+ name: "UI Controls",
1468
+ type: "ui",
1469
+ version: "1.0.0",
1470
+ async init(pluginApi) {
1471
+ api = pluginApi;
1472
+ styleEl = document.createElement("style");
1473
+ styleEl.textContent = styles;
1474
+ document.head.appendChild(styleEl);
1475
+ if (config.theme) {
1476
+ this.setTheme(config.theme);
1477
+ }
1478
+ const container = api.container;
1479
+ if (!container) {
1480
+ api.logger.error("UI plugin: container not found");
1481
+ return;
1482
+ }
1483
+ const containerStyle = getComputedStyle(container);
1484
+ if (containerStyle.position === "static") {
1485
+ container.style.position = "relative";
1486
+ }
1487
+ gradient = document.createElement("div");
1488
+ gradient.className = "sp-gradient sp-gradient--visible";
1489
+ container.appendChild(gradient);
1490
+ bufferingIndicator = document.createElement("div");
1491
+ bufferingIndicator.className = "sp-buffering";
1492
+ bufferingIndicator.innerHTML = icons.spinner;
1493
+ bufferingIndicator.setAttribute("aria-hidden", "true");
1494
+ container.appendChild(bufferingIndicator);
1495
+ progressBar = new ProgressBar(api);
1496
+ container.appendChild(progressBar.render());
1497
+ progressBar.show();
1498
+ controlBar = document.createElement("div");
1499
+ controlBar.className = "sp-controls sp-controls--visible";
1500
+ controlBar.setAttribute("role", "toolbar");
1501
+ controlBar.setAttribute("aria-label", "Video controls");
1502
+ for (const slot of layout) {
1503
+ const control = createControl(slot);
1504
+ if (control) {
1505
+ controls.push(control);
1506
+ controlBar.appendChild(control.render());
1507
+ }
1508
+ }
1509
+ container.appendChild(controlBar);
1510
+ container.addEventListener("mousemove", handleInteraction);
1511
+ container.addEventListener("mouseenter", handleInteraction);
1512
+ container.addEventListener("mouseleave", handleMouseLeave);
1513
+ container.addEventListener("touchstart", handleInteraction, { passive: true });
1514
+ container.addEventListener("click", handleInteraction);
1515
+ document.addEventListener("keydown", handleKeyDown);
1516
+ stateUnsubscribe = api.subscribeToState(updateControls);
1517
+ document.addEventListener("fullscreenchange", updateControls);
1518
+ updateControls();
1519
+ if (!container.hasAttribute("tabindex")) {
1520
+ container.setAttribute("tabindex", "0");
1521
+ }
1522
+ api.logger.debug("UI controls plugin initialized");
1523
+ },
1524
+ async destroy() {
1525
+ if (hideTimeout) {
1526
+ clearTimeout(hideTimeout);
1527
+ hideTimeout = null;
1528
+ }
1529
+ stateUnsubscribe?.();
1530
+ stateUnsubscribe = null;
1531
+ if (api?.container) {
1532
+ api.container.removeEventListener("mousemove", handleInteraction);
1533
+ api.container.removeEventListener("mouseenter", handleInteraction);
1534
+ api.container.removeEventListener("mouseleave", handleMouseLeave);
1535
+ api.container.removeEventListener("touchstart", handleInteraction);
1536
+ api.container.removeEventListener("click", handleInteraction);
1537
+ }
1538
+ document.removeEventListener("keydown", handleKeyDown);
1539
+ document.removeEventListener("fullscreenchange", updateControls);
1540
+ controls.forEach((c) => c.destroy());
1541
+ controls = [];
1542
+ progressBar?.destroy();
1543
+ progressBar = null;
1544
+ controlBar?.remove();
1545
+ controlBar = null;
1546
+ gradient?.remove();
1547
+ gradient = null;
1548
+ bufferingIndicator?.remove();
1549
+ bufferingIndicator = null;
1550
+ styleEl?.remove();
1551
+ styleEl = null;
1552
+ api?.logger.debug("UI controls plugin destroyed");
1553
+ },
1554
+ // Public API
1555
+ show() {
1556
+ showControls();
1557
+ },
1558
+ hide() {
1559
+ controlsVisible = false;
1560
+ controlBar?.classList.remove("sp-controls--visible");
1561
+ controlBar?.classList.add("sp-controls--hidden");
1562
+ gradient?.classList.remove("sp-gradient--visible");
1563
+ progressBar?.hide();
1564
+ api?.setState("controlsVisible", false);
1565
+ },
1566
+ setTheme(theme) {
1567
+ const root = api?.container || document.documentElement;
1568
+ if (theme.primaryColor) {
1569
+ root.style.setProperty("--sp-color", theme.primaryColor);
1570
+ }
1571
+ if (theme.accentColor) {
1572
+ root.style.setProperty("--sp-accent", theme.accentColor);
1573
+ }
1574
+ if (theme.backgroundColor) {
1575
+ root.style.setProperty("--sp-bg", theme.backgroundColor);
1576
+ }
1577
+ if (theme.controlBarHeight) {
1578
+ root.style.setProperty("--sp-control-height", `${theme.controlBarHeight}px`);
1579
+ }
1580
+ if (theme.iconSize) {
1581
+ root.style.setProperty("--sp-icon-size", `${theme.iconSize}px`);
1582
+ }
1583
+ },
1584
+ getControlBar() {
1585
+ return controlBar;
1586
+ }
1587
+ };
1588
+ }
1589
+ var index_default = uiPlugin;
1590
+ // Annotate the CommonJS export names for ESM import in node:
1591
+ 0 && (module.exports = {
1592
+ formatLiveTime,
1593
+ formatTime,
1594
+ icons,
1595
+ styles,
1596
+ uiPlugin
1597
+ });