@scarlett-player/ui 0.4.1 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +1212 -44
- package/dist/index.d.cts +5 -4
- package/dist/index.d.ts +5 -4
- package/dist/index.js +1212 -44
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -138,7 +138,12 @@ var styles = `
|
|
|
138
138
|
transition: height 0.15s ease;
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
-
|
|
141
|
+
@media (hover: hover) {
|
|
142
|
+
.sp-progress-wrapper:hover .sp-progress {
|
|
143
|
+
height: 5px;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
142
147
|
.sp-progress--dragging {
|
|
143
148
|
height: 5px;
|
|
144
149
|
}
|
|
@@ -184,11 +189,34 @@ var styles = `
|
|
|
184
189
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
|
185
190
|
}
|
|
186
191
|
|
|
187
|
-
|
|
192
|
+
@media (hover: hover) {
|
|
193
|
+
.sp-progress-wrapper:hover .sp-progress__handle {
|
|
194
|
+
transform: translate(-50%, -50%) scale(1);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
188
198
|
.sp-progress--dragging .sp-progress__handle {
|
|
189
199
|
transform: translate(-50%, -50%) scale(1);
|
|
190
200
|
}
|
|
191
201
|
|
|
202
|
+
/* Thumbnail Preview */
|
|
203
|
+
.sp-thumbnail-preview {
|
|
204
|
+
position: absolute;
|
|
205
|
+
bottom: calc(100% + 8px);
|
|
206
|
+
transform: translateX(-50%);
|
|
207
|
+
pointer-events: none;
|
|
208
|
+
display: none;
|
|
209
|
+
z-index: 21;
|
|
210
|
+
border-radius: 4px;
|
|
211
|
+
overflow: hidden;
|
|
212
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
|
|
213
|
+
border: 2px solid rgba(255, 255, 255, 0.2);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.sp-thumbnail-preview__img {
|
|
217
|
+
background-repeat: no-repeat;
|
|
218
|
+
}
|
|
219
|
+
|
|
192
220
|
/* Progress Tooltip */
|
|
193
221
|
.sp-progress__tooltip {
|
|
194
222
|
position: absolute;
|
|
@@ -208,8 +236,10 @@ var styles = `
|
|
|
208
236
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
|
209
237
|
}
|
|
210
238
|
|
|
211
|
-
|
|
212
|
-
|
|
239
|
+
@media (hover: hover) {
|
|
240
|
+
.sp-progress-wrapper:hover .sp-progress__tooltip {
|
|
241
|
+
opacity: 1;
|
|
242
|
+
}
|
|
213
243
|
}
|
|
214
244
|
|
|
215
245
|
/* ============================================
|
|
@@ -229,9 +259,11 @@ var styles = `
|
|
|
229
259
|
flex-shrink: 0;
|
|
230
260
|
}
|
|
231
261
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
262
|
+
@media (hover: hover) {
|
|
263
|
+
.sp-control:hover {
|
|
264
|
+
color: #fff;
|
|
265
|
+
background: rgba(255, 255, 255, 0.1);
|
|
266
|
+
}
|
|
235
267
|
}
|
|
236
268
|
|
|
237
269
|
.sp-control:active {
|
|
@@ -300,7 +332,12 @@ var styles = `
|
|
|
300
332
|
transition: width 0.2s ease;
|
|
301
333
|
}
|
|
302
334
|
|
|
303
|
-
|
|
335
|
+
@media (hover: hover) {
|
|
336
|
+
.sp-volume:hover .sp-volume__slider-wrap {
|
|
337
|
+
width: 64px;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
304
341
|
.sp-volume:focus-within .sp-volume__slider-wrap {
|
|
305
342
|
width: 64px;
|
|
306
343
|
}
|
|
@@ -343,8 +380,10 @@ var styles = `
|
|
|
343
380
|
transition: background 0.15s ease, opacity 0.15s ease;
|
|
344
381
|
}
|
|
345
382
|
|
|
346
|
-
|
|
347
|
-
|
|
383
|
+
@media (hover: hover) {
|
|
384
|
+
.sp-live:hover {
|
|
385
|
+
background: rgba(255, 255, 255, 0.1);
|
|
386
|
+
}
|
|
348
387
|
}
|
|
349
388
|
|
|
350
389
|
.sp-live__dot {
|
|
@@ -363,6 +402,16 @@ var styles = `
|
|
|
363
402
|
animation: none;
|
|
364
403
|
}
|
|
365
404
|
|
|
405
|
+
.sp-live--behind span {
|
|
406
|
+
text-decoration: underline;
|
|
407
|
+
text-underline-offset: 2px;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/* Progress bar live mode: accent color for filled bar */
|
|
411
|
+
.sp-progress--live .sp-progress__filled {
|
|
412
|
+
background: var(--sp-accent, #e50914);
|
|
413
|
+
}
|
|
414
|
+
|
|
366
415
|
@keyframes sp-pulse {
|
|
367
416
|
0%, 100% { opacity: 1; }
|
|
368
417
|
50% { opacity: 0.4; }
|
|
@@ -443,6 +492,169 @@ var styles = `
|
|
|
443
492
|
opacity: 1;
|
|
444
493
|
}
|
|
445
494
|
|
|
495
|
+
/* ============================================
|
|
496
|
+
Settings Menu (Gear Icon)
|
|
497
|
+
============================================ */
|
|
498
|
+
.sp-settings {
|
|
499
|
+
position: relative;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
.sp-settings__btn {
|
|
503
|
+
display: flex;
|
|
504
|
+
align-items: center;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
.sp-settings-panel {
|
|
508
|
+
position: absolute;
|
|
509
|
+
bottom: calc(100% + 8px);
|
|
510
|
+
right: 0;
|
|
511
|
+
background: rgba(20, 20, 20, 0.95);
|
|
512
|
+
backdrop-filter: blur(8px);
|
|
513
|
+
-webkit-backdrop-filter: blur(8px);
|
|
514
|
+
border-radius: 8px;
|
|
515
|
+
min-width: 200px;
|
|
516
|
+
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4);
|
|
517
|
+
opacity: 0;
|
|
518
|
+
visibility: hidden;
|
|
519
|
+
transform: translateY(8px);
|
|
520
|
+
transition: opacity 0.15s ease, transform 0.15s ease, visibility 0.15s;
|
|
521
|
+
z-index: 20;
|
|
522
|
+
overflow: hidden;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
.sp-settings-panel--open {
|
|
526
|
+
opacity: 1;
|
|
527
|
+
visibility: visible;
|
|
528
|
+
transform: translateY(0);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/* Main menu rows */
|
|
532
|
+
.sp-settings-panel--main {
|
|
533
|
+
padding: 4px 0;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
.sp-settings-panel__row {
|
|
537
|
+
display: flex;
|
|
538
|
+
align-items: center;
|
|
539
|
+
justify-content: space-between;
|
|
540
|
+
padding: 10px 16px;
|
|
541
|
+
font-size: 13px;
|
|
542
|
+
color: rgba(255, 255, 255, 0.9);
|
|
543
|
+
cursor: pointer;
|
|
544
|
+
transition: background 0.1s ease;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
.sp-settings-panel__row:hover {
|
|
548
|
+
background: rgba(255, 255, 255, 0.1);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
.sp-settings-panel__label {
|
|
552
|
+
font-weight: 500;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
.sp-settings-panel__value {
|
|
556
|
+
display: flex;
|
|
557
|
+
align-items: center;
|
|
558
|
+
gap: 4px;
|
|
559
|
+
color: rgba(255, 255, 255, 0.6);
|
|
560
|
+
font-size: 12px;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
.sp-settings-panel__arrow {
|
|
564
|
+
display: flex;
|
|
565
|
+
align-items: center;
|
|
566
|
+
transform: rotate(-90deg);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
.sp-settings-panel__arrow svg {
|
|
570
|
+
width: 16px;
|
|
571
|
+
height: 16px;
|
|
572
|
+
fill: currentColor;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/* Sub-menu panels */
|
|
576
|
+
.sp-settings-panel--sub {
|
|
577
|
+
padding: 0;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
.sp-settings-panel__header {
|
|
581
|
+
display: flex;
|
|
582
|
+
align-items: center;
|
|
583
|
+
gap: 8px;
|
|
584
|
+
padding: 10px 16px;
|
|
585
|
+
font-size: 13px;
|
|
586
|
+
font-weight: 600;
|
|
587
|
+
color: rgba(255, 255, 255, 0.9);
|
|
588
|
+
cursor: pointer;
|
|
589
|
+
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
|
590
|
+
transition: background 0.1s ease;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
.sp-settings-panel__header:hover {
|
|
594
|
+
background: rgba(255, 255, 255, 0.1);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
.sp-settings-panel__back {
|
|
598
|
+
display: flex;
|
|
599
|
+
align-items: center;
|
|
600
|
+
transform: rotate(-90deg);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
.sp-settings-panel__back svg {
|
|
604
|
+
width: 16px;
|
|
605
|
+
height: 16px;
|
|
606
|
+
fill: currentColor;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
.sp-settings-panel__header-label {
|
|
610
|
+
flex: 1;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
.sp-settings-panel__item {
|
|
614
|
+
display: flex;
|
|
615
|
+
align-items: center;
|
|
616
|
+
justify-content: space-between;
|
|
617
|
+
padding: 10px 16px;
|
|
618
|
+
font-size: 13px;
|
|
619
|
+
color: rgba(255, 255, 255, 0.8);
|
|
620
|
+
cursor: pointer;
|
|
621
|
+
transition: background 0.1s ease, color 0.1s ease;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
.sp-settings-panel__item:hover {
|
|
625
|
+
background: rgba(255, 255, 255, 0.1);
|
|
626
|
+
color: #fff;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
.sp-settings-panel__item--active {
|
|
630
|
+
color: var(--sp-accent, #e50914);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
.sp-settings-panel__check {
|
|
634
|
+
width: 16px;
|
|
635
|
+
height: 16px;
|
|
636
|
+
fill: currentColor;
|
|
637
|
+
margin-left: 8px;
|
|
638
|
+
opacity: 0;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
.sp-settings-panel__check svg {
|
|
642
|
+
width: 16px;
|
|
643
|
+
height: 16px;
|
|
644
|
+
fill: currentColor;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
.sp-settings-panel__item--active .sp-settings-panel__check {
|
|
648
|
+
opacity: 1;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/* ============================================
|
|
652
|
+
Captions Button
|
|
653
|
+
============================================ */
|
|
654
|
+
.sp-captions--active {
|
|
655
|
+
color: var(--sp-accent, #e50914);
|
|
656
|
+
}
|
|
657
|
+
|
|
446
658
|
/* ============================================
|
|
447
659
|
Cast Button States
|
|
448
660
|
============================================ */
|
|
@@ -454,6 +666,122 @@ var styles = `
|
|
|
454
666
|
opacity: 0.4;
|
|
455
667
|
}
|
|
456
668
|
|
|
669
|
+
/* ============================================
|
|
670
|
+
Error Overlay
|
|
671
|
+
============================================ */
|
|
672
|
+
.sp-error-overlay {
|
|
673
|
+
position: absolute;
|
|
674
|
+
top: 0;
|
|
675
|
+
left: 0;
|
|
676
|
+
right: 0;
|
|
677
|
+
bottom: 0;
|
|
678
|
+
background: rgba(0, 0, 0, 0.85);
|
|
679
|
+
display: flex;
|
|
680
|
+
align-items: center;
|
|
681
|
+
justify-content: center;
|
|
682
|
+
z-index: 25;
|
|
683
|
+
opacity: 0;
|
|
684
|
+
visibility: hidden;
|
|
685
|
+
transition: opacity 0.25s ease, visibility 0.25s;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
.sp-error-overlay--visible {
|
|
689
|
+
opacity: 1;
|
|
690
|
+
visibility: visible;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
.sp-error-overlay__content {
|
|
694
|
+
display: flex;
|
|
695
|
+
flex-direction: column;
|
|
696
|
+
align-items: center;
|
|
697
|
+
text-align: center;
|
|
698
|
+
padding: 24px;
|
|
699
|
+
max-width: 360px;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
.sp-error-overlay__icon {
|
|
703
|
+
color: rgba(255, 255, 255, 0.7);
|
|
704
|
+
margin-bottom: 16px;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
.sp-error-overlay__icon svg {
|
|
708
|
+
width: 48px;
|
|
709
|
+
height: 48px;
|
|
710
|
+
fill: currentColor;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
.sp-error-overlay__message {
|
|
714
|
+
color: rgba(255, 255, 255, 0.9);
|
|
715
|
+
font-size: 15px;
|
|
716
|
+
line-height: 1.5;
|
|
717
|
+
margin: 0 0 24px;
|
|
718
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
.sp-error-overlay__actions {
|
|
722
|
+
display: flex;
|
|
723
|
+
gap: 12px;
|
|
724
|
+
flex-wrap: wrap;
|
|
725
|
+
justify-content: center;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
.sp-error-overlay__retry {
|
|
729
|
+
background: var(--sp-accent, #e50914);
|
|
730
|
+
color: #fff;
|
|
731
|
+
border: none;
|
|
732
|
+
padding: 12px 24px;
|
|
733
|
+
font-size: 14px;
|
|
734
|
+
font-weight: 600;
|
|
735
|
+
border-radius: 6px;
|
|
736
|
+
cursor: pointer;
|
|
737
|
+
min-width: 120px;
|
|
738
|
+
min-height: 44px;
|
|
739
|
+
transition: background 0.15s ease, transform 0.15s ease;
|
|
740
|
+
font-family: inherit;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
.sp-error-overlay__retry:hover {
|
|
744
|
+
filter: brightness(1.1);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
.sp-error-overlay__retry:active {
|
|
748
|
+
transform: scale(0.96);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
.sp-error-overlay__retry:focus-visible {
|
|
752
|
+
outline: 2px solid #fff;
|
|
753
|
+
outline-offset: 2px;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
.sp-error-overlay__dismiss {
|
|
757
|
+
background: none;
|
|
758
|
+
color: rgba(255, 255, 255, 0.7);
|
|
759
|
+
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
760
|
+
padding: 12px 24px;
|
|
761
|
+
font-size: 14px;
|
|
762
|
+
font-weight: 500;
|
|
763
|
+
border-radius: 6px;
|
|
764
|
+
cursor: pointer;
|
|
765
|
+
min-width: 100px;
|
|
766
|
+
min-height: 44px;
|
|
767
|
+
transition: color 0.15s ease, border-color 0.15s ease, transform 0.15s ease;
|
|
768
|
+
font-family: inherit;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
.sp-error-overlay__dismiss:hover {
|
|
772
|
+
color: #fff;
|
|
773
|
+
border-color: rgba(255, 255, 255, 0.5);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
.sp-error-overlay__dismiss:active {
|
|
777
|
+
transform: scale(0.96);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
.sp-error-overlay__dismiss:focus-visible {
|
|
781
|
+
outline: 2px solid #fff;
|
|
782
|
+
outline-offset: 2px;
|
|
783
|
+
}
|
|
784
|
+
|
|
457
785
|
/* ============================================
|
|
458
786
|
Buffering Indicator
|
|
459
787
|
============================================ */
|
|
@@ -501,7 +829,14 @@ var styles = `
|
|
|
501
829
|
.sp-control,
|
|
502
830
|
.sp-volume__slider-wrap,
|
|
503
831
|
.sp-quality-menu,
|
|
504
|
-
.sp-
|
|
832
|
+
.sp-settings-panel,
|
|
833
|
+
.sp-settings-panel__row,
|
|
834
|
+
.sp-settings-panel__item,
|
|
835
|
+
.sp-settings-panel__header,
|
|
836
|
+
.sp-buffering,
|
|
837
|
+
.sp-error-overlay,
|
|
838
|
+
.sp-error-overlay__retry,
|
|
839
|
+
.sp-error-overlay__dismiss {
|
|
505
840
|
transition: none;
|
|
506
841
|
}
|
|
507
842
|
|
|
@@ -547,8 +882,9 @@ var icons = {
|
|
|
547
882
|
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
883
|
skipForward: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M4 18l8.5-6L4 6v12zm9-12v12l8.5-6L13 6z"/></svg>`,
|
|
549
884
|
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"/><
|
|
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"/><
|
|
885
|
+
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"/><path d="M10.9 16V11.73l-.72.36-.48-.86 1.48-.73h.85V16h-1.13zm2.77-2.14c0-.66.13-1.2.38-1.6.26-.41.66-.62 1.2-.62.55 0 .95.21 1.21.62.25.4.38.94.38 1.6 0 .67-.13 1.2-.38 1.61-.26.41-.66.61-1.21.61-.54 0-.94-.2-1.2-.61-.25-.41-.38-.94-.38-1.61zm1.12 0c0 .45.05.79.15 1.03.1.23.26.35.48.35s.38-.12.49-.35c.1-.24.15-.58.15-1.03s-.05-.78-.15-1.02c-.11-.23-.27-.35-.49-.35s-.38.12-.48.35c-.1.24-.15.57-.15 1.02z"/></svg>`,
|
|
886
|
+
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"/><path d="M10.9 16V11.73l-.72.36-.48-.86 1.48-.73h.85V16h-1.13zm2.77-2.14c0-.66.13-1.2.38-1.6.26-.41.66-.62 1.2-.62.55 0 .95.21 1.21.62.25.4.38.94.38 1.6 0 .67-.13 1.2-.38 1.61-.26.41-.66.61-1.21.61-.54 0-.94-.2-1.2-.61-.25-.41-.38-.94-.38-1.61zm1.12 0c0 .45.05.79.15 1.03.1.23.26.35.48.35s.38-.12.49-.35c.1-.24.15-.58.15-1.03s-.05-.78-.15-1.02c-.11-.23-.27-.35-.49-.35s-.38.12-.48.35c-.1.24-.15.57-.15 1.02z"/></svg>`,
|
|
887
|
+
error: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/></svg>`
|
|
552
888
|
};
|
|
553
889
|
|
|
554
890
|
// src/utils/dom.ts
|
|
@@ -664,6 +1000,70 @@ var PlayButton = class {
|
|
|
664
1000
|
}
|
|
665
1001
|
};
|
|
666
1002
|
|
|
1003
|
+
// src/controls/ThumbnailPreview.ts
|
|
1004
|
+
var ThumbnailPreview = class {
|
|
1005
|
+
constructor() {
|
|
1006
|
+
this.config = null;
|
|
1007
|
+
this.loaded = false;
|
|
1008
|
+
this.el = createElement("div", { className: "sp-thumbnail-preview" });
|
|
1009
|
+
this.img = createElement("div", { className: "sp-thumbnail-preview__img" });
|
|
1010
|
+
this.el.appendChild(this.img);
|
|
1011
|
+
}
|
|
1012
|
+
getElement() {
|
|
1013
|
+
return this.el;
|
|
1014
|
+
}
|
|
1015
|
+
setConfig(config) {
|
|
1016
|
+
this.config = config;
|
|
1017
|
+
this.loaded = false;
|
|
1018
|
+
if (config) {
|
|
1019
|
+
this.img.style.width = `${config.width}px`;
|
|
1020
|
+
this.img.style.height = `${config.height}px`;
|
|
1021
|
+
this.el.style.width = `${config.width}px`;
|
|
1022
|
+
this.el.style.height = `${config.height}px`;
|
|
1023
|
+
const preload = new Image();
|
|
1024
|
+
preload.onload = () => {
|
|
1025
|
+
this.loaded = true;
|
|
1026
|
+
};
|
|
1027
|
+
preload.onerror = () => {
|
|
1028
|
+
this.config = null;
|
|
1029
|
+
this.loaded = false;
|
|
1030
|
+
};
|
|
1031
|
+
preload.src = config.src;
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
/**
|
|
1035
|
+
* Update the thumbnail to show the frame at the given time.
|
|
1036
|
+
* @param time Time in seconds
|
|
1037
|
+
* @param percent Position as 0-1 fraction (for horizontal positioning)
|
|
1038
|
+
*/
|
|
1039
|
+
show(time, percent) {
|
|
1040
|
+
if (!this.config || !this.loaded) {
|
|
1041
|
+
this.el.style.display = "none";
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
const { src, width, height, columns, interval } = this.config;
|
|
1045
|
+
const index = Math.floor(time / interval);
|
|
1046
|
+
const col = index % columns;
|
|
1047
|
+
const row = Math.floor(index / columns);
|
|
1048
|
+
this.img.style.backgroundImage = `url(${src})`;
|
|
1049
|
+
this.img.style.backgroundPosition = `-${col * width}px -${row * height}px`;
|
|
1050
|
+
this.img.style.backgroundSize = `${columns * width}px auto`;
|
|
1051
|
+
this.img.style.width = `${width}px`;
|
|
1052
|
+
this.img.style.height = `${height}px`;
|
|
1053
|
+
this.el.style.left = `${percent * 100}%`;
|
|
1054
|
+
this.el.style.display = "";
|
|
1055
|
+
}
|
|
1056
|
+
hide() {
|
|
1057
|
+
this.el.style.display = "none";
|
|
1058
|
+
}
|
|
1059
|
+
isConfigured() {
|
|
1060
|
+
return this.config !== null;
|
|
1061
|
+
}
|
|
1062
|
+
destroy() {
|
|
1063
|
+
this.el.remove();
|
|
1064
|
+
}
|
|
1065
|
+
};
|
|
1066
|
+
|
|
667
1067
|
// src/controls/ProgressBar.ts
|
|
668
1068
|
var ProgressBar = class {
|
|
669
1069
|
constructor(api) {
|
|
@@ -705,36 +1105,99 @@ var ProgressBar = class {
|
|
|
705
1105
|
}
|
|
706
1106
|
}
|
|
707
1107
|
};
|
|
1108
|
+
this.onTouchStart = (e) => {
|
|
1109
|
+
e.preventDefault();
|
|
1110
|
+
const video = getVideo(this.api.container);
|
|
1111
|
+
this.wasPlayingBeforeDrag = video ? !video.paused : false;
|
|
1112
|
+
this.isDragging = true;
|
|
1113
|
+
this.el.classList.add("sp-progress--dragging");
|
|
1114
|
+
this.lastSeekTime = 0;
|
|
1115
|
+
this.seek(e.touches[0].clientX, true);
|
|
1116
|
+
};
|
|
1117
|
+
this.onDocTouchMove = (e) => {
|
|
1118
|
+
if (this.isDragging) {
|
|
1119
|
+
e.preventDefault();
|
|
1120
|
+
this.seek(e.touches[0].clientX);
|
|
1121
|
+
this.updateVisualPosition(e.touches[0].clientX);
|
|
1122
|
+
}
|
|
1123
|
+
};
|
|
1124
|
+
this.onTouchEnd = (e) => {
|
|
1125
|
+
if (this.isDragging) {
|
|
1126
|
+
const clientX = e.changedTouches?.[0]?.clientX;
|
|
1127
|
+
if (clientX !== void 0) {
|
|
1128
|
+
this.seek(clientX, true);
|
|
1129
|
+
}
|
|
1130
|
+
this.isDragging = false;
|
|
1131
|
+
this.el.classList.remove("sp-progress--dragging");
|
|
1132
|
+
if (this.wasPlayingBeforeDrag) {
|
|
1133
|
+
const video = getVideo(this.api.container);
|
|
1134
|
+
if (video && video.paused) {
|
|
1135
|
+
const resumePlayback = () => {
|
|
1136
|
+
video.removeEventListener("seeked", resumePlayback);
|
|
1137
|
+
video.play().catch(() => {
|
|
1138
|
+
});
|
|
1139
|
+
};
|
|
1140
|
+
video.addEventListener("seeked", resumePlayback);
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
this.tooltip.style.opacity = "0";
|
|
1144
|
+
this.thumbnailPreview.hide();
|
|
1145
|
+
}
|
|
1146
|
+
};
|
|
708
1147
|
this.onMouseMove = (e) => {
|
|
709
1148
|
this.updateTooltip(e.clientX);
|
|
710
1149
|
};
|
|
711
1150
|
this.onMouseLeave = () => {
|
|
712
1151
|
if (!this.isDragging) {
|
|
713
1152
|
this.tooltip.style.opacity = "0";
|
|
1153
|
+
this.thumbnailPreview.hide();
|
|
714
1154
|
}
|
|
715
1155
|
};
|
|
716
1156
|
this.onKeyDown = (e) => {
|
|
717
1157
|
const video = getVideo(this.api.container);
|
|
718
1158
|
if (!video) return;
|
|
719
1159
|
const step = 5;
|
|
720
|
-
const
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
1160
|
+
const live = this.api.getState("live");
|
|
1161
|
+
const seekableRange = this.api.getState("seekableRange");
|
|
1162
|
+
if (live && seekableRange) {
|
|
1163
|
+
switch (e.key) {
|
|
1164
|
+
case "ArrowLeft":
|
|
1165
|
+
e.preventDefault();
|
|
1166
|
+
video.currentTime = Math.max(seekableRange.start, video.currentTime - step);
|
|
1167
|
+
break;
|
|
1168
|
+
case "ArrowRight":
|
|
1169
|
+
e.preventDefault();
|
|
1170
|
+
video.currentTime = Math.min(seekableRange.end, video.currentTime + step);
|
|
1171
|
+
break;
|
|
1172
|
+
case "Home":
|
|
1173
|
+
e.preventDefault();
|
|
1174
|
+
video.currentTime = seekableRange.start;
|
|
1175
|
+
break;
|
|
1176
|
+
case "End":
|
|
1177
|
+
e.preventDefault();
|
|
1178
|
+
video.currentTime = seekableRange.end;
|
|
1179
|
+
break;
|
|
1180
|
+
}
|
|
1181
|
+
} else {
|
|
1182
|
+
const duration = this.api.getState("duration") || 0;
|
|
1183
|
+
switch (e.key) {
|
|
1184
|
+
case "ArrowLeft":
|
|
1185
|
+
e.preventDefault();
|
|
1186
|
+
video.currentTime = Math.max(0, video.currentTime - step);
|
|
1187
|
+
break;
|
|
1188
|
+
case "ArrowRight":
|
|
1189
|
+
e.preventDefault();
|
|
1190
|
+
video.currentTime = Math.min(duration, video.currentTime + step);
|
|
1191
|
+
break;
|
|
1192
|
+
case "Home":
|
|
1193
|
+
e.preventDefault();
|
|
1194
|
+
video.currentTime = 0;
|
|
1195
|
+
break;
|
|
1196
|
+
case "End":
|
|
1197
|
+
e.preventDefault();
|
|
1198
|
+
video.currentTime = duration;
|
|
1199
|
+
break;
|
|
1200
|
+
}
|
|
738
1201
|
}
|
|
739
1202
|
};
|
|
740
1203
|
this.api = api;
|
|
@@ -746,10 +1209,12 @@ var ProgressBar = class {
|
|
|
746
1209
|
this.handle = createElement("div", { className: "sp-progress__handle" });
|
|
747
1210
|
this.tooltip = createElement("div", { className: "sp-progress__tooltip" });
|
|
748
1211
|
this.tooltip.textContent = "0:00";
|
|
1212
|
+
this.thumbnailPreview = new ThumbnailPreview();
|
|
749
1213
|
track.appendChild(this.buffered);
|
|
750
1214
|
track.appendChild(this.filled);
|
|
751
1215
|
track.appendChild(this.handle);
|
|
752
1216
|
this.el.appendChild(track);
|
|
1217
|
+
this.el.appendChild(this.thumbnailPreview.getElement());
|
|
753
1218
|
this.el.appendChild(this.tooltip);
|
|
754
1219
|
this.wrapper.appendChild(this.el);
|
|
755
1220
|
this.el.setAttribute("role", "slider");
|
|
@@ -759,9 +1224,13 @@ var ProgressBar = class {
|
|
|
759
1224
|
this.wrapper.addEventListener("mousedown", this.onMouseDown);
|
|
760
1225
|
this.wrapper.addEventListener("mousemove", this.onMouseMove);
|
|
761
1226
|
this.wrapper.addEventListener("mouseleave", this.onMouseLeave);
|
|
1227
|
+
this.wrapper.addEventListener("touchstart", this.onTouchStart, { passive: false });
|
|
762
1228
|
this.el.addEventListener("keydown", this.onKeyDown);
|
|
763
1229
|
document.addEventListener("mousemove", this.onDocMouseMove);
|
|
764
1230
|
document.addEventListener("mouseup", this.onMouseUp);
|
|
1231
|
+
document.addEventListener("touchmove", this.onDocTouchMove, { passive: false });
|
|
1232
|
+
document.addEventListener("touchend", this.onTouchEnd);
|
|
1233
|
+
document.addEventListener("touchcancel", this.onTouchEnd);
|
|
765
1234
|
}
|
|
766
1235
|
render() {
|
|
767
1236
|
return this.wrapper;
|
|
@@ -774,11 +1243,40 @@ var ProgressBar = class {
|
|
|
774
1243
|
hide() {
|
|
775
1244
|
this.wrapper.classList.remove("sp-progress-wrapper--visible");
|
|
776
1245
|
}
|
|
1246
|
+
/** Set thumbnail sprite configuration */
|
|
1247
|
+
setThumbnails(config) {
|
|
1248
|
+
this.thumbnailPreview.setConfig(config);
|
|
1249
|
+
}
|
|
777
1250
|
update() {
|
|
778
1251
|
const currentTime = this.api.getState("currentTime") || 0;
|
|
779
1252
|
const duration = this.api.getState("duration") || 0;
|
|
780
1253
|
const bufferedRanges = this.api.getState("buffered");
|
|
781
|
-
|
|
1254
|
+
const live = this.api.getState("live");
|
|
1255
|
+
const seekableRange = this.api.getState("seekableRange");
|
|
1256
|
+
const thumbnails = this.api.getState("thumbnails");
|
|
1257
|
+
if (thumbnails && !this.thumbnailPreview.isConfigured()) {
|
|
1258
|
+
this.thumbnailPreview.setConfig(thumbnails);
|
|
1259
|
+
}
|
|
1260
|
+
this.el.classList.toggle("sp-progress--live", !!live);
|
|
1261
|
+
if (live && seekableRange) {
|
|
1262
|
+
const rangeLength = seekableRange.end - seekableRange.start;
|
|
1263
|
+
if (rangeLength > 0) {
|
|
1264
|
+
const progress = (currentTime - seekableRange.start) / rangeLength * 100;
|
|
1265
|
+
this.filled.style.width = `${Math.max(0, Math.min(100, progress))}%`;
|
|
1266
|
+
this.handle.style.left = `${Math.max(0, Math.min(100, progress))}%`;
|
|
1267
|
+
}
|
|
1268
|
+
if (bufferedRanges && bufferedRanges.length > 0) {
|
|
1269
|
+
const rangeLength2 = seekableRange.end - seekableRange.start;
|
|
1270
|
+
if (rangeLength2 > 0) {
|
|
1271
|
+
const bufferedEnd = bufferedRanges.end(bufferedRanges.length - 1);
|
|
1272
|
+
const bufferedPercent = (bufferedEnd - seekableRange.start) / rangeLength2 * 100;
|
|
1273
|
+
this.buffered.style.width = `${Math.max(0, Math.min(100, bufferedPercent))}%`;
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
this.el.setAttribute("aria-valuemax", String(Math.floor(seekableRange.end)));
|
|
1277
|
+
this.el.setAttribute("aria-valuenow", String(Math.floor(currentTime)));
|
|
1278
|
+
this.el.setAttribute("aria-valuetext", `${Math.floor(seekableRange.end - currentTime)} seconds behind live`);
|
|
1279
|
+
} else if (duration > 0) {
|
|
782
1280
|
const progress = currentTime / duration * 100;
|
|
783
1281
|
this.filled.style.width = `${progress}%`;
|
|
784
1282
|
this.handle.style.left = `${progress}%`;
|
|
@@ -795,6 +1293,12 @@ var ProgressBar = class {
|
|
|
795
1293
|
getTimeFromPosition(clientX) {
|
|
796
1294
|
const rect = this.el.getBoundingClientRect();
|
|
797
1295
|
const percent = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
|
|
1296
|
+
const live = this.api.getState("live");
|
|
1297
|
+
const seekableRange = this.api.getState("seekableRange");
|
|
1298
|
+
if (live && seekableRange) {
|
|
1299
|
+
const rangeLength = seekableRange.end - seekableRange.start;
|
|
1300
|
+
return seekableRange.start + percent * rangeLength;
|
|
1301
|
+
}
|
|
798
1302
|
const duration = this.api.getState("duration") || 0;
|
|
799
1303
|
return percent * duration;
|
|
800
1304
|
}
|
|
@@ -802,8 +1306,18 @@ var ProgressBar = class {
|
|
|
802
1306
|
const rect = this.el.getBoundingClientRect();
|
|
803
1307
|
const percent = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
|
|
804
1308
|
const time = this.getTimeFromPosition(clientX);
|
|
805
|
-
this.
|
|
1309
|
+
const live = this.api.getState("live");
|
|
1310
|
+
const seekableRange = this.api.getState("seekableRange");
|
|
1311
|
+
if (live && seekableRange) {
|
|
1312
|
+
const behindLive = seekableRange.end - time;
|
|
1313
|
+
this.tooltip.textContent = formatLiveTime(behindLive);
|
|
1314
|
+
} else {
|
|
1315
|
+
this.tooltip.textContent = formatTime(time);
|
|
1316
|
+
}
|
|
806
1317
|
this.tooltip.style.left = `${percent * 100}%`;
|
|
1318
|
+
if (this.thumbnailPreview.isConfigured()) {
|
|
1319
|
+
this.thumbnailPreview.show(time, percent);
|
|
1320
|
+
}
|
|
807
1321
|
}
|
|
808
1322
|
updateVisualPosition(clientX) {
|
|
809
1323
|
const rect = this.el.getBoundingClientRect();
|
|
@@ -826,8 +1340,13 @@ var ProgressBar = class {
|
|
|
826
1340
|
this.wrapper.removeEventListener("mousedown", this.onMouseDown);
|
|
827
1341
|
this.wrapper.removeEventListener("mousemove", this.onMouseMove);
|
|
828
1342
|
this.wrapper.removeEventListener("mouseleave", this.onMouseLeave);
|
|
1343
|
+
this.wrapper.removeEventListener("touchstart", this.onTouchStart);
|
|
829
1344
|
document.removeEventListener("mousemove", this.onDocMouseMove);
|
|
830
1345
|
document.removeEventListener("mouseup", this.onMouseUp);
|
|
1346
|
+
document.removeEventListener("touchmove", this.onDocTouchMove);
|
|
1347
|
+
document.removeEventListener("touchend", this.onTouchEnd);
|
|
1348
|
+
document.removeEventListener("touchcancel", this.onTouchEnd);
|
|
1349
|
+
this.thumbnailPreview.destroy();
|
|
831
1350
|
this.wrapper.remove();
|
|
832
1351
|
}
|
|
833
1352
|
};
|
|
@@ -880,6 +1399,20 @@ var VolumeControl = class {
|
|
|
880
1399
|
this.onMouseUp = () => {
|
|
881
1400
|
this.isDragging = false;
|
|
882
1401
|
};
|
|
1402
|
+
this.onTouchStart = (e) => {
|
|
1403
|
+
e.preventDefault();
|
|
1404
|
+
this.isDragging = true;
|
|
1405
|
+
this.setVolume(this.getVolumeFromPosition(e.touches[0].clientX));
|
|
1406
|
+
};
|
|
1407
|
+
this.onDocTouchMove = (e) => {
|
|
1408
|
+
if (this.isDragging) {
|
|
1409
|
+
e.preventDefault();
|
|
1410
|
+
this.setVolume(this.getVolumeFromPosition(e.touches[0].clientX));
|
|
1411
|
+
}
|
|
1412
|
+
};
|
|
1413
|
+
this.onTouchEnd = () => {
|
|
1414
|
+
this.isDragging = false;
|
|
1415
|
+
};
|
|
883
1416
|
this.onKeyDown = (e) => {
|
|
884
1417
|
const video = getVideo(this.api.container);
|
|
885
1418
|
if (!video) return;
|
|
@@ -919,9 +1452,13 @@ var VolumeControl = class {
|
|
|
919
1452
|
this.el.appendChild(this.btn);
|
|
920
1453
|
this.el.appendChild(sliderWrap);
|
|
921
1454
|
this.slider.addEventListener("mousedown", this.onMouseDown);
|
|
1455
|
+
this.slider.addEventListener("touchstart", this.onTouchStart, { passive: false });
|
|
922
1456
|
this.slider.addEventListener("keydown", this.onKeyDown);
|
|
923
1457
|
document.addEventListener("mousemove", this.onDocMouseMove);
|
|
924
1458
|
document.addEventListener("mouseup", this.onMouseUp);
|
|
1459
|
+
document.addEventListener("touchmove", this.onDocTouchMove, { passive: false });
|
|
1460
|
+
document.addEventListener("touchend", this.onTouchEnd);
|
|
1461
|
+
document.addEventListener("touchcancel", this.onTouchEnd);
|
|
925
1462
|
}
|
|
926
1463
|
render() {
|
|
927
1464
|
return this.el;
|
|
@@ -966,8 +1503,14 @@ var VolumeControl = class {
|
|
|
966
1503
|
return Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
|
|
967
1504
|
}
|
|
968
1505
|
destroy() {
|
|
1506
|
+
this.slider.removeEventListener("mousedown", this.onMouseDown);
|
|
1507
|
+
this.slider.removeEventListener("touchstart", this.onTouchStart);
|
|
1508
|
+
this.slider.removeEventListener("keydown", this.onKeyDown);
|
|
969
1509
|
document.removeEventListener("mousemove", this.onDocMouseMove);
|
|
970
1510
|
document.removeEventListener("mouseup", this.onMouseUp);
|
|
1511
|
+
document.removeEventListener("touchmove", this.onDocTouchMove);
|
|
1512
|
+
document.removeEventListener("touchend", this.onTouchEnd);
|
|
1513
|
+
document.removeEventListener("touchcancel", this.onTouchEnd);
|
|
971
1514
|
this.el.remove();
|
|
972
1515
|
}
|
|
973
1516
|
};
|
|
@@ -975,19 +1518,27 @@ var VolumeControl = class {
|
|
|
975
1518
|
// src/controls/LiveIndicator.ts
|
|
976
1519
|
var LiveIndicator = class {
|
|
977
1520
|
constructor(api) {
|
|
978
|
-
this.
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
this.
|
|
982
|
-
this.el.setAttribute("aria-label", "Seek to live");
|
|
983
|
-
this.el.setAttribute("tabindex", "0");
|
|
984
|
-
this.el.onclick = () => this.seekToLive();
|
|
985
|
-
this.el.onkeydown = (e) => {
|
|
1521
|
+
this.handleClick = () => {
|
|
1522
|
+
this.seekToLive();
|
|
1523
|
+
};
|
|
1524
|
+
this.handleKeyDown = (e) => {
|
|
986
1525
|
if (e.key === "Enter" || e.key === " ") {
|
|
987
1526
|
e.preventDefault();
|
|
988
1527
|
this.seekToLive();
|
|
989
1528
|
}
|
|
990
1529
|
};
|
|
1530
|
+
this.api = api;
|
|
1531
|
+
this.el = createElement("div", { className: "sp-live" });
|
|
1532
|
+
this.dot = createElement("div", { className: "sp-live__dot" });
|
|
1533
|
+
this.label = document.createElement("span");
|
|
1534
|
+
this.label.textContent = "LIVE";
|
|
1535
|
+
this.el.appendChild(this.dot);
|
|
1536
|
+
this.el.appendChild(this.label);
|
|
1537
|
+
this.el.setAttribute("role", "button");
|
|
1538
|
+
this.el.setAttribute("aria-label", "Seek to live");
|
|
1539
|
+
this.el.setAttribute("tabindex", "0");
|
|
1540
|
+
this.el.addEventListener("click", this.handleClick);
|
|
1541
|
+
this.el.addEventListener("keydown", this.handleKeyDown);
|
|
991
1542
|
}
|
|
992
1543
|
render() {
|
|
993
1544
|
return this.el;
|
|
@@ -998,8 +1549,12 @@ var LiveIndicator = class {
|
|
|
998
1549
|
this.el.style.display = live ? "" : "none";
|
|
999
1550
|
if (liveEdge) {
|
|
1000
1551
|
this.el.classList.remove("sp-live--behind");
|
|
1552
|
+
this.label.textContent = "LIVE";
|
|
1553
|
+
this.el.setAttribute("aria-label", "At live edge");
|
|
1001
1554
|
} else {
|
|
1002
1555
|
this.el.classList.add("sp-live--behind");
|
|
1556
|
+
this.label.textContent = "GO LIVE";
|
|
1557
|
+
this.el.setAttribute("aria-label", "Seek to live");
|
|
1003
1558
|
}
|
|
1004
1559
|
}
|
|
1005
1560
|
seekToLive() {
|
|
@@ -1011,6 +1566,8 @@ var LiveIndicator = class {
|
|
|
1011
1566
|
}
|
|
1012
1567
|
}
|
|
1013
1568
|
destroy() {
|
|
1569
|
+
this.el.removeEventListener("click", this.handleClick);
|
|
1570
|
+
this.el.removeEventListener("keydown", this.handleKeyDown);
|
|
1014
1571
|
this.el.remove();
|
|
1015
1572
|
}
|
|
1016
1573
|
};
|
|
@@ -1321,14 +1878,588 @@ var Spacer = class {
|
|
|
1321
1878
|
}
|
|
1322
1879
|
};
|
|
1323
1880
|
|
|
1881
|
+
// src/controls/ErrorOverlay.ts
|
|
1882
|
+
function getUserMessage(error) {
|
|
1883
|
+
if (!error) return "Something went wrong.";
|
|
1884
|
+
const msg = error.message?.toLowerCase() || "";
|
|
1885
|
+
if (msg.includes("network") || msg.includes("timeout") || msg.includes("fetch") || msg.includes("connection")) {
|
|
1886
|
+
return "Having trouble connecting. Check your internet and try again.";
|
|
1887
|
+
}
|
|
1888
|
+
if (msg.includes("manifest")) {
|
|
1889
|
+
return "Unable to load video. Please try again.";
|
|
1890
|
+
}
|
|
1891
|
+
if (msg.includes("decode") || msg.includes("media") || msg.includes("format") || msg.includes("codec")) {
|
|
1892
|
+
return "This video can't be played right now.";
|
|
1893
|
+
}
|
|
1894
|
+
if (msg.includes("not found") || msg.includes("404") || msg.includes("source") || msg.includes("not supported")) {
|
|
1895
|
+
return "Video not found.";
|
|
1896
|
+
}
|
|
1897
|
+
return "Something went wrong.";
|
|
1898
|
+
}
|
|
1899
|
+
var ErrorOverlay = class {
|
|
1900
|
+
constructor(api) {
|
|
1901
|
+
this.visible = false;
|
|
1902
|
+
this.lastSource = null;
|
|
1903
|
+
this.handleRetry = () => {
|
|
1904
|
+
if (this.retryBtn.disabled) return;
|
|
1905
|
+
this.retryBtn.disabled = true;
|
|
1906
|
+
this.hide();
|
|
1907
|
+
const source = this.api.getState("source");
|
|
1908
|
+
const src = source?.src || this.lastSource;
|
|
1909
|
+
if (src) {
|
|
1910
|
+
this.api.emit("error:retry", { src });
|
|
1911
|
+
const video = this.api.container.querySelector("video");
|
|
1912
|
+
if (video) {
|
|
1913
|
+
video.src = src;
|
|
1914
|
+
video.load();
|
|
1915
|
+
video.play().catch(() => {
|
|
1916
|
+
});
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
setTimeout(() => {
|
|
1920
|
+
this.retryBtn.disabled = false;
|
|
1921
|
+
}, 1e3);
|
|
1922
|
+
};
|
|
1923
|
+
this.handleDismiss = () => {
|
|
1924
|
+
this.hide();
|
|
1925
|
+
this.api.emit("error:dismiss", void 0);
|
|
1926
|
+
};
|
|
1927
|
+
this.api = api;
|
|
1928
|
+
const overlay = document.createElement("div");
|
|
1929
|
+
overlay.className = "sp-error-overlay";
|
|
1930
|
+
overlay.setAttribute("role", "alert");
|
|
1931
|
+
overlay.setAttribute("aria-live", "assertive");
|
|
1932
|
+
const content = document.createElement("div");
|
|
1933
|
+
content.className = "sp-error-overlay__content";
|
|
1934
|
+
const iconEl = document.createElement("div");
|
|
1935
|
+
iconEl.className = "sp-error-overlay__icon";
|
|
1936
|
+
iconEl.innerHTML = icons.error;
|
|
1937
|
+
const messageEl = document.createElement("p");
|
|
1938
|
+
messageEl.className = "sp-error-overlay__message";
|
|
1939
|
+
messageEl.textContent = "Something went wrong.";
|
|
1940
|
+
const actions = document.createElement("div");
|
|
1941
|
+
actions.className = "sp-error-overlay__actions";
|
|
1942
|
+
this.retryBtn = document.createElement("button");
|
|
1943
|
+
this.retryBtn.className = "sp-error-overlay__retry";
|
|
1944
|
+
this.retryBtn.setAttribute("type", "button");
|
|
1945
|
+
this.retryBtn.setAttribute("aria-label", "Try again");
|
|
1946
|
+
this.retryBtn.textContent = "Try Again";
|
|
1947
|
+
this.retryBtn.addEventListener("click", this.handleRetry);
|
|
1948
|
+
this.dismissBtn = document.createElement("button");
|
|
1949
|
+
this.dismissBtn.className = "sp-error-overlay__dismiss";
|
|
1950
|
+
this.dismissBtn.setAttribute("type", "button");
|
|
1951
|
+
this.dismissBtn.setAttribute("aria-label", "Go back");
|
|
1952
|
+
this.dismissBtn.textContent = "Go Back";
|
|
1953
|
+
this.dismissBtn.addEventListener("click", this.handleDismiss);
|
|
1954
|
+
actions.appendChild(this.retryBtn);
|
|
1955
|
+
actions.appendChild(this.dismissBtn);
|
|
1956
|
+
content.appendChild(iconEl);
|
|
1957
|
+
content.appendChild(messageEl);
|
|
1958
|
+
content.appendChild(actions);
|
|
1959
|
+
overlay.appendChild(content);
|
|
1960
|
+
this.el = overlay;
|
|
1961
|
+
}
|
|
1962
|
+
render() {
|
|
1963
|
+
return this.el;
|
|
1964
|
+
}
|
|
1965
|
+
/** Show the error overlay with the given error */
|
|
1966
|
+
show(error) {
|
|
1967
|
+
const message = getUserMessage(error);
|
|
1968
|
+
const messageEl = this.el.querySelector(".sp-error-overlay__message");
|
|
1969
|
+
if (messageEl) {
|
|
1970
|
+
messageEl.textContent = message;
|
|
1971
|
+
}
|
|
1972
|
+
const source = this.api.getState("source");
|
|
1973
|
+
if (source?.src) {
|
|
1974
|
+
this.lastSource = source.src;
|
|
1975
|
+
}
|
|
1976
|
+
this.visible = true;
|
|
1977
|
+
this.retryBtn.disabled = false;
|
|
1978
|
+
this.el.classList.add("sp-error-overlay--visible");
|
|
1979
|
+
}
|
|
1980
|
+
/** Hide the error overlay */
|
|
1981
|
+
hide() {
|
|
1982
|
+
this.visible = false;
|
|
1983
|
+
this.el.classList.remove("sp-error-overlay--visible");
|
|
1984
|
+
}
|
|
1985
|
+
isVisible() {
|
|
1986
|
+
return this.visible;
|
|
1987
|
+
}
|
|
1988
|
+
update() {
|
|
1989
|
+
const playbackState = this.api.getState("playbackState");
|
|
1990
|
+
if (this.visible && playbackState !== "error" && playbackState !== "loading") {
|
|
1991
|
+
const playing = this.api.getState("playing");
|
|
1992
|
+
if (playing) {
|
|
1993
|
+
this.hide();
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
destroy() {
|
|
1998
|
+
this.retryBtn.removeEventListener("click", this.handleRetry);
|
|
1999
|
+
this.dismissBtn.removeEventListener("click", this.handleDismiss);
|
|
2000
|
+
this.el.remove();
|
|
2001
|
+
}
|
|
2002
|
+
};
|
|
2003
|
+
|
|
2004
|
+
// src/controls/SettingsMenu.ts
|
|
2005
|
+
var SPEED_OPTIONS = [
|
|
2006
|
+
{ label: "0.5x", value: 0.5 },
|
|
2007
|
+
{ label: "0.75x", value: 0.75 },
|
|
2008
|
+
{ label: "Normal", value: 1 },
|
|
2009
|
+
{ label: "1.25x", value: 1.25 },
|
|
2010
|
+
{ label: "1.5x", value: 1.5 },
|
|
2011
|
+
{ label: "2x", value: 2 }
|
|
2012
|
+
];
|
|
2013
|
+
var SettingsMenu = class {
|
|
2014
|
+
constructor(api) {
|
|
2015
|
+
this.isOpen = false;
|
|
2016
|
+
this.currentPanel = "main";
|
|
2017
|
+
this.lastQualitiesJson = "";
|
|
2018
|
+
this.api = api;
|
|
2019
|
+
this.el = createElement("div", { className: "sp-settings" });
|
|
2020
|
+
this.btn = createButton("sp-settings__btn", "Settings", icons.settings);
|
|
2021
|
+
this.btn.setAttribute("aria-haspopup", "true");
|
|
2022
|
+
this.btn.setAttribute("aria-expanded", "false");
|
|
2023
|
+
this.btn.addEventListener("click", (e) => {
|
|
2024
|
+
e.stopPropagation();
|
|
2025
|
+
this.toggle();
|
|
2026
|
+
});
|
|
2027
|
+
this.panel = createElement("div", { className: "sp-settings-panel" });
|
|
2028
|
+
this.panel.setAttribute("role", "menu");
|
|
2029
|
+
this.panel.addEventListener("click", (e) => e.stopPropagation());
|
|
2030
|
+
this.el.appendChild(this.btn);
|
|
2031
|
+
this.el.appendChild(this.panel);
|
|
2032
|
+
this.closeHandler = (e) => {
|
|
2033
|
+
if (!this.el.contains(e.target)) {
|
|
2034
|
+
this.close();
|
|
2035
|
+
}
|
|
2036
|
+
};
|
|
2037
|
+
document.addEventListener("click", this.closeHandler);
|
|
2038
|
+
this.keyHandler = (e) => {
|
|
2039
|
+
if (!this.isOpen) return;
|
|
2040
|
+
if (e.key === "Escape") {
|
|
2041
|
+
e.preventDefault();
|
|
2042
|
+
e.stopPropagation();
|
|
2043
|
+
if (this.currentPanel !== "main") {
|
|
2044
|
+
this.showPanel("main");
|
|
2045
|
+
} else {
|
|
2046
|
+
this.close();
|
|
2047
|
+
this.btn.focus();
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
};
|
|
2051
|
+
document.addEventListener("keydown", this.keyHandler);
|
|
2052
|
+
}
|
|
2053
|
+
render() {
|
|
2054
|
+
return this.el;
|
|
2055
|
+
}
|
|
2056
|
+
update() {
|
|
2057
|
+
const qualities = this.api.getState("qualities") || [];
|
|
2058
|
+
const qualitiesJson = JSON.stringify(qualities.map((q) => q.id));
|
|
2059
|
+
if (qualitiesJson !== this.lastQualitiesJson) {
|
|
2060
|
+
this.lastQualitiesJson = qualitiesJson;
|
|
2061
|
+
if (this.isOpen && this.currentPanel === "quality") {
|
|
2062
|
+
this.renderQualityPanel();
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
if (this.isOpen) {
|
|
2066
|
+
if (this.currentPanel === "quality") {
|
|
2067
|
+
this.updateQualityActiveStates();
|
|
2068
|
+
} else if (this.currentPanel === "speed") {
|
|
2069
|
+
this.updateSpeedActiveStates();
|
|
2070
|
+
} else if (this.currentPanel === "captions") {
|
|
2071
|
+
this.updateCaptionsActiveStates();
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
toggle() {
|
|
2076
|
+
this.isOpen ? this.close() : this.open();
|
|
2077
|
+
}
|
|
2078
|
+
open() {
|
|
2079
|
+
this.isOpen = true;
|
|
2080
|
+
this.currentPanel = "main";
|
|
2081
|
+
this.renderMainPanel();
|
|
2082
|
+
this.panel.classList.add("sp-settings-panel--open");
|
|
2083
|
+
this.btn.setAttribute("aria-expanded", "true");
|
|
2084
|
+
}
|
|
2085
|
+
close() {
|
|
2086
|
+
this.isOpen = false;
|
|
2087
|
+
this.currentPanel = "main";
|
|
2088
|
+
this.panel.classList.remove("sp-settings-panel--open");
|
|
2089
|
+
this.btn.setAttribute("aria-expanded", "false");
|
|
2090
|
+
}
|
|
2091
|
+
showPanel(panel) {
|
|
2092
|
+
this.currentPanel = panel;
|
|
2093
|
+
switch (panel) {
|
|
2094
|
+
case "main":
|
|
2095
|
+
this.renderMainPanel();
|
|
2096
|
+
break;
|
|
2097
|
+
case "quality":
|
|
2098
|
+
this.renderQualityPanel();
|
|
2099
|
+
break;
|
|
2100
|
+
case "speed":
|
|
2101
|
+
this.renderSpeedPanel();
|
|
2102
|
+
break;
|
|
2103
|
+
case "captions":
|
|
2104
|
+
this.renderCaptionsPanel();
|
|
2105
|
+
break;
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
renderMainPanel() {
|
|
2109
|
+
this.panel.innerHTML = "";
|
|
2110
|
+
this.panel.className = "sp-settings-panel sp-settings-panel--open sp-settings-panel--main";
|
|
2111
|
+
const qualities = this.api.getState("qualities") || [];
|
|
2112
|
+
const currentQuality = this.api.getState("currentQuality");
|
|
2113
|
+
const playbackRate = this.api.getState("playbackRate") ?? 1;
|
|
2114
|
+
if (qualities.length > 0) {
|
|
2115
|
+
const qualityRow = this.createMainRow(
|
|
2116
|
+
"Quality",
|
|
2117
|
+
currentQuality?.label || "Auto",
|
|
2118
|
+
() => this.showPanel("quality")
|
|
2119
|
+
);
|
|
2120
|
+
this.panel.appendChild(qualityRow);
|
|
2121
|
+
}
|
|
2122
|
+
const textTracks = this.api.getState("textTracks") || [];
|
|
2123
|
+
if (textTracks.length > 0) {
|
|
2124
|
+
const currentTextTrack = this.api.getState("currentTextTrack");
|
|
2125
|
+
const captionsLabel = currentTextTrack ? currentTextTrack.label : "Off";
|
|
2126
|
+
const captionsRow = this.createMainRow(
|
|
2127
|
+
"Captions",
|
|
2128
|
+
captionsLabel,
|
|
2129
|
+
() => this.showPanel("captions")
|
|
2130
|
+
);
|
|
2131
|
+
this.panel.appendChild(captionsRow);
|
|
2132
|
+
}
|
|
2133
|
+
const speedLabel = playbackRate === 1 ? "Normal" : `${playbackRate}x`;
|
|
2134
|
+
const speedRow = this.createMainRow(
|
|
2135
|
+
"Speed",
|
|
2136
|
+
speedLabel,
|
|
2137
|
+
() => this.showPanel("speed")
|
|
2138
|
+
);
|
|
2139
|
+
this.panel.appendChild(speedRow);
|
|
2140
|
+
}
|
|
2141
|
+
createMainRow(label, value, onClick2) {
|
|
2142
|
+
const row = createElement("div", { className: "sp-settings-panel__row" });
|
|
2143
|
+
row.setAttribute("role", "menuitem");
|
|
2144
|
+
row.setAttribute("tabindex", "0");
|
|
2145
|
+
row.setAttribute("aria-haspopup", "true");
|
|
2146
|
+
const labelEl = createElement("span", { className: "sp-settings-panel__label" });
|
|
2147
|
+
labelEl.textContent = label;
|
|
2148
|
+
const rightSide = createElement("span", { className: "sp-settings-panel__value" });
|
|
2149
|
+
rightSide.textContent = value;
|
|
2150
|
+
const arrow = createElement("span", { className: "sp-settings-panel__arrow" });
|
|
2151
|
+
arrow.innerHTML = icons.chevronDown;
|
|
2152
|
+
rightSide.appendChild(arrow);
|
|
2153
|
+
row.appendChild(labelEl);
|
|
2154
|
+
row.appendChild(rightSide);
|
|
2155
|
+
row.addEventListener("click", (e) => {
|
|
2156
|
+
e.preventDefault();
|
|
2157
|
+
onClick2();
|
|
2158
|
+
});
|
|
2159
|
+
row.addEventListener("keydown", (e) => {
|
|
2160
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
2161
|
+
e.preventDefault();
|
|
2162
|
+
onClick2();
|
|
2163
|
+
}
|
|
2164
|
+
});
|
|
2165
|
+
return row;
|
|
2166
|
+
}
|
|
2167
|
+
renderQualityPanel() {
|
|
2168
|
+
this.panel.innerHTML = "";
|
|
2169
|
+
this.panel.className = "sp-settings-panel sp-settings-panel--open sp-settings-panel--sub";
|
|
2170
|
+
const header = this.createSubHeader("Quality");
|
|
2171
|
+
this.panel.appendChild(header);
|
|
2172
|
+
const qualities = this.api.getState("qualities") || [];
|
|
2173
|
+
const currentQuality = this.api.getState("currentQuality");
|
|
2174
|
+
const activeId = currentQuality?.id || "auto";
|
|
2175
|
+
const autoItem = this.createMenuItem("Auto", "auto", activeId === "auto");
|
|
2176
|
+
autoItem.addEventListener("click", (e) => {
|
|
2177
|
+
e.preventDefault();
|
|
2178
|
+
this.selectQuality("auto");
|
|
2179
|
+
});
|
|
2180
|
+
this.panel.appendChild(autoItem);
|
|
2181
|
+
const sorted = [...qualities].sort(
|
|
2182
|
+
(a, b) => b.height - a.height
|
|
2183
|
+
);
|
|
2184
|
+
for (const q of sorted) {
|
|
2185
|
+
if (q.id === "auto") continue;
|
|
2186
|
+
const item = this.createMenuItem(q.label, q.id, q.id === activeId);
|
|
2187
|
+
item.addEventListener("click", (e) => {
|
|
2188
|
+
e.preventDefault();
|
|
2189
|
+
this.selectQuality(q.id);
|
|
2190
|
+
});
|
|
2191
|
+
this.panel.appendChild(item);
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
renderSpeedPanel() {
|
|
2195
|
+
this.panel.innerHTML = "";
|
|
2196
|
+
this.panel.className = "sp-settings-panel sp-settings-panel--open sp-settings-panel--sub";
|
|
2197
|
+
const header = this.createSubHeader("Speed");
|
|
2198
|
+
this.panel.appendChild(header);
|
|
2199
|
+
const currentRate = this.api.getState("playbackRate") ?? 1;
|
|
2200
|
+
for (const opt of SPEED_OPTIONS) {
|
|
2201
|
+
const isActive = Math.abs(currentRate - opt.value) < 0.01;
|
|
2202
|
+
const item = this.createMenuItem(opt.label, String(opt.value), isActive);
|
|
2203
|
+
item.addEventListener("click", (e) => {
|
|
2204
|
+
e.preventDefault();
|
|
2205
|
+
this.selectSpeed(opt.value);
|
|
2206
|
+
});
|
|
2207
|
+
this.panel.appendChild(item);
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
renderCaptionsPanel() {
|
|
2211
|
+
this.panel.innerHTML = "";
|
|
2212
|
+
this.panel.className = "sp-settings-panel sp-settings-panel--open sp-settings-panel--sub";
|
|
2213
|
+
const header = this.createSubHeader("Captions");
|
|
2214
|
+
this.panel.appendChild(header);
|
|
2215
|
+
const textTracks = this.api.getState("textTracks") || [];
|
|
2216
|
+
const currentTextTrack = this.api.getState("currentTextTrack");
|
|
2217
|
+
const activeId = currentTextTrack?.id || "off";
|
|
2218
|
+
const offItem = this.createMenuItem("Off", "off", activeId === "off");
|
|
2219
|
+
offItem.addEventListener("click", (e) => {
|
|
2220
|
+
e.preventDefault();
|
|
2221
|
+
this.selectCaption(null);
|
|
2222
|
+
});
|
|
2223
|
+
this.panel.appendChild(offItem);
|
|
2224
|
+
for (const track of textTracks) {
|
|
2225
|
+
const item = this.createMenuItem(track.label, track.id, track.id === activeId);
|
|
2226
|
+
item.addEventListener("click", (e) => {
|
|
2227
|
+
e.preventDefault();
|
|
2228
|
+
this.selectCaption(track.id);
|
|
2229
|
+
});
|
|
2230
|
+
this.panel.appendChild(item);
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
selectCaption(trackId) {
|
|
2234
|
+
this.api.emit("track:text", { trackId });
|
|
2235
|
+
this.close();
|
|
2236
|
+
}
|
|
2237
|
+
updateCaptionsActiveStates() {
|
|
2238
|
+
const currentTextTrack = this.api.getState("currentTextTrack");
|
|
2239
|
+
const activeId = currentTextTrack?.id || "off";
|
|
2240
|
+
const items = this.panel.querySelectorAll(".sp-settings-panel__item");
|
|
2241
|
+
items.forEach((item) => {
|
|
2242
|
+
const id = item.getAttribute("data-id");
|
|
2243
|
+
item.classList.toggle("sp-settings-panel__item--active", id === activeId);
|
|
2244
|
+
});
|
|
2245
|
+
}
|
|
2246
|
+
createSubHeader(title) {
|
|
2247
|
+
const header = createElement("div", { className: "sp-settings-panel__header" });
|
|
2248
|
+
header.setAttribute("role", "menuitem");
|
|
2249
|
+
header.setAttribute("tabindex", "0");
|
|
2250
|
+
const backArrow = createElement("span", { className: "sp-settings-panel__back" });
|
|
2251
|
+
backArrow.innerHTML = icons.chevronUp;
|
|
2252
|
+
const label = createElement("span", { className: "sp-settings-panel__header-label" });
|
|
2253
|
+
label.textContent = title;
|
|
2254
|
+
header.appendChild(backArrow);
|
|
2255
|
+
header.appendChild(label);
|
|
2256
|
+
header.addEventListener("click", (e) => {
|
|
2257
|
+
e.preventDefault();
|
|
2258
|
+
this.showPanel("main");
|
|
2259
|
+
});
|
|
2260
|
+
header.addEventListener("keydown", (e) => {
|
|
2261
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
2262
|
+
e.preventDefault();
|
|
2263
|
+
this.showPanel("main");
|
|
2264
|
+
}
|
|
2265
|
+
});
|
|
2266
|
+
return header;
|
|
2267
|
+
}
|
|
2268
|
+
createMenuItem(label, dataId, isActive) {
|
|
2269
|
+
const item = createElement("div", {
|
|
2270
|
+
className: `sp-settings-panel__item${isActive ? " sp-settings-panel__item--active" : ""}`
|
|
2271
|
+
});
|
|
2272
|
+
item.setAttribute("role", "menuitem");
|
|
2273
|
+
item.setAttribute("tabindex", "0");
|
|
2274
|
+
item.setAttribute("data-id", dataId);
|
|
2275
|
+
const labelEl = createElement("span");
|
|
2276
|
+
labelEl.textContent = label;
|
|
2277
|
+
const check = createElement("span", { className: "sp-settings-panel__check" });
|
|
2278
|
+
check.innerHTML = icons.checkmark;
|
|
2279
|
+
item.appendChild(labelEl);
|
|
2280
|
+
item.appendChild(check);
|
|
2281
|
+
item.addEventListener("keydown", (e) => {
|
|
2282
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
2283
|
+
e.preventDefault();
|
|
2284
|
+
item.click();
|
|
2285
|
+
}
|
|
2286
|
+
});
|
|
2287
|
+
return item;
|
|
2288
|
+
}
|
|
2289
|
+
selectQuality(qualityId) {
|
|
2290
|
+
this.api.emit("quality:select", {
|
|
2291
|
+
quality: qualityId,
|
|
2292
|
+
auto: qualityId === "auto"
|
|
2293
|
+
});
|
|
2294
|
+
this.close();
|
|
2295
|
+
}
|
|
2296
|
+
selectSpeed(rate) {
|
|
2297
|
+
this.api.emit("playback:ratechange", { rate });
|
|
2298
|
+
const video = this.api.container.querySelector("video");
|
|
2299
|
+
if (video) {
|
|
2300
|
+
video.playbackRate = rate;
|
|
2301
|
+
}
|
|
2302
|
+
this.close();
|
|
2303
|
+
}
|
|
2304
|
+
updateQualityActiveStates() {
|
|
2305
|
+
const currentQuality = this.api.getState("currentQuality");
|
|
2306
|
+
const activeId = currentQuality?.id || "auto";
|
|
2307
|
+
const items = this.panel.querySelectorAll(".sp-settings-panel__item");
|
|
2308
|
+
items.forEach((item) => {
|
|
2309
|
+
const id = item.getAttribute("data-id");
|
|
2310
|
+
item.classList.toggle("sp-settings-panel__item--active", id === activeId);
|
|
2311
|
+
});
|
|
2312
|
+
}
|
|
2313
|
+
updateSpeedActiveStates() {
|
|
2314
|
+
const currentRate = this.api.getState("playbackRate") ?? 1;
|
|
2315
|
+
const items = this.panel.querySelectorAll(".sp-settings-panel__item");
|
|
2316
|
+
items.forEach((item) => {
|
|
2317
|
+
const id = item.getAttribute("data-id");
|
|
2318
|
+
const value = parseFloat(id || "1");
|
|
2319
|
+
item.classList.toggle(
|
|
2320
|
+
"sp-settings-panel__item--active",
|
|
2321
|
+
Math.abs(currentRate - value) < 0.01
|
|
2322
|
+
);
|
|
2323
|
+
});
|
|
2324
|
+
}
|
|
2325
|
+
getPanel() {
|
|
2326
|
+
return this.currentPanel;
|
|
2327
|
+
}
|
|
2328
|
+
isMenuOpen() {
|
|
2329
|
+
return this.isOpen;
|
|
2330
|
+
}
|
|
2331
|
+
destroy() {
|
|
2332
|
+
document.removeEventListener("click", this.closeHandler);
|
|
2333
|
+
document.removeEventListener("keydown", this.keyHandler);
|
|
2334
|
+
this.el.remove();
|
|
2335
|
+
}
|
|
2336
|
+
};
|
|
2337
|
+
|
|
2338
|
+
// src/controls/SkipButton.ts
|
|
2339
|
+
var DEFAULT_SKIP_SECONDS = 10;
|
|
2340
|
+
var SkipButton = class {
|
|
2341
|
+
constructor(api, direction, seconds = DEFAULT_SKIP_SECONDS) {
|
|
2342
|
+
this.clickHandler = () => {
|
|
2343
|
+
this.skip();
|
|
2344
|
+
};
|
|
2345
|
+
this.api = api;
|
|
2346
|
+
this.direction = direction;
|
|
2347
|
+
this.seconds = seconds;
|
|
2348
|
+
const icon = direction === "backward" ? icons.replay10 : icons.forward10;
|
|
2349
|
+
const label = direction === "backward" ? `Rewind ${seconds} seconds` : `Forward ${seconds} seconds`;
|
|
2350
|
+
this.el = createButton(
|
|
2351
|
+
`sp-skip sp-skip--${direction}`,
|
|
2352
|
+
label,
|
|
2353
|
+
icon
|
|
2354
|
+
);
|
|
2355
|
+
this.el.addEventListener("click", this.clickHandler);
|
|
2356
|
+
}
|
|
2357
|
+
render() {
|
|
2358
|
+
return this.el;
|
|
2359
|
+
}
|
|
2360
|
+
update() {
|
|
2361
|
+
const live = this.api.getState("live");
|
|
2362
|
+
const duration = this.api.getState("duration") ?? 0;
|
|
2363
|
+
const seekableRange = this.api.getState("seekableRange");
|
|
2364
|
+
if (live && !seekableRange) {
|
|
2365
|
+
this.el.style.display = "none";
|
|
2366
|
+
return;
|
|
2367
|
+
}
|
|
2368
|
+
if (live && seekableRange) {
|
|
2369
|
+
this.el.style.display = "";
|
|
2370
|
+
return;
|
|
2371
|
+
}
|
|
2372
|
+
if (duration === 0) {
|
|
2373
|
+
this.el.style.display = "none";
|
|
2374
|
+
return;
|
|
2375
|
+
}
|
|
2376
|
+
this.el.style.display = "";
|
|
2377
|
+
}
|
|
2378
|
+
skip() {
|
|
2379
|
+
const video = getVideo(this.api.container);
|
|
2380
|
+
if (!video) return;
|
|
2381
|
+
const live = this.api.getState("live");
|
|
2382
|
+
const seekableRange = this.api.getState("seekableRange");
|
|
2383
|
+
if (live && seekableRange) {
|
|
2384
|
+
if (this.direction === "backward") {
|
|
2385
|
+
video.currentTime = Math.max(seekableRange.start, video.currentTime - this.seconds);
|
|
2386
|
+
} else {
|
|
2387
|
+
video.currentTime = Math.min(seekableRange.end, video.currentTime + this.seconds);
|
|
2388
|
+
}
|
|
2389
|
+
return;
|
|
2390
|
+
}
|
|
2391
|
+
const duration = video.duration || 0;
|
|
2392
|
+
if (!duration || !isFinite(duration)) return;
|
|
2393
|
+
if (this.direction === "backward") {
|
|
2394
|
+
video.currentTime = Math.max(0, video.currentTime - this.seconds);
|
|
2395
|
+
} else {
|
|
2396
|
+
video.currentTime = Math.min(duration, video.currentTime + this.seconds);
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
destroy() {
|
|
2400
|
+
this.el.removeEventListener("click", this.clickHandler);
|
|
2401
|
+
this.el.remove();
|
|
2402
|
+
}
|
|
2403
|
+
};
|
|
2404
|
+
|
|
2405
|
+
// src/controls/CaptionsButton.ts
|
|
2406
|
+
var CaptionsButton = class {
|
|
2407
|
+
constructor(api) {
|
|
2408
|
+
this.clickHandler = () => {
|
|
2409
|
+
this.toggle();
|
|
2410
|
+
};
|
|
2411
|
+
this.api = api;
|
|
2412
|
+
this.el = createButton("sp-captions", "Captions", icons.captionsOff);
|
|
2413
|
+
this.el.addEventListener("click", this.clickHandler);
|
|
2414
|
+
}
|
|
2415
|
+
render() {
|
|
2416
|
+
return this.el;
|
|
2417
|
+
}
|
|
2418
|
+
update() {
|
|
2419
|
+
const textTracks = this.api.getState("textTracks") || [];
|
|
2420
|
+
const currentTrack = this.api.getState("currentTextTrack");
|
|
2421
|
+
if (textTracks.length === 0) {
|
|
2422
|
+
this.el.style.display = "none";
|
|
2423
|
+
return;
|
|
2424
|
+
}
|
|
2425
|
+
this.el.style.display = "";
|
|
2426
|
+
if (currentTrack) {
|
|
2427
|
+
this.el.innerHTML = icons.captions;
|
|
2428
|
+
this.el.setAttribute("aria-label", `Captions: ${currentTrack.label}`);
|
|
2429
|
+
this.el.classList.add("sp-captions--active");
|
|
2430
|
+
} else {
|
|
2431
|
+
this.el.innerHTML = icons.captionsOff;
|
|
2432
|
+
this.el.setAttribute("aria-label", "Captions");
|
|
2433
|
+
this.el.classList.remove("sp-captions--active");
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
toggle() {
|
|
2437
|
+
const textTracks = this.api.getState("textTracks") || [];
|
|
2438
|
+
const currentTrack = this.api.getState("currentTextTrack");
|
|
2439
|
+
if (textTracks.length === 0) return;
|
|
2440
|
+
if (currentTrack) {
|
|
2441
|
+
this.api.emit("track:text", { trackId: null });
|
|
2442
|
+
} else {
|
|
2443
|
+
this.api.emit("track:text", { trackId: textTracks[0].id });
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
destroy() {
|
|
2447
|
+
this.el.removeEventListener("click", this.clickHandler);
|
|
2448
|
+
this.el.remove();
|
|
2449
|
+
}
|
|
2450
|
+
};
|
|
2451
|
+
|
|
1324
2452
|
// src/index.ts
|
|
1325
2453
|
var DEFAULT_LAYOUT = [
|
|
1326
2454
|
"play",
|
|
2455
|
+
"skip-backward",
|
|
2456
|
+
"skip-forward",
|
|
1327
2457
|
"volume",
|
|
1328
2458
|
"time",
|
|
1329
2459
|
"live-indicator",
|
|
1330
2460
|
"spacer",
|
|
1331
|
-
"
|
|
2461
|
+
"settings",
|
|
2462
|
+
"captions",
|
|
1332
2463
|
"chromecast",
|
|
1333
2464
|
"airplay",
|
|
1334
2465
|
"pip",
|
|
@@ -1341,10 +2472,12 @@ function uiPlugin(config = {}) {
|
|
|
1341
2472
|
let gradient = null;
|
|
1342
2473
|
let progressBar = null;
|
|
1343
2474
|
let bufferingIndicator = null;
|
|
2475
|
+
let errorOverlay = null;
|
|
1344
2476
|
let styleEl = null;
|
|
1345
2477
|
let controls = [];
|
|
1346
2478
|
let hideTimeout = null;
|
|
1347
2479
|
let stateUnsubscribe = null;
|
|
2480
|
+
let errorUnsubscribe = null;
|
|
1348
2481
|
let controlsVisible = true;
|
|
1349
2482
|
const layout = config.controls || DEFAULT_LAYOUT;
|
|
1350
2483
|
const hideDelay = config.hideDelay ?? DEFAULT_HIDE_DELAY;
|
|
@@ -1352,6 +2485,10 @@ function uiPlugin(config = {}) {
|
|
|
1352
2485
|
switch (slot) {
|
|
1353
2486
|
case "play":
|
|
1354
2487
|
return new PlayButton(api);
|
|
2488
|
+
case "skip-backward":
|
|
2489
|
+
return new SkipButton(api, "backward");
|
|
2490
|
+
case "skip-forward":
|
|
2491
|
+
return new SkipButton(api, "forward");
|
|
1355
2492
|
case "volume":
|
|
1356
2493
|
return new VolumeControl(api);
|
|
1357
2494
|
case "progress":
|
|
@@ -1362,6 +2499,10 @@ function uiPlugin(config = {}) {
|
|
|
1362
2499
|
return new LiveIndicator(api);
|
|
1363
2500
|
case "quality":
|
|
1364
2501
|
return new QualityMenu(api);
|
|
2502
|
+
case "settings":
|
|
2503
|
+
return new SettingsMenu(api);
|
|
2504
|
+
case "captions":
|
|
2505
|
+
return new CaptionsButton(api);
|
|
1365
2506
|
case "chromecast":
|
|
1366
2507
|
return new CastButton(api, "chromecast");
|
|
1367
2508
|
case "airplay":
|
|
@@ -1385,6 +2526,7 @@ function uiPlugin(config = {}) {
|
|
|
1385
2526
|
const isLoading = playbackState === "loading";
|
|
1386
2527
|
const showSpinner = waiting || seeking && !api?.getState("paused") || isLoading;
|
|
1387
2528
|
bufferingIndicator?.classList.toggle("sp-buffering--visible", !!showSpinner);
|
|
2529
|
+
errorOverlay?.update();
|
|
1388
2530
|
};
|
|
1389
2531
|
const showControls = () => {
|
|
1390
2532
|
if (controlsVisible) {
|
|
@@ -1423,8 +2565,14 @@ function uiPlugin(config = {}) {
|
|
|
1423
2565
|
};
|
|
1424
2566
|
const handleKeyDown = (e) => {
|
|
1425
2567
|
if (!api.container.contains(document.activeElement)) return;
|
|
2568
|
+
const activeEl = document.activeElement;
|
|
2569
|
+
if (activeEl instanceof HTMLInputElement || activeEl instanceof HTMLTextAreaElement || activeEl instanceof HTMLSelectElement || activeEl?.isContentEditable) {
|
|
2570
|
+
return;
|
|
2571
|
+
}
|
|
1426
2572
|
const video = api.container.querySelector("video");
|
|
1427
2573
|
if (!video) return;
|
|
2574
|
+
const live = api.getState("live");
|
|
2575
|
+
const seekableRange = api.getState("seekableRange");
|
|
1428
2576
|
switch (e.key) {
|
|
1429
2577
|
case " ":
|
|
1430
2578
|
case "k":
|
|
@@ -1445,12 +2593,20 @@ function uiPlugin(config = {}) {
|
|
|
1445
2593
|
break;
|
|
1446
2594
|
case "ArrowLeft":
|
|
1447
2595
|
e.preventDefault();
|
|
1448
|
-
|
|
2596
|
+
if (live && seekableRange) {
|
|
2597
|
+
video.currentTime = Math.max(seekableRange.start, video.currentTime - 5);
|
|
2598
|
+
} else {
|
|
2599
|
+
video.currentTime = Math.max(0, video.currentTime - 5);
|
|
2600
|
+
}
|
|
1449
2601
|
showControls();
|
|
1450
2602
|
break;
|
|
1451
2603
|
case "ArrowRight":
|
|
1452
2604
|
e.preventDefault();
|
|
1453
|
-
|
|
2605
|
+
if (live && seekableRange) {
|
|
2606
|
+
video.currentTime = Math.min(seekableRange.end, video.currentTime + 5);
|
|
2607
|
+
} else {
|
|
2608
|
+
video.currentTime = Math.min(video.duration || 0, video.currentTime + 5);
|
|
2609
|
+
}
|
|
1454
2610
|
showControls();
|
|
1455
2611
|
break;
|
|
1456
2612
|
case "ArrowUp":
|
|
@@ -1496,6 +2652,14 @@ function uiPlugin(config = {}) {
|
|
|
1496
2652
|
bufferingIndicator.innerHTML = icons.spinner;
|
|
1497
2653
|
bufferingIndicator.setAttribute("aria-hidden", "true");
|
|
1498
2654
|
container.appendChild(bufferingIndicator);
|
|
2655
|
+
errorOverlay = new ErrorOverlay(api);
|
|
2656
|
+
container.appendChild(errorOverlay.render());
|
|
2657
|
+
errorUnsubscribe = api.on("error", (payload) => {
|
|
2658
|
+
if (payload?.fatal) {
|
|
2659
|
+
const error = api.getState("error") || new Error(payload.message || "Playback error");
|
|
2660
|
+
errorOverlay?.show(error);
|
|
2661
|
+
}
|
|
2662
|
+
});
|
|
1499
2663
|
progressBar = new ProgressBar(api);
|
|
1500
2664
|
container.appendChild(progressBar.render());
|
|
1501
2665
|
if (!isPlaying) {
|
|
@@ -1539,6 +2703,8 @@ function uiPlugin(config = {}) {
|
|
|
1539
2703
|
}
|
|
1540
2704
|
stateUnsubscribe?.();
|
|
1541
2705
|
stateUnsubscribe = null;
|
|
2706
|
+
errorUnsubscribe?.();
|
|
2707
|
+
errorUnsubscribe = null;
|
|
1542
2708
|
if (api?.container) {
|
|
1543
2709
|
api.container.removeEventListener("mousemove", handleInteraction);
|
|
1544
2710
|
api.container.removeEventListener("mouseenter", handleInteraction);
|
|
@@ -1552,6 +2718,8 @@ function uiPlugin(config = {}) {
|
|
|
1552
2718
|
controls = [];
|
|
1553
2719
|
progressBar?.destroy();
|
|
1554
2720
|
progressBar = null;
|
|
2721
|
+
errorOverlay?.destroy();
|
|
2722
|
+
errorOverlay = null;
|
|
1555
2723
|
controlBar?.remove();
|
|
1556
2724
|
controlBar = null;
|
|
1557
2725
|
gradient?.remove();
|