@kaushal-satani-aipxperts/player-angular 0.4.1 → 0.4.2

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.
@@ -1,2191 +0,0 @@
1
- import { UvpPlayerCore, cacheVideo, isCached, removeCachedVideo } from "@kaushal-satani-aipxperts/player-core";
2
- import { icons } from "feather-icons";
3
- const DEFAULT_TAG = "uvp-player";
4
- const OBSERVED_ATTRIBUTES = [
5
- "source-url",
6
- "source-type",
7
- "source-qualities",
8
- "autoplay",
9
- "muted",
10
- "poster",
11
- "controls",
12
- "auto-next",
13
- "loop",
14
- "playback-rate",
15
- "volume",
16
- "custom-controls",
17
- "playlist",
18
- "scale-mode"
19
- ];
20
- /**
21
- * Universal Video Player custom element.
22
- *
23
- * Provides a framework-agnostic video player with support for MP4, HLS, and DASH,
24
- * including offline caching and custom UI controls.
25
- *
26
- * @example
27
- * <uvp-player source-url="https://example.com/video.mp4"></uvp-player>
28
- */
29
- export class UvpPlayerElement extends HTMLElement {
30
- static get observedAttributes() {
31
- return [...OBSERVED_ATTRIBUTES];
32
- }
33
- constructor() {
34
- super();
35
- this.cleanupCallbacks = [];
36
- this.isCustomControlsMode = false;
37
- this.showControls = true;
38
- this.isUserSeeking = false;
39
- this.controlsHideTimer = null;
40
- this.isFullscreen = false;
41
- this.isInteractionActive = true;
42
- this.isLocked = false;
43
- this.progressRafId = null;
44
- this.lastStableDuration = 0;
45
- this.handleDocumentClick = (event) => {
46
- const target = event.target;
47
- const isInsidePlayer = this.contains(target);
48
- if (!isInsidePlayer) {
49
- this.closeSettingsMenu();
50
- this.playlistPanel.classList.remove("open");
51
- }
52
- };
53
- this.handleVideoProgress = () => {
54
- let duration = this.videoEl.duration;
55
- // Stabilize duration: different quality levels in HLS/DASH can have slightly different
56
- // reported durations. We "lock" to the maximum seen duration for the current source
57
- // to prevent the progress bar from jittering during quality transitions.
58
- if (Number.isFinite(duration) && duration > 0) {
59
- if (duration > this.lastStableDuration) {
60
- this.lastStableDuration = duration;
61
- }
62
- else {
63
- duration = this.lastStableDuration;
64
- }
65
- }
66
- else if (this.lastStableDuration > 0) {
67
- duration = this.lastStableDuration;
68
- }
69
- else {
70
- duration = 0;
71
- }
72
- const current = this.videoEl.currentTime || 0;
73
- const playedPercent = duration > 0 ? (current / duration) * 100 : 0;
74
- if (!this.isUserSeeking && duration > 0) {
75
- this.progressInput.value = String(playedPercent);
76
- }
77
- this.updateProgressVisual(playedPercent);
78
- this.progressBufferedEl.style.width = `${this.getBufferedPercent(duration, current)}%`;
79
- this.currentTimeEl.textContent = this.formatTime(current);
80
- this.durationEl.textContent = this.formatTime(duration);
81
- };
82
- this.handleQualityUiRefresh = () => {
83
- this.refreshQualityUi();
84
- };
85
- this.handlePlayStateChange = () => {
86
- const paused = this.videoEl.paused;
87
- this.setButtonIcon(this.controlPlayPauseButton, paused ? "play" : "pause");
88
- this.controlPlayPauseButton.setAttribute("aria-label", paused ? "Play" : "Pause");
89
- if (paused) {
90
- this.stopProgressLoop();
91
- }
92
- else {
93
- this.startProgressLoop();
94
- }
95
- this.markInteraction();
96
- this.updateNavigationButtons();
97
- };
98
- this.handleVolumeChange = () => {
99
- const muted = this.videoEl.muted || this.videoEl.volume === 0;
100
- this.setButtonIcon(this.muteButton, muted ? "volume-x" : "volume-2");
101
- this.muteButton.setAttribute("aria-label", muted ? "Unmute" : "Mute");
102
- const displayVolume = muted ? 0 : (this.videoEl.volume ?? 0);
103
- this.volumeInput.value = String(displayVolume);
104
- // Update track background for custom fill look
105
- const percent = displayVolume * 100;
106
- this.volumeInput.style.background = `linear-gradient(to right, var(--uvp-accent) 0%, var(--uvp-accent) ${percent}%, rgba(255, 255, 255, 0.2) ${percent}%, rgba(255, 255, 255, 0.2) 100%)`;
107
- };
108
- this.handleFullscreenChange = () => {
109
- const fsEl = document.fullscreenElement ||
110
- document.webkitFullscreenElement ||
111
- document.mozFullScreenElement ||
112
- document.msFullscreenElement;
113
- this.isFullscreen = Boolean(fsEl);
114
- this.setButtonIcon(this.fullscreenButton, this.isFullscreen ? "minimize" : "maximize");
115
- this.fullscreenButton.setAttribute("aria-label", this.isFullscreen ? "Exit fullscreen" : "Enter fullscreen");
116
- };
117
- this.isProcessingFullscreen = false;
118
- this.handleKeyDown = (event) => {
119
- // Ignore if user is typing in an input or textarea
120
- const target = event.target;
121
- if (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable) {
122
- return;
123
- }
124
- // Only handle shortcuts if the player is visible or was recently interacted with
125
- // For a more robust implementation, we could check if the player is in view.
126
- switch (event.code) {
127
- case "Space":
128
- event.preventDefault();
129
- if (this.videoEl.paused)
130
- void this.play();
131
- else
132
- this.pause();
133
- break;
134
- case "ArrowRight":
135
- event.preventDefault();
136
- this.seek(this.videoEl.currentTime + 10);
137
- break;
138
- case "ArrowLeft":
139
- event.preventDefault();
140
- this.seek(this.videoEl.currentTime - 10);
141
- break;
142
- case "ArrowUp":
143
- event.preventDefault();
144
- this.setVolume(this.videoEl.volume + 0.1);
145
- if (this.videoEl.muted)
146
- this.core.setMuted(false);
147
- this.handleVolumeChange();
148
- this.markInteraction();
149
- break;
150
- case "ArrowDown":
151
- event.preventDefault();
152
- this.setVolume(this.videoEl.volume - 0.1);
153
- this.handleVolumeChange();
154
- this.markInteraction();
155
- break;
156
- case "KeyF":
157
- event.preventDefault();
158
- this.toggleFullscreen();
159
- break;
160
- case "KeyM":
161
- event.preventDefault();
162
- this.core.setMuted(!this.videoEl.muted);
163
- this.handleVolumeChange();
164
- break;
165
- case "Escape":
166
- if (this.isFullscreen) {
167
- event.preventDefault();
168
- this.toggleFullscreen();
169
- }
170
- break;
171
- }
172
- };
173
- this.markInteraction = () => {
174
- if (this.isCustomControlsMode || !this.showControls)
175
- return;
176
- this.shellEl.classList.remove("ui-hidden");
177
- this.clearHideTimer();
178
- if (this.videoEl.paused)
179
- return;
180
- this.controlsHideTimer = window.setTimeout(() => {
181
- this.shellEl.classList.add("ui-hidden");
182
- this.closeSettingsMenu();
183
- }, 2200);
184
- };
185
- this.shadowRootRef = this.attachShadow({ mode: "open" });
186
- this.shadowRootRef.innerHTML = `
187
- <style>
188
- * {
189
- box-sizing: border-box;
190
- }
191
-
192
- :host {
193
- display: block;
194
- width: 100%;
195
- height: 100%;
196
- min-width: 100%;
197
- min-height: 100%;
198
- --uvp-bg: #2A2F35;
199
- --uvp-control-bg: rgba(96, 21, 107, 0.85);
200
- --uvp-text: #FFFFFF;
201
- --uvp-accent: #A400BC;
202
- --uvp-accent-2: #4664E1;
203
- --uvp-accent-3: #00D17F;
204
- --uvp-border: rgba(255, 255, 255, 0.15);
205
- --uvp-radius: 14px;
206
- }
207
-
208
- .shell {
209
- position: relative;
210
- width: 100%;
211
- height: 100%;
212
- background: #000;
213
- overflow: hidden;
214
- display: flex;
215
- align-items: center;
216
- justify-content: center;
217
- border-radius: 12px;
218
- box-shadow: 0 20px 50px rgba(0, 0, 0, 0.5);
219
- border: 1px solid rgba(255, 255, 255, 0.1);
220
- }
221
-
222
- :host(:fullscreen) .shell,
223
- :host(:-webkit-full-screen) .shell,
224
- :host(:-moz-full-screen) .shell,
225
- .shell:fullscreen,
226
- .shell:-webkit-full-screen,
227
- .shell:-moz-full-screen {
228
- width: 100vw;
229
- height: 100vh;
230
- border-radius: 0;
231
- border: none;
232
- max-width: none;
233
- max-height: none;
234
- }
235
-
236
- .settings-wrap {
237
- position: relative;
238
- display: flex;
239
- align-items: center;
240
- }
241
-
242
- .settings-menu {
243
- position: absolute;
244
- bottom: calc(100% + 12px);
245
- right: -20px;
246
- background: rgba(15, 15, 15, 0.98);
247
- border: 1px solid rgba(255, 255, 255, 0.1);
248
- border-radius: 12px;
249
- width: 220px;
250
- max-height: 350px;
251
- overflow-y: auto;
252
- padding: 6px;
253
- display: flex;
254
- flex-direction: column;
255
- z-index: 10;
256
- box-shadow: 0 10px 40px rgba(0, 0, 0, 0.6);
257
- transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.2s ease;
258
- transform-origin: bottom right;
259
- }
260
-
261
- .settings-menu.hidden {
262
- display: none;
263
- opacity: 0;
264
- transform: scale(0.95);
265
- }
266
-
267
- .menu-screen {
268
- display: flex;
269
- flex-direction: column;
270
- gap: 2px;
271
- }
272
-
273
- .menu-header {
274
- display: flex;
275
- align-items: center;
276
- padding: 6px 8px;
277
- border-bottom: 1px solid rgba(255, 255, 255, 0.1);
278
- margin-bottom: 4px;
279
- }
280
-
281
- .menu-back {
282
- background: transparent !important;
283
- border: none !important;
284
- color: var(--uvp-text);
285
- cursor: pointer;
286
- padding: 0;
287
- margin-right: 8px;
288
- display: flex;
289
- align-items: center;
290
- justify-content: center;
291
- border-radius: 50%;
292
- width: 24px;
293
- height: 24px;
294
- box-shadow: none !important;
295
- }
296
-
297
- .menu-back:hover {
298
- background: rgba(255, 255, 255, 0.1) !important;
299
- }
300
-
301
- .menu-title {
302
- font-weight: 600;
303
- font-size: 13px;
304
- }
305
-
306
- .settings-item {
307
- border: none !important;
308
- background: transparent !important;
309
- color: var(--uvp-text);
310
- border-radius: 6px;
311
- min-height: 32px;
312
- height: auto !important;
313
- display: flex;
314
- align-items: center;
315
- justify-content: space-between;
316
- padding: 0 8px;
317
- font: 12px/1 'Outfit', Inter, system-ui, sans-serif;
318
- transition: background 150ms ease;
319
- cursor: pointer;
320
- }
321
-
322
- .settings-item:hover {
323
- background: rgba(255, 255, 255, 0.1);
324
- }
325
-
326
- .settings-item .item-left {
327
- display: flex;
328
- align-items: center;
329
- gap: 10px;
330
- }
331
-
332
- .settings-item .item-right {
333
- display: flex;
334
- align-items: center;
335
- gap: 6px;
336
- color: rgba(255, 255, 255, 0.5);
337
- font-size: 12px;
338
- }
339
-
340
- .settings-item svg {
341
- opacity: 0.7;
342
- }
343
-
344
- /* Mobile Bottom Sheet */
345
-
346
-
347
- /* Toggle Switch Styles */
348
- .switch {
349
- position: relative;
350
- display: inline-block;
351
- width: 34px;
352
- height: 20px;
353
- pointer-events: none;
354
- }
355
-
356
- .switch input {
357
- opacity: 0;
358
- width: 0;
359
- height: 0;
360
- }
361
-
362
- .slider {
363
- position: absolute;
364
- cursor: pointer;
365
- top: 0;
366
- left: 0;
367
- right: 0;
368
- bottom: 0;
369
- background-color: rgba(255, 255, 255, 0.2);
370
- transition: .3s;
371
- border-radius: 34px;
372
- }
373
-
374
- .slider:before {
375
- position: absolute;
376
- content: "";
377
- height: 14px;
378
- width: 14px;
379
- left: 3px;
380
- bottom: 3px;
381
- background-color: white;
382
- transition: .3s;
383
- border-radius: 50%;
384
- }
385
-
386
- input:checked + .slider {
387
- background-color: var(--uvp-accent);
388
- }
389
-
390
- input:checked + .slider:before {
391
- transform: translateX(14px);
392
- }
393
-
394
- /* Playlist Panel */
395
- .playlist-panel {
396
- position: absolute;
397
- top: 0;
398
- right: 0;
399
- bottom: 0;
400
- width: 320px;
401
- background: #1a1a1a;
402
- border-left: 1px solid rgba(255, 255, 255, 0.1);
403
- z-index: 100;
404
- transform: translateX(100%);
405
- transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
406
- display: flex;
407
- flex-direction: column;
408
- box-shadow: -10px 0 30px rgba(0, 0, 0, 0.5);
409
- pointer-events: auto;
410
- }
411
-
412
- .playlist-panel.open {
413
- transform: translateX(0);
414
- }
415
-
416
- .playlist-header {
417
- padding: 20px;
418
- border-bottom: 1px solid rgba(255, 255, 255, 0.1);
419
- display: flex;
420
- align-items: center;
421
- justify-content: space-between;
422
- }
423
-
424
- .playlist-header h3 {
425
- margin: 0;
426
- font-size: 16px;
427
- font-weight: 600;
428
- color: var(--uvp-text);
429
- }
430
-
431
- .playlist-close {
432
- background: transparent;
433
- border: none;
434
- color: rgba(255, 255, 255, 0.5);
435
- cursor: pointer;
436
- padding: 4px;
437
- transition: color 0.2s;
438
- }
439
-
440
- .playlist-close:hover {
441
- color: var(--uvp-text);
442
- }
443
-
444
- .playlist-items {
445
- flex: 1;
446
- overflow-y: auto;
447
- padding: 8px 0;
448
- }
449
-
450
- /* Custom Scrollbar for Playlist */
451
- .playlist-items::-webkit-scrollbar {
452
- width: 6px;
453
- }
454
- .playlist-items::-webkit-scrollbar-track {
455
- background: transparent;
456
- }
457
- .playlist-items::-webkit-scrollbar-thumb {
458
- background: rgba(255, 255, 255, 0.1);
459
- border-radius: 3px;
460
- }
461
- .playlist-items::-webkit-scrollbar-thumb:hover {
462
- background: rgba(255, 255, 255, 0.2);
463
- }
464
-
465
- .playlist-item {
466
- display: flex;
467
- align-items: center;
468
- gap: 12px;
469
- padding: 12px 20px;
470
- cursor: pointer;
471
- transition: background 0.2s;
472
- border-left: 3px solid transparent;
473
- }
474
-
475
- .playlist-item:hover {
476
- background: rgba(255, 255, 255, 0.05);
477
- }
478
-
479
- .playlist-item.active {
480
- background: rgba(164, 0, 188, 0.15);
481
- border-left-color: var(--uvp-accent);
482
- }
483
-
484
- .playlist-item-thumb {
485
- width: 80px;
486
- height: 45px;
487
- background: #333;
488
- border-radius: 4px;
489
- overflow: hidden;
490
- flex-shrink: 0;
491
- position: relative;
492
- display: flex;
493
- align-items: center;
494
- justify-content: center;
495
- }
496
-
497
- .playlist-item-thumb::after {
498
- content: '';
499
- display: block;
500
- width: 20px;
501
- height: 20px;
502
- background: rgba(255, 255, 255, 0.1);
503
- border-radius: 50%;
504
- }
505
-
506
- .playlist-item-thumb img {
507
- position: absolute;
508
- top: 0;
509
- left: 0;
510
- width: 100%;
511
- height: 100%;
512
- object-fit: cover;
513
- z-index: 2;
514
- }
515
-
516
- .playlist-item-info {
517
- flex: 1;
518
- min-width: 0;
519
- }
520
-
521
- .playlist-item-title {
522
- font-size: 14px;
523
- font-weight: 500;
524
- color: var(--uvp-text);
525
- margin-bottom: 2px;
526
- white-space: nowrap;
527
- overflow: hidden;
528
- text-overflow: ellipsis;
529
- }
530
-
531
- .playlist-item-meta {
532
- font-size: 12px;
533
- color: rgba(255, 255, 255, 0.5);
534
- }
535
-
536
- .playlist-item.active .playlist-item-title {
537
- color: var(--uvp-accent);
538
- }
539
-
540
- video {
541
- width: 100%;
542
- height: 100%;
543
- display: block;
544
- background: #000;
545
- transition: object-fit 0.3s ease;
546
- }
547
-
548
- .shell.scale-fit video {
549
- object-fit: contain;
550
- }
551
- .shell.scale-fill video {
552
- object-fit: fill;
553
- }
554
- .shell.scale-zoom video {
555
- object-fit: cover;
556
- }
557
-
558
- .center-actions {
559
- position: absolute;
560
- left: 50%;
561
- top: 50%;
562
- transform: translate(-50%, -50%);
563
- display: flex;
564
- align-items: center;
565
- gap: 16px;
566
- transition: opacity 200ms ease;
567
- }
568
-
569
- .center-actions button {
570
- width: 64px;
571
- height: 64px;
572
- border-radius: 999px;
573
- border: none;
574
- background: transparent;
575
- color: #FFFFFF;
576
- display: grid;
577
- place-items: center;
578
- cursor: pointer;
579
- transition: transform 200ms ease, background 200ms ease, box-shadow 200ms ease;
580
- }
581
-
582
- .center-actions button:hover:not(:disabled) {
583
- transform: scale(1.1);
584
- background: rgba(255, 255, 255, 0.15);
585
- }
586
-
587
- .center-actions button:active:not(:disabled) {
588
- transform: scale(0.95);
589
- }
590
-
591
- .center-actions .center-play-pause {
592
- width: 80px;
593
- height: 80px;
594
- background: linear-gradient(135deg, #A400BC 0%, #60156B 100%);
595
- border: 2px solid rgba(255, 255, 255, 0.4);
596
- box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
597
- }
598
-
599
- .center-actions .center-play-pause:hover:not(:disabled) {
600
- background: linear-gradient(135deg, #D54DE8 0%, #A400BC 100%);
601
- box-shadow: 0 6px 20px rgba(164, 0, 188, 0.4);
602
- }
603
-
604
- .center-actions button svg,
605
- .icon-btn svg {
606
- width: 24px;
607
- height: 24px;
608
- stroke: currentColor;
609
- display: block;
610
- margin: 0 auto;
611
- }
612
-
613
- .center-actions .center-play-pause svg {
614
- width: 32px;
615
- height: 32px;
616
- }
617
-
618
- .center-actions button:disabled,
619
- .controls-row button:disabled {
620
- opacity: 0.35;
621
- cursor: not-allowed;
622
- pointer-events: none;
623
- }
624
-
625
- .controls {
626
- position: absolute;
627
- left: 0;
628
- right: 0;
629
- bottom: 0;
630
- padding: 16px;
631
- background: linear-gradient(to top, rgba(0, 0, 0, 0.75) 0%, rgba(0, 0, 0, 0.3) 70%, transparent 100%);
632
- display: flex;
633
- flex-direction: column;
634
- gap: 12px;
635
- transition: opacity 250ms ease;
636
- }
637
-
638
- .top-overlay {
639
- position: absolute;
640
- top: 0;
641
- left: 0;
642
- right: 0;
643
- height: 80px;
644
- background: linear-gradient(to bottom, rgba(0, 0, 0, 0.75) 0%, rgba(0, 0, 0, 0.3) 70%, transparent 100%);
645
- z-index: 6;
646
- transition: opacity 250ms ease;
647
- pointer-events: none;
648
- }
649
-
650
- .top-controls {
651
- position: absolute;
652
- top: 0;
653
- right: 0;
654
- padding: 12px 20px;
655
- display: flex;
656
- gap: 12px;
657
- z-index: 7;
658
- color: var(--uvp-text);
659
- transition: opacity 250ms ease;
660
- }
661
-
662
- .shell.ui-hidden .top-overlay,
663
- .shell.ui-hidden .top-controls {
664
- opacity: 0;
665
- pointer-events: none;
666
- }
667
-
668
- .shell.locked .controls,
669
- .shell.locked .playlist-panel,
670
- .shell.locked .settings-wrap,
671
- .shell.locked .playlist-toggle,
672
- .shell.locked .aspect-shortcut {
673
- display: none !important;
674
- }
675
-
676
- .controls-row {
677
- display: flex;
678
- align-items: center;
679
- gap: 12px;
680
- color: var(--uvp-text);
681
- font: 14px/1.2 'Outfit', Inter, system-ui, sans-serif;
682
- }
683
-
684
- .controls-row button,
685
- .controls-row select,
686
- .top-controls button {
687
- border-radius: 10px;
688
- border: 1px solid rgba(255, 255, 255, 0.2);
689
- background: rgba(96, 21, 107, 0.4);
690
- color: var(--uvp-text);
691
- height: 36px;
692
- padding: 0 12px;
693
- cursor: pointer;
694
- transition: all 200ms ease;
695
- }
696
-
697
- .icon-btn {
698
- width: 40px;
699
- min-width: 40px;
700
- height: 40px;
701
- border: none !important;
702
- background: transparent !important;
703
- box-shadow: none;
704
- padding: 0;
705
- display: inline-flex;
706
- align-items: center;
707
- justify-content: center;
708
- border-radius: 999px !important;
709
- }
710
-
711
- .icon-btn:hover {
712
- background: rgba(255, 255, 255, 0.15) !important;
713
- transform: translateY(-1px);
714
- }
715
-
716
- .icon-btn:active {
717
- transform: translateY(0);
718
- }
719
-
720
- .speed-wrap {
721
- position: relative;
722
- }
723
-
724
- .speed-button {
725
- border: none !important;
726
- background: transparent !important;
727
- color: var(--uvp-text);
728
- font-weight: 700;
729
- min-width: 40px;
730
- height: 36px !important;
731
- padding: 0 8px !important;
732
- border-radius: 8px !important;
733
- }
734
-
735
- .speed-button:hover {
736
- background: rgba(255, 255, 255, 0.15) !important;
737
- }
738
-
739
- .speed-list, .quality-list {
740
- display: flex;
741
- flex-direction: column;
742
- gap: 2px;
743
- padding: 4px;
744
- }
745
-
746
- .speed-item, .quality-item, .aspect-item {
747
- border: none !important;
748
- background: transparent !important;
749
- color: var(--uvp-text);
750
- border-radius: 6px;
751
- min-height: 32px;
752
- height: auto !important;
753
- display: flex;
754
- align-items: center;
755
- justify-content: space-between;
756
- padding: 0 8px;
757
- font: 12px/1 'Outfit', Inter, system-ui, sans-serif;
758
- transition: all 150ms ease;
759
- cursor: pointer;
760
- width: 100%;
761
- text-align: left;
762
- }
763
-
764
- .speed-item:hover, .quality-item:hover, .aspect-item:hover {
765
- background: rgba(255, 255, 255, 0.08) !important;
766
- }
767
-
768
- .speed-item.active, .quality-item.active, .aspect-item.active {
769
- color: var(--uvp-accent);
770
- font-weight: 600;
771
- background: rgba(164, 0, 188, 0.1) !important;
772
- }
773
-
774
- .speed-item .check, .quality-item .check, .aspect-item .check {
775
- width: 18px;
776
- height: 18px;
777
- display: inline-flex;
778
- align-items: center;
779
- justify-content: center;
780
- opacity: 0;
781
- flex-shrink: 0;
782
- stroke: currentColor;
783
- }
784
-
785
- .speed-item.active .check, .quality-item.active .check, .aspect-item.active .check {
786
- opacity: 1;
787
- }
788
-
789
- .spacer {
790
- flex: 1;
791
- }
792
-
793
- .controls-row input[type="range"] {
794
- accent-color: var(--uvp-accent);
795
- }
796
-
797
- .progress-wrap {
798
- position: relative;
799
- flex: 1;
800
- height: 20px;
801
- display: flex;
802
- align-items: center;
803
- overflow: visible;
804
- margin: 0 12px;
805
- }
806
-
807
- .progress-track,
808
- .progress-buffered,
809
- .progress-played {
810
- position: absolute;
811
- left: 0;
812
- right: 0;
813
- height: 6px;
814
- border-radius: 999px;
815
- }
816
-
817
- .progress-track {
818
- background: rgba(255, 255, 255, 0.2);
819
- z-index: 1;
820
- }
821
-
822
- .progress-buffered {
823
- width: 0%;
824
- right: auto;
825
- background: rgba(255, 255, 255, 0.4);
826
- transition: width 240ms linear;
827
- z-index: 2;
828
- }
829
-
830
- .progress-played {
831
- width: 0%;
832
- right: auto;
833
- background: linear-gradient(90deg, #A400BC 0%, #D54DE8 100%);
834
- transition: width 90ms linear;
835
- z-index: 3;
836
- }
837
-
838
-
839
- .progress-thumb {
840
- position: absolute;
841
- top: 50%;
842
- left: 0%;
843
- width: 16px;
844
- height: 16px;
845
- border-radius: 999px;
846
- background: #FFFFFF;
847
- box-shadow: 0 0 10px rgba(0, 0, 0, 0.3), 0 0 0 4px rgba(164, 0, 188, 0.3);
848
- transform: translate(-50%, -50%);
849
- transition: left 90ms linear, transform 150ms ease;
850
- z-index: 4;
851
- pointer-events: none;
852
- }
853
-
854
- /* Removed hover scaling from progress thumb */
855
-
856
- .progress {
857
- position: absolute;
858
- inset: 0;
859
- width: 100%;
860
- margin: 0;
861
- -webkit-appearance: none;
862
- appearance: none;
863
- background: transparent;
864
- cursor: pointer;
865
- z-index: 5;
866
- }
867
-
868
- .progress::-webkit-slider-runnable-track {
869
- height: 20px;
870
- background: transparent;
871
- }
872
-
873
- .progress::-webkit-slider-thumb {
874
- -webkit-appearance: none;
875
- appearance: none;
876
- width: 20px;
877
- height: 20px;
878
- background: transparent;
879
- }
880
-
881
- .volume {
882
- width: 70px;
883
- height: 4px;
884
- -webkit-appearance: none;
885
- appearance: none;
886
- background: rgba(255, 255, 255, 0.2);
887
- border-radius: 999px;
888
- outline: none;
889
- }
890
-
891
- .volume::-webkit-slider-thumb {
892
- -webkit-appearance: none;
893
- appearance: none;
894
- width: 12px;
895
- height: 12px;
896
- background: var(--uvp-accent);
897
- border-radius: 50%;
898
- cursor: pointer;
899
- border: 2px solid #fff;
900
- box-shadow: 0 0 5px rgba(0,0,0,0.3);
901
- transition: transform 150ms ease;
902
- }
903
-
904
- .volume::-moz-range-thumb {
905
- width: 12px;
906
- height: 12px;
907
- background: var(--uvp-accent);
908
- border-radius: 50%;
909
- cursor: pointer;
910
- border: 2px solid #fff;
911
- box-shadow: 0 0 5px rgba(0,0,0,0.3);
912
- }
913
-
914
- .spacer {
915
- flex: 1;
916
- }
917
-
918
- .time {
919
- min-width: 50px;
920
- text-align: center;
921
- font-variant-numeric: tabular-nums;
922
- font-weight: 500;
923
- }
924
-
925
- .hidden {
926
- display: none !important;
927
- }
928
-
929
- .shell.ui-hidden .controls,
930
- .shell.ui-hidden .center-actions {
931
- opacity: 0;
932
- pointer-events: none;
933
- }
934
-
935
- @media (max-width: 600px) {
936
- .settings-menu {
937
- position: fixed;
938
- bottom: 0;
939
- left: 0;
940
- right: 0;
941
- width: 100%;
942
- max-height: 70vh;
943
- border-radius: 24px 24px 0 0;
944
- padding: 16px 8px 32px 8px;
945
- transform-origin: bottom center;
946
- }
947
-
948
- .settings-menu.hidden {
949
- transform: translateY(100%);
950
- }
951
-
952
- .settings-wrap {
953
- position: static;
954
- }
955
-
956
- .settings-scrim {
957
- position: fixed;
958
- inset: 0;
959
- background: rgba(0, 0, 0, 0.6);
960
- backdrop-filter: blur(4px);
961
- z-index: 9;
962
- opacity: 0;
963
- pointer-events: none;
964
- transition: opacity 0.3s ease;
965
- }
966
-
967
- .settings-scrim.visible {
968
- opacity: 1;
969
- pointer-events: auto;
970
- }
971
-
972
- .mute, .volume {
973
- display: none !important;
974
- }
975
-
976
- .time {
977
- font-size: 11px;
978
- min-width: 36px;
979
- }
980
-
981
- .progress-wrap {
982
- margin: 0 12px;
983
- }
984
-
985
- .controls-row {
986
- gap: 0px;
987
- }
988
-
989
- .controls {
990
- gap: 4px;
991
- padding: 12px;
992
- }
993
-
994
- /* Mobile Playlist Bottom Sheet */
995
- .playlist-panel {
996
- position: fixed;
997
- bottom: 0;
998
- left: 0;
999
- right: 0;
1000
- top: auto;
1001
- width: 100%;
1002
- height: auto;
1003
- background: rgba(15, 15, 15, 0.98);
1004
- border-radius: 24px 24px 0 0;
1005
- transform: translateY(100%);
1006
- border-left: none;
1007
- border-top: 1px solid rgba(255, 255, 255, 0.1);
1008
- z-index: 1000;
1009
- }
1010
-
1011
- .playlist-panel.open {
1012
- transform: translateY(0);
1013
- }
1014
-
1015
- .playlist-header {
1016
- padding: 12px 20px;
1017
- border-bottom: 1px solid rgba(255, 255, 255, 0.05);
1018
- }
1019
-
1020
- .playlist-header h3 {
1021
- font-size: 14px;
1022
- }
1023
-
1024
- .playlist-items {
1025
- display: flex;
1026
- flex-direction: row;
1027
- overflow-x: auto;
1028
- padding: 16px 20px 24px 20px;
1029
- gap: 16px;
1030
- scroll-snap-type: x mandatory;
1031
- scroll-padding: 0 20px;
1032
- -webkit-overflow-scrolling: touch;
1033
- }
1034
-
1035
- .playlist-items::-webkit-scrollbar {
1036
- display: none;
1037
- }
1038
-
1039
- .playlist-item {
1040
- flex-direction: column;
1041
- width: 140px;
1042
- padding: 0;
1043
- background: transparent;
1044
- border-left: none;
1045
- border-bottom: 3px solid transparent;
1046
- align-items: flex-start;
1047
- gap: 8px;
1048
- flex-shrink: 0;
1049
- scroll-snap-align: start;
1050
- }
1051
-
1052
- .playlist-item.active {
1053
- background: transparent;
1054
- border-left-color: transparent;
1055
- border-bottom-color: var(--uvp-accent);
1056
- }
1057
-
1058
- .playlist-item-thumb {
1059
- width: 100%;
1060
- height: 78px;
1061
- border-radius: 8px;
1062
- }
1063
-
1064
- .playlist-item-info {
1065
- padding: 2px 4px;
1066
- }
1067
-
1068
- .playlist-item-title {
1069
- font-size: 12px;
1070
- white-space: normal;
1071
- display: -webkit-box;
1072
- -webkit-line-clamp: 2;
1073
- -webkit-box-orient: vertical;
1074
- overflow: hidden;
1075
- line-height: 1.3;
1076
- }
1077
-
1078
- .playlist-item-meta {
1079
- font-size: 10px;
1080
- margin-top: 2px;
1081
- }
1082
- }
1083
- </style>
1084
- <div class="shell">
1085
- <video part="video"></video>
1086
- <div class="top-overlay"></div>
1087
- <div class="top-controls">
1088
- <button class="aspect-shortcut icon-btn" part="aspect-shortcut" aria-label="Cycle Aspect Ratio"></button>
1089
- <button class="lock-button icon-btn" part="lock-button" aria-label="Lock controls"></button>
1090
- </div>
1091
- <div class="playlist-panel" part="playlist-panel">
1092
- <div class="playlist-header">
1093
- <h3>Playlist</h3>
1094
- <button class="playlist-close" aria-label="Close playlist"></button>
1095
- </div>
1096
- <div class="playlist-items" part="playlist-items"></div>
1097
- </div>
1098
- <div class="center-actions" part="center-actions">
1099
- <!-- Center buttons removed/moved to bottom bar -->
1100
- </div>
1101
- <div class="controls" part="controls">
1102
- <div class="controls-row">
1103
- <span class="time current-time">0:00</span>
1104
- <div class="progress-wrap" part="progress-wrap">
1105
- <div class="progress-track"></div>
1106
- <div class="progress-buffered"></div>
1107
- <div class="progress-played"></div>
1108
- <div class="progress-thumb"></div>
1109
- <input class="progress" part="progress" type="range" min="0" max="100" step="0.1" value="0" />
1110
- </div>
1111
- <span class="time duration">0:00</span>
1112
- </div>
1113
- <div class="controls-row">
1114
- <button class="control-prev icon-btn" part="control-prev" aria-label="Previous"></button>
1115
- <button class="control-play-pause icon-btn" part="control-play-pause" aria-label="Play"></button>
1116
- <button class="control-next icon-btn" part="control-next" aria-label="Next"></button>
1117
- <button part="mute" class="mute icon-btn" aria-label="Mute"></button>
1118
- <input class="volume" part="volume" type="range" min="0" max="1" step="0.05" value="1" />
1119
- <span class="spacer"></span>
1120
- <div class="settings-scrim"></div>
1121
- <div class="settings-wrap">
1122
- <button part="settings" class="settings-button icon-btn" aria-label="Settings" aria-expanded="false"></button>
1123
- <div class="settings-menu hidden" part="settings-menu">
1124
- <!-- Main Screen -->
1125
- <div class="menu-screen" data-screen="main">
1126
- <div class="settings-item" data-action="quality-screen">
1127
- <div class="item-left">
1128
- <span class="icon-quality"></span>
1129
- <span>Quality</span>
1130
- </div>
1131
- <div class="item-right">
1132
- <span class="current-quality-value">Auto</span>
1133
- <span class="icon-chevron-right"></span>
1134
- </div>
1135
- </div>
1136
- <div class="settings-item" data-action="speed-screen">
1137
- <div class="item-left">
1138
- <span class="icon-speed"></span>
1139
- <span>Playback Speed</span>
1140
- </div>
1141
- <div class="item-right">
1142
- <span class="current-speed-value">1x</span>
1143
- <span class="icon-chevron-right"></span>
1144
- </div>
1145
- </div>
1146
- <div class="settings-item" data-action="aspect-screen">
1147
- <div class="item-left">
1148
- <span class="icon-aspect"></span>
1149
- <span>Aspect Ratio</span>
1150
- </div>
1151
- <div class="item-right">
1152
- <span class="current-aspect-value">Fit</span>
1153
- <span class="icon-chevron-right"></span>
1154
- </div>
1155
- </div>
1156
- <div class="settings-item" data-action="autoplay">
1157
- <div class="item-left">
1158
- <span class="icon-play"></span>
1159
- <span>Auto Play</span>
1160
- </div>
1161
- <label class="switch">
1162
- <input type="checkbox" class="autoplay-toggle">
1163
- <span class="slider"></span>
1164
- </label>
1165
- </div>
1166
- <div class="settings-item" data-action="loop">
1167
- <div class="item-left">
1168
- <span class="icon-repeat"></span>
1169
- <span>Loop</span>
1170
- </div>
1171
- <label class="switch">
1172
- <input type="checkbox" class="loop-toggle">
1173
- <span class="slider"></span>
1174
- </label>
1175
- </div>
1176
- <div class="settings-item" data-action="download">
1177
- <div class="item-left">
1178
- <span class="download-icon"></span>
1179
- <span class="download-text">Download</span>
1180
- </div>
1181
- </div>
1182
- </div>
1183
-
1184
- <!-- Quality Screen -->
1185
- <div class="menu-screen hidden" data-screen="quality">
1186
- <div class="menu-header">
1187
- <button class="menu-back" data-target="main">
1188
- <span class="icon-arrow-left"></span>
1189
- </button>
1190
- <span class="menu-title">Quality</span>
1191
- </div>
1192
- <div class="quality-list"></div>
1193
- </div>
1194
-
1195
- <!-- Speed Screen -->
1196
- <div class="menu-screen hidden" data-screen="speed">
1197
- <div class="menu-header">
1198
- <button class="menu-back" data-target="main">
1199
- <span class="icon-arrow-left"></span>
1200
- </button>
1201
- <span class="menu-title">Playback Speed</span>
1202
- </div>
1203
- <div class="speed-list">
1204
- <button class="speed-item" data-rate="0.5"><span>0.5x</span><span class="check"></span></button>
1205
- <button class="speed-item active" data-rate="1"><span>1x</span><span class="check"></span></button>
1206
- <button class="speed-item" data-rate="1.5"><span>1.5x</span><span class="check"></span></button>
1207
- <button class="speed-item" data-rate="2"><span>2x</span><span class="check"></span></button>
1208
- </div>
1209
- </div>
1210
-
1211
- <!-- Aspect Screen -->
1212
- <div class="menu-screen hidden" data-screen="aspect">
1213
- <div class="menu-header">
1214
- <button class="menu-back" data-target="main">
1215
- <span class="icon-arrow-left"></span>
1216
- </button>
1217
- <span class="menu-title">Aspect Ratio</span>
1218
- </div>
1219
- <div class="aspect-list">
1220
- <button class="aspect-item" data-mode="fit"><span>Fit</span><span class="check"></span></button>
1221
- <button class="aspect-item" data-mode="fill"><span>Stretch</span><span class="check"></span></button>
1222
- <button class="aspect-item" data-mode="zoom"><span>Zoom</span><span class="check"></span></button>
1223
- </div>
1224
- </div>
1225
- </div>
1226
- </div>
1227
- <button part="playlist-toggle" class="playlist-toggle icon-btn hidden" aria-label="Show playlist"></button>
1228
- <button part="fullscreen" class="fullscreen icon-btn" aria-label="Enter fullscreen"></button>
1229
- </div>
1230
- </div>
1231
- <slot name="controls" class="hidden"></slot>
1232
- </div>
1233
- `;
1234
- this.shellEl = this.shadowRootRef.querySelector(".shell");
1235
- this.videoEl = this.shadowRootRef.querySelector("video");
1236
- this.centerActionsEl = this.shadowRootRef.querySelector(".center-actions");
1237
- this.controlPrevButton = this.shadowRootRef.querySelector(".control-prev");
1238
- this.controlPlayPauseButton = this.shadowRootRef.querySelector(".control-play-pause");
1239
- this.controlNextButton = this.shadowRootRef.querySelector(".control-next");
1240
- this.progressPlayedEl = this.shadowRootRef.querySelector(".progress-played");
1241
- this.progressBufferedEl = this.shadowRootRef.querySelector(".progress-buffered");
1242
- this.progressThumbEl = this.shadowRootRef.querySelector(".progress-thumb");
1243
- this.controlsEl = this.shadowRootRef.querySelector(".controls");
1244
- this.progressInput = this.shadowRootRef.querySelector(".progress");
1245
- this.currentTimeEl = this.shadowRootRef.querySelector(".current-time");
1246
- this.durationEl = this.shadowRootRef.querySelector(".duration");
1247
- this.muteButton = this.shadowRootRef.querySelector(".mute");
1248
- this.volumeInput = this.shadowRootRef.querySelector(".volume");
1249
- this.settingsScrim = this.shadowRootRef.querySelector(".settings-scrim");
1250
- this.settingsWrap = this.shadowRootRef.querySelector(".settings-wrap");
1251
- this.settingsButton = this.shadowRootRef.querySelector(".settings-button");
1252
- this.settingsMenu = this.shadowRootRef.querySelector(".settings-menu");
1253
- this.settingsMainScreen = this.shadowRootRef.querySelector('[data-screen="main"]');
1254
- this.settingsQualityScreen = this.shadowRootRef.querySelector('[data-screen="quality"]');
1255
- this.settingsAspectScreen = this.shadowRootRef.querySelector('[data-screen="aspect"]');
1256
- this.settingsDownloadScreen = this.shadowRootRef.querySelector('[data-screen="download"]');
1257
- this.fullscreenButton = this.shadowRootRef.querySelector(".fullscreen");
1258
- this.autoplayToggle = this.shadowRootRef.querySelector(".autoplay-toggle");
1259
- this.loopToggle = this.shadowRootRef.querySelector(".loop-toggle");
1260
- this.playlistButton = this.shadowRootRef.querySelector(".playlist-toggle");
1261
- this.playlistPanel = this.shadowRootRef.querySelector(".playlist-panel");
1262
- this.playlistCloseButton = this.shadowRootRef.querySelector(".playlist-close");
1263
- this.playlistItemsContainer = this.shadowRootRef.querySelector(".playlist-items");
1264
- this.lockButton = this.shadowRootRef.querySelector(".lock-button");
1265
- this.aspectShortcut = this.shadowRootRef.querySelector(".aspect-shortcut");
1266
- this.slotEl = this.shadowRootRef.querySelector('slot[name="controls"]');
1267
- this.isCustomControlsMode = this.readBooleanAttr("custom-controls", false);
1268
- this.showControls = this.readBooleanAttr("controls", true);
1269
- this.core = new UvpPlayerCore(this.readConfigFromAttributes());
1270
- this.core.attachMediaElement(this.videoEl);
1271
- this.bindUiEvents();
1272
- this.updateUiVisibility();
1273
- this.refreshDownloadUi();
1274
- }
1275
- connectedCallback() {
1276
- if (!this.core.getMediaElement()) {
1277
- this.core.attachMediaElement(this.videoEl);
1278
- }
1279
- const events = [
1280
- "ready",
1281
- "sourcechange",
1282
- "play",
1283
- "pause",
1284
- "timeupdate",
1285
- "ended",
1286
- "playlistend",
1287
- "qualitychange",
1288
- "downloadstatus",
1289
- "qualitiesdiscovered",
1290
- "error"
1291
- ];
1292
- this.cleanupCallbacks = events.map((eventName) => this.core.on(eventName, (detail) => {
1293
- if (eventName === "sourcechange") {
1294
- this.lastStableDuration = 0;
1295
- this.handlePlayStateChange();
1296
- }
1297
- this.dispatchEvent(new CustomEvent(eventName, {
1298
- detail,
1299
- bubbles: true,
1300
- composed: true
1301
- }));
1302
- }));
1303
- this.cleanupCallbacks.push(this.core.on("qualitiesdiscovered", () => {
1304
- this.refreshQualityUi();
1305
- }));
1306
- this.cleanupCallbacks.push(this.core.on("qualitychange", () => {
1307
- this.refreshQualityUi();
1308
- }));
1309
- this.cleanupCallbacks.push(this.core.on("scaleModeChange", () => {
1310
- this.refreshScaleModeUi();
1311
- }));
1312
- this.videoEl.addEventListener("timeupdate", this.handleVideoProgress);
1313
- this.videoEl.addEventListener("loadedmetadata", this.handleVideoProgress);
1314
- this.videoEl.addEventListener("progress", this.handleVideoProgress);
1315
- this.videoEl.addEventListener("play", this.handlePlayStateChange);
1316
- this.videoEl.addEventListener("pause", this.handlePlayStateChange);
1317
- this.videoEl.addEventListener("volumechange", this.handleVolumeChange);
1318
- this.videoEl.addEventListener("loadedmetadata", this.handleQualityUiRefresh);
1319
- document.addEventListener("fullscreenchange", this.handleFullscreenChange);
1320
- document.addEventListener("webkitfullscreenchange", this.handleFullscreenChange);
1321
- document.addEventListener("mozfullscreenchange", this.handleFullscreenChange);
1322
- document.addEventListener("MSFullscreenChange", this.handleFullscreenChange);
1323
- this.handlePlayStateChange();
1324
- this.handleVideoProgress();
1325
- this.handleVolumeChange();
1326
- this.handleFullscreenChange();
1327
- const config = this.core.getConfig();
1328
- this.autoplayToggle.checked = Boolean(config.autoplay || config.autoNext);
1329
- this.loopToggle.checked = Boolean(config.loop);
1330
- this.renderStaticIcons();
1331
- this.refreshPlaylistUi();
1332
- this.refreshQualityUi();
1333
- this.refreshScaleModeUi();
1334
- this.core.on("sourcechange", () => {
1335
- this.updateNavigationButtons();
1336
- this.refreshQualityUi();
1337
- this.refreshPlaylistUi();
1338
- this.updateUiVisibility();
1339
- this.refreshDownloadUi();
1340
- });
1341
- this.markInteraction();
1342
- document.addEventListener("keydown", this.handleKeyDown);
1343
- document.addEventListener("click", this.handleDocumentClick);
1344
- }
1345
- refreshPlaylistUi() {
1346
- const playlist = this.core.getPlaylist();
1347
- const currentIndex = this.core.getCurrentIndex();
1348
- this.playlistButton.classList.toggle("hidden", playlist.length <= 1);
1349
- this.playlistItemsContainer.innerHTML = "";
1350
- playlist.forEach((item, index) => {
1351
- const el = document.createElement("div");
1352
- el.className = `playlist-item ${index === currentIndex ? "active" : ""}`;
1353
- el.innerHTML = `
1354
- <div class="playlist-item-thumb">
1355
- ${item.poster ? `<img src="${item.poster}" alt="">` : ""}
1356
- </div>
1357
- <div class="playlist-item-info">
1358
- <div class="playlist-item-title">${item.title || `Video ${index + 1}`}</div>
1359
- <div class="playlist-item-meta">${item.type.toUpperCase()}</div>
1360
- </div>
1361
- `;
1362
- el.addEventListener("click", () => {
1363
- this.core.goTo(index);
1364
- this.playlistPanel.classList.remove("open");
1365
- this.markInteraction();
1366
- });
1367
- this.playlistItemsContainer.appendChild(el);
1368
- });
1369
- }
1370
- disconnectedCallback() {
1371
- document.removeEventListener("keydown", this.handleKeyDown);
1372
- document.removeEventListener("click", this.handleDocumentClick);
1373
- this.cleanupCallbacks.forEach((cleanup) => cleanup());
1374
- this.cleanupCallbacks = [];
1375
- this.videoEl.removeEventListener("timeupdate", this.handleVideoProgress);
1376
- this.videoEl.removeEventListener("loadedmetadata", this.handleVideoProgress);
1377
- this.videoEl.removeEventListener("progress", this.handleVideoProgress);
1378
- this.videoEl.removeEventListener("play", this.handlePlayStateChange);
1379
- this.videoEl.removeEventListener("pause", this.handlePlayStateChange);
1380
- this.videoEl.removeEventListener("volumechange", this.handleVolumeChange);
1381
- this.videoEl.removeEventListener("loadedmetadata", this.handleQualityUiRefresh);
1382
- document.removeEventListener("fullscreenchange", this.handleFullscreenChange);
1383
- document.removeEventListener("webkitfullscreenchange", this.handleFullscreenChange);
1384
- document.removeEventListener("mozfullscreenchange", this.handleFullscreenChange);
1385
- document.removeEventListener("MSFullscreenChange", this.handleFullscreenChange);
1386
- this.shellEl.removeEventListener("mousemove", this.markInteraction);
1387
- this.shellEl.removeEventListener("click", this.markInteraction);
1388
- this.shellEl.removeEventListener("touchstart", this.markInteraction);
1389
- this.shellEl.removeEventListener("mouseenter", this.markInteraction);
1390
- this.clearHideTimer();
1391
- this.stopProgressLoop();
1392
- this.core.detachMediaElement();
1393
- }
1394
- attributeChangedCallback(name, _oldValue, newValue) {
1395
- if (name === "scale-mode") {
1396
- const mode = newValue;
1397
- this.core.setScaleMode(mode);
1398
- return;
1399
- }
1400
- this.applyAttributeToCore(name);
1401
- }
1402
- /**
1403
- * Starts video playback.
1404
- * @returns A promise that resolves when playback starts.
1405
- */
1406
- play() {
1407
- return this.core.play();
1408
- }
1409
- /**
1410
- * Pauses video playback.
1411
- */
1412
- pause() {
1413
- this.core.pause();
1414
- }
1415
- /**
1416
- * Seeks to a specific time in the video.
1417
- * @param seconds - The time in seconds to seek to.
1418
- */
1419
- seek(seconds) {
1420
- this.core.seek(seconds);
1421
- }
1422
- /**
1423
- * Plays the next item in the playlist.
1424
- */
1425
- next() {
1426
- this.core.next();
1427
- }
1428
- /**
1429
- * Plays the previous item in the playlist.
1430
- */
1431
- previous() {
1432
- this.core.previous();
1433
- }
1434
- /**
1435
- * Sets the playback rate (speed) of the video.
1436
- * @param rate - The playback rate (e.g., 1.0, 1.5, 2.0).
1437
- */
1438
- setPlaybackRate(rate) {
1439
- this.core.setPlaybackRate(rate);
1440
- }
1441
- /**
1442
- * Sets the volume of the video.
1443
- * @param value - The volume level (0.0 to 1.0).
1444
- */
1445
- setVolume(value) {
1446
- this.core.setVolume(value);
1447
- }
1448
- /**
1449
- * Returns the underlying player core instance for advanced control.
1450
- * @returns The UvpPlayerCore instance.
1451
- */
1452
- getApi() {
1453
- return this.core;
1454
- }
1455
- readConfigFromAttributes() {
1456
- return {
1457
- source: this.buildSourceFromAttributes(),
1458
- autoplay: this.hasAttribute("autoplay"),
1459
- muted: this.hasAttribute("muted"),
1460
- poster: this.getAttribute("poster") ?? undefined,
1461
- controls: false,
1462
- autoNext: this.readBooleanAttr("auto-next", false),
1463
- loop: this.readBooleanAttr("loop", false),
1464
- playbackRate: this.readNumberAttr("playback-rate", 1),
1465
- volume: this.readNumberAttr("volume", 1),
1466
- scaleMode: this.getAttribute("scale-mode") ?? "fit"
1467
- };
1468
- }
1469
- buildSourceFromAttributes() {
1470
- const url = this.getAttribute("source-url");
1471
- if (!url)
1472
- return undefined;
1473
- const type = this.getAttribute("source-type") ?? "mp4";
1474
- const qualities = this.parseSourceQualitiesAttr();
1475
- return {
1476
- id: "single",
1477
- url,
1478
- type,
1479
- ...(qualities.length > 0 ? { qualities } : {})
1480
- };
1481
- }
1482
- parseSourceQualitiesAttr() {
1483
- const raw = this.getAttribute("source-qualities");
1484
- if (!raw?.trim())
1485
- return [];
1486
- try {
1487
- const parsed = JSON.parse(raw);
1488
- if (!Array.isArray(parsed))
1489
- return [];
1490
- const out = [];
1491
- for (const entry of parsed) {
1492
- if (!entry || typeof entry !== "object")
1493
- continue;
1494
- const obj = entry;
1495
- const id = typeof obj.id === "string" ? obj.id : "";
1496
- if (!id)
1497
- continue;
1498
- const label = typeof obj.label === "string"
1499
- ? obj.label
1500
- : typeof obj.height === "number" && Number.isFinite(obj.height)
1501
- ? `${obj.height}p`
1502
- : id;
1503
- const item = { id, label };
1504
- if (typeof obj.height === "number" && Number.isFinite(obj.height)) {
1505
- item.height = obj.height;
1506
- }
1507
- if (typeof obj.bitrateKbps === "number" && Number.isFinite(obj.bitrateKbps)) {
1508
- item.bitrateKbps = obj.bitrateKbps;
1509
- }
1510
- out.push(item);
1511
- }
1512
- return out;
1513
- }
1514
- catch {
1515
- return [];
1516
- }
1517
- }
1518
- applyAttributeToCore(name) {
1519
- switch (name) {
1520
- case "source-url":
1521
- case "source-type":
1522
- case "source-qualities":
1523
- this.core.updateConfig({
1524
- source: this.buildSourceFromAttributes()
1525
- });
1526
- this.updateNavigationButtons();
1527
- this.refreshQualityUi();
1528
- break;
1529
- case "playlist":
1530
- this.core.updateConfig({
1531
- playlist: this.parsePlaylistAttr()
1532
- });
1533
- this.updateNavigationButtons();
1534
- this.refreshQualityUi();
1535
- this.refreshPlaylistUi();
1536
- break;
1537
- case "autoplay":
1538
- this.core.updateConfig({ autoplay: this.hasAttribute("autoplay") });
1539
- break;
1540
- case "muted":
1541
- this.core.updateConfig({ muted: this.hasAttribute("muted") });
1542
- break;
1543
- case "poster":
1544
- this.core.updateConfig({ poster: this.getAttribute("poster") ?? "" });
1545
- break;
1546
- case "controls":
1547
- this.showControls = this.readBooleanAttr("controls", true);
1548
- this.updateUiVisibility();
1549
- break;
1550
- case "auto-next":
1551
- this.core.updateConfig({ autoNext: this.readBooleanAttr("auto-next", false) });
1552
- this.updateNavigationButtons();
1553
- break;
1554
- case "loop":
1555
- this.core.updateConfig({ loop: this.readBooleanAttr("loop", false) });
1556
- this.updateNavigationButtons();
1557
- break;
1558
- case "playback-rate":
1559
- this.core.setPlaybackRate(this.readNumberAttr("playback-rate", 1));
1560
- this.updateSpeedUi(this.readNumberAttr("playback-rate", 1));
1561
- break;
1562
- case "volume":
1563
- this.core.setVolume(this.readNumberAttr("volume", 1));
1564
- break;
1565
- case "custom-controls":
1566
- this.isCustomControlsMode = this.readBooleanAttr("custom-controls", false);
1567
- this.updateUiVisibility();
1568
- break;
1569
- default:
1570
- break;
1571
- }
1572
- }
1573
- readBooleanAttr(name, fallback) {
1574
- if (!this.hasAttribute(name))
1575
- return fallback;
1576
- const value = this.getAttribute(name);
1577
- if (value === "" || value === "true")
1578
- return true;
1579
- if (value === "false")
1580
- return false;
1581
- return fallback;
1582
- }
1583
- readNumberAttr(name, fallback) {
1584
- const value = this.getAttribute(name);
1585
- if (value == null)
1586
- return fallback;
1587
- const parsed = Number(value);
1588
- return Number.isFinite(parsed) ? parsed : fallback;
1589
- }
1590
- bindUiEvents() {
1591
- this.controlPlayPauseButton.addEventListener("click", () => {
1592
- if (this.videoEl.paused)
1593
- void this.play();
1594
- else
1595
- this.pause();
1596
- this.markInteraction();
1597
- });
1598
- this.controlPrevButton.addEventListener("click", () => {
1599
- if (this.controlPrevButton.disabled)
1600
- return;
1601
- this.previous();
1602
- this.updateNavigationButtons();
1603
- this.markInteraction();
1604
- });
1605
- this.controlNextButton.addEventListener("click", () => {
1606
- if (this.controlNextButton.disabled)
1607
- return;
1608
- this.next();
1609
- this.updateNavigationButtons();
1610
- this.markInteraction();
1611
- });
1612
- this.progressInput.addEventListener("input", () => {
1613
- this.isUserSeeking = true;
1614
- const percent = Number(this.progressInput.value);
1615
- const duration = this.videoEl.duration || 0;
1616
- this.currentTimeEl.textContent = this.formatTime((percent / 100) * duration);
1617
- this.updateProgressVisual(percent);
1618
- this.markInteraction();
1619
- });
1620
- this.progressInput.addEventListener("change", () => {
1621
- const duration = this.videoEl.duration || 0;
1622
- if (duration > 0) {
1623
- const percent = Number(this.progressInput.value);
1624
- this.seek((percent / 100) * duration);
1625
- }
1626
- this.isUserSeeking = false;
1627
- this.markInteraction();
1628
- });
1629
- this.muteButton.addEventListener("click", () => {
1630
- this.core.setMuted(!this.videoEl.muted);
1631
- this.handleVolumeChange();
1632
- this.markInteraction();
1633
- });
1634
- this.volumeInput.addEventListener("input", () => {
1635
- const value = Number(this.volumeInput.value);
1636
- this.setVolume(value);
1637
- if (value > 0 && this.videoEl.muted)
1638
- this.core.setMuted(false);
1639
- this.handleVolumeChange();
1640
- this.markInteraction();
1641
- });
1642
- this.fullscreenButton.addEventListener("click", () => {
1643
- this.toggleFullscreen();
1644
- });
1645
- this.lockButton.addEventListener("click", (event) => {
1646
- event.stopPropagation();
1647
- this.isLocked = !this.isLocked;
1648
- this.shellEl.classList.toggle("locked", this.isLocked);
1649
- this.refreshLockButtonIcon();
1650
- this.markInteraction();
1651
- });
1652
- this.aspectShortcut.addEventListener("click", (event) => {
1653
- event.stopPropagation();
1654
- const currentMode = this.core.getScaleMode();
1655
- const modes = ["fit", "fill", "zoom"];
1656
- const nextIndex = (modes.indexOf(currentMode) + 1) % modes.length;
1657
- this.core.setScaleMode(modes[nextIndex]);
1658
- this.markInteraction();
1659
- });
1660
- this.settingsButton.addEventListener("click", (event) => {
1661
- event.stopPropagation();
1662
- const isHidden = this.settingsMenu.classList.contains("hidden");
1663
- if (isHidden) {
1664
- this.openSettingsMenu();
1665
- this.playlistPanel.classList.remove("open");
1666
- }
1667
- else {
1668
- this.closeSettingsMenu();
1669
- }
1670
- this.markInteraction();
1671
- });
1672
- this.settingsScrim.addEventListener("click", () => {
1673
- this.closeSettingsMenu();
1674
- this.playlistPanel.classList.remove("open");
1675
- });
1676
- // Screen Transitions
1677
- this.settingsMenu.querySelectorAll(".settings-item[data-action]").forEach((item) => {
1678
- item.addEventListener("click", (event) => {
1679
- event.stopPropagation();
1680
- const action = item.dataset.action;
1681
- if (action === "quality-screen") {
1682
- this.switchMenuScreen("quality");
1683
- }
1684
- else if (action === "speed-screen") {
1685
- this.switchMenuScreen("speed");
1686
- }
1687
- else if (action === "aspect-screen") {
1688
- this.switchMenuScreen("aspect");
1689
- }
1690
- else if (action === "autoplay") {
1691
- const newState = !this.autoplayToggle.checked;
1692
- this.autoplayToggle.checked = newState;
1693
- this.core.updateConfig({ autoplay: newState, autoNext: newState });
1694
- }
1695
- else if (action === "loop") {
1696
- const newState = !this.loopToggle.checked;
1697
- this.loopToggle.checked = newState;
1698
- this.core.updateConfig({ loop: newState });
1699
- }
1700
- else if (action === "download") {
1701
- this.handleDownloadAction(item);
1702
- }
1703
- this.markInteraction();
1704
- });
1705
- });
1706
- this.settingsMenu.querySelectorAll(".menu-back").forEach((btn) => {
1707
- btn.addEventListener("click", (event) => {
1708
- event.stopPropagation();
1709
- const target = btn.dataset.target || "main";
1710
- this.switchMenuScreen(target);
1711
- this.markInteraction();
1712
- });
1713
- });
1714
- // Speed Selection
1715
- this.settingsMenu.querySelectorAll(".speed-item").forEach((item) => {
1716
- item.addEventListener("click", (event) => {
1717
- event.stopPropagation();
1718
- const rate = Number(item.dataset.rate);
1719
- this.setPlaybackRate(rate);
1720
- this.updateSpeedUi(rate);
1721
- this.closeSettingsMenu();
1722
- this.markInteraction();
1723
- });
1724
- });
1725
- // Aspect Selection
1726
- this.settingsMenu.querySelectorAll(".aspect-item").forEach((item) => {
1727
- item.addEventListener("click", (event) => {
1728
- event.stopPropagation();
1729
- const mode = item.dataset.mode;
1730
- this.core.setScaleMode(mode);
1731
- this.closeSettingsMenu();
1732
- this.markInteraction();
1733
- });
1734
- });
1735
- this.playlistButton.addEventListener("click", (event) => {
1736
- event.stopPropagation();
1737
- const isOpen = this.playlistPanel.classList.toggle("open");
1738
- if (isOpen) {
1739
- this.closeSettingsMenu();
1740
- }
1741
- this.settingsScrim.classList.toggle("visible", isOpen);
1742
- this.markInteraction();
1743
- });
1744
- this.playlistCloseButton.addEventListener("click", (event) => {
1745
- event.stopPropagation();
1746
- this.playlistPanel.classList.remove("open");
1747
- this.settingsScrim.classList.remove("visible");
1748
- this.markInteraction();
1749
- });
1750
- this.shadowRootRef.addEventListener("click", (event) => {
1751
- const target = event.target;
1752
- const isInsideSettings = this.settingsWrap.contains(target);
1753
- const isInsidePlaylist = this.playlistPanel.contains(target) || this.playlistButton.contains(target);
1754
- const isInsideScrim = this.settingsScrim.contains(target);
1755
- if (!isInsideSettings || isInsideScrim) {
1756
- this.closeSettingsMenu();
1757
- }
1758
- if (!isInsidePlaylist) {
1759
- this.playlistPanel.classList.remove("open");
1760
- }
1761
- });
1762
- this.videoEl.addEventListener("ratechange", () => {
1763
- this.updateSpeedUi(this.videoEl.playbackRate || 1);
1764
- this.markInteraction();
1765
- });
1766
- this.fullscreenButton.addEventListener("click", () => {
1767
- this.toggleFullscreen();
1768
- });
1769
- this.shellEl.addEventListener("mousemove", this.markInteraction);
1770
- this.shellEl.addEventListener("click", this.markInteraction);
1771
- this.shellEl.addEventListener("touchstart", this.markInteraction);
1772
- this.shellEl.addEventListener("mouseenter", this.markInteraction);
1773
- this.shadowRootRef.querySelectorAll(".settings-item").forEach((item) => {
1774
- item.addEventListener("click", () => {
1775
- const action = item.dataset.action;
1776
- if (action === "quality-screen")
1777
- this.switchMenuScreen("quality");
1778
- if (action === "speed-screen")
1779
- this.switchMenuScreen("speed");
1780
- if (action === "aspect-screen")
1781
- this.switchMenuScreen("aspect");
1782
- if (action === "download-screen")
1783
- this.switchMenuScreen("download");
1784
- });
1785
- });
1786
- this.updateNavigationButtons();
1787
- }
1788
- openSettingsMenu() {
1789
- this.refreshQualityUi();
1790
- this.refreshScaleModeUi();
1791
- this.refreshDownloadUi();
1792
- this.switchMenuScreen("main");
1793
- this.settingsMenu.classList.remove("hidden");
1794
- this.settingsButton.setAttribute("aria-expanded", "true");
1795
- this.settingsScrim.classList.add("visible");
1796
- }
1797
- closeSettingsMenu() {
1798
- this.settingsMenu.classList.add("hidden");
1799
- this.settingsButton.setAttribute("aria-expanded", "false");
1800
- this.settingsScrim.classList.remove("visible");
1801
- }
1802
- switchMenuScreen(screenId) {
1803
- this.settingsMenu.querySelectorAll(".menu-screen").forEach((screen) => {
1804
- const isTarget = screen.dataset.screen === screenId;
1805
- screen.classList.toggle("hidden", !isTarget);
1806
- });
1807
- }
1808
- handleDownloadAction(item) {
1809
- const source = this.core.getCurrentSource();
1810
- if (!source)
1811
- return;
1812
- if (source.type !== "mp4" && source.type !== "mov" && source.type !== "hls" && source.type !== "dash") {
1813
- alert("Downloading is only supported for MP4, MOV, HLS, and DASH formats.");
1814
- return;
1815
- }
1816
- const isDownloading = item.dataset.status === "downloading";
1817
- const isDownloaded = item.dataset.status === "downloaded";
1818
- if (isDownloading)
1819
- return;
1820
- if (isDownloaded) {
1821
- removeCachedVideo(source.id).then(() => {
1822
- this.refreshDownloadUi();
1823
- });
1824
- }
1825
- else {
1826
- item.dataset.status = "downloading";
1827
- const textEl = item.querySelector(".download-text");
1828
- if (textEl)
1829
- textEl.textContent = "Downloading 0%";
1830
- cacheVideo(source.id, source.url, (data) => {
1831
- if (data.status === "in_progress") {
1832
- if (textEl)
1833
- textEl.textContent = `Downloading ${Math.round(data.progress || 0)}%`;
1834
- }
1835
- else if (data.status === "completed") {
1836
- item.dataset.status = "none";
1837
- this.refreshDownloadUi();
1838
- // Optional: Auto-switch source logic can be added here
1839
- }
1840
- else if (data.status === "error") {
1841
- if (textEl)
1842
- textEl.textContent = "Download failed";
1843
- setTimeout(() => {
1844
- item.dataset.status = "none";
1845
- this.refreshDownloadUi();
1846
- }, 2000);
1847
- }
1848
- }, source.type);
1849
- }
1850
- }
1851
- refreshDownloadUi() {
1852
- const source = this.core.getCurrentSource();
1853
- const downloadItem = this.settingsMenu.querySelector('[data-action="download"]');
1854
- if (!downloadItem)
1855
- return;
1856
- if (!source || (source.type !== "mp4" && source.type !== "mov" && source.type !== "hls" && source.type !== "dash")) {
1857
- downloadItem.style.opacity = "0.5";
1858
- downloadItem.style.pointerEvents = "none";
1859
- const textEl = downloadItem.querySelector(".download-text");
1860
- if (textEl)
1861
- textEl.textContent = "Download (Unsupported)";
1862
- return;
1863
- }
1864
- downloadItem.style.opacity = "1";
1865
- downloadItem.style.pointerEvents = "auto";
1866
- const textEl = downloadItem.querySelector(".download-text");
1867
- const iconEl = downloadItem.querySelector(".download-icon");
1868
- if (downloadItem.dataset.status === "downloading") {
1869
- // Don't overwrite downloading state
1870
- return;
1871
- }
1872
- isCached(source.id).then((cached) => {
1873
- // Re-check if source is still the same after async call
1874
- if (this.core.getCurrentSource()?.id !== source.id)
1875
- return;
1876
- if (cached) {
1877
- downloadItem.dataset.status = "downloaded";
1878
- if (textEl)
1879
- textEl.textContent = "Remove Download";
1880
- if (iconEl) {
1881
- iconEl.innerHTML = icons.trash.toSvg({ width: "16", height: "16", "stroke-width": "2.5" });
1882
- }
1883
- }
1884
- else {
1885
- downloadItem.dataset.status = "none";
1886
- if (textEl)
1887
- textEl.textContent = "Download";
1888
- if (iconEl) {
1889
- iconEl.innerHTML = icons.download.toSvg({ width: "16", height: "16", "stroke-width": "2.5" });
1890
- }
1891
- }
1892
- });
1893
- }
1894
- updateUiVisibility() {
1895
- const useCustom = this.isCustomControlsMode;
1896
- const controlsEnabled = this.showControls;
1897
- this.videoEl.controls = false;
1898
- this.controlsEl.classList.toggle("hidden", useCustom || !controlsEnabled);
1899
- this.centerActionsEl.classList.toggle("hidden", useCustom || !controlsEnabled);
1900
- this.slotEl.classList.toggle("hidden", !useCustom || !controlsEnabled);
1901
- if (useCustom || !controlsEnabled) {
1902
- this.shellEl.classList.remove("ui-hidden");
1903
- this.clearHideTimer();
1904
- }
1905
- else {
1906
- this.markInteraction();
1907
- }
1908
- }
1909
- toggleFullscreen() {
1910
- if (this.isProcessingFullscreen)
1911
- return;
1912
- this.isProcessingFullscreen = true;
1913
- setTimeout(() => {
1914
- this.isProcessingFullscreen = false;
1915
- }, 1000);
1916
- const doc = document;
1917
- const el = this.shellEl;
1918
- if (this.isFullscreen) {
1919
- if (document.exitFullscreen) {
1920
- void document.exitFullscreen();
1921
- }
1922
- else if (doc.webkitExitFullscreen) {
1923
- doc.webkitExitFullscreen();
1924
- }
1925
- else if (doc.mozCancelFullScreen) {
1926
- doc.mozCancelFullScreen();
1927
- }
1928
- else if (doc.msExitFullscreen) {
1929
- doc.msExitFullscreen();
1930
- }
1931
- }
1932
- else {
1933
- try {
1934
- if (el.requestFullscreen) {
1935
- void el.requestFullscreen();
1936
- }
1937
- else if (el.webkitRequestFullscreen) {
1938
- el.webkitRequestFullscreen();
1939
- }
1940
- else if (el.mozRequestFullScreen) {
1941
- el.mozRequestFullScreen();
1942
- }
1943
- else if (el.msRequestFullscreen) {
1944
- el.msRequestFullscreen();
1945
- }
1946
- }
1947
- catch (err) {
1948
- console.error("Fullscreen request failed:", err);
1949
- }
1950
- }
1951
- this.markInteraction();
1952
- }
1953
- formatTime(seconds) {
1954
- if (!Number.isFinite(seconds) || seconds <= 0)
1955
- return "0:00";
1956
- const total = Math.floor(seconds);
1957
- const mins = Math.floor(total / 60);
1958
- const secs = total % 60;
1959
- return `${mins}:${String(secs).padStart(2, "0")}`;
1960
- }
1961
- clearHideTimer() {
1962
- if (this.controlsHideTimer == null)
1963
- return;
1964
- window.clearTimeout(this.controlsHideTimer);
1965
- this.controlsHideTimer = null;
1966
- }
1967
- updateProgressVisual(percent) {
1968
- const clamped = Math.max(0, Math.min(100, percent));
1969
- this.progressPlayedEl.style.width = `${clamped}%`;
1970
- this.progressThumbEl.style.left = `${clamped}%`;
1971
- }
1972
- updateNavigationButtons() {
1973
- const canPrev = this.core.canGoPrevious();
1974
- const canNext = this.core.canGoNext();
1975
- this.controlPrevButton.disabled = !canPrev;
1976
- this.controlNextButton.disabled = !canNext;
1977
- }
1978
- parsePlaylistAttr() {
1979
- const raw = this.getAttribute("playlist");
1980
- if (!raw)
1981
- return [];
1982
- try {
1983
- const parsed = JSON.parse(raw);
1984
- if (!Array.isArray(parsed))
1985
- return [];
1986
- return parsed.map((item) => ({
1987
- ...item,
1988
- id: item.id || Math.random().toString(36).substring(7)
1989
- }));
1990
- }
1991
- catch {
1992
- return [];
1993
- }
1994
- }
1995
- refreshQualityUi() {
1996
- const qualities = this.core.getQualities();
1997
- const currentQuality = this.core.getCurrentQuality();
1998
- const currentQualityLabel = currentQuality?.label || "Auto";
1999
- // Update overlay value text
2000
- const valueEl = this.settingsMenu.querySelector(".current-quality-value");
2001
- if (valueEl)
2002
- valueEl.textContent = currentQualityLabel;
2003
- const listEl = this.settingsMenu.querySelector(".quality-list");
2004
- if (!listEl)
2005
- return;
2006
- listEl.innerHTML = "";
2007
- const createItem = (id, label) => {
2008
- const el = document.createElement("button");
2009
- const isActive = id === (currentQuality?.id || null);
2010
- el.className = `quality-item ${isActive ? "active" : ""}`;
2011
- el.innerHTML = `<span>${label}</span><span class="check"></span>`;
2012
- const checkEl = el.querySelector(".check");
2013
- if (checkEl) {
2014
- checkEl.innerHTML = icons.check.toSvg({ width: "16", height: "16", "stroke-width": "2.5" });
2015
- }
2016
- el.addEventListener("click", (event) => {
2017
- event.stopPropagation();
2018
- this.core.setSelectedQuality(id);
2019
- this.closeSettingsMenu();
2020
- this.markInteraction();
2021
- });
2022
- return el;
2023
- };
2024
- // Always add Auto
2025
- listEl.appendChild(createItem(null, "Auto"));
2026
- qualities.forEach((q) => {
2027
- const label = q.label || (q.height ? `${q.height}p` : q.id);
2028
- listEl.appendChild(createItem(q.id, label));
2029
- });
2030
- }
2031
- renderAspectMenu() {
2032
- this.settingsAspectScreen.innerHTML = `
2033
- <div class="settings-header">
2034
- <button class="back-button">Back</button>
2035
- <span>Aspect Ratio</span>
2036
- </div>
2037
- <div class="settings-options">
2038
- <div class="settings-option" data-mode="fit">
2039
- <span>Fit</span>
2040
- <span class="check-mark"></span>
2041
- </div>
2042
- <div class="settings-option" data-mode="fill">
2043
- <span>Stretch</span>
2044
- <span class="check-mark"></span>
2045
- </div>
2046
- <div class="settings-option" data-mode="zoom">
2047
- <span>Zoom</span>
2048
- <span class="check-mark"></span>
2049
- </div>
2050
- </div>
2051
- `;
2052
- const backBtn = this.settingsAspectScreen.querySelector(".back-button");
2053
- backBtn?.addEventListener("click", () => this.switchMenuScreen("main"));
2054
- const options = this.settingsAspectScreen.querySelectorAll(".settings-option");
2055
- const currentMode = this.core.getScaleMode();
2056
- options.forEach((opt) => {
2057
- const mode = opt.dataset.mode;
2058
- if (mode === currentMode) {
2059
- opt.classList.add("active");
2060
- }
2061
- opt.addEventListener("click", () => {
2062
- this.core.setScaleMode(mode);
2063
- this.switchMenuScreen("main");
2064
- });
2065
- });
2066
- }
2067
- refreshScaleModeUi() {
2068
- const mode = this.core.getScaleMode();
2069
- this.shellEl.classList.remove("scale-fit", "scale-fill", "scale-zoom");
2070
- this.shellEl.classList.add(`scale-${mode}`);
2071
- // Update active state in sub-menu
2072
- this.settingsMenu.querySelectorAll(".aspect-item").forEach((item) => {
2073
- const itemMode = item.dataset.mode;
2074
- item.classList.toggle("active", itemMode === mode);
2075
- });
2076
- // Update label in main menu
2077
- const label = this.shadowRootRef.querySelector(".current-aspect-value");
2078
- if (label) {
2079
- const labels = {
2080
- fit: "Fit",
2081
- fill: "Stretch",
2082
- zoom: "Zoom"
2083
- };
2084
- label.textContent = labels[mode] || "Fit";
2085
- }
2086
- }
2087
- renderStaticIcons() {
2088
- this.setButtonIcon(this.controlPrevButton, "skip-back");
2089
- this.setButtonIcon(this.controlNextButton, "skip-forward");
2090
- this.setButtonIcon(this.muteButton, "volume-2");
2091
- this.setButtonIcon(this.settingsButton, "settings");
2092
- this.setButtonIcon(this.fullscreenButton, "maximize");
2093
- this.setButtonIcon(this.playlistButton, "list");
2094
- this.setButtonIcon(this.playlistCloseButton, "x");
2095
- this.setButtonIcon(this.aspectShortcut, "box");
2096
- this.refreshLockButtonIcon();
2097
- // Unified Menu Icons
2098
- const setIcon = (selector, name, size = "18") => {
2099
- this.settingsMenu.querySelectorAll(selector).forEach((el) => {
2100
- el.innerHTML = icons[name].toSvg({ width: size, height: size, "stroke-width": "2" });
2101
- });
2102
- };
2103
- setIcon(".icon-quality", "monitor");
2104
- setIcon(".icon-speed", "fast-forward");
2105
- setIcon(".icon-aspect", "box");
2106
- setIcon(".icon-play", "play");
2107
- setIcon(".icon-repeat", "repeat");
2108
- setIcon(".icon-arrow-left", "arrow-left", "20");
2109
- this.settingsMenu.querySelectorAll(".icon-chevron-right").forEach((el) => {
2110
- el.innerHTML = icons["chevron-right"].toSvg({ width: "16", height: "16", "stroke-width": "2.5" });
2111
- });
2112
- const downloadIcon = this.settingsMenu.querySelector(".download-icon");
2113
- if (downloadIcon) {
2114
- downloadIcon.innerHTML = icons.download.toSvg({ width: "16", height: "16", "stroke-width": "2.5" });
2115
- }
2116
- this.settingsMenu.querySelectorAll(".check").forEach((element) => {
2117
- element.innerHTML = icons.check.toSvg({
2118
- width: "16",
2119
- height: "16",
2120
- "stroke-width": "2.5"
2121
- });
2122
- });
2123
- }
2124
- setButtonIcon(button, iconName) {
2125
- const icon = icons[iconName];
2126
- if (!icon)
2127
- return;
2128
- button.innerHTML = icon.toSvg({
2129
- width: "26",
2130
- height: "26",
2131
- "stroke-width": "2.2"
2132
- });
2133
- }
2134
- refreshLockButtonIcon() {
2135
- const iconName = this.isLocked ? "lock" : "unlock";
2136
- this.setButtonIcon(this.lockButton, iconName);
2137
- }
2138
- updateSpeedUi(rate) {
2139
- const normalized = [0.5, 1, 1.5, 2].includes(rate) ? rate : 1;
2140
- const valueEl = this.settingsMenu.querySelector(".current-speed-value");
2141
- if (valueEl)
2142
- valueEl.textContent = `${normalized}x`;
2143
- this.settingsMenu.querySelectorAll(".speed-item").forEach((item) => {
2144
- const itemRate = Number(item.dataset.rate);
2145
- item.classList.toggle("active", itemRate === normalized);
2146
- });
2147
- }
2148
- getBufferedPercent(duration, currentTime) {
2149
- if (!duration || !Number.isFinite(duration) || duration <= 0)
2150
- return 0;
2151
- const ranges = this.videoEl.buffered;
2152
- if (!ranges || ranges.length === 0)
2153
- return 0;
2154
- let end = 0;
2155
- for (let i = 0; i < ranges.length; i += 1) {
2156
- const start = ranges.start(i);
2157
- const rangeEnd = ranges.end(i);
2158
- if (currentTime >= start && currentTime <= rangeEnd) {
2159
- end = Math.max(end, rangeEnd);
2160
- }
2161
- else {
2162
- end = Math.max(end, rangeEnd);
2163
- }
2164
- }
2165
- return Math.max(0, Math.min(100, (end / duration) * 100));
2166
- }
2167
- startProgressLoop() {
2168
- this.stopProgressLoop();
2169
- const tick = () => {
2170
- if (this.videoEl.paused || this.videoEl.ended) {
2171
- this.progressRafId = null;
2172
- return;
2173
- }
2174
- this.handleVideoProgress();
2175
- this.progressRafId = window.requestAnimationFrame(tick);
2176
- };
2177
- this.progressRafId = window.requestAnimationFrame(tick);
2178
- }
2179
- stopProgressLoop() {
2180
- if (this.progressRafId == null)
2181
- return;
2182
- window.cancelAnimationFrame(this.progressRafId);
2183
- this.progressRafId = null;
2184
- }
2185
- }
2186
- export function defineUvpPlayerElement(tagName = DEFAULT_TAG) {
2187
- if (!customElements.get(tagName)) {
2188
- customElements.define(tagName, UvpPlayerElement);
2189
- }
2190
- }
2191
- //# sourceMappingURL=index.js.map