@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.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
|
|
@@ -616,12 +952,11 @@ var PlayButton = class {
|
|
|
616
952
|
const video = getVideo(this.api.container);
|
|
617
953
|
if (!video) return;
|
|
618
954
|
const ended = this.api.getState("ended");
|
|
619
|
-
const playing = this.api.getState("playing");
|
|
620
955
|
if (ended) {
|
|
621
956
|
video.currentTime = 0;
|
|
622
957
|
video.play().catch(() => {
|
|
623
958
|
});
|
|
624
|
-
} else if (
|
|
959
|
+
} else if (!video.paused) {
|
|
625
960
|
video.pause();
|
|
626
961
|
} else {
|
|
627
962
|
video.play().catch(() => {
|
|
@@ -634,6 +969,70 @@ var PlayButton = class {
|
|
|
634
969
|
}
|
|
635
970
|
};
|
|
636
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
|
+
|
|
637
1036
|
// src/controls/ProgressBar.ts
|
|
638
1037
|
var ProgressBar = class {
|
|
639
1038
|
constructor(api) {
|
|
@@ -675,36 +1074,99 @@ var ProgressBar = class {
|
|
|
675
1074
|
}
|
|
676
1075
|
}
|
|
677
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
|
+
};
|
|
678
1116
|
this.onMouseMove = (e) => {
|
|
679
1117
|
this.updateTooltip(e.clientX);
|
|
680
1118
|
};
|
|
681
1119
|
this.onMouseLeave = () => {
|
|
682
1120
|
if (!this.isDragging) {
|
|
683
1121
|
this.tooltip.style.opacity = "0";
|
|
1122
|
+
this.thumbnailPreview.hide();
|
|
684
1123
|
}
|
|
685
1124
|
};
|
|
686
1125
|
this.onKeyDown = (e) => {
|
|
687
1126
|
const video = getVideo(this.api.container);
|
|
688
1127
|
if (!video) return;
|
|
689
1128
|
const step = 5;
|
|
690
|
-
const
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
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
|
+
}
|
|
708
1170
|
}
|
|
709
1171
|
};
|
|
710
1172
|
this.api = api;
|
|
@@ -716,10 +1178,12 @@ var ProgressBar = class {
|
|
|
716
1178
|
this.handle = createElement("div", { className: "sp-progress__handle" });
|
|
717
1179
|
this.tooltip = createElement("div", { className: "sp-progress__tooltip" });
|
|
718
1180
|
this.tooltip.textContent = "0:00";
|
|
1181
|
+
this.thumbnailPreview = new ThumbnailPreview();
|
|
719
1182
|
track.appendChild(this.buffered);
|
|
720
1183
|
track.appendChild(this.filled);
|
|
721
1184
|
track.appendChild(this.handle);
|
|
722
1185
|
this.el.appendChild(track);
|
|
1186
|
+
this.el.appendChild(this.thumbnailPreview.getElement());
|
|
723
1187
|
this.el.appendChild(this.tooltip);
|
|
724
1188
|
this.wrapper.appendChild(this.el);
|
|
725
1189
|
this.el.setAttribute("role", "slider");
|
|
@@ -729,9 +1193,13 @@ var ProgressBar = class {
|
|
|
729
1193
|
this.wrapper.addEventListener("mousedown", this.onMouseDown);
|
|
730
1194
|
this.wrapper.addEventListener("mousemove", this.onMouseMove);
|
|
731
1195
|
this.wrapper.addEventListener("mouseleave", this.onMouseLeave);
|
|
1196
|
+
this.wrapper.addEventListener("touchstart", this.onTouchStart, { passive: false });
|
|
732
1197
|
this.el.addEventListener("keydown", this.onKeyDown);
|
|
733
1198
|
document.addEventListener("mousemove", this.onDocMouseMove);
|
|
734
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);
|
|
735
1203
|
}
|
|
736
1204
|
render() {
|
|
737
1205
|
return this.wrapper;
|
|
@@ -744,11 +1212,40 @@ var ProgressBar = class {
|
|
|
744
1212
|
hide() {
|
|
745
1213
|
this.wrapper.classList.remove("sp-progress-wrapper--visible");
|
|
746
1214
|
}
|
|
1215
|
+
/** Set thumbnail sprite configuration */
|
|
1216
|
+
setThumbnails(config) {
|
|
1217
|
+
this.thumbnailPreview.setConfig(config);
|
|
1218
|
+
}
|
|
747
1219
|
update() {
|
|
748
1220
|
const currentTime = this.api.getState("currentTime") || 0;
|
|
749
1221
|
const duration = this.api.getState("duration") || 0;
|
|
750
1222
|
const bufferedRanges = this.api.getState("buffered");
|
|
751
|
-
|
|
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) {
|
|
752
1249
|
const progress = currentTime / duration * 100;
|
|
753
1250
|
this.filled.style.width = `${progress}%`;
|
|
754
1251
|
this.handle.style.left = `${progress}%`;
|
|
@@ -765,6 +1262,12 @@ var ProgressBar = class {
|
|
|
765
1262
|
getTimeFromPosition(clientX) {
|
|
766
1263
|
const rect = this.el.getBoundingClientRect();
|
|
767
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
|
+
}
|
|
768
1271
|
const duration = this.api.getState("duration") || 0;
|
|
769
1272
|
return percent * duration;
|
|
770
1273
|
}
|
|
@@ -772,8 +1275,18 @@ var ProgressBar = class {
|
|
|
772
1275
|
const rect = this.el.getBoundingClientRect();
|
|
773
1276
|
const percent = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
|
|
774
1277
|
const time = this.getTimeFromPosition(clientX);
|
|
775
|
-
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
|
+
}
|
|
776
1286
|
this.tooltip.style.left = `${percent * 100}%`;
|
|
1287
|
+
if (this.thumbnailPreview.isConfigured()) {
|
|
1288
|
+
this.thumbnailPreview.show(time, percent);
|
|
1289
|
+
}
|
|
777
1290
|
}
|
|
778
1291
|
updateVisualPosition(clientX) {
|
|
779
1292
|
const rect = this.el.getBoundingClientRect();
|
|
@@ -796,8 +1309,13 @@ var ProgressBar = class {
|
|
|
796
1309
|
this.wrapper.removeEventListener("mousedown", this.onMouseDown);
|
|
797
1310
|
this.wrapper.removeEventListener("mousemove", this.onMouseMove);
|
|
798
1311
|
this.wrapper.removeEventListener("mouseleave", this.onMouseLeave);
|
|
1312
|
+
this.wrapper.removeEventListener("touchstart", this.onTouchStart);
|
|
799
1313
|
document.removeEventListener("mousemove", this.onDocMouseMove);
|
|
800
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();
|
|
801
1319
|
this.wrapper.remove();
|
|
802
1320
|
}
|
|
803
1321
|
};
|
|
@@ -850,6 +1368,20 @@ var VolumeControl = class {
|
|
|
850
1368
|
this.onMouseUp = () => {
|
|
851
1369
|
this.isDragging = false;
|
|
852
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
|
+
};
|
|
853
1385
|
this.onKeyDown = (e) => {
|
|
854
1386
|
const video = getVideo(this.api.container);
|
|
855
1387
|
if (!video) return;
|
|
@@ -889,9 +1421,13 @@ var VolumeControl = class {
|
|
|
889
1421
|
this.el.appendChild(this.btn);
|
|
890
1422
|
this.el.appendChild(sliderWrap);
|
|
891
1423
|
this.slider.addEventListener("mousedown", this.onMouseDown);
|
|
1424
|
+
this.slider.addEventListener("touchstart", this.onTouchStart, { passive: false });
|
|
892
1425
|
this.slider.addEventListener("keydown", this.onKeyDown);
|
|
893
1426
|
document.addEventListener("mousemove", this.onDocMouseMove);
|
|
894
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);
|
|
895
1431
|
}
|
|
896
1432
|
render() {
|
|
897
1433
|
return this.el;
|
|
@@ -936,8 +1472,14 @@ var VolumeControl = class {
|
|
|
936
1472
|
return Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
|
|
937
1473
|
}
|
|
938
1474
|
destroy() {
|
|
1475
|
+
this.slider.removeEventListener("mousedown", this.onMouseDown);
|
|
1476
|
+
this.slider.removeEventListener("touchstart", this.onTouchStart);
|
|
1477
|
+
this.slider.removeEventListener("keydown", this.onKeyDown);
|
|
939
1478
|
document.removeEventListener("mousemove", this.onDocMouseMove);
|
|
940
1479
|
document.removeEventListener("mouseup", this.onMouseUp);
|
|
1480
|
+
document.removeEventListener("touchmove", this.onDocTouchMove);
|
|
1481
|
+
document.removeEventListener("touchend", this.onTouchEnd);
|
|
1482
|
+
document.removeEventListener("touchcancel", this.onTouchEnd);
|
|
941
1483
|
this.el.remove();
|
|
942
1484
|
}
|
|
943
1485
|
};
|
|
@@ -945,19 +1487,27 @@ var VolumeControl = class {
|
|
|
945
1487
|
// src/controls/LiveIndicator.ts
|
|
946
1488
|
var LiveIndicator = class {
|
|
947
1489
|
constructor(api) {
|
|
948
|
-
this.
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
this.
|
|
952
|
-
this.el.setAttribute("aria-label", "Seek to live");
|
|
953
|
-
this.el.setAttribute("tabindex", "0");
|
|
954
|
-
this.el.onclick = () => this.seekToLive();
|
|
955
|
-
this.el.onkeydown = (e) => {
|
|
1490
|
+
this.handleClick = () => {
|
|
1491
|
+
this.seekToLive();
|
|
1492
|
+
};
|
|
1493
|
+
this.handleKeyDown = (e) => {
|
|
956
1494
|
if (e.key === "Enter" || e.key === " ") {
|
|
957
1495
|
e.preventDefault();
|
|
958
1496
|
this.seekToLive();
|
|
959
1497
|
}
|
|
960
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);
|
|
961
1511
|
}
|
|
962
1512
|
render() {
|
|
963
1513
|
return this.el;
|
|
@@ -968,8 +1518,12 @@ var LiveIndicator = class {
|
|
|
968
1518
|
this.el.style.display = live ? "" : "none";
|
|
969
1519
|
if (liveEdge) {
|
|
970
1520
|
this.el.classList.remove("sp-live--behind");
|
|
1521
|
+
this.label.textContent = "LIVE";
|
|
1522
|
+
this.el.setAttribute("aria-label", "At live edge");
|
|
971
1523
|
} else {
|
|
972
1524
|
this.el.classList.add("sp-live--behind");
|
|
1525
|
+
this.label.textContent = "GO LIVE";
|
|
1526
|
+
this.el.setAttribute("aria-label", "Seek to live");
|
|
973
1527
|
}
|
|
974
1528
|
}
|
|
975
1529
|
seekToLive() {
|
|
@@ -981,6 +1535,8 @@ var LiveIndicator = class {
|
|
|
981
1535
|
}
|
|
982
1536
|
}
|
|
983
1537
|
destroy() {
|
|
1538
|
+
this.el.removeEventListener("click", this.handleClick);
|
|
1539
|
+
this.el.removeEventListener("keydown", this.handleKeyDown);
|
|
984
1540
|
this.el.remove();
|
|
985
1541
|
}
|
|
986
1542
|
};
|
|
@@ -1291,14 +1847,588 @@ var Spacer = class {
|
|
|
1291
1847
|
}
|
|
1292
1848
|
};
|
|
1293
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
|
+
|
|
1294
2421
|
// src/index.ts
|
|
1295
2422
|
var DEFAULT_LAYOUT = [
|
|
1296
2423
|
"play",
|
|
2424
|
+
"skip-backward",
|
|
2425
|
+
"skip-forward",
|
|
1297
2426
|
"volume",
|
|
1298
2427
|
"time",
|
|
1299
2428
|
"live-indicator",
|
|
1300
2429
|
"spacer",
|
|
1301
|
-
"
|
|
2430
|
+
"settings",
|
|
2431
|
+
"captions",
|
|
1302
2432
|
"chromecast",
|
|
1303
2433
|
"airplay",
|
|
1304
2434
|
"pip",
|
|
@@ -1311,10 +2441,12 @@ function uiPlugin(config = {}) {
|
|
|
1311
2441
|
let gradient = null;
|
|
1312
2442
|
let progressBar = null;
|
|
1313
2443
|
let bufferingIndicator = null;
|
|
2444
|
+
let errorOverlay = null;
|
|
1314
2445
|
let styleEl = null;
|
|
1315
2446
|
let controls = [];
|
|
1316
2447
|
let hideTimeout = null;
|
|
1317
2448
|
let stateUnsubscribe = null;
|
|
2449
|
+
let errorUnsubscribe = null;
|
|
1318
2450
|
let controlsVisible = true;
|
|
1319
2451
|
const layout = config.controls || DEFAULT_LAYOUT;
|
|
1320
2452
|
const hideDelay = config.hideDelay ?? DEFAULT_HIDE_DELAY;
|
|
@@ -1322,6 +2454,10 @@ function uiPlugin(config = {}) {
|
|
|
1322
2454
|
switch (slot) {
|
|
1323
2455
|
case "play":
|
|
1324
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");
|
|
1325
2461
|
case "volume":
|
|
1326
2462
|
return new VolumeControl(api);
|
|
1327
2463
|
case "progress":
|
|
@@ -1332,6 +2468,10 @@ function uiPlugin(config = {}) {
|
|
|
1332
2468
|
return new LiveIndicator(api);
|
|
1333
2469
|
case "quality":
|
|
1334
2470
|
return new QualityMenu(api);
|
|
2471
|
+
case "settings":
|
|
2472
|
+
return new SettingsMenu(api);
|
|
2473
|
+
case "captions":
|
|
2474
|
+
return new CaptionsButton(api);
|
|
1335
2475
|
case "chromecast":
|
|
1336
2476
|
return new CastButton(api, "chromecast");
|
|
1337
2477
|
case "airplay":
|
|
@@ -1355,6 +2495,7 @@ function uiPlugin(config = {}) {
|
|
|
1355
2495
|
const isLoading = playbackState === "loading";
|
|
1356
2496
|
const showSpinner = waiting || seeking && !api?.getState("paused") || isLoading;
|
|
1357
2497
|
bufferingIndicator?.classList.toggle("sp-buffering--visible", !!showSpinner);
|
|
2498
|
+
errorOverlay?.update();
|
|
1358
2499
|
};
|
|
1359
2500
|
const showControls = () => {
|
|
1360
2501
|
if (controlsVisible) {
|
|
@@ -1393,8 +2534,14 @@ function uiPlugin(config = {}) {
|
|
|
1393
2534
|
};
|
|
1394
2535
|
const handleKeyDown = (e) => {
|
|
1395
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
|
+
}
|
|
1396
2541
|
const video = api.container.querySelector("video");
|
|
1397
2542
|
if (!video) return;
|
|
2543
|
+
const live = api.getState("live");
|
|
2544
|
+
const seekableRange = api.getState("seekableRange");
|
|
1398
2545
|
switch (e.key) {
|
|
1399
2546
|
case " ":
|
|
1400
2547
|
case "k":
|
|
@@ -1415,12 +2562,20 @@ function uiPlugin(config = {}) {
|
|
|
1415
2562
|
break;
|
|
1416
2563
|
case "ArrowLeft":
|
|
1417
2564
|
e.preventDefault();
|
|
1418
|
-
|
|
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
|
+
}
|
|
1419
2570
|
showControls();
|
|
1420
2571
|
break;
|
|
1421
2572
|
case "ArrowRight":
|
|
1422
2573
|
e.preventDefault();
|
|
1423
|
-
|
|
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
|
+
}
|
|
1424
2579
|
showControls();
|
|
1425
2580
|
break;
|
|
1426
2581
|
case "ArrowUp":
|
|
@@ -1457,19 +2612,30 @@ function uiPlugin(config = {}) {
|
|
|
1457
2612
|
if (containerStyle.position === "static") {
|
|
1458
2613
|
container.style.position = "relative";
|
|
1459
2614
|
}
|
|
2615
|
+
const isPlaying = api.getState("playing");
|
|
1460
2616
|
gradient = document.createElement("div");
|
|
1461
|
-
gradient.className = "sp-gradient sp-gradient--visible";
|
|
2617
|
+
gradient.className = isPlaying ? "sp-gradient" : "sp-gradient sp-gradient--visible";
|
|
1462
2618
|
container.appendChild(gradient);
|
|
1463
2619
|
bufferingIndicator = document.createElement("div");
|
|
1464
2620
|
bufferingIndicator.className = "sp-buffering";
|
|
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) {
|
|
2635
|
+
progressBar.show();
|
|
2636
|
+
}
|
|
1471
2637
|
controlBar = document.createElement("div");
|
|
1472
|
-
controlBar.className = "sp-controls sp-controls--visible";
|
|
2638
|
+
controlBar.className = isPlaying ? "sp-controls sp-controls--hidden" : "sp-controls sp-controls--visible";
|
|
1473
2639
|
controlBar.setAttribute("role", "toolbar");
|
|
1474
2640
|
controlBar.setAttribute("aria-label", "Video controls");
|
|
1475
2641
|
for (const slot of layout) {
|
|
@@ -1492,6 +2658,11 @@ function uiPlugin(config = {}) {
|
|
|
1492
2658
|
if (!container.hasAttribute("tabindex")) {
|
|
1493
2659
|
container.setAttribute("tabindex", "0");
|
|
1494
2660
|
}
|
|
2661
|
+
controlsVisible = !isPlaying;
|
|
2662
|
+
api.setState("controlsVisible", controlsVisible);
|
|
2663
|
+
if (isPlaying) {
|
|
2664
|
+
resetHideTimer();
|
|
2665
|
+
}
|
|
1495
2666
|
api.logger.debug("UI controls plugin initialized");
|
|
1496
2667
|
},
|
|
1497
2668
|
async destroy() {
|
|
@@ -1501,6 +2672,8 @@ function uiPlugin(config = {}) {
|
|
|
1501
2672
|
}
|
|
1502
2673
|
stateUnsubscribe?.();
|
|
1503
2674
|
stateUnsubscribe = null;
|
|
2675
|
+
errorUnsubscribe?.();
|
|
2676
|
+
errorUnsubscribe = null;
|
|
1504
2677
|
if (api?.container) {
|
|
1505
2678
|
api.container.removeEventListener("mousemove", handleInteraction);
|
|
1506
2679
|
api.container.removeEventListener("mouseenter", handleInteraction);
|
|
@@ -1514,6 +2687,8 @@ function uiPlugin(config = {}) {
|
|
|
1514
2687
|
controls = [];
|
|
1515
2688
|
progressBar?.destroy();
|
|
1516
2689
|
progressBar = null;
|
|
2690
|
+
errorOverlay?.destroy();
|
|
2691
|
+
errorOverlay = null;
|
|
1517
2692
|
controlBar?.remove();
|
|
1518
2693
|
controlBar = null;
|
|
1519
2694
|
gradient?.remove();
|