@scarlett-player/ui 0.4.1 → 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 +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.js
CHANGED
|
@@ -107,7 +107,12 @@ var styles = `
|
|
|
107
107
|
transition: height 0.15s ease;
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
|
|
110
|
+
@media (hover: hover) {
|
|
111
|
+
.sp-progress-wrapper:hover .sp-progress {
|
|
112
|
+
height: 5px;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
111
116
|
.sp-progress--dragging {
|
|
112
117
|
height: 5px;
|
|
113
118
|
}
|
|
@@ -153,11 +158,34 @@ var styles = `
|
|
|
153
158
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
|
154
159
|
}
|
|
155
160
|
|
|
156
|
-
|
|
161
|
+
@media (hover: hover) {
|
|
162
|
+
.sp-progress-wrapper:hover .sp-progress__handle {
|
|
163
|
+
transform: translate(-50%, -50%) scale(1);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
157
167
|
.sp-progress--dragging .sp-progress__handle {
|
|
158
168
|
transform: translate(-50%, -50%) scale(1);
|
|
159
169
|
}
|
|
160
170
|
|
|
171
|
+
/* Thumbnail Preview */
|
|
172
|
+
.sp-thumbnail-preview {
|
|
173
|
+
position: absolute;
|
|
174
|
+
bottom: calc(100% + 8px);
|
|
175
|
+
transform: translateX(-50%);
|
|
176
|
+
pointer-events: none;
|
|
177
|
+
display: none;
|
|
178
|
+
z-index: 21;
|
|
179
|
+
border-radius: 4px;
|
|
180
|
+
overflow: hidden;
|
|
181
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
|
|
182
|
+
border: 2px solid rgba(255, 255, 255, 0.2);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.sp-thumbnail-preview__img {
|
|
186
|
+
background-repeat: no-repeat;
|
|
187
|
+
}
|
|
188
|
+
|
|
161
189
|
/* Progress Tooltip */
|
|
162
190
|
.sp-progress__tooltip {
|
|
163
191
|
position: absolute;
|
|
@@ -177,8 +205,10 @@ var styles = `
|
|
|
177
205
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
|
178
206
|
}
|
|
179
207
|
|
|
180
|
-
|
|
181
|
-
|
|
208
|
+
@media (hover: hover) {
|
|
209
|
+
.sp-progress-wrapper:hover .sp-progress__tooltip {
|
|
210
|
+
opacity: 1;
|
|
211
|
+
}
|
|
182
212
|
}
|
|
183
213
|
|
|
184
214
|
/* ============================================
|
|
@@ -198,9 +228,11 @@ var styles = `
|
|
|
198
228
|
flex-shrink: 0;
|
|
199
229
|
}
|
|
200
230
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
231
|
+
@media (hover: hover) {
|
|
232
|
+
.sp-control:hover {
|
|
233
|
+
color: #fff;
|
|
234
|
+
background: rgba(255, 255, 255, 0.1);
|
|
235
|
+
}
|
|
204
236
|
}
|
|
205
237
|
|
|
206
238
|
.sp-control:active {
|
|
@@ -269,7 +301,12 @@ var styles = `
|
|
|
269
301
|
transition: width 0.2s ease;
|
|
270
302
|
}
|
|
271
303
|
|
|
272
|
-
|
|
304
|
+
@media (hover: hover) {
|
|
305
|
+
.sp-volume:hover .sp-volume__slider-wrap {
|
|
306
|
+
width: 64px;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
273
310
|
.sp-volume:focus-within .sp-volume__slider-wrap {
|
|
274
311
|
width: 64px;
|
|
275
312
|
}
|
|
@@ -312,8 +349,10 @@ var styles = `
|
|
|
312
349
|
transition: background 0.15s ease, opacity 0.15s ease;
|
|
313
350
|
}
|
|
314
351
|
|
|
315
|
-
|
|
316
|
-
|
|
352
|
+
@media (hover: hover) {
|
|
353
|
+
.sp-live:hover {
|
|
354
|
+
background: rgba(255, 255, 255, 0.1);
|
|
355
|
+
}
|
|
317
356
|
}
|
|
318
357
|
|
|
319
358
|
.sp-live__dot {
|
|
@@ -332,6 +371,16 @@ var styles = `
|
|
|
332
371
|
animation: none;
|
|
333
372
|
}
|
|
334
373
|
|
|
374
|
+
.sp-live--behind span {
|
|
375
|
+
text-decoration: underline;
|
|
376
|
+
text-underline-offset: 2px;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/* Progress bar live mode: accent color for filled bar */
|
|
380
|
+
.sp-progress--live .sp-progress__filled {
|
|
381
|
+
background: var(--sp-accent, #e50914);
|
|
382
|
+
}
|
|
383
|
+
|
|
335
384
|
@keyframes sp-pulse {
|
|
336
385
|
0%, 100% { opacity: 1; }
|
|
337
386
|
50% { opacity: 0.4; }
|
|
@@ -412,6 +461,169 @@ var styles = `
|
|
|
412
461
|
opacity: 1;
|
|
413
462
|
}
|
|
414
463
|
|
|
464
|
+
/* ============================================
|
|
465
|
+
Settings Menu (Gear Icon)
|
|
466
|
+
============================================ */
|
|
467
|
+
.sp-settings {
|
|
468
|
+
position: relative;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
.sp-settings__btn {
|
|
472
|
+
display: flex;
|
|
473
|
+
align-items: center;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
.sp-settings-panel {
|
|
477
|
+
position: absolute;
|
|
478
|
+
bottom: calc(100% + 8px);
|
|
479
|
+
right: 0;
|
|
480
|
+
background: rgba(20, 20, 20, 0.95);
|
|
481
|
+
backdrop-filter: blur(8px);
|
|
482
|
+
-webkit-backdrop-filter: blur(8px);
|
|
483
|
+
border-radius: 8px;
|
|
484
|
+
min-width: 200px;
|
|
485
|
+
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4);
|
|
486
|
+
opacity: 0;
|
|
487
|
+
visibility: hidden;
|
|
488
|
+
transform: translateY(8px);
|
|
489
|
+
transition: opacity 0.15s ease, transform 0.15s ease, visibility 0.15s;
|
|
490
|
+
z-index: 20;
|
|
491
|
+
overflow: hidden;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
.sp-settings-panel--open {
|
|
495
|
+
opacity: 1;
|
|
496
|
+
visibility: visible;
|
|
497
|
+
transform: translateY(0);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/* Main menu rows */
|
|
501
|
+
.sp-settings-panel--main {
|
|
502
|
+
padding: 4px 0;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
.sp-settings-panel__row {
|
|
506
|
+
display: flex;
|
|
507
|
+
align-items: center;
|
|
508
|
+
justify-content: space-between;
|
|
509
|
+
padding: 10px 16px;
|
|
510
|
+
font-size: 13px;
|
|
511
|
+
color: rgba(255, 255, 255, 0.9);
|
|
512
|
+
cursor: pointer;
|
|
513
|
+
transition: background 0.1s ease;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
.sp-settings-panel__row:hover {
|
|
517
|
+
background: rgba(255, 255, 255, 0.1);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
.sp-settings-panel__label {
|
|
521
|
+
font-weight: 500;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
.sp-settings-panel__value {
|
|
525
|
+
display: flex;
|
|
526
|
+
align-items: center;
|
|
527
|
+
gap: 4px;
|
|
528
|
+
color: rgba(255, 255, 255, 0.6);
|
|
529
|
+
font-size: 12px;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
.sp-settings-panel__arrow {
|
|
533
|
+
display: flex;
|
|
534
|
+
align-items: center;
|
|
535
|
+
transform: rotate(-90deg);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
.sp-settings-panel__arrow svg {
|
|
539
|
+
width: 16px;
|
|
540
|
+
height: 16px;
|
|
541
|
+
fill: currentColor;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/* Sub-menu panels */
|
|
545
|
+
.sp-settings-panel--sub {
|
|
546
|
+
padding: 0;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
.sp-settings-panel__header {
|
|
550
|
+
display: flex;
|
|
551
|
+
align-items: center;
|
|
552
|
+
gap: 8px;
|
|
553
|
+
padding: 10px 16px;
|
|
554
|
+
font-size: 13px;
|
|
555
|
+
font-weight: 600;
|
|
556
|
+
color: rgba(255, 255, 255, 0.9);
|
|
557
|
+
cursor: pointer;
|
|
558
|
+
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
|
559
|
+
transition: background 0.1s ease;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
.sp-settings-panel__header:hover {
|
|
563
|
+
background: rgba(255, 255, 255, 0.1);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
.sp-settings-panel__back {
|
|
567
|
+
display: flex;
|
|
568
|
+
align-items: center;
|
|
569
|
+
transform: rotate(-90deg);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
.sp-settings-panel__back svg {
|
|
573
|
+
width: 16px;
|
|
574
|
+
height: 16px;
|
|
575
|
+
fill: currentColor;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
.sp-settings-panel__header-label {
|
|
579
|
+
flex: 1;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
.sp-settings-panel__item {
|
|
583
|
+
display: flex;
|
|
584
|
+
align-items: center;
|
|
585
|
+
justify-content: space-between;
|
|
586
|
+
padding: 10px 16px;
|
|
587
|
+
font-size: 13px;
|
|
588
|
+
color: rgba(255, 255, 255, 0.8);
|
|
589
|
+
cursor: pointer;
|
|
590
|
+
transition: background 0.1s ease, color 0.1s ease;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
.sp-settings-panel__item:hover {
|
|
594
|
+
background: rgba(255, 255, 255, 0.1);
|
|
595
|
+
color: #fff;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
.sp-settings-panel__item--active {
|
|
599
|
+
color: var(--sp-accent, #e50914);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
.sp-settings-panel__check {
|
|
603
|
+
width: 16px;
|
|
604
|
+
height: 16px;
|
|
605
|
+
fill: currentColor;
|
|
606
|
+
margin-left: 8px;
|
|
607
|
+
opacity: 0;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
.sp-settings-panel__check svg {
|
|
611
|
+
width: 16px;
|
|
612
|
+
height: 16px;
|
|
613
|
+
fill: currentColor;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
.sp-settings-panel__item--active .sp-settings-panel__check {
|
|
617
|
+
opacity: 1;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/* ============================================
|
|
621
|
+
Captions Button
|
|
622
|
+
============================================ */
|
|
623
|
+
.sp-captions--active {
|
|
624
|
+
color: var(--sp-accent, #e50914);
|
|
625
|
+
}
|
|
626
|
+
|
|
415
627
|
/* ============================================
|
|
416
628
|
Cast Button States
|
|
417
629
|
============================================ */
|
|
@@ -423,6 +635,122 @@ var styles = `
|
|
|
423
635
|
opacity: 0.4;
|
|
424
636
|
}
|
|
425
637
|
|
|
638
|
+
/* ============================================
|
|
639
|
+
Error Overlay
|
|
640
|
+
============================================ */
|
|
641
|
+
.sp-error-overlay {
|
|
642
|
+
position: absolute;
|
|
643
|
+
top: 0;
|
|
644
|
+
left: 0;
|
|
645
|
+
right: 0;
|
|
646
|
+
bottom: 0;
|
|
647
|
+
background: rgba(0, 0, 0, 0.85);
|
|
648
|
+
display: flex;
|
|
649
|
+
align-items: center;
|
|
650
|
+
justify-content: center;
|
|
651
|
+
z-index: 25;
|
|
652
|
+
opacity: 0;
|
|
653
|
+
visibility: hidden;
|
|
654
|
+
transition: opacity 0.25s ease, visibility 0.25s;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
.sp-error-overlay--visible {
|
|
658
|
+
opacity: 1;
|
|
659
|
+
visibility: visible;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
.sp-error-overlay__content {
|
|
663
|
+
display: flex;
|
|
664
|
+
flex-direction: column;
|
|
665
|
+
align-items: center;
|
|
666
|
+
text-align: center;
|
|
667
|
+
padding: 24px;
|
|
668
|
+
max-width: 360px;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
.sp-error-overlay__icon {
|
|
672
|
+
color: rgba(255, 255, 255, 0.7);
|
|
673
|
+
margin-bottom: 16px;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
.sp-error-overlay__icon svg {
|
|
677
|
+
width: 48px;
|
|
678
|
+
height: 48px;
|
|
679
|
+
fill: currentColor;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
.sp-error-overlay__message {
|
|
683
|
+
color: rgba(255, 255, 255, 0.9);
|
|
684
|
+
font-size: 15px;
|
|
685
|
+
line-height: 1.5;
|
|
686
|
+
margin: 0 0 24px;
|
|
687
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
.sp-error-overlay__actions {
|
|
691
|
+
display: flex;
|
|
692
|
+
gap: 12px;
|
|
693
|
+
flex-wrap: wrap;
|
|
694
|
+
justify-content: center;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
.sp-error-overlay__retry {
|
|
698
|
+
background: var(--sp-accent, #e50914);
|
|
699
|
+
color: #fff;
|
|
700
|
+
border: none;
|
|
701
|
+
padding: 12px 24px;
|
|
702
|
+
font-size: 14px;
|
|
703
|
+
font-weight: 600;
|
|
704
|
+
border-radius: 6px;
|
|
705
|
+
cursor: pointer;
|
|
706
|
+
min-width: 120px;
|
|
707
|
+
min-height: 44px;
|
|
708
|
+
transition: background 0.15s ease, transform 0.15s ease;
|
|
709
|
+
font-family: inherit;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
.sp-error-overlay__retry:hover {
|
|
713
|
+
filter: brightness(1.1);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
.sp-error-overlay__retry:active {
|
|
717
|
+
transform: scale(0.96);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
.sp-error-overlay__retry:focus-visible {
|
|
721
|
+
outline: 2px solid #fff;
|
|
722
|
+
outline-offset: 2px;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
.sp-error-overlay__dismiss {
|
|
726
|
+
background: none;
|
|
727
|
+
color: rgba(255, 255, 255, 0.7);
|
|
728
|
+
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
729
|
+
padding: 12px 24px;
|
|
730
|
+
font-size: 14px;
|
|
731
|
+
font-weight: 500;
|
|
732
|
+
border-radius: 6px;
|
|
733
|
+
cursor: pointer;
|
|
734
|
+
min-width: 100px;
|
|
735
|
+
min-height: 44px;
|
|
736
|
+
transition: color 0.15s ease, border-color 0.15s ease, transform 0.15s ease;
|
|
737
|
+
font-family: inherit;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
.sp-error-overlay__dismiss:hover {
|
|
741
|
+
color: #fff;
|
|
742
|
+
border-color: rgba(255, 255, 255, 0.5);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
.sp-error-overlay__dismiss:active {
|
|
746
|
+
transform: scale(0.96);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
.sp-error-overlay__dismiss:focus-visible {
|
|
750
|
+
outline: 2px solid #fff;
|
|
751
|
+
outline-offset: 2px;
|
|
752
|
+
}
|
|
753
|
+
|
|
426
754
|
/* ============================================
|
|
427
755
|
Buffering Indicator
|
|
428
756
|
============================================ */
|
|
@@ -470,7 +798,14 @@ var styles = `
|
|
|
470
798
|
.sp-control,
|
|
471
799
|
.sp-volume__slider-wrap,
|
|
472
800
|
.sp-quality-menu,
|
|
473
|
-
.sp-
|
|
801
|
+
.sp-settings-panel,
|
|
802
|
+
.sp-settings-panel__row,
|
|
803
|
+
.sp-settings-panel__item,
|
|
804
|
+
.sp-settings-panel__header,
|
|
805
|
+
.sp-buffering,
|
|
806
|
+
.sp-error-overlay,
|
|
807
|
+
.sp-error-overlay__retry,
|
|
808
|
+
.sp-error-overlay__dismiss {
|
|
474
809
|
transition: none;
|
|
475
810
|
}
|
|
476
811
|
|
|
@@ -516,8 +851,9 @@ var icons = {
|
|
|
516
851
|
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>`,
|
|
517
852
|
skipForward: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M4 18l8.5-6L4 6v12zm9-12v12l8.5-6L13 6z"/></svg>`,
|
|
518
853
|
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>`,
|
|
519
|
-
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"/><
|
|
520
|
-
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"/><
|
|
854
|
+
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>`,
|
|
855
|
+
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>`,
|
|
856
|
+
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>`
|
|
521
857
|
};
|
|
522
858
|
|
|
523
859
|
// src/utils/dom.ts
|
|
@@ -633,6 +969,70 @@ var PlayButton = class {
|
|
|
633
969
|
}
|
|
634
970
|
};
|
|
635
971
|
|
|
972
|
+
// src/controls/ThumbnailPreview.ts
|
|
973
|
+
var ThumbnailPreview = class {
|
|
974
|
+
constructor() {
|
|
975
|
+
this.config = null;
|
|
976
|
+
this.loaded = false;
|
|
977
|
+
this.el = createElement("div", { className: "sp-thumbnail-preview" });
|
|
978
|
+
this.img = createElement("div", { className: "sp-thumbnail-preview__img" });
|
|
979
|
+
this.el.appendChild(this.img);
|
|
980
|
+
}
|
|
981
|
+
getElement() {
|
|
982
|
+
return this.el;
|
|
983
|
+
}
|
|
984
|
+
setConfig(config) {
|
|
985
|
+
this.config = config;
|
|
986
|
+
this.loaded = false;
|
|
987
|
+
if (config) {
|
|
988
|
+
this.img.style.width = `${config.width}px`;
|
|
989
|
+
this.img.style.height = `${config.height}px`;
|
|
990
|
+
this.el.style.width = `${config.width}px`;
|
|
991
|
+
this.el.style.height = `${config.height}px`;
|
|
992
|
+
const preload = new Image();
|
|
993
|
+
preload.onload = () => {
|
|
994
|
+
this.loaded = true;
|
|
995
|
+
};
|
|
996
|
+
preload.onerror = () => {
|
|
997
|
+
this.config = null;
|
|
998
|
+
this.loaded = false;
|
|
999
|
+
};
|
|
1000
|
+
preload.src = config.src;
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
/**
|
|
1004
|
+
* Update the thumbnail to show the frame at the given time.
|
|
1005
|
+
* @param time Time in seconds
|
|
1006
|
+
* @param percent Position as 0-1 fraction (for horizontal positioning)
|
|
1007
|
+
*/
|
|
1008
|
+
show(time, percent) {
|
|
1009
|
+
if (!this.config || !this.loaded) {
|
|
1010
|
+
this.el.style.display = "none";
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
const { src, width, height, columns, interval } = this.config;
|
|
1014
|
+
const index = Math.floor(time / interval);
|
|
1015
|
+
const col = index % columns;
|
|
1016
|
+
const row = Math.floor(index / columns);
|
|
1017
|
+
this.img.style.backgroundImage = `url(${src})`;
|
|
1018
|
+
this.img.style.backgroundPosition = `-${col * width}px -${row * height}px`;
|
|
1019
|
+
this.img.style.backgroundSize = `${columns * width}px auto`;
|
|
1020
|
+
this.img.style.width = `${width}px`;
|
|
1021
|
+
this.img.style.height = `${height}px`;
|
|
1022
|
+
this.el.style.left = `${percent * 100}%`;
|
|
1023
|
+
this.el.style.display = "";
|
|
1024
|
+
}
|
|
1025
|
+
hide() {
|
|
1026
|
+
this.el.style.display = "none";
|
|
1027
|
+
}
|
|
1028
|
+
isConfigured() {
|
|
1029
|
+
return this.config !== null;
|
|
1030
|
+
}
|
|
1031
|
+
destroy() {
|
|
1032
|
+
this.el.remove();
|
|
1033
|
+
}
|
|
1034
|
+
};
|
|
1035
|
+
|
|
636
1036
|
// src/controls/ProgressBar.ts
|
|
637
1037
|
var ProgressBar = class {
|
|
638
1038
|
constructor(api) {
|
|
@@ -674,36 +1074,99 @@ var ProgressBar = class {
|
|
|
674
1074
|
}
|
|
675
1075
|
}
|
|
676
1076
|
};
|
|
1077
|
+
this.onTouchStart = (e) => {
|
|
1078
|
+
e.preventDefault();
|
|
1079
|
+
const video = getVideo(this.api.container);
|
|
1080
|
+
this.wasPlayingBeforeDrag = video ? !video.paused : false;
|
|
1081
|
+
this.isDragging = true;
|
|
1082
|
+
this.el.classList.add("sp-progress--dragging");
|
|
1083
|
+
this.lastSeekTime = 0;
|
|
1084
|
+
this.seek(e.touches[0].clientX, true);
|
|
1085
|
+
};
|
|
1086
|
+
this.onDocTouchMove = (e) => {
|
|
1087
|
+
if (this.isDragging) {
|
|
1088
|
+
e.preventDefault();
|
|
1089
|
+
this.seek(e.touches[0].clientX);
|
|
1090
|
+
this.updateVisualPosition(e.touches[0].clientX);
|
|
1091
|
+
}
|
|
1092
|
+
};
|
|
1093
|
+
this.onTouchEnd = (e) => {
|
|
1094
|
+
if (this.isDragging) {
|
|
1095
|
+
const clientX = e.changedTouches?.[0]?.clientX;
|
|
1096
|
+
if (clientX !== void 0) {
|
|
1097
|
+
this.seek(clientX, true);
|
|
1098
|
+
}
|
|
1099
|
+
this.isDragging = false;
|
|
1100
|
+
this.el.classList.remove("sp-progress--dragging");
|
|
1101
|
+
if (this.wasPlayingBeforeDrag) {
|
|
1102
|
+
const video = getVideo(this.api.container);
|
|
1103
|
+
if (video && video.paused) {
|
|
1104
|
+
const resumePlayback = () => {
|
|
1105
|
+
video.removeEventListener("seeked", resumePlayback);
|
|
1106
|
+
video.play().catch(() => {
|
|
1107
|
+
});
|
|
1108
|
+
};
|
|
1109
|
+
video.addEventListener("seeked", resumePlayback);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
this.tooltip.style.opacity = "0";
|
|
1113
|
+
this.thumbnailPreview.hide();
|
|
1114
|
+
}
|
|
1115
|
+
};
|
|
677
1116
|
this.onMouseMove = (e) => {
|
|
678
1117
|
this.updateTooltip(e.clientX);
|
|
679
1118
|
};
|
|
680
1119
|
this.onMouseLeave = () => {
|
|
681
1120
|
if (!this.isDragging) {
|
|
682
1121
|
this.tooltip.style.opacity = "0";
|
|
1122
|
+
this.thumbnailPreview.hide();
|
|
683
1123
|
}
|
|
684
1124
|
};
|
|
685
1125
|
this.onKeyDown = (e) => {
|
|
686
1126
|
const video = getVideo(this.api.container);
|
|
687
1127
|
if (!video) return;
|
|
688
1128
|
const step = 5;
|
|
689
|
-
const
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
1129
|
+
const live = this.api.getState("live");
|
|
1130
|
+
const seekableRange = this.api.getState("seekableRange");
|
|
1131
|
+
if (live && seekableRange) {
|
|
1132
|
+
switch (e.key) {
|
|
1133
|
+
case "ArrowLeft":
|
|
1134
|
+
e.preventDefault();
|
|
1135
|
+
video.currentTime = Math.max(seekableRange.start, video.currentTime - step);
|
|
1136
|
+
break;
|
|
1137
|
+
case "ArrowRight":
|
|
1138
|
+
e.preventDefault();
|
|
1139
|
+
video.currentTime = Math.min(seekableRange.end, video.currentTime + step);
|
|
1140
|
+
break;
|
|
1141
|
+
case "Home":
|
|
1142
|
+
e.preventDefault();
|
|
1143
|
+
video.currentTime = seekableRange.start;
|
|
1144
|
+
break;
|
|
1145
|
+
case "End":
|
|
1146
|
+
e.preventDefault();
|
|
1147
|
+
video.currentTime = seekableRange.end;
|
|
1148
|
+
break;
|
|
1149
|
+
}
|
|
1150
|
+
} else {
|
|
1151
|
+
const duration = this.api.getState("duration") || 0;
|
|
1152
|
+
switch (e.key) {
|
|
1153
|
+
case "ArrowLeft":
|
|
1154
|
+
e.preventDefault();
|
|
1155
|
+
video.currentTime = Math.max(0, video.currentTime - step);
|
|
1156
|
+
break;
|
|
1157
|
+
case "ArrowRight":
|
|
1158
|
+
e.preventDefault();
|
|
1159
|
+
video.currentTime = Math.min(duration, video.currentTime + step);
|
|
1160
|
+
break;
|
|
1161
|
+
case "Home":
|
|
1162
|
+
e.preventDefault();
|
|
1163
|
+
video.currentTime = 0;
|
|
1164
|
+
break;
|
|
1165
|
+
case "End":
|
|
1166
|
+
e.preventDefault();
|
|
1167
|
+
video.currentTime = duration;
|
|
1168
|
+
break;
|
|
1169
|
+
}
|
|
707
1170
|
}
|
|
708
1171
|
};
|
|
709
1172
|
this.api = api;
|
|
@@ -715,10 +1178,12 @@ var ProgressBar = class {
|
|
|
715
1178
|
this.handle = createElement("div", { className: "sp-progress__handle" });
|
|
716
1179
|
this.tooltip = createElement("div", { className: "sp-progress__tooltip" });
|
|
717
1180
|
this.tooltip.textContent = "0:00";
|
|
1181
|
+
this.thumbnailPreview = new ThumbnailPreview();
|
|
718
1182
|
track.appendChild(this.buffered);
|
|
719
1183
|
track.appendChild(this.filled);
|
|
720
1184
|
track.appendChild(this.handle);
|
|
721
1185
|
this.el.appendChild(track);
|
|
1186
|
+
this.el.appendChild(this.thumbnailPreview.getElement());
|
|
722
1187
|
this.el.appendChild(this.tooltip);
|
|
723
1188
|
this.wrapper.appendChild(this.el);
|
|
724
1189
|
this.el.setAttribute("role", "slider");
|
|
@@ -728,9 +1193,13 @@ var ProgressBar = class {
|
|
|
728
1193
|
this.wrapper.addEventListener("mousedown", this.onMouseDown);
|
|
729
1194
|
this.wrapper.addEventListener("mousemove", this.onMouseMove);
|
|
730
1195
|
this.wrapper.addEventListener("mouseleave", this.onMouseLeave);
|
|
1196
|
+
this.wrapper.addEventListener("touchstart", this.onTouchStart, { passive: false });
|
|
731
1197
|
this.el.addEventListener("keydown", this.onKeyDown);
|
|
732
1198
|
document.addEventListener("mousemove", this.onDocMouseMove);
|
|
733
1199
|
document.addEventListener("mouseup", this.onMouseUp);
|
|
1200
|
+
document.addEventListener("touchmove", this.onDocTouchMove, { passive: false });
|
|
1201
|
+
document.addEventListener("touchend", this.onTouchEnd);
|
|
1202
|
+
document.addEventListener("touchcancel", this.onTouchEnd);
|
|
734
1203
|
}
|
|
735
1204
|
render() {
|
|
736
1205
|
return this.wrapper;
|
|
@@ -743,11 +1212,40 @@ var ProgressBar = class {
|
|
|
743
1212
|
hide() {
|
|
744
1213
|
this.wrapper.classList.remove("sp-progress-wrapper--visible");
|
|
745
1214
|
}
|
|
1215
|
+
/** Set thumbnail sprite configuration */
|
|
1216
|
+
setThumbnails(config) {
|
|
1217
|
+
this.thumbnailPreview.setConfig(config);
|
|
1218
|
+
}
|
|
746
1219
|
update() {
|
|
747
1220
|
const currentTime = this.api.getState("currentTime") || 0;
|
|
748
1221
|
const duration = this.api.getState("duration") || 0;
|
|
749
1222
|
const bufferedRanges = this.api.getState("buffered");
|
|
750
|
-
|
|
1223
|
+
const live = this.api.getState("live");
|
|
1224
|
+
const seekableRange = this.api.getState("seekableRange");
|
|
1225
|
+
const thumbnails = this.api.getState("thumbnails");
|
|
1226
|
+
if (thumbnails && !this.thumbnailPreview.isConfigured()) {
|
|
1227
|
+
this.thumbnailPreview.setConfig(thumbnails);
|
|
1228
|
+
}
|
|
1229
|
+
this.el.classList.toggle("sp-progress--live", !!live);
|
|
1230
|
+
if (live && seekableRange) {
|
|
1231
|
+
const rangeLength = seekableRange.end - seekableRange.start;
|
|
1232
|
+
if (rangeLength > 0) {
|
|
1233
|
+
const progress = (currentTime - seekableRange.start) / rangeLength * 100;
|
|
1234
|
+
this.filled.style.width = `${Math.max(0, Math.min(100, progress))}%`;
|
|
1235
|
+
this.handle.style.left = `${Math.max(0, Math.min(100, progress))}%`;
|
|
1236
|
+
}
|
|
1237
|
+
if (bufferedRanges && bufferedRanges.length > 0) {
|
|
1238
|
+
const rangeLength2 = seekableRange.end - seekableRange.start;
|
|
1239
|
+
if (rangeLength2 > 0) {
|
|
1240
|
+
const bufferedEnd = bufferedRanges.end(bufferedRanges.length - 1);
|
|
1241
|
+
const bufferedPercent = (bufferedEnd - seekableRange.start) / rangeLength2 * 100;
|
|
1242
|
+
this.buffered.style.width = `${Math.max(0, Math.min(100, bufferedPercent))}%`;
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
this.el.setAttribute("aria-valuemax", String(Math.floor(seekableRange.end)));
|
|
1246
|
+
this.el.setAttribute("aria-valuenow", String(Math.floor(currentTime)));
|
|
1247
|
+
this.el.setAttribute("aria-valuetext", `${Math.floor(seekableRange.end - currentTime)} seconds behind live`);
|
|
1248
|
+
} else if (duration > 0) {
|
|
751
1249
|
const progress = currentTime / duration * 100;
|
|
752
1250
|
this.filled.style.width = `${progress}%`;
|
|
753
1251
|
this.handle.style.left = `${progress}%`;
|
|
@@ -764,6 +1262,12 @@ var ProgressBar = class {
|
|
|
764
1262
|
getTimeFromPosition(clientX) {
|
|
765
1263
|
const rect = this.el.getBoundingClientRect();
|
|
766
1264
|
const percent = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
|
|
1265
|
+
const live = this.api.getState("live");
|
|
1266
|
+
const seekableRange = this.api.getState("seekableRange");
|
|
1267
|
+
if (live && seekableRange) {
|
|
1268
|
+
const rangeLength = seekableRange.end - seekableRange.start;
|
|
1269
|
+
return seekableRange.start + percent * rangeLength;
|
|
1270
|
+
}
|
|
767
1271
|
const duration = this.api.getState("duration") || 0;
|
|
768
1272
|
return percent * duration;
|
|
769
1273
|
}
|
|
@@ -771,8 +1275,18 @@ var ProgressBar = class {
|
|
|
771
1275
|
const rect = this.el.getBoundingClientRect();
|
|
772
1276
|
const percent = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
|
|
773
1277
|
const time = this.getTimeFromPosition(clientX);
|
|
774
|
-
this.
|
|
1278
|
+
const live = this.api.getState("live");
|
|
1279
|
+
const seekableRange = this.api.getState("seekableRange");
|
|
1280
|
+
if (live && seekableRange) {
|
|
1281
|
+
const behindLive = seekableRange.end - time;
|
|
1282
|
+
this.tooltip.textContent = formatLiveTime(behindLive);
|
|
1283
|
+
} else {
|
|
1284
|
+
this.tooltip.textContent = formatTime(time);
|
|
1285
|
+
}
|
|
775
1286
|
this.tooltip.style.left = `${percent * 100}%`;
|
|
1287
|
+
if (this.thumbnailPreview.isConfigured()) {
|
|
1288
|
+
this.thumbnailPreview.show(time, percent);
|
|
1289
|
+
}
|
|
776
1290
|
}
|
|
777
1291
|
updateVisualPosition(clientX) {
|
|
778
1292
|
const rect = this.el.getBoundingClientRect();
|
|
@@ -795,8 +1309,13 @@ var ProgressBar = class {
|
|
|
795
1309
|
this.wrapper.removeEventListener("mousedown", this.onMouseDown);
|
|
796
1310
|
this.wrapper.removeEventListener("mousemove", this.onMouseMove);
|
|
797
1311
|
this.wrapper.removeEventListener("mouseleave", this.onMouseLeave);
|
|
1312
|
+
this.wrapper.removeEventListener("touchstart", this.onTouchStart);
|
|
798
1313
|
document.removeEventListener("mousemove", this.onDocMouseMove);
|
|
799
1314
|
document.removeEventListener("mouseup", this.onMouseUp);
|
|
1315
|
+
document.removeEventListener("touchmove", this.onDocTouchMove);
|
|
1316
|
+
document.removeEventListener("touchend", this.onTouchEnd);
|
|
1317
|
+
document.removeEventListener("touchcancel", this.onTouchEnd);
|
|
1318
|
+
this.thumbnailPreview.destroy();
|
|
800
1319
|
this.wrapper.remove();
|
|
801
1320
|
}
|
|
802
1321
|
};
|
|
@@ -849,6 +1368,20 @@ var VolumeControl = class {
|
|
|
849
1368
|
this.onMouseUp = () => {
|
|
850
1369
|
this.isDragging = false;
|
|
851
1370
|
};
|
|
1371
|
+
this.onTouchStart = (e) => {
|
|
1372
|
+
e.preventDefault();
|
|
1373
|
+
this.isDragging = true;
|
|
1374
|
+
this.setVolume(this.getVolumeFromPosition(e.touches[0].clientX));
|
|
1375
|
+
};
|
|
1376
|
+
this.onDocTouchMove = (e) => {
|
|
1377
|
+
if (this.isDragging) {
|
|
1378
|
+
e.preventDefault();
|
|
1379
|
+
this.setVolume(this.getVolumeFromPosition(e.touches[0].clientX));
|
|
1380
|
+
}
|
|
1381
|
+
};
|
|
1382
|
+
this.onTouchEnd = () => {
|
|
1383
|
+
this.isDragging = false;
|
|
1384
|
+
};
|
|
852
1385
|
this.onKeyDown = (e) => {
|
|
853
1386
|
const video = getVideo(this.api.container);
|
|
854
1387
|
if (!video) return;
|
|
@@ -888,9 +1421,13 @@ var VolumeControl = class {
|
|
|
888
1421
|
this.el.appendChild(this.btn);
|
|
889
1422
|
this.el.appendChild(sliderWrap);
|
|
890
1423
|
this.slider.addEventListener("mousedown", this.onMouseDown);
|
|
1424
|
+
this.slider.addEventListener("touchstart", this.onTouchStart, { passive: false });
|
|
891
1425
|
this.slider.addEventListener("keydown", this.onKeyDown);
|
|
892
1426
|
document.addEventListener("mousemove", this.onDocMouseMove);
|
|
893
1427
|
document.addEventListener("mouseup", this.onMouseUp);
|
|
1428
|
+
document.addEventListener("touchmove", this.onDocTouchMove, { passive: false });
|
|
1429
|
+
document.addEventListener("touchend", this.onTouchEnd);
|
|
1430
|
+
document.addEventListener("touchcancel", this.onTouchEnd);
|
|
894
1431
|
}
|
|
895
1432
|
render() {
|
|
896
1433
|
return this.el;
|
|
@@ -935,8 +1472,14 @@ var VolumeControl = class {
|
|
|
935
1472
|
return Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
|
|
936
1473
|
}
|
|
937
1474
|
destroy() {
|
|
1475
|
+
this.slider.removeEventListener("mousedown", this.onMouseDown);
|
|
1476
|
+
this.slider.removeEventListener("touchstart", this.onTouchStart);
|
|
1477
|
+
this.slider.removeEventListener("keydown", this.onKeyDown);
|
|
938
1478
|
document.removeEventListener("mousemove", this.onDocMouseMove);
|
|
939
1479
|
document.removeEventListener("mouseup", this.onMouseUp);
|
|
1480
|
+
document.removeEventListener("touchmove", this.onDocTouchMove);
|
|
1481
|
+
document.removeEventListener("touchend", this.onTouchEnd);
|
|
1482
|
+
document.removeEventListener("touchcancel", this.onTouchEnd);
|
|
940
1483
|
this.el.remove();
|
|
941
1484
|
}
|
|
942
1485
|
};
|
|
@@ -944,19 +1487,27 @@ var VolumeControl = class {
|
|
|
944
1487
|
// src/controls/LiveIndicator.ts
|
|
945
1488
|
var LiveIndicator = class {
|
|
946
1489
|
constructor(api) {
|
|
947
|
-
this.
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
this.
|
|
951
|
-
this.el.setAttribute("aria-label", "Seek to live");
|
|
952
|
-
this.el.setAttribute("tabindex", "0");
|
|
953
|
-
this.el.onclick = () => this.seekToLive();
|
|
954
|
-
this.el.onkeydown = (e) => {
|
|
1490
|
+
this.handleClick = () => {
|
|
1491
|
+
this.seekToLive();
|
|
1492
|
+
};
|
|
1493
|
+
this.handleKeyDown = (e) => {
|
|
955
1494
|
if (e.key === "Enter" || e.key === " ") {
|
|
956
1495
|
e.preventDefault();
|
|
957
1496
|
this.seekToLive();
|
|
958
1497
|
}
|
|
959
1498
|
};
|
|
1499
|
+
this.api = api;
|
|
1500
|
+
this.el = createElement("div", { className: "sp-live" });
|
|
1501
|
+
this.dot = createElement("div", { className: "sp-live__dot" });
|
|
1502
|
+
this.label = document.createElement("span");
|
|
1503
|
+
this.label.textContent = "LIVE";
|
|
1504
|
+
this.el.appendChild(this.dot);
|
|
1505
|
+
this.el.appendChild(this.label);
|
|
1506
|
+
this.el.setAttribute("role", "button");
|
|
1507
|
+
this.el.setAttribute("aria-label", "Seek to live");
|
|
1508
|
+
this.el.setAttribute("tabindex", "0");
|
|
1509
|
+
this.el.addEventListener("click", this.handleClick);
|
|
1510
|
+
this.el.addEventListener("keydown", this.handleKeyDown);
|
|
960
1511
|
}
|
|
961
1512
|
render() {
|
|
962
1513
|
return this.el;
|
|
@@ -967,8 +1518,12 @@ var LiveIndicator = class {
|
|
|
967
1518
|
this.el.style.display = live ? "" : "none";
|
|
968
1519
|
if (liveEdge) {
|
|
969
1520
|
this.el.classList.remove("sp-live--behind");
|
|
1521
|
+
this.label.textContent = "LIVE";
|
|
1522
|
+
this.el.setAttribute("aria-label", "At live edge");
|
|
970
1523
|
} else {
|
|
971
1524
|
this.el.classList.add("sp-live--behind");
|
|
1525
|
+
this.label.textContent = "GO LIVE";
|
|
1526
|
+
this.el.setAttribute("aria-label", "Seek to live");
|
|
972
1527
|
}
|
|
973
1528
|
}
|
|
974
1529
|
seekToLive() {
|
|
@@ -980,6 +1535,8 @@ var LiveIndicator = class {
|
|
|
980
1535
|
}
|
|
981
1536
|
}
|
|
982
1537
|
destroy() {
|
|
1538
|
+
this.el.removeEventListener("click", this.handleClick);
|
|
1539
|
+
this.el.removeEventListener("keydown", this.handleKeyDown);
|
|
983
1540
|
this.el.remove();
|
|
984
1541
|
}
|
|
985
1542
|
};
|
|
@@ -1290,14 +1847,588 @@ var Spacer = class {
|
|
|
1290
1847
|
}
|
|
1291
1848
|
};
|
|
1292
1849
|
|
|
1850
|
+
// src/controls/ErrorOverlay.ts
|
|
1851
|
+
function getUserMessage(error) {
|
|
1852
|
+
if (!error) return "Something went wrong.";
|
|
1853
|
+
const msg = error.message?.toLowerCase() || "";
|
|
1854
|
+
if (msg.includes("network") || msg.includes("timeout") || msg.includes("fetch") || msg.includes("connection")) {
|
|
1855
|
+
return "Having trouble connecting. Check your internet and try again.";
|
|
1856
|
+
}
|
|
1857
|
+
if (msg.includes("manifest")) {
|
|
1858
|
+
return "Unable to load video. Please try again.";
|
|
1859
|
+
}
|
|
1860
|
+
if (msg.includes("decode") || msg.includes("media") || msg.includes("format") || msg.includes("codec")) {
|
|
1861
|
+
return "This video can't be played right now.";
|
|
1862
|
+
}
|
|
1863
|
+
if (msg.includes("not found") || msg.includes("404") || msg.includes("source") || msg.includes("not supported")) {
|
|
1864
|
+
return "Video not found.";
|
|
1865
|
+
}
|
|
1866
|
+
return "Something went wrong.";
|
|
1867
|
+
}
|
|
1868
|
+
var ErrorOverlay = class {
|
|
1869
|
+
constructor(api) {
|
|
1870
|
+
this.visible = false;
|
|
1871
|
+
this.lastSource = null;
|
|
1872
|
+
this.handleRetry = () => {
|
|
1873
|
+
if (this.retryBtn.disabled) return;
|
|
1874
|
+
this.retryBtn.disabled = true;
|
|
1875
|
+
this.hide();
|
|
1876
|
+
const source = this.api.getState("source");
|
|
1877
|
+
const src = source?.src || this.lastSource;
|
|
1878
|
+
if (src) {
|
|
1879
|
+
this.api.emit("error:retry", { src });
|
|
1880
|
+
const video = this.api.container.querySelector("video");
|
|
1881
|
+
if (video) {
|
|
1882
|
+
video.src = src;
|
|
1883
|
+
video.load();
|
|
1884
|
+
video.play().catch(() => {
|
|
1885
|
+
});
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
setTimeout(() => {
|
|
1889
|
+
this.retryBtn.disabled = false;
|
|
1890
|
+
}, 1e3);
|
|
1891
|
+
};
|
|
1892
|
+
this.handleDismiss = () => {
|
|
1893
|
+
this.hide();
|
|
1894
|
+
this.api.emit("error:dismiss", void 0);
|
|
1895
|
+
};
|
|
1896
|
+
this.api = api;
|
|
1897
|
+
const overlay = document.createElement("div");
|
|
1898
|
+
overlay.className = "sp-error-overlay";
|
|
1899
|
+
overlay.setAttribute("role", "alert");
|
|
1900
|
+
overlay.setAttribute("aria-live", "assertive");
|
|
1901
|
+
const content = document.createElement("div");
|
|
1902
|
+
content.className = "sp-error-overlay__content";
|
|
1903
|
+
const iconEl = document.createElement("div");
|
|
1904
|
+
iconEl.className = "sp-error-overlay__icon";
|
|
1905
|
+
iconEl.innerHTML = icons.error;
|
|
1906
|
+
const messageEl = document.createElement("p");
|
|
1907
|
+
messageEl.className = "sp-error-overlay__message";
|
|
1908
|
+
messageEl.textContent = "Something went wrong.";
|
|
1909
|
+
const actions = document.createElement("div");
|
|
1910
|
+
actions.className = "sp-error-overlay__actions";
|
|
1911
|
+
this.retryBtn = document.createElement("button");
|
|
1912
|
+
this.retryBtn.className = "sp-error-overlay__retry";
|
|
1913
|
+
this.retryBtn.setAttribute("type", "button");
|
|
1914
|
+
this.retryBtn.setAttribute("aria-label", "Try again");
|
|
1915
|
+
this.retryBtn.textContent = "Try Again";
|
|
1916
|
+
this.retryBtn.addEventListener("click", this.handleRetry);
|
|
1917
|
+
this.dismissBtn = document.createElement("button");
|
|
1918
|
+
this.dismissBtn.className = "sp-error-overlay__dismiss";
|
|
1919
|
+
this.dismissBtn.setAttribute("type", "button");
|
|
1920
|
+
this.dismissBtn.setAttribute("aria-label", "Go back");
|
|
1921
|
+
this.dismissBtn.textContent = "Go Back";
|
|
1922
|
+
this.dismissBtn.addEventListener("click", this.handleDismiss);
|
|
1923
|
+
actions.appendChild(this.retryBtn);
|
|
1924
|
+
actions.appendChild(this.dismissBtn);
|
|
1925
|
+
content.appendChild(iconEl);
|
|
1926
|
+
content.appendChild(messageEl);
|
|
1927
|
+
content.appendChild(actions);
|
|
1928
|
+
overlay.appendChild(content);
|
|
1929
|
+
this.el = overlay;
|
|
1930
|
+
}
|
|
1931
|
+
render() {
|
|
1932
|
+
return this.el;
|
|
1933
|
+
}
|
|
1934
|
+
/** Show the error overlay with the given error */
|
|
1935
|
+
show(error) {
|
|
1936
|
+
const message = getUserMessage(error);
|
|
1937
|
+
const messageEl = this.el.querySelector(".sp-error-overlay__message");
|
|
1938
|
+
if (messageEl) {
|
|
1939
|
+
messageEl.textContent = message;
|
|
1940
|
+
}
|
|
1941
|
+
const source = this.api.getState("source");
|
|
1942
|
+
if (source?.src) {
|
|
1943
|
+
this.lastSource = source.src;
|
|
1944
|
+
}
|
|
1945
|
+
this.visible = true;
|
|
1946
|
+
this.retryBtn.disabled = false;
|
|
1947
|
+
this.el.classList.add("sp-error-overlay--visible");
|
|
1948
|
+
}
|
|
1949
|
+
/** Hide the error overlay */
|
|
1950
|
+
hide() {
|
|
1951
|
+
this.visible = false;
|
|
1952
|
+
this.el.classList.remove("sp-error-overlay--visible");
|
|
1953
|
+
}
|
|
1954
|
+
isVisible() {
|
|
1955
|
+
return this.visible;
|
|
1956
|
+
}
|
|
1957
|
+
update() {
|
|
1958
|
+
const playbackState = this.api.getState("playbackState");
|
|
1959
|
+
if (this.visible && playbackState !== "error" && playbackState !== "loading") {
|
|
1960
|
+
const playing = this.api.getState("playing");
|
|
1961
|
+
if (playing) {
|
|
1962
|
+
this.hide();
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
destroy() {
|
|
1967
|
+
this.retryBtn.removeEventListener("click", this.handleRetry);
|
|
1968
|
+
this.dismissBtn.removeEventListener("click", this.handleDismiss);
|
|
1969
|
+
this.el.remove();
|
|
1970
|
+
}
|
|
1971
|
+
};
|
|
1972
|
+
|
|
1973
|
+
// src/controls/SettingsMenu.ts
|
|
1974
|
+
var SPEED_OPTIONS = [
|
|
1975
|
+
{ label: "0.5x", value: 0.5 },
|
|
1976
|
+
{ label: "0.75x", value: 0.75 },
|
|
1977
|
+
{ label: "Normal", value: 1 },
|
|
1978
|
+
{ label: "1.25x", value: 1.25 },
|
|
1979
|
+
{ label: "1.5x", value: 1.5 },
|
|
1980
|
+
{ label: "2x", value: 2 }
|
|
1981
|
+
];
|
|
1982
|
+
var SettingsMenu = class {
|
|
1983
|
+
constructor(api) {
|
|
1984
|
+
this.isOpen = false;
|
|
1985
|
+
this.currentPanel = "main";
|
|
1986
|
+
this.lastQualitiesJson = "";
|
|
1987
|
+
this.api = api;
|
|
1988
|
+
this.el = createElement("div", { className: "sp-settings" });
|
|
1989
|
+
this.btn = createButton("sp-settings__btn", "Settings", icons.settings);
|
|
1990
|
+
this.btn.setAttribute("aria-haspopup", "true");
|
|
1991
|
+
this.btn.setAttribute("aria-expanded", "false");
|
|
1992
|
+
this.btn.addEventListener("click", (e) => {
|
|
1993
|
+
e.stopPropagation();
|
|
1994
|
+
this.toggle();
|
|
1995
|
+
});
|
|
1996
|
+
this.panel = createElement("div", { className: "sp-settings-panel" });
|
|
1997
|
+
this.panel.setAttribute("role", "menu");
|
|
1998
|
+
this.panel.addEventListener("click", (e) => e.stopPropagation());
|
|
1999
|
+
this.el.appendChild(this.btn);
|
|
2000
|
+
this.el.appendChild(this.panel);
|
|
2001
|
+
this.closeHandler = (e) => {
|
|
2002
|
+
if (!this.el.contains(e.target)) {
|
|
2003
|
+
this.close();
|
|
2004
|
+
}
|
|
2005
|
+
};
|
|
2006
|
+
document.addEventListener("click", this.closeHandler);
|
|
2007
|
+
this.keyHandler = (e) => {
|
|
2008
|
+
if (!this.isOpen) return;
|
|
2009
|
+
if (e.key === "Escape") {
|
|
2010
|
+
e.preventDefault();
|
|
2011
|
+
e.stopPropagation();
|
|
2012
|
+
if (this.currentPanel !== "main") {
|
|
2013
|
+
this.showPanel("main");
|
|
2014
|
+
} else {
|
|
2015
|
+
this.close();
|
|
2016
|
+
this.btn.focus();
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
};
|
|
2020
|
+
document.addEventListener("keydown", this.keyHandler);
|
|
2021
|
+
}
|
|
2022
|
+
render() {
|
|
2023
|
+
return this.el;
|
|
2024
|
+
}
|
|
2025
|
+
update() {
|
|
2026
|
+
const qualities = this.api.getState("qualities") || [];
|
|
2027
|
+
const qualitiesJson = JSON.stringify(qualities.map((q) => q.id));
|
|
2028
|
+
if (qualitiesJson !== this.lastQualitiesJson) {
|
|
2029
|
+
this.lastQualitiesJson = qualitiesJson;
|
|
2030
|
+
if (this.isOpen && this.currentPanel === "quality") {
|
|
2031
|
+
this.renderQualityPanel();
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
if (this.isOpen) {
|
|
2035
|
+
if (this.currentPanel === "quality") {
|
|
2036
|
+
this.updateQualityActiveStates();
|
|
2037
|
+
} else if (this.currentPanel === "speed") {
|
|
2038
|
+
this.updateSpeedActiveStates();
|
|
2039
|
+
} else if (this.currentPanel === "captions") {
|
|
2040
|
+
this.updateCaptionsActiveStates();
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
toggle() {
|
|
2045
|
+
this.isOpen ? this.close() : this.open();
|
|
2046
|
+
}
|
|
2047
|
+
open() {
|
|
2048
|
+
this.isOpen = true;
|
|
2049
|
+
this.currentPanel = "main";
|
|
2050
|
+
this.renderMainPanel();
|
|
2051
|
+
this.panel.classList.add("sp-settings-panel--open");
|
|
2052
|
+
this.btn.setAttribute("aria-expanded", "true");
|
|
2053
|
+
}
|
|
2054
|
+
close() {
|
|
2055
|
+
this.isOpen = false;
|
|
2056
|
+
this.currentPanel = "main";
|
|
2057
|
+
this.panel.classList.remove("sp-settings-panel--open");
|
|
2058
|
+
this.btn.setAttribute("aria-expanded", "false");
|
|
2059
|
+
}
|
|
2060
|
+
showPanel(panel) {
|
|
2061
|
+
this.currentPanel = panel;
|
|
2062
|
+
switch (panel) {
|
|
2063
|
+
case "main":
|
|
2064
|
+
this.renderMainPanel();
|
|
2065
|
+
break;
|
|
2066
|
+
case "quality":
|
|
2067
|
+
this.renderQualityPanel();
|
|
2068
|
+
break;
|
|
2069
|
+
case "speed":
|
|
2070
|
+
this.renderSpeedPanel();
|
|
2071
|
+
break;
|
|
2072
|
+
case "captions":
|
|
2073
|
+
this.renderCaptionsPanel();
|
|
2074
|
+
break;
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
renderMainPanel() {
|
|
2078
|
+
this.panel.innerHTML = "";
|
|
2079
|
+
this.panel.className = "sp-settings-panel sp-settings-panel--open sp-settings-panel--main";
|
|
2080
|
+
const qualities = this.api.getState("qualities") || [];
|
|
2081
|
+
const currentQuality = this.api.getState("currentQuality");
|
|
2082
|
+
const playbackRate = this.api.getState("playbackRate") ?? 1;
|
|
2083
|
+
if (qualities.length > 0) {
|
|
2084
|
+
const qualityRow = this.createMainRow(
|
|
2085
|
+
"Quality",
|
|
2086
|
+
currentQuality?.label || "Auto",
|
|
2087
|
+
() => this.showPanel("quality")
|
|
2088
|
+
);
|
|
2089
|
+
this.panel.appendChild(qualityRow);
|
|
2090
|
+
}
|
|
2091
|
+
const textTracks = this.api.getState("textTracks") || [];
|
|
2092
|
+
if (textTracks.length > 0) {
|
|
2093
|
+
const currentTextTrack = this.api.getState("currentTextTrack");
|
|
2094
|
+
const captionsLabel = currentTextTrack ? currentTextTrack.label : "Off";
|
|
2095
|
+
const captionsRow = this.createMainRow(
|
|
2096
|
+
"Captions",
|
|
2097
|
+
captionsLabel,
|
|
2098
|
+
() => this.showPanel("captions")
|
|
2099
|
+
);
|
|
2100
|
+
this.panel.appendChild(captionsRow);
|
|
2101
|
+
}
|
|
2102
|
+
const speedLabel = playbackRate === 1 ? "Normal" : `${playbackRate}x`;
|
|
2103
|
+
const speedRow = this.createMainRow(
|
|
2104
|
+
"Speed",
|
|
2105
|
+
speedLabel,
|
|
2106
|
+
() => this.showPanel("speed")
|
|
2107
|
+
);
|
|
2108
|
+
this.panel.appendChild(speedRow);
|
|
2109
|
+
}
|
|
2110
|
+
createMainRow(label, value, onClick2) {
|
|
2111
|
+
const row = createElement("div", { className: "sp-settings-panel__row" });
|
|
2112
|
+
row.setAttribute("role", "menuitem");
|
|
2113
|
+
row.setAttribute("tabindex", "0");
|
|
2114
|
+
row.setAttribute("aria-haspopup", "true");
|
|
2115
|
+
const labelEl = createElement("span", { className: "sp-settings-panel__label" });
|
|
2116
|
+
labelEl.textContent = label;
|
|
2117
|
+
const rightSide = createElement("span", { className: "sp-settings-panel__value" });
|
|
2118
|
+
rightSide.textContent = value;
|
|
2119
|
+
const arrow = createElement("span", { className: "sp-settings-panel__arrow" });
|
|
2120
|
+
arrow.innerHTML = icons.chevronDown;
|
|
2121
|
+
rightSide.appendChild(arrow);
|
|
2122
|
+
row.appendChild(labelEl);
|
|
2123
|
+
row.appendChild(rightSide);
|
|
2124
|
+
row.addEventListener("click", (e) => {
|
|
2125
|
+
e.preventDefault();
|
|
2126
|
+
onClick2();
|
|
2127
|
+
});
|
|
2128
|
+
row.addEventListener("keydown", (e) => {
|
|
2129
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
2130
|
+
e.preventDefault();
|
|
2131
|
+
onClick2();
|
|
2132
|
+
}
|
|
2133
|
+
});
|
|
2134
|
+
return row;
|
|
2135
|
+
}
|
|
2136
|
+
renderQualityPanel() {
|
|
2137
|
+
this.panel.innerHTML = "";
|
|
2138
|
+
this.panel.className = "sp-settings-panel sp-settings-panel--open sp-settings-panel--sub";
|
|
2139
|
+
const header = this.createSubHeader("Quality");
|
|
2140
|
+
this.panel.appendChild(header);
|
|
2141
|
+
const qualities = this.api.getState("qualities") || [];
|
|
2142
|
+
const currentQuality = this.api.getState("currentQuality");
|
|
2143
|
+
const activeId = currentQuality?.id || "auto";
|
|
2144
|
+
const autoItem = this.createMenuItem("Auto", "auto", activeId === "auto");
|
|
2145
|
+
autoItem.addEventListener("click", (e) => {
|
|
2146
|
+
e.preventDefault();
|
|
2147
|
+
this.selectQuality("auto");
|
|
2148
|
+
});
|
|
2149
|
+
this.panel.appendChild(autoItem);
|
|
2150
|
+
const sorted = [...qualities].sort(
|
|
2151
|
+
(a, b) => b.height - a.height
|
|
2152
|
+
);
|
|
2153
|
+
for (const q of sorted) {
|
|
2154
|
+
if (q.id === "auto") continue;
|
|
2155
|
+
const item = this.createMenuItem(q.label, q.id, q.id === activeId);
|
|
2156
|
+
item.addEventListener("click", (e) => {
|
|
2157
|
+
e.preventDefault();
|
|
2158
|
+
this.selectQuality(q.id);
|
|
2159
|
+
});
|
|
2160
|
+
this.panel.appendChild(item);
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
renderSpeedPanel() {
|
|
2164
|
+
this.panel.innerHTML = "";
|
|
2165
|
+
this.panel.className = "sp-settings-panel sp-settings-panel--open sp-settings-panel--sub";
|
|
2166
|
+
const header = this.createSubHeader("Speed");
|
|
2167
|
+
this.panel.appendChild(header);
|
|
2168
|
+
const currentRate = this.api.getState("playbackRate") ?? 1;
|
|
2169
|
+
for (const opt of SPEED_OPTIONS) {
|
|
2170
|
+
const isActive = Math.abs(currentRate - opt.value) < 0.01;
|
|
2171
|
+
const item = this.createMenuItem(opt.label, String(opt.value), isActive);
|
|
2172
|
+
item.addEventListener("click", (e) => {
|
|
2173
|
+
e.preventDefault();
|
|
2174
|
+
this.selectSpeed(opt.value);
|
|
2175
|
+
});
|
|
2176
|
+
this.panel.appendChild(item);
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
2179
|
+
renderCaptionsPanel() {
|
|
2180
|
+
this.panel.innerHTML = "";
|
|
2181
|
+
this.panel.className = "sp-settings-panel sp-settings-panel--open sp-settings-panel--sub";
|
|
2182
|
+
const header = this.createSubHeader("Captions");
|
|
2183
|
+
this.panel.appendChild(header);
|
|
2184
|
+
const textTracks = this.api.getState("textTracks") || [];
|
|
2185
|
+
const currentTextTrack = this.api.getState("currentTextTrack");
|
|
2186
|
+
const activeId = currentTextTrack?.id || "off";
|
|
2187
|
+
const offItem = this.createMenuItem("Off", "off", activeId === "off");
|
|
2188
|
+
offItem.addEventListener("click", (e) => {
|
|
2189
|
+
e.preventDefault();
|
|
2190
|
+
this.selectCaption(null);
|
|
2191
|
+
});
|
|
2192
|
+
this.panel.appendChild(offItem);
|
|
2193
|
+
for (const track of textTracks) {
|
|
2194
|
+
const item = this.createMenuItem(track.label, track.id, track.id === activeId);
|
|
2195
|
+
item.addEventListener("click", (e) => {
|
|
2196
|
+
e.preventDefault();
|
|
2197
|
+
this.selectCaption(track.id);
|
|
2198
|
+
});
|
|
2199
|
+
this.panel.appendChild(item);
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
selectCaption(trackId) {
|
|
2203
|
+
this.api.emit("track:text", { trackId });
|
|
2204
|
+
this.close();
|
|
2205
|
+
}
|
|
2206
|
+
updateCaptionsActiveStates() {
|
|
2207
|
+
const currentTextTrack = this.api.getState("currentTextTrack");
|
|
2208
|
+
const activeId = currentTextTrack?.id || "off";
|
|
2209
|
+
const items = this.panel.querySelectorAll(".sp-settings-panel__item");
|
|
2210
|
+
items.forEach((item) => {
|
|
2211
|
+
const id = item.getAttribute("data-id");
|
|
2212
|
+
item.classList.toggle("sp-settings-panel__item--active", id === activeId);
|
|
2213
|
+
});
|
|
2214
|
+
}
|
|
2215
|
+
createSubHeader(title) {
|
|
2216
|
+
const header = createElement("div", { className: "sp-settings-panel__header" });
|
|
2217
|
+
header.setAttribute("role", "menuitem");
|
|
2218
|
+
header.setAttribute("tabindex", "0");
|
|
2219
|
+
const backArrow = createElement("span", { className: "sp-settings-panel__back" });
|
|
2220
|
+
backArrow.innerHTML = icons.chevronUp;
|
|
2221
|
+
const label = createElement("span", { className: "sp-settings-panel__header-label" });
|
|
2222
|
+
label.textContent = title;
|
|
2223
|
+
header.appendChild(backArrow);
|
|
2224
|
+
header.appendChild(label);
|
|
2225
|
+
header.addEventListener("click", (e) => {
|
|
2226
|
+
e.preventDefault();
|
|
2227
|
+
this.showPanel("main");
|
|
2228
|
+
});
|
|
2229
|
+
header.addEventListener("keydown", (e) => {
|
|
2230
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
2231
|
+
e.preventDefault();
|
|
2232
|
+
this.showPanel("main");
|
|
2233
|
+
}
|
|
2234
|
+
});
|
|
2235
|
+
return header;
|
|
2236
|
+
}
|
|
2237
|
+
createMenuItem(label, dataId, isActive) {
|
|
2238
|
+
const item = createElement("div", {
|
|
2239
|
+
className: `sp-settings-panel__item${isActive ? " sp-settings-panel__item--active" : ""}`
|
|
2240
|
+
});
|
|
2241
|
+
item.setAttribute("role", "menuitem");
|
|
2242
|
+
item.setAttribute("tabindex", "0");
|
|
2243
|
+
item.setAttribute("data-id", dataId);
|
|
2244
|
+
const labelEl = createElement("span");
|
|
2245
|
+
labelEl.textContent = label;
|
|
2246
|
+
const check = createElement("span", { className: "sp-settings-panel__check" });
|
|
2247
|
+
check.innerHTML = icons.checkmark;
|
|
2248
|
+
item.appendChild(labelEl);
|
|
2249
|
+
item.appendChild(check);
|
|
2250
|
+
item.addEventListener("keydown", (e) => {
|
|
2251
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
2252
|
+
e.preventDefault();
|
|
2253
|
+
item.click();
|
|
2254
|
+
}
|
|
2255
|
+
});
|
|
2256
|
+
return item;
|
|
2257
|
+
}
|
|
2258
|
+
selectQuality(qualityId) {
|
|
2259
|
+
this.api.emit("quality:select", {
|
|
2260
|
+
quality: qualityId,
|
|
2261
|
+
auto: qualityId === "auto"
|
|
2262
|
+
});
|
|
2263
|
+
this.close();
|
|
2264
|
+
}
|
|
2265
|
+
selectSpeed(rate) {
|
|
2266
|
+
this.api.emit("playback:ratechange", { rate });
|
|
2267
|
+
const video = this.api.container.querySelector("video");
|
|
2268
|
+
if (video) {
|
|
2269
|
+
video.playbackRate = rate;
|
|
2270
|
+
}
|
|
2271
|
+
this.close();
|
|
2272
|
+
}
|
|
2273
|
+
updateQualityActiveStates() {
|
|
2274
|
+
const currentQuality = this.api.getState("currentQuality");
|
|
2275
|
+
const activeId = currentQuality?.id || "auto";
|
|
2276
|
+
const items = this.panel.querySelectorAll(".sp-settings-panel__item");
|
|
2277
|
+
items.forEach((item) => {
|
|
2278
|
+
const id = item.getAttribute("data-id");
|
|
2279
|
+
item.classList.toggle("sp-settings-panel__item--active", id === activeId);
|
|
2280
|
+
});
|
|
2281
|
+
}
|
|
2282
|
+
updateSpeedActiveStates() {
|
|
2283
|
+
const currentRate = this.api.getState("playbackRate") ?? 1;
|
|
2284
|
+
const items = this.panel.querySelectorAll(".sp-settings-panel__item");
|
|
2285
|
+
items.forEach((item) => {
|
|
2286
|
+
const id = item.getAttribute("data-id");
|
|
2287
|
+
const value = parseFloat(id || "1");
|
|
2288
|
+
item.classList.toggle(
|
|
2289
|
+
"sp-settings-panel__item--active",
|
|
2290
|
+
Math.abs(currentRate - value) < 0.01
|
|
2291
|
+
);
|
|
2292
|
+
});
|
|
2293
|
+
}
|
|
2294
|
+
getPanel() {
|
|
2295
|
+
return this.currentPanel;
|
|
2296
|
+
}
|
|
2297
|
+
isMenuOpen() {
|
|
2298
|
+
return this.isOpen;
|
|
2299
|
+
}
|
|
2300
|
+
destroy() {
|
|
2301
|
+
document.removeEventListener("click", this.closeHandler);
|
|
2302
|
+
document.removeEventListener("keydown", this.keyHandler);
|
|
2303
|
+
this.el.remove();
|
|
2304
|
+
}
|
|
2305
|
+
};
|
|
2306
|
+
|
|
2307
|
+
// src/controls/SkipButton.ts
|
|
2308
|
+
var DEFAULT_SKIP_SECONDS = 10;
|
|
2309
|
+
var SkipButton = class {
|
|
2310
|
+
constructor(api, direction, seconds = DEFAULT_SKIP_SECONDS) {
|
|
2311
|
+
this.clickHandler = () => {
|
|
2312
|
+
this.skip();
|
|
2313
|
+
};
|
|
2314
|
+
this.api = api;
|
|
2315
|
+
this.direction = direction;
|
|
2316
|
+
this.seconds = seconds;
|
|
2317
|
+
const icon = direction === "backward" ? icons.replay10 : icons.forward10;
|
|
2318
|
+
const label = direction === "backward" ? `Rewind ${seconds} seconds` : `Forward ${seconds} seconds`;
|
|
2319
|
+
this.el = createButton(
|
|
2320
|
+
`sp-skip sp-skip--${direction}`,
|
|
2321
|
+
label,
|
|
2322
|
+
icon
|
|
2323
|
+
);
|
|
2324
|
+
this.el.addEventListener("click", this.clickHandler);
|
|
2325
|
+
}
|
|
2326
|
+
render() {
|
|
2327
|
+
return this.el;
|
|
2328
|
+
}
|
|
2329
|
+
update() {
|
|
2330
|
+
const live = this.api.getState("live");
|
|
2331
|
+
const duration = this.api.getState("duration") ?? 0;
|
|
2332
|
+
const seekableRange = this.api.getState("seekableRange");
|
|
2333
|
+
if (live && !seekableRange) {
|
|
2334
|
+
this.el.style.display = "none";
|
|
2335
|
+
return;
|
|
2336
|
+
}
|
|
2337
|
+
if (live && seekableRange) {
|
|
2338
|
+
this.el.style.display = "";
|
|
2339
|
+
return;
|
|
2340
|
+
}
|
|
2341
|
+
if (duration === 0) {
|
|
2342
|
+
this.el.style.display = "none";
|
|
2343
|
+
return;
|
|
2344
|
+
}
|
|
2345
|
+
this.el.style.display = "";
|
|
2346
|
+
}
|
|
2347
|
+
skip() {
|
|
2348
|
+
const video = getVideo(this.api.container);
|
|
2349
|
+
if (!video) return;
|
|
2350
|
+
const live = this.api.getState("live");
|
|
2351
|
+
const seekableRange = this.api.getState("seekableRange");
|
|
2352
|
+
if (live && seekableRange) {
|
|
2353
|
+
if (this.direction === "backward") {
|
|
2354
|
+
video.currentTime = Math.max(seekableRange.start, video.currentTime - this.seconds);
|
|
2355
|
+
} else {
|
|
2356
|
+
video.currentTime = Math.min(seekableRange.end, video.currentTime + this.seconds);
|
|
2357
|
+
}
|
|
2358
|
+
return;
|
|
2359
|
+
}
|
|
2360
|
+
const duration = video.duration || 0;
|
|
2361
|
+
if (!duration || !isFinite(duration)) return;
|
|
2362
|
+
if (this.direction === "backward") {
|
|
2363
|
+
video.currentTime = Math.max(0, video.currentTime - this.seconds);
|
|
2364
|
+
} else {
|
|
2365
|
+
video.currentTime = Math.min(duration, video.currentTime + this.seconds);
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
destroy() {
|
|
2369
|
+
this.el.removeEventListener("click", this.clickHandler);
|
|
2370
|
+
this.el.remove();
|
|
2371
|
+
}
|
|
2372
|
+
};
|
|
2373
|
+
|
|
2374
|
+
// src/controls/CaptionsButton.ts
|
|
2375
|
+
var CaptionsButton = class {
|
|
2376
|
+
constructor(api) {
|
|
2377
|
+
this.clickHandler = () => {
|
|
2378
|
+
this.toggle();
|
|
2379
|
+
};
|
|
2380
|
+
this.api = api;
|
|
2381
|
+
this.el = createButton("sp-captions", "Captions", icons.captionsOff);
|
|
2382
|
+
this.el.addEventListener("click", this.clickHandler);
|
|
2383
|
+
}
|
|
2384
|
+
render() {
|
|
2385
|
+
return this.el;
|
|
2386
|
+
}
|
|
2387
|
+
update() {
|
|
2388
|
+
const textTracks = this.api.getState("textTracks") || [];
|
|
2389
|
+
const currentTrack = this.api.getState("currentTextTrack");
|
|
2390
|
+
if (textTracks.length === 0) {
|
|
2391
|
+
this.el.style.display = "none";
|
|
2392
|
+
return;
|
|
2393
|
+
}
|
|
2394
|
+
this.el.style.display = "";
|
|
2395
|
+
if (currentTrack) {
|
|
2396
|
+
this.el.innerHTML = icons.captions;
|
|
2397
|
+
this.el.setAttribute("aria-label", `Captions: ${currentTrack.label}`);
|
|
2398
|
+
this.el.classList.add("sp-captions--active");
|
|
2399
|
+
} else {
|
|
2400
|
+
this.el.innerHTML = icons.captionsOff;
|
|
2401
|
+
this.el.setAttribute("aria-label", "Captions");
|
|
2402
|
+
this.el.classList.remove("sp-captions--active");
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
toggle() {
|
|
2406
|
+
const textTracks = this.api.getState("textTracks") || [];
|
|
2407
|
+
const currentTrack = this.api.getState("currentTextTrack");
|
|
2408
|
+
if (textTracks.length === 0) return;
|
|
2409
|
+
if (currentTrack) {
|
|
2410
|
+
this.api.emit("track:text", { trackId: null });
|
|
2411
|
+
} else {
|
|
2412
|
+
this.api.emit("track:text", { trackId: textTracks[0].id });
|
|
2413
|
+
}
|
|
2414
|
+
}
|
|
2415
|
+
destroy() {
|
|
2416
|
+
this.el.removeEventListener("click", this.clickHandler);
|
|
2417
|
+
this.el.remove();
|
|
2418
|
+
}
|
|
2419
|
+
};
|
|
2420
|
+
|
|
1293
2421
|
// src/index.ts
|
|
1294
2422
|
var DEFAULT_LAYOUT = [
|
|
1295
2423
|
"play",
|
|
2424
|
+
"skip-backward",
|
|
2425
|
+
"skip-forward",
|
|
1296
2426
|
"volume",
|
|
1297
2427
|
"time",
|
|
1298
2428
|
"live-indicator",
|
|
1299
2429
|
"spacer",
|
|
1300
|
-
"
|
|
2430
|
+
"settings",
|
|
2431
|
+
"captions",
|
|
1301
2432
|
"chromecast",
|
|
1302
2433
|
"airplay",
|
|
1303
2434
|
"pip",
|
|
@@ -1310,10 +2441,12 @@ function uiPlugin(config = {}) {
|
|
|
1310
2441
|
let gradient = null;
|
|
1311
2442
|
let progressBar = null;
|
|
1312
2443
|
let bufferingIndicator = null;
|
|
2444
|
+
let errorOverlay = null;
|
|
1313
2445
|
let styleEl = null;
|
|
1314
2446
|
let controls = [];
|
|
1315
2447
|
let hideTimeout = null;
|
|
1316
2448
|
let stateUnsubscribe = null;
|
|
2449
|
+
let errorUnsubscribe = null;
|
|
1317
2450
|
let controlsVisible = true;
|
|
1318
2451
|
const layout = config.controls || DEFAULT_LAYOUT;
|
|
1319
2452
|
const hideDelay = config.hideDelay ?? DEFAULT_HIDE_DELAY;
|
|
@@ -1321,6 +2454,10 @@ function uiPlugin(config = {}) {
|
|
|
1321
2454
|
switch (slot) {
|
|
1322
2455
|
case "play":
|
|
1323
2456
|
return new PlayButton(api);
|
|
2457
|
+
case "skip-backward":
|
|
2458
|
+
return new SkipButton(api, "backward");
|
|
2459
|
+
case "skip-forward":
|
|
2460
|
+
return new SkipButton(api, "forward");
|
|
1324
2461
|
case "volume":
|
|
1325
2462
|
return new VolumeControl(api);
|
|
1326
2463
|
case "progress":
|
|
@@ -1331,6 +2468,10 @@ function uiPlugin(config = {}) {
|
|
|
1331
2468
|
return new LiveIndicator(api);
|
|
1332
2469
|
case "quality":
|
|
1333
2470
|
return new QualityMenu(api);
|
|
2471
|
+
case "settings":
|
|
2472
|
+
return new SettingsMenu(api);
|
|
2473
|
+
case "captions":
|
|
2474
|
+
return new CaptionsButton(api);
|
|
1334
2475
|
case "chromecast":
|
|
1335
2476
|
return new CastButton(api, "chromecast");
|
|
1336
2477
|
case "airplay":
|
|
@@ -1354,6 +2495,7 @@ function uiPlugin(config = {}) {
|
|
|
1354
2495
|
const isLoading = playbackState === "loading";
|
|
1355
2496
|
const showSpinner = waiting || seeking && !api?.getState("paused") || isLoading;
|
|
1356
2497
|
bufferingIndicator?.classList.toggle("sp-buffering--visible", !!showSpinner);
|
|
2498
|
+
errorOverlay?.update();
|
|
1357
2499
|
};
|
|
1358
2500
|
const showControls = () => {
|
|
1359
2501
|
if (controlsVisible) {
|
|
@@ -1392,8 +2534,14 @@ function uiPlugin(config = {}) {
|
|
|
1392
2534
|
};
|
|
1393
2535
|
const handleKeyDown = (e) => {
|
|
1394
2536
|
if (!api.container.contains(document.activeElement)) return;
|
|
2537
|
+
const activeEl = document.activeElement;
|
|
2538
|
+
if (activeEl instanceof HTMLInputElement || activeEl instanceof HTMLTextAreaElement || activeEl instanceof HTMLSelectElement || activeEl?.isContentEditable) {
|
|
2539
|
+
return;
|
|
2540
|
+
}
|
|
1395
2541
|
const video = api.container.querySelector("video");
|
|
1396
2542
|
if (!video) return;
|
|
2543
|
+
const live = api.getState("live");
|
|
2544
|
+
const seekableRange = api.getState("seekableRange");
|
|
1397
2545
|
switch (e.key) {
|
|
1398
2546
|
case " ":
|
|
1399
2547
|
case "k":
|
|
@@ -1414,12 +2562,20 @@ function uiPlugin(config = {}) {
|
|
|
1414
2562
|
break;
|
|
1415
2563
|
case "ArrowLeft":
|
|
1416
2564
|
e.preventDefault();
|
|
1417
|
-
|
|
2565
|
+
if (live && seekableRange) {
|
|
2566
|
+
video.currentTime = Math.max(seekableRange.start, video.currentTime - 5);
|
|
2567
|
+
} else {
|
|
2568
|
+
video.currentTime = Math.max(0, video.currentTime - 5);
|
|
2569
|
+
}
|
|
1418
2570
|
showControls();
|
|
1419
2571
|
break;
|
|
1420
2572
|
case "ArrowRight":
|
|
1421
2573
|
e.preventDefault();
|
|
1422
|
-
|
|
2574
|
+
if (live && seekableRange) {
|
|
2575
|
+
video.currentTime = Math.min(seekableRange.end, video.currentTime + 5);
|
|
2576
|
+
} else {
|
|
2577
|
+
video.currentTime = Math.min(video.duration || 0, video.currentTime + 5);
|
|
2578
|
+
}
|
|
1423
2579
|
showControls();
|
|
1424
2580
|
break;
|
|
1425
2581
|
case "ArrowUp":
|
|
@@ -1465,6 +2621,14 @@ function uiPlugin(config = {}) {
|
|
|
1465
2621
|
bufferingIndicator.innerHTML = icons.spinner;
|
|
1466
2622
|
bufferingIndicator.setAttribute("aria-hidden", "true");
|
|
1467
2623
|
container.appendChild(bufferingIndicator);
|
|
2624
|
+
errorOverlay = new ErrorOverlay(api);
|
|
2625
|
+
container.appendChild(errorOverlay.render());
|
|
2626
|
+
errorUnsubscribe = api.on("error", (payload) => {
|
|
2627
|
+
if (payload?.fatal) {
|
|
2628
|
+
const error = api.getState("error") || new Error(payload.message || "Playback error");
|
|
2629
|
+
errorOverlay?.show(error);
|
|
2630
|
+
}
|
|
2631
|
+
});
|
|
1468
2632
|
progressBar = new ProgressBar(api);
|
|
1469
2633
|
container.appendChild(progressBar.render());
|
|
1470
2634
|
if (!isPlaying) {
|
|
@@ -1508,6 +2672,8 @@ function uiPlugin(config = {}) {
|
|
|
1508
2672
|
}
|
|
1509
2673
|
stateUnsubscribe?.();
|
|
1510
2674
|
stateUnsubscribe = null;
|
|
2675
|
+
errorUnsubscribe?.();
|
|
2676
|
+
errorUnsubscribe = null;
|
|
1511
2677
|
if (api?.container) {
|
|
1512
2678
|
api.container.removeEventListener("mousemove", handleInteraction);
|
|
1513
2679
|
api.container.removeEventListener("mouseenter", handleInteraction);
|
|
@@ -1521,6 +2687,8 @@ function uiPlugin(config = {}) {
|
|
|
1521
2687
|
controls = [];
|
|
1522
2688
|
progressBar?.destroy();
|
|
1523
2689
|
progressBar = null;
|
|
2690
|
+
errorOverlay?.destroy();
|
|
2691
|
+
errorOverlay = null;
|
|
1524
2692
|
controlBar?.remove();
|
|
1525
2693
|
controlBar = null;
|
|
1526
2694
|
gradient?.remove();
|