@scarlett-player/ui 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +1224 -49
- package/dist/index.d.cts +5 -4
- package/dist/index.d.ts +5 -4
- package/dist/index.js +1224 -49
- 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
|
|
@@ -647,12 +983,11 @@ var PlayButton = class {
|
|
|
647
983
|
const video = getVideo(this.api.container);
|
|
648
984
|
if (!video) return;
|
|
649
985
|
const ended = this.api.getState("ended");
|
|
650
|
-
const playing = this.api.getState("playing");
|
|
651
986
|
if (ended) {
|
|
652
987
|
video.currentTime = 0;
|
|
653
988
|
video.play().catch(() => {
|
|
654
989
|
});
|
|
655
|
-
} else if (
|
|
990
|
+
} else if (!video.paused) {
|
|
656
991
|
video.pause();
|
|
657
992
|
} else {
|
|
658
993
|
video.play().catch(() => {
|
|
@@ -665,6 +1000,70 @@ var PlayButton = class {
|
|
|
665
1000
|
}
|
|
666
1001
|
};
|
|
667
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
|
+
|
|
668
1067
|
// src/controls/ProgressBar.ts
|
|
669
1068
|
var ProgressBar = class {
|
|
670
1069
|
constructor(api) {
|
|
@@ -706,36 +1105,99 @@ var ProgressBar = class {
|
|
|
706
1105
|
}
|
|
707
1106
|
}
|
|
708
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
|
+
};
|
|
709
1147
|
this.onMouseMove = (e) => {
|
|
710
1148
|
this.updateTooltip(e.clientX);
|
|
711
1149
|
};
|
|
712
1150
|
this.onMouseLeave = () => {
|
|
713
1151
|
if (!this.isDragging) {
|
|
714
1152
|
this.tooltip.style.opacity = "0";
|
|
1153
|
+
this.thumbnailPreview.hide();
|
|
715
1154
|
}
|
|
716
1155
|
};
|
|
717
1156
|
this.onKeyDown = (e) => {
|
|
718
1157
|
const video = getVideo(this.api.container);
|
|
719
1158
|
if (!video) return;
|
|
720
1159
|
const step = 5;
|
|
721
|
-
const
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
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
|
+
}
|
|
739
1201
|
}
|
|
740
1202
|
};
|
|
741
1203
|
this.api = api;
|
|
@@ -747,10 +1209,12 @@ var ProgressBar = class {
|
|
|
747
1209
|
this.handle = createElement("div", { className: "sp-progress__handle" });
|
|
748
1210
|
this.tooltip = createElement("div", { className: "sp-progress__tooltip" });
|
|
749
1211
|
this.tooltip.textContent = "0:00";
|
|
1212
|
+
this.thumbnailPreview = new ThumbnailPreview();
|
|
750
1213
|
track.appendChild(this.buffered);
|
|
751
1214
|
track.appendChild(this.filled);
|
|
752
1215
|
track.appendChild(this.handle);
|
|
753
1216
|
this.el.appendChild(track);
|
|
1217
|
+
this.el.appendChild(this.thumbnailPreview.getElement());
|
|
754
1218
|
this.el.appendChild(this.tooltip);
|
|
755
1219
|
this.wrapper.appendChild(this.el);
|
|
756
1220
|
this.el.setAttribute("role", "slider");
|
|
@@ -760,9 +1224,13 @@ var ProgressBar = class {
|
|
|
760
1224
|
this.wrapper.addEventListener("mousedown", this.onMouseDown);
|
|
761
1225
|
this.wrapper.addEventListener("mousemove", this.onMouseMove);
|
|
762
1226
|
this.wrapper.addEventListener("mouseleave", this.onMouseLeave);
|
|
1227
|
+
this.wrapper.addEventListener("touchstart", this.onTouchStart, { passive: false });
|
|
763
1228
|
this.el.addEventListener("keydown", this.onKeyDown);
|
|
764
1229
|
document.addEventListener("mousemove", this.onDocMouseMove);
|
|
765
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);
|
|
766
1234
|
}
|
|
767
1235
|
render() {
|
|
768
1236
|
return this.wrapper;
|
|
@@ -775,11 +1243,40 @@ var ProgressBar = class {
|
|
|
775
1243
|
hide() {
|
|
776
1244
|
this.wrapper.classList.remove("sp-progress-wrapper--visible");
|
|
777
1245
|
}
|
|
1246
|
+
/** Set thumbnail sprite configuration */
|
|
1247
|
+
setThumbnails(config) {
|
|
1248
|
+
this.thumbnailPreview.setConfig(config);
|
|
1249
|
+
}
|
|
778
1250
|
update() {
|
|
779
1251
|
const currentTime = this.api.getState("currentTime") || 0;
|
|
780
1252
|
const duration = this.api.getState("duration") || 0;
|
|
781
1253
|
const bufferedRanges = this.api.getState("buffered");
|
|
782
|
-
|
|
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) {
|
|
783
1280
|
const progress = currentTime / duration * 100;
|
|
784
1281
|
this.filled.style.width = `${progress}%`;
|
|
785
1282
|
this.handle.style.left = `${progress}%`;
|
|
@@ -796,6 +1293,12 @@ var ProgressBar = class {
|
|
|
796
1293
|
getTimeFromPosition(clientX) {
|
|
797
1294
|
const rect = this.el.getBoundingClientRect();
|
|
798
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
|
+
}
|
|
799
1302
|
const duration = this.api.getState("duration") || 0;
|
|
800
1303
|
return percent * duration;
|
|
801
1304
|
}
|
|
@@ -803,8 +1306,18 @@ var ProgressBar = class {
|
|
|
803
1306
|
const rect = this.el.getBoundingClientRect();
|
|
804
1307
|
const percent = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
|
|
805
1308
|
const time = this.getTimeFromPosition(clientX);
|
|
806
|
-
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
|
+
}
|
|
807
1317
|
this.tooltip.style.left = `${percent * 100}%`;
|
|
1318
|
+
if (this.thumbnailPreview.isConfigured()) {
|
|
1319
|
+
this.thumbnailPreview.show(time, percent);
|
|
1320
|
+
}
|
|
808
1321
|
}
|
|
809
1322
|
updateVisualPosition(clientX) {
|
|
810
1323
|
const rect = this.el.getBoundingClientRect();
|
|
@@ -827,8 +1340,13 @@ var ProgressBar = class {
|
|
|
827
1340
|
this.wrapper.removeEventListener("mousedown", this.onMouseDown);
|
|
828
1341
|
this.wrapper.removeEventListener("mousemove", this.onMouseMove);
|
|
829
1342
|
this.wrapper.removeEventListener("mouseleave", this.onMouseLeave);
|
|
1343
|
+
this.wrapper.removeEventListener("touchstart", this.onTouchStart);
|
|
830
1344
|
document.removeEventListener("mousemove", this.onDocMouseMove);
|
|
831
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();
|
|
832
1350
|
this.wrapper.remove();
|
|
833
1351
|
}
|
|
834
1352
|
};
|
|
@@ -881,6 +1399,20 @@ var VolumeControl = class {
|
|
|
881
1399
|
this.onMouseUp = () => {
|
|
882
1400
|
this.isDragging = false;
|
|
883
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
|
+
};
|
|
884
1416
|
this.onKeyDown = (e) => {
|
|
885
1417
|
const video = getVideo(this.api.container);
|
|
886
1418
|
if (!video) return;
|
|
@@ -920,9 +1452,13 @@ var VolumeControl = class {
|
|
|
920
1452
|
this.el.appendChild(this.btn);
|
|
921
1453
|
this.el.appendChild(sliderWrap);
|
|
922
1454
|
this.slider.addEventListener("mousedown", this.onMouseDown);
|
|
1455
|
+
this.slider.addEventListener("touchstart", this.onTouchStart, { passive: false });
|
|
923
1456
|
this.slider.addEventListener("keydown", this.onKeyDown);
|
|
924
1457
|
document.addEventListener("mousemove", this.onDocMouseMove);
|
|
925
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);
|
|
926
1462
|
}
|
|
927
1463
|
render() {
|
|
928
1464
|
return this.el;
|
|
@@ -967,8 +1503,14 @@ var VolumeControl = class {
|
|
|
967
1503
|
return Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
|
|
968
1504
|
}
|
|
969
1505
|
destroy() {
|
|
1506
|
+
this.slider.removeEventListener("mousedown", this.onMouseDown);
|
|
1507
|
+
this.slider.removeEventListener("touchstart", this.onTouchStart);
|
|
1508
|
+
this.slider.removeEventListener("keydown", this.onKeyDown);
|
|
970
1509
|
document.removeEventListener("mousemove", this.onDocMouseMove);
|
|
971
1510
|
document.removeEventListener("mouseup", this.onMouseUp);
|
|
1511
|
+
document.removeEventListener("touchmove", this.onDocTouchMove);
|
|
1512
|
+
document.removeEventListener("touchend", this.onTouchEnd);
|
|
1513
|
+
document.removeEventListener("touchcancel", this.onTouchEnd);
|
|
972
1514
|
this.el.remove();
|
|
973
1515
|
}
|
|
974
1516
|
};
|
|
@@ -976,19 +1518,27 @@ var VolumeControl = class {
|
|
|
976
1518
|
// src/controls/LiveIndicator.ts
|
|
977
1519
|
var LiveIndicator = class {
|
|
978
1520
|
constructor(api) {
|
|
979
|
-
this.
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
this.
|
|
983
|
-
this.el.setAttribute("aria-label", "Seek to live");
|
|
984
|
-
this.el.setAttribute("tabindex", "0");
|
|
985
|
-
this.el.onclick = () => this.seekToLive();
|
|
986
|
-
this.el.onkeydown = (e) => {
|
|
1521
|
+
this.handleClick = () => {
|
|
1522
|
+
this.seekToLive();
|
|
1523
|
+
};
|
|
1524
|
+
this.handleKeyDown = (e) => {
|
|
987
1525
|
if (e.key === "Enter" || e.key === " ") {
|
|
988
1526
|
e.preventDefault();
|
|
989
1527
|
this.seekToLive();
|
|
990
1528
|
}
|
|
991
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);
|
|
992
1542
|
}
|
|
993
1543
|
render() {
|
|
994
1544
|
return this.el;
|
|
@@ -999,8 +1549,12 @@ var LiveIndicator = class {
|
|
|
999
1549
|
this.el.style.display = live ? "" : "none";
|
|
1000
1550
|
if (liveEdge) {
|
|
1001
1551
|
this.el.classList.remove("sp-live--behind");
|
|
1552
|
+
this.label.textContent = "LIVE";
|
|
1553
|
+
this.el.setAttribute("aria-label", "At live edge");
|
|
1002
1554
|
} else {
|
|
1003
1555
|
this.el.classList.add("sp-live--behind");
|
|
1556
|
+
this.label.textContent = "GO LIVE";
|
|
1557
|
+
this.el.setAttribute("aria-label", "Seek to live");
|
|
1004
1558
|
}
|
|
1005
1559
|
}
|
|
1006
1560
|
seekToLive() {
|
|
@@ -1012,6 +1566,8 @@ var LiveIndicator = class {
|
|
|
1012
1566
|
}
|
|
1013
1567
|
}
|
|
1014
1568
|
destroy() {
|
|
1569
|
+
this.el.removeEventListener("click", this.handleClick);
|
|
1570
|
+
this.el.removeEventListener("keydown", this.handleKeyDown);
|
|
1015
1571
|
this.el.remove();
|
|
1016
1572
|
}
|
|
1017
1573
|
};
|
|
@@ -1322,14 +1878,588 @@ var Spacer = class {
|
|
|
1322
1878
|
}
|
|
1323
1879
|
};
|
|
1324
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
|
+
|
|
1325
2452
|
// src/index.ts
|
|
1326
2453
|
var DEFAULT_LAYOUT = [
|
|
1327
2454
|
"play",
|
|
2455
|
+
"skip-backward",
|
|
2456
|
+
"skip-forward",
|
|
1328
2457
|
"volume",
|
|
1329
2458
|
"time",
|
|
1330
2459
|
"live-indicator",
|
|
1331
2460
|
"spacer",
|
|
1332
|
-
"
|
|
2461
|
+
"settings",
|
|
2462
|
+
"captions",
|
|
1333
2463
|
"chromecast",
|
|
1334
2464
|
"airplay",
|
|
1335
2465
|
"pip",
|
|
@@ -1342,10 +2472,12 @@ function uiPlugin(config = {}) {
|
|
|
1342
2472
|
let gradient = null;
|
|
1343
2473
|
let progressBar = null;
|
|
1344
2474
|
let bufferingIndicator = null;
|
|
2475
|
+
let errorOverlay = null;
|
|
1345
2476
|
let styleEl = null;
|
|
1346
2477
|
let controls = [];
|
|
1347
2478
|
let hideTimeout = null;
|
|
1348
2479
|
let stateUnsubscribe = null;
|
|
2480
|
+
let errorUnsubscribe = null;
|
|
1349
2481
|
let controlsVisible = true;
|
|
1350
2482
|
const layout = config.controls || DEFAULT_LAYOUT;
|
|
1351
2483
|
const hideDelay = config.hideDelay ?? DEFAULT_HIDE_DELAY;
|
|
@@ -1353,6 +2485,10 @@ function uiPlugin(config = {}) {
|
|
|
1353
2485
|
switch (slot) {
|
|
1354
2486
|
case "play":
|
|
1355
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");
|
|
1356
2492
|
case "volume":
|
|
1357
2493
|
return new VolumeControl(api);
|
|
1358
2494
|
case "progress":
|
|
@@ -1363,6 +2499,10 @@ function uiPlugin(config = {}) {
|
|
|
1363
2499
|
return new LiveIndicator(api);
|
|
1364
2500
|
case "quality":
|
|
1365
2501
|
return new QualityMenu(api);
|
|
2502
|
+
case "settings":
|
|
2503
|
+
return new SettingsMenu(api);
|
|
2504
|
+
case "captions":
|
|
2505
|
+
return new CaptionsButton(api);
|
|
1366
2506
|
case "chromecast":
|
|
1367
2507
|
return new CastButton(api, "chromecast");
|
|
1368
2508
|
case "airplay":
|
|
@@ -1386,6 +2526,7 @@ function uiPlugin(config = {}) {
|
|
|
1386
2526
|
const isLoading = playbackState === "loading";
|
|
1387
2527
|
const showSpinner = waiting || seeking && !api?.getState("paused") || isLoading;
|
|
1388
2528
|
bufferingIndicator?.classList.toggle("sp-buffering--visible", !!showSpinner);
|
|
2529
|
+
errorOverlay?.update();
|
|
1389
2530
|
};
|
|
1390
2531
|
const showControls = () => {
|
|
1391
2532
|
if (controlsVisible) {
|
|
@@ -1424,8 +2565,14 @@ function uiPlugin(config = {}) {
|
|
|
1424
2565
|
};
|
|
1425
2566
|
const handleKeyDown = (e) => {
|
|
1426
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
|
+
}
|
|
1427
2572
|
const video = api.container.querySelector("video");
|
|
1428
2573
|
if (!video) return;
|
|
2574
|
+
const live = api.getState("live");
|
|
2575
|
+
const seekableRange = api.getState("seekableRange");
|
|
1429
2576
|
switch (e.key) {
|
|
1430
2577
|
case " ":
|
|
1431
2578
|
case "k":
|
|
@@ -1446,12 +2593,20 @@ function uiPlugin(config = {}) {
|
|
|
1446
2593
|
break;
|
|
1447
2594
|
case "ArrowLeft":
|
|
1448
2595
|
e.preventDefault();
|
|
1449
|
-
|
|
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
|
+
}
|
|
1450
2601
|
showControls();
|
|
1451
2602
|
break;
|
|
1452
2603
|
case "ArrowRight":
|
|
1453
2604
|
e.preventDefault();
|
|
1454
|
-
|
|
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
|
+
}
|
|
1455
2610
|
showControls();
|
|
1456
2611
|
break;
|
|
1457
2612
|
case "ArrowUp":
|
|
@@ -1488,19 +2643,30 @@ function uiPlugin(config = {}) {
|
|
|
1488
2643
|
if (containerStyle.position === "static") {
|
|
1489
2644
|
container.style.position = "relative";
|
|
1490
2645
|
}
|
|
2646
|
+
const isPlaying = api.getState("playing");
|
|
1491
2647
|
gradient = document.createElement("div");
|
|
1492
|
-
gradient.className = "sp-gradient sp-gradient--visible";
|
|
2648
|
+
gradient.className = isPlaying ? "sp-gradient" : "sp-gradient sp-gradient--visible";
|
|
1493
2649
|
container.appendChild(gradient);
|
|
1494
2650
|
bufferingIndicator = document.createElement("div");
|
|
1495
2651
|
bufferingIndicator.className = "sp-buffering";
|
|
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) {
|
|
2666
|
+
progressBar.show();
|
|
2667
|
+
}
|
|
1502
2668
|
controlBar = document.createElement("div");
|
|
1503
|
-
controlBar.className = "sp-controls sp-controls--visible";
|
|
2669
|
+
controlBar.className = isPlaying ? "sp-controls sp-controls--hidden" : "sp-controls sp-controls--visible";
|
|
1504
2670
|
controlBar.setAttribute("role", "toolbar");
|
|
1505
2671
|
controlBar.setAttribute("aria-label", "Video controls");
|
|
1506
2672
|
for (const slot of layout) {
|
|
@@ -1523,6 +2689,11 @@ function uiPlugin(config = {}) {
|
|
|
1523
2689
|
if (!container.hasAttribute("tabindex")) {
|
|
1524
2690
|
container.setAttribute("tabindex", "0");
|
|
1525
2691
|
}
|
|
2692
|
+
controlsVisible = !isPlaying;
|
|
2693
|
+
api.setState("controlsVisible", controlsVisible);
|
|
2694
|
+
if (isPlaying) {
|
|
2695
|
+
resetHideTimer();
|
|
2696
|
+
}
|
|
1526
2697
|
api.logger.debug("UI controls plugin initialized");
|
|
1527
2698
|
},
|
|
1528
2699
|
async destroy() {
|
|
@@ -1532,6 +2703,8 @@ function uiPlugin(config = {}) {
|
|
|
1532
2703
|
}
|
|
1533
2704
|
stateUnsubscribe?.();
|
|
1534
2705
|
stateUnsubscribe = null;
|
|
2706
|
+
errorUnsubscribe?.();
|
|
2707
|
+
errorUnsubscribe = null;
|
|
1535
2708
|
if (api?.container) {
|
|
1536
2709
|
api.container.removeEventListener("mousemove", handleInteraction);
|
|
1537
2710
|
api.container.removeEventListener("mouseenter", handleInteraction);
|
|
@@ -1545,6 +2718,8 @@ function uiPlugin(config = {}) {
|
|
|
1545
2718
|
controls = [];
|
|
1546
2719
|
progressBar?.destroy();
|
|
1547
2720
|
progressBar = null;
|
|
2721
|
+
errorOverlay?.destroy();
|
|
2722
|
+
errorOverlay = null;
|
|
1548
2723
|
controlBar?.remove();
|
|
1549
2724
|
controlBar = null;
|
|
1550
2725
|
gradient?.remove();
|