@livepeer-frameworks/player-core 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -9
- package/dist/player.css +182 -42
- package/package.json +1 -1
- package/src/core/ABRController.ts +38 -36
- package/src/core/CodecUtils.ts +49 -46
- package/src/core/Disposable.ts +4 -4
- package/src/core/EventEmitter.ts +1 -1
- package/src/core/GatewayClient.ts +41 -39
- package/src/core/InteractionController.ts +89 -82
- package/src/core/LiveDurationProxy.ts +14 -15
- package/src/core/MetaTrackManager.ts +73 -65
- package/src/core/MistReporter.ts +72 -45
- package/src/core/MistSignaling.ts +59 -56
- package/src/core/PlayerController.ts +527 -384
- package/src/core/PlayerInterface.ts +83 -59
- package/src/core/PlayerManager.ts +79 -133
- package/src/core/PlayerRegistry.ts +59 -42
- package/src/core/QualityMonitor.ts +38 -31
- package/src/core/ScreenWakeLockManager.ts +8 -9
- package/src/core/SeekingUtils.ts +31 -22
- package/src/core/StreamStateClient.ts +74 -68
- package/src/core/SubtitleManager.ts +24 -22
- package/src/core/TelemetryReporter.ts +34 -31
- package/src/core/TimeFormat.ts +13 -17
- package/src/core/TimerManager.ts +24 -8
- package/src/core/UrlUtils.ts +20 -17
- package/src/core/detector.ts +44 -44
- package/src/core/index.ts +57 -48
- package/src/core/scorer.ts +136 -141
- package/src/core/selector.ts +2 -6
- package/src/global.d.ts +1 -1
- package/src/index.ts +46 -35
- package/src/players/DashJsPlayer.ts +164 -115
- package/src/players/HlsJsPlayer.ts +132 -78
- package/src/players/MewsWsPlayer/SourceBufferManager.ts +41 -36
- package/src/players/MewsWsPlayer/WebSocketManager.ts +9 -9
- package/src/players/MewsWsPlayer/index.ts +192 -152
- package/src/players/MewsWsPlayer/types.ts +21 -21
- package/src/players/MistPlayer.ts +45 -26
- package/src/players/MistWebRTCPlayer/index.ts +175 -129
- package/src/players/NativePlayer.ts +203 -143
- package/src/players/VideoJsPlayer.ts +170 -118
- package/src/players/WebCodecsPlayer/JitterBuffer.ts +6 -7
- package/src/players/WebCodecsPlayer/LatencyProfiles.ts +43 -43
- package/src/players/WebCodecsPlayer/RawChunkParser.ts +10 -10
- package/src/players/WebCodecsPlayer/SyncController.ts +45 -53
- package/src/players/WebCodecsPlayer/WebSocketController.ts +66 -68
- package/src/players/WebCodecsPlayer/index.ts +263 -221
- package/src/players/WebCodecsPlayer/polyfills/MediaStreamTrackGenerator.ts +12 -17
- package/src/players/WebCodecsPlayer/types.ts +56 -56
- package/src/players/WebCodecsPlayer/worker/decoder.worker.ts +238 -182
- package/src/players/WebCodecsPlayer/worker/types.ts +31 -31
- package/src/players/index.ts +8 -8
- package/src/styles/animations.css +2 -1
- package/src/styles/player.css +182 -42
- package/src/styles/tailwind.css +473 -159
- package/src/types.ts +43 -43
- package/src/vanilla/FrameWorksPlayer.ts +29 -14
- package/src/vanilla/index.ts +4 -4
package/README.md
CHANGED
|
@@ -15,36 +15,37 @@ npm i @livepeer-frameworks/player-core
|
|
|
15
15
|
## Basic Usage
|
|
16
16
|
|
|
17
17
|
```ts
|
|
18
|
-
import { PlayerController } from
|
|
18
|
+
import { PlayerController } from "@livepeer-frameworks/player-core";
|
|
19
19
|
|
|
20
20
|
const controller = new PlayerController({
|
|
21
|
-
contentId:
|
|
22
|
-
contentType:
|
|
23
|
-
gatewayUrl:
|
|
21
|
+
contentId: "pk_...", // playbackId
|
|
22
|
+
contentType: "live",
|
|
23
|
+
gatewayUrl: "https://your-bridge/graphql",
|
|
24
24
|
debug: true,
|
|
25
25
|
});
|
|
26
26
|
|
|
27
|
-
const container = document.getElementById(
|
|
27
|
+
const container = document.getElementById("player")!;
|
|
28
28
|
await controller.attach(container);
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
Notes:
|
|
32
|
+
|
|
32
33
|
- There is **no default gateway**; provide `gatewayUrl` unless you pass `endpoints` or `mistUrl`.
|
|
33
34
|
|
|
34
35
|
### Direct MistServer Node (mistUrl)
|
|
35
36
|
|
|
36
37
|
```ts
|
|
37
38
|
const controller = new PlayerController({
|
|
38
|
-
contentId:
|
|
39
|
-
contentType:
|
|
40
|
-
mistUrl:
|
|
39
|
+
contentId: "pk_...",
|
|
40
|
+
contentType: "live",
|
|
41
|
+
mistUrl: "https://edge.example.com",
|
|
41
42
|
});
|
|
42
43
|
```
|
|
43
44
|
|
|
44
45
|
### Styles
|
|
45
46
|
|
|
46
47
|
```ts
|
|
47
|
-
import
|
|
48
|
+
import "@livepeer-frameworks/player-core/player.css";
|
|
48
49
|
```
|
|
49
50
|
|
|
50
51
|
## Controls & Shortcuts
|
|
@@ -73,6 +74,7 @@ The player ships with keyboard/mouse shortcuts when the player is focused (click
|
|
|
73
74
|
| Click/Tap and hold | 2x speed | Disabled on live-only |
|
|
74
75
|
|
|
75
76
|
**Constraints**
|
|
77
|
+
|
|
76
78
|
- Live-only streams disable seeking/skip/2x hold and frame-step.
|
|
77
79
|
- Live with DVR buffer enables the same shortcuts as VOD.
|
|
78
80
|
- Frame stepping only moves within already buffered ranges (no network seek). WebCodecs supports true frame stepping when paused.
|
package/dist/player.css
CHANGED
|
@@ -16,25 +16,25 @@
|
|
|
16
16
|
*/
|
|
17
17
|
.fw-player-surface {
|
|
18
18
|
/* Tokyo Night color palette */
|
|
19
|
-
--tn-bg-dark: 235 21% 11%;
|
|
20
|
-
--tn-bg: 233 23% 17%;
|
|
21
|
-
--tn-bg-highlight: 233 23% 21%;
|
|
22
|
-
--tn-bg-visual: 232 27% 25%;
|
|
19
|
+
--tn-bg-dark: 235 21% 11%; /* #1a1b26 - Darkest (slab backgrounds) */
|
|
20
|
+
--tn-bg: 233 23% 17%; /* #24283b - Main background */
|
|
21
|
+
--tn-bg-highlight: 233 23% 21%; /* #292e42 - Elevated surfaces */
|
|
22
|
+
--tn-bg-visual: 232 27% 25%; /* #33395e - Selection/active states */
|
|
23
23
|
|
|
24
24
|
/* Text hierarchy */
|
|
25
|
-
--tn-fg: 223 27% 76%;
|
|
26
|
-
--tn-fg-bright: 220 13% 91%;
|
|
27
|
-
--tn-fg-dark: 224 16% 53%;
|
|
28
|
-
--tn-fg-gutter: 228 15% 45%;
|
|
25
|
+
--tn-fg: 223 27% 76%; /* #a9b1d6 - Primary text */
|
|
26
|
+
--tn-fg-bright: 220 13% 91%; /* #e2e4ea - Bright/highlighted text */
|
|
27
|
+
--tn-fg-dark: 224 16% 53%; /* #787c99 - Secondary text (muted) */
|
|
28
|
+
--tn-fg-gutter: 228 15% 45%; /* #5a607f - Borders, seams */
|
|
29
29
|
|
|
30
30
|
/* Accent colors (semantic) */
|
|
31
|
-
--tn-blue: 218 79% 73%;
|
|
32
|
-
--tn-green: 95 53% 55%;
|
|
33
|
-
--tn-red: 348 74% 64%;
|
|
34
|
-
--tn-yellow: 35 79% 64%;
|
|
35
|
-
--tn-purple: 267 82% 77%;
|
|
36
|
-
--tn-cyan: 178 64% 63%;
|
|
37
|
-
--tn-teal: 162 66% 62%;
|
|
31
|
+
--tn-blue: 218 79% 73%; /* #7aa2f7 - Primary actions */
|
|
32
|
+
--tn-green: 95 53% 55%; /* #9ece6a - Success */
|
|
33
|
+
--tn-red: 348 74% 64%; /* #f7768e - Destructive, live */
|
|
34
|
+
--tn-yellow: 35 79% 64%; /* #e0af68 - Warnings */
|
|
35
|
+
--tn-purple: 267 82% 77%; /* #bb9af7 - Secondary accent */
|
|
36
|
+
--tn-cyan: 178 64% 63%; /* #7dcfff - Info */
|
|
37
|
+
--tn-teal: 162 66% 62%; /* #73daca - Terminal green */
|
|
38
38
|
|
|
39
39
|
/* Player-internal variables (not shared with host) */
|
|
40
40
|
--fw-background: var(--tn-bg);
|
|
@@ -74,7 +74,8 @@
|
|
|
74
74
|
ANIMATIONS
|
|
75
75
|
===================================================== */
|
|
76
76
|
@keyframes float {
|
|
77
|
-
0%,
|
|
77
|
+
0%,
|
|
78
|
+
100% {
|
|
78
79
|
transform: translateY(0px) scale(1);
|
|
79
80
|
}
|
|
80
81
|
50% {
|
|
@@ -161,6 +162,7 @@
|
|
|
161
162
|
background: var(--fw-controls-bg);
|
|
162
163
|
border-top: 1px solid var(--fw-seam);
|
|
163
164
|
backdrop-filter: blur(8px);
|
|
165
|
+
pointer-events: auto;
|
|
164
166
|
}
|
|
165
167
|
|
|
166
168
|
/* Control group - seamed sections within bar */
|
|
@@ -183,7 +185,9 @@
|
|
|
183
185
|
border-radius: 0;
|
|
184
186
|
background: transparent;
|
|
185
187
|
color: hsl(var(--tn-fg));
|
|
186
|
-
transition:
|
|
188
|
+
transition:
|
|
189
|
+
background-color 0.15s,
|
|
190
|
+
color 0.15s;
|
|
187
191
|
cursor: pointer;
|
|
188
192
|
border: none;
|
|
189
193
|
}
|
|
@@ -201,10 +205,18 @@
|
|
|
201
205
|
}
|
|
202
206
|
|
|
203
207
|
/* Status indicators */
|
|
204
|
-
.fw-status-online {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
.fw-status-
|
|
208
|
+
.fw-status-online {
|
|
209
|
+
color: hsl(var(--tn-green));
|
|
210
|
+
}
|
|
211
|
+
.fw-status-offline {
|
|
212
|
+
color: hsl(var(--tn-red));
|
|
213
|
+
}
|
|
214
|
+
.fw-status-warning {
|
|
215
|
+
color: hsl(var(--tn-yellow));
|
|
216
|
+
}
|
|
217
|
+
.fw-status-info {
|
|
218
|
+
color: hsl(var(--tn-cyan));
|
|
219
|
+
}
|
|
208
220
|
|
|
209
221
|
/* =====================================================
|
|
210
222
|
PLAYER CONTAINER STYLES (Slab-based)
|
|
@@ -215,7 +227,7 @@
|
|
|
215
227
|
height: 100%;
|
|
216
228
|
width: 100%;
|
|
217
229
|
overflow: hidden;
|
|
218
|
-
border-radius: 0;
|
|
230
|
+
border-radius: 0; /* Slabs don't have rounded corners */
|
|
219
231
|
background-color: hsl(var(--tn-bg-dark));
|
|
220
232
|
border: 1px solid hsl(var(--tn-fg-gutter) / 0.3);
|
|
221
233
|
}
|
|
@@ -317,7 +329,9 @@
|
|
|
317
329
|
overflow-y: auto;
|
|
318
330
|
background: hsl(var(--tn-bg-dark));
|
|
319
331
|
border: 1px solid hsl(var(--tn-fg-gutter) / 0.3);
|
|
320
|
-
box-shadow:
|
|
332
|
+
box-shadow:
|
|
333
|
+
0 10px 15px -3px rgb(0 0 0 / 0.1),
|
|
334
|
+
0 4px 6px -4px rgb(0 0 0 / 0.1);
|
|
321
335
|
border-radius: 0.25rem;
|
|
322
336
|
z-index: 50;
|
|
323
337
|
}
|
|
@@ -358,7 +372,9 @@
|
|
|
358
372
|
color: hsl(var(--tn-fg));
|
|
359
373
|
border: none;
|
|
360
374
|
cursor: pointer;
|
|
361
|
-
transition:
|
|
375
|
+
transition:
|
|
376
|
+
background-color 0.15s,
|
|
377
|
+
color 0.15s;
|
|
362
378
|
}
|
|
363
379
|
|
|
364
380
|
.fw-settings-btn:hover {
|
|
@@ -413,7 +429,9 @@
|
|
|
413
429
|
background: hsl(var(--tn-bg-dark));
|
|
414
430
|
padding: 0;
|
|
415
431
|
color: hsl(var(--tn-fg));
|
|
416
|
-
box-shadow:
|
|
432
|
+
box-shadow:
|
|
433
|
+
0 10px 15px -3px rgb(0 0 0 / 0.1),
|
|
434
|
+
0 4px 6px -4px rgb(0 0 0 / 0.1);
|
|
417
435
|
}
|
|
418
436
|
|
|
419
437
|
.fw-context-menu-item {
|
|
@@ -426,7 +444,9 @@
|
|
|
426
444
|
font-size: 0.875rem;
|
|
427
445
|
outline: none;
|
|
428
446
|
color: hsl(var(--tn-fg));
|
|
429
|
-
transition:
|
|
447
|
+
transition:
|
|
448
|
+
background-color 0.15s,
|
|
449
|
+
color 0.15s;
|
|
430
450
|
}
|
|
431
451
|
|
|
432
452
|
.fw-context-menu-item:hover,
|
|
@@ -467,7 +487,9 @@
|
|
|
467
487
|
font-size: 0.875rem;
|
|
468
488
|
outline: none;
|
|
469
489
|
color: hsl(var(--tn-fg));
|
|
470
|
-
transition:
|
|
490
|
+
transition:
|
|
491
|
+
background-color 0.15s,
|
|
492
|
+
color 0.15s;
|
|
471
493
|
}
|
|
472
494
|
|
|
473
495
|
.fw-context-menu-checkbox:hover,
|
|
@@ -496,7 +518,9 @@
|
|
|
496
518
|
font-weight: 700;
|
|
497
519
|
text-transform: uppercase;
|
|
498
520
|
letter-spacing: 0.05em;
|
|
499
|
-
transition:
|
|
521
|
+
transition:
|
|
522
|
+
background-color 0.15s,
|
|
523
|
+
color 0.15s;
|
|
500
524
|
border: none;
|
|
501
525
|
cursor: pointer;
|
|
502
526
|
}
|
|
@@ -517,26 +541,27 @@
|
|
|
517
541
|
color: white;
|
|
518
542
|
}
|
|
519
543
|
|
|
520
|
-
|
|
521
544
|
/* --- Volume Control --- */
|
|
522
545
|
.fw-volume-group {
|
|
523
546
|
display: flex;
|
|
524
547
|
align-items: center;
|
|
525
|
-
border-radius: 0
|
|
526
|
-
transition:
|
|
548
|
+
border-radius: 0;
|
|
549
|
+
transition:
|
|
550
|
+
background-color 0.2s,
|
|
551
|
+
width 0.2s;
|
|
527
552
|
cursor: pointer;
|
|
528
553
|
}
|
|
529
554
|
|
|
530
555
|
.fw-volume-group--expanded {
|
|
531
|
-
background:
|
|
556
|
+
background: hsl(var(--tn-bg-visual) / 0.5);
|
|
532
557
|
}
|
|
533
558
|
|
|
534
559
|
.fw-volume-group:hover {
|
|
535
|
-
background:
|
|
560
|
+
background: hsl(var(--tn-bg-visual) / 0.5);
|
|
536
561
|
}
|
|
537
562
|
|
|
538
563
|
.fw-volume-group--expanded:hover {
|
|
539
|
-
background:
|
|
564
|
+
background: hsl(var(--tn-bg-visual) / 0.5);
|
|
540
565
|
}
|
|
541
566
|
|
|
542
567
|
.fw-volume-group--disabled {
|
|
@@ -565,7 +590,9 @@
|
|
|
565
590
|
display: flex;
|
|
566
591
|
align-items: center;
|
|
567
592
|
overflow: hidden;
|
|
568
|
-
transition:
|
|
593
|
+
transition:
|
|
594
|
+
width 0.2s ease-out,
|
|
595
|
+
opacity 0.2s ease-out;
|
|
569
596
|
}
|
|
570
597
|
|
|
571
598
|
.fw-volume-slider-wrapper--collapsed {
|
|
@@ -579,6 +606,105 @@
|
|
|
579
606
|
opacity: 1;
|
|
580
607
|
}
|
|
581
608
|
|
|
609
|
+
/* --- Slider Component (shared by React/Svelte) --- */
|
|
610
|
+
.fw-slider {
|
|
611
|
+
position: relative;
|
|
612
|
+
display: flex;
|
|
613
|
+
width: 100%;
|
|
614
|
+
height: 1.25rem;
|
|
615
|
+
touch-action: none;
|
|
616
|
+
user-select: none;
|
|
617
|
+
align-items: center;
|
|
618
|
+
cursor: pointer;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
.fw-slider--vertical {
|
|
622
|
+
flex-direction: column;
|
|
623
|
+
width: 1.25rem;
|
|
624
|
+
height: 100%;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
.fw-slider-track {
|
|
628
|
+
position: absolute;
|
|
629
|
+
left: 0;
|
|
630
|
+
right: 0;
|
|
631
|
+
height: 4px;
|
|
632
|
+
border-radius: 9999px;
|
|
633
|
+
background: hsl(var(--tn-fg-gutter) / 0.4);
|
|
634
|
+
overflow: hidden;
|
|
635
|
+
transition: height 0.15s ease;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
.fw-slider:hover .fw-slider-track {
|
|
639
|
+
height: 6px;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
.fw-slider--vertical .fw-slider-track {
|
|
643
|
+
top: 0;
|
|
644
|
+
bottom: 0;
|
|
645
|
+
left: 50%;
|
|
646
|
+
right: auto;
|
|
647
|
+
width: 4px;
|
|
648
|
+
height: auto;
|
|
649
|
+
transform: translateX(-50%);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
.fw-slider--vertical:hover .fw-slider-track {
|
|
653
|
+
width: 6px;
|
|
654
|
+
height: auto;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
.fw-slider-range {
|
|
658
|
+
position: absolute;
|
|
659
|
+
height: 100%;
|
|
660
|
+
border-radius: 9999px;
|
|
661
|
+
background: hsl(var(--tn-fg));
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
.fw-slider-range--accent {
|
|
665
|
+
background: hsl(var(--tn-cyan));
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
.fw-slider--vertical .fw-slider-range {
|
|
669
|
+
width: 100%;
|
|
670
|
+
height: auto;
|
|
671
|
+
bottom: 0;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
.fw-slider-thumb {
|
|
675
|
+
display: block;
|
|
676
|
+
width: 10px;
|
|
677
|
+
height: 10px;
|
|
678
|
+
border-radius: 50%;
|
|
679
|
+
background: hsl(var(--tn-fg));
|
|
680
|
+
border: none;
|
|
681
|
+
box-shadow: 0 2px 4px rgb(0 0 0 / 0.3);
|
|
682
|
+
cursor: pointer;
|
|
683
|
+
transition: width 0.15s ease, height 0.15s ease, box-shadow 0.15s ease;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
.fw-slider:hover .fw-slider-thumb {
|
|
687
|
+
width: 14px;
|
|
688
|
+
height: 14px;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
.fw-slider-thumb:focus {
|
|
692
|
+
outline: none;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
.fw-slider-thumb:focus-visible {
|
|
696
|
+
box-shadow: 0 0 0 2px hsl(var(--tn-fg) / 0.5);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
.fw-slider-thumb--accent {
|
|
700
|
+
background: hsl(var(--tn-cyan));
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
.fw-slider-thumb[data-disabled] {
|
|
704
|
+
pointer-events: none;
|
|
705
|
+
opacity: 0.5;
|
|
706
|
+
}
|
|
707
|
+
|
|
582
708
|
/* --- Time Display --- */
|
|
583
709
|
.fw-time-display {
|
|
584
710
|
font-family: ui-monospace, monospace;
|
|
@@ -676,7 +802,9 @@
|
|
|
676
802
|
background: hsl(var(--tn-blue));
|
|
677
803
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
|
678
804
|
transform: translate(-50%, -50%);
|
|
679
|
-
transition:
|
|
805
|
+
transition:
|
|
806
|
+
opacity 0.15s ease,
|
|
807
|
+
transform 0.15s ease;
|
|
680
808
|
opacity: 0;
|
|
681
809
|
pointer-events: none;
|
|
682
810
|
}
|
|
@@ -949,7 +1077,9 @@
|
|
|
949
1077
|
border: none;
|
|
950
1078
|
color: hsl(var(--tn-fg-dark));
|
|
951
1079
|
cursor: pointer;
|
|
952
|
-
transition:
|
|
1080
|
+
transition:
|
|
1081
|
+
background-color 0.15s ease,
|
|
1082
|
+
color 0.15s ease;
|
|
953
1083
|
}
|
|
954
1084
|
|
|
955
1085
|
.fw-error-close:hover {
|
|
@@ -980,7 +1110,9 @@
|
|
|
980
1110
|
background: transparent;
|
|
981
1111
|
border: none;
|
|
982
1112
|
cursor: pointer;
|
|
983
|
-
transition:
|
|
1113
|
+
transition:
|
|
1114
|
+
background-color 0.15s ease,
|
|
1115
|
+
color 0.15s ease;
|
|
984
1116
|
}
|
|
985
1117
|
|
|
986
1118
|
.fw-error-btn:hover {
|
|
@@ -1076,7 +1208,9 @@
|
|
|
1076
1208
|
border-right: 1px solid hsl(var(--tn-fg-gutter) / 0.3);
|
|
1077
1209
|
color: hsl(var(--tn-fg-dark));
|
|
1078
1210
|
cursor: pointer;
|
|
1079
|
-
transition:
|
|
1211
|
+
transition:
|
|
1212
|
+
background-color 0.15s,
|
|
1213
|
+
color 0.15s;
|
|
1080
1214
|
}
|
|
1081
1215
|
|
|
1082
1216
|
.fw-dev-tab:hover {
|
|
@@ -1183,7 +1317,9 @@
|
|
|
1183
1317
|
border: none;
|
|
1184
1318
|
color: hsl(var(--tn-fg-dark));
|
|
1185
1319
|
cursor: pointer;
|
|
1186
|
-
transition:
|
|
1320
|
+
transition:
|
|
1321
|
+
background-color 0.15s,
|
|
1322
|
+
color 0.15s;
|
|
1187
1323
|
}
|
|
1188
1324
|
|
|
1189
1325
|
.fw-dev-mode-btn:hover {
|
|
@@ -1211,7 +1347,9 @@
|
|
|
1211
1347
|
color: hsl(var(--tn-fg));
|
|
1212
1348
|
font-size: 0.75rem;
|
|
1213
1349
|
cursor: pointer;
|
|
1214
|
-
transition:
|
|
1350
|
+
transition:
|
|
1351
|
+
background-color 0.15s,
|
|
1352
|
+
color 0.15s;
|
|
1215
1353
|
}
|
|
1216
1354
|
|
|
1217
1355
|
.fw-dev-action-btn:last-child {
|
|
@@ -1378,7 +1516,9 @@
|
|
|
1378
1516
|
z-index: 50;
|
|
1379
1517
|
background: hsl(var(--tn-bg-dark));
|
|
1380
1518
|
border: 1px solid hsl(var(--tn-fg-gutter));
|
|
1381
|
-
box-shadow:
|
|
1519
|
+
box-shadow:
|
|
1520
|
+
0 4px 6px -1px rgb(0 0 0 / 0.1),
|
|
1521
|
+
0 2px 4px -2px rgb(0 0 0 / 0.1);
|
|
1382
1522
|
padding: 0.5rem;
|
|
1383
1523
|
font-size: 10px;
|
|
1384
1524
|
white-space: nowrap;
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import type { ABRMode, ABROptions, PlaybackQuality, QualityLevel } from
|
|
2
|
-
import { TimerManager } from
|
|
1
|
+
import type { ABRMode, ABROptions, PlaybackQuality, QualityLevel } from "../types";
|
|
2
|
+
import { TimerManager } from "./TimerManager";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Default ABR options
|
|
6
6
|
*/
|
|
7
7
|
const DEFAULT_OPTIONS: Required<ABROptions> = {
|
|
8
|
-
mode:
|
|
8
|
+
mode: "auto",
|
|
9
9
|
maxResolution: { width: 1920, height: 1080 },
|
|
10
10
|
maxBitrate: 8000000, // 8 Mbps
|
|
11
11
|
minBufferForUpgrade: 10,
|
|
@@ -18,7 +18,7 @@ export interface ABRControllerConfig {
|
|
|
18
18
|
/** Callback to get available qualities */
|
|
19
19
|
getQualities: () => QualityLevel[];
|
|
20
20
|
/** Callback to select a quality */
|
|
21
|
-
selectQuality: (id: string |
|
|
21
|
+
selectQuality: (id: string | "auto") => void;
|
|
22
22
|
/** Callback to get current quality */
|
|
23
23
|
getCurrentQuality?: () => QualityLevel | null;
|
|
24
24
|
/** Callback to get bandwidth estimate (bits per second) - typically from player stats */
|
|
@@ -27,7 +27,7 @@ export interface ABRControllerConfig {
|
|
|
27
27
|
debug?: boolean;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
export type ABRDecision =
|
|
30
|
+
export type ABRDecision = "upgrade" | "downgrade" | "maintain" | "none";
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
33
|
* ABRController - Adaptive Bitrate Controller
|
|
@@ -54,8 +54,8 @@ export class ABRController {
|
|
|
54
54
|
private options: Required<ABROptions>;
|
|
55
55
|
private config: ABRControllerConfig;
|
|
56
56
|
private videoElement: HTMLVideoElement | null = null;
|
|
57
|
-
private currentQualityId: string |
|
|
58
|
-
private lastDecision: ABRDecision =
|
|
57
|
+
private currentQualityId: string | "auto" = "auto";
|
|
58
|
+
private lastDecision: ABRDecision = "none";
|
|
59
59
|
private lastDecisionTime = 0;
|
|
60
60
|
private resizeObserver: ResizeObserver | null = null;
|
|
61
61
|
private qualityChangeCallbacks: Array<(level: QualityLevel) => void> = [];
|
|
@@ -96,18 +96,18 @@ export class ABRController {
|
|
|
96
96
|
this.stop();
|
|
97
97
|
this.videoElement = videoElement;
|
|
98
98
|
|
|
99
|
-
if (this.options.mode ===
|
|
100
|
-
this.log(
|
|
99
|
+
if (this.options.mode === "manual") {
|
|
100
|
+
this.log("Manual mode - no automatic ABR");
|
|
101
101
|
return;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
// Setup resize observer for ABR_resize mode
|
|
105
|
-
if (this.options.mode ===
|
|
105
|
+
if (this.options.mode === "resize" || this.options.mode === "auto") {
|
|
106
106
|
this.setupResizeObserver();
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
// Start active bandwidth monitoring for bitrate mode
|
|
110
|
-
if (this.options.mode ===
|
|
110
|
+
if (this.options.mode === "bitrate" || this.options.mode === "auto") {
|
|
111
111
|
this.startActiveMonitoring();
|
|
112
112
|
}
|
|
113
113
|
}
|
|
@@ -133,7 +133,7 @@ export class ABRController {
|
|
|
133
133
|
this.timers.startInterval(
|
|
134
134
|
() => this.checkBandwidthAndSwitch(),
|
|
135
135
|
ABRController.MONITORING_INTERVAL_MS,
|
|
136
|
-
|
|
136
|
+
"monitoring"
|
|
137
137
|
);
|
|
138
138
|
|
|
139
139
|
// Initial check
|
|
@@ -182,8 +182,10 @@ export class ABRController {
|
|
|
182
182
|
if (smoothedBandwidth < currentBitrate * ABRController.DOWNGRADE_THRESHOLD) {
|
|
183
183
|
const lowerQuality = this.findLowerQuality(qualities, currentQuality);
|
|
184
184
|
if (lowerQuality) {
|
|
185
|
-
this.log(
|
|
186
|
-
|
|
185
|
+
this.log(
|
|
186
|
+
`ABR: bandwidth ${Math.round(smoothedBandwidth / 1000)}kbps < ${Math.round((currentBitrate * ABRController.DOWNGRADE_THRESHOLD) / 1000)}kbps threshold -> downgrading to ${lowerQuality.label}`
|
|
187
|
+
);
|
|
188
|
+
this.lastDecision = "downgrade";
|
|
187
189
|
this.lastDecisionTime = now;
|
|
188
190
|
this.lastDowngradeTime = now;
|
|
189
191
|
this.selectQuality(lowerQuality.id);
|
|
@@ -201,11 +203,14 @@ export class ABRController {
|
|
|
201
203
|
// D2: Hysteresis - require 1.5x headroom to upgrade
|
|
202
204
|
// Once at a quality level, stay until bandwidth drops below 1.2x (not 1.0x)
|
|
203
205
|
const shouldUpgrade = smoothedBandwidth >= targetBitrate * ABRController.UPGRADE_HEADROOM;
|
|
204
|
-
const _canHoldHigher =
|
|
206
|
+
const _canHoldHigher =
|
|
207
|
+
smoothedBandwidth >= targetBitrate * ABRController.UPGRADE_HOLD_THRESHOLD;
|
|
205
208
|
|
|
206
209
|
if (shouldUpgrade) {
|
|
207
|
-
this.log(
|
|
208
|
-
|
|
210
|
+
this.log(
|
|
211
|
+
`ABR: bandwidth ${Math.round(smoothedBandwidth / 1000)}kbps >= ${Math.round((targetBitrate * ABRController.UPGRADE_HEADROOM) / 1000)}kbps headroom -> upgrading to ${higherQuality.label}`
|
|
212
|
+
);
|
|
213
|
+
this.lastDecision = "upgrade";
|
|
209
214
|
this.lastDecisionTime = now;
|
|
210
215
|
this.lastUpgradeTime = now;
|
|
211
216
|
this.selectQuality(higherQuality.id);
|
|
@@ -282,7 +287,7 @@ export class ABRController {
|
|
|
282
287
|
* Handle viewport resize (ABR_resize mode)
|
|
283
288
|
*/
|
|
284
289
|
private handleResize(width: number, height: number): void {
|
|
285
|
-
if (this.options.mode !==
|
|
290
|
+
if (this.options.mode !== "resize" && this.options.mode !== "auto") {
|
|
286
291
|
return;
|
|
287
292
|
}
|
|
288
293
|
|
|
@@ -291,7 +296,10 @@ export class ABRController {
|
|
|
291
296
|
|
|
292
297
|
// Find best quality for viewport size
|
|
293
298
|
const targetWidth = Math.min(width * window.devicePixelRatio, this.options.maxResolution.width);
|
|
294
|
-
const targetHeight = Math.min(
|
|
299
|
+
const targetHeight = Math.min(
|
|
300
|
+
height * window.devicePixelRatio,
|
|
301
|
+
this.options.maxResolution.height
|
|
302
|
+
);
|
|
295
303
|
|
|
296
304
|
const bestQuality = this.findBestQualityForResolution(qualities, targetWidth, targetHeight);
|
|
297
305
|
|
|
@@ -307,7 +315,7 @@ export class ABRController {
|
|
|
307
315
|
* Called by QualityMonitor when playback quality drops
|
|
308
316
|
*/
|
|
309
317
|
handleQualityDegraded(quality: PlaybackQuality): void {
|
|
310
|
-
if (this.options.mode !==
|
|
318
|
+
if (this.options.mode !== "bitrate" && this.options.mode !== "auto") {
|
|
311
319
|
return;
|
|
312
320
|
}
|
|
313
321
|
|
|
@@ -327,7 +335,7 @@ export class ABRController {
|
|
|
327
335
|
|
|
328
336
|
if (lowerQuality) {
|
|
329
337
|
this.log(`Bitrate ABR: score ${quality.score} -> downgrading to ${lowerQuality.label}`);
|
|
330
|
-
this.lastDecision =
|
|
338
|
+
this.lastDecision = "downgrade";
|
|
331
339
|
this.lastDecisionTime = now;
|
|
332
340
|
this.lastDowngradeTime = now;
|
|
333
341
|
this.selectQuality(lowerQuality.id);
|
|
@@ -342,7 +350,7 @@ export class ABRController {
|
|
|
342
350
|
* Called when conditions are good enough to try higher quality
|
|
343
351
|
*/
|
|
344
352
|
handleQualityImproved(quality: PlaybackQuality): void {
|
|
345
|
-
if (this.options.mode !==
|
|
353
|
+
if (this.options.mode !== "bitrate" && this.options.mode !== "auto") {
|
|
346
354
|
return;
|
|
347
355
|
}
|
|
348
356
|
|
|
@@ -363,7 +371,7 @@ export class ABRController {
|
|
|
363
371
|
|
|
364
372
|
if (higherQuality && this.isWithinConstraints(higherQuality)) {
|
|
365
373
|
this.log(`Bitrate ABR: score ${quality.score} -> upgrading to ${higherQuality.label}`);
|
|
366
|
-
this.lastDecision =
|
|
374
|
+
this.lastDecision = "upgrade";
|
|
367
375
|
this.lastDecisionTime = now;
|
|
368
376
|
this.lastUpgradeTime = now;
|
|
369
377
|
this.selectQuality(higherQuality.id);
|
|
@@ -381,7 +389,7 @@ export class ABRController {
|
|
|
381
389
|
targetHeight: number
|
|
382
390
|
): QualityLevel | null {
|
|
383
391
|
// Filter out qualities that exceed constraints
|
|
384
|
-
const validQualities = qualities.filter(q => this.isWithinConstraints(q));
|
|
392
|
+
const validQualities = qualities.filter((q) => this.isWithinConstraints(q));
|
|
385
393
|
|
|
386
394
|
if (validQualities.length === 0) return null;
|
|
387
395
|
|
|
@@ -409,10 +417,7 @@ export class ABRController {
|
|
|
409
417
|
/**
|
|
410
418
|
* Find a lower quality level
|
|
411
419
|
*/
|
|
412
|
-
private findLowerQuality(
|
|
413
|
-
qualities: QualityLevel[],
|
|
414
|
-
current: QualityLevel
|
|
415
|
-
): QualityLevel | null {
|
|
420
|
+
private findLowerQuality(qualities: QualityLevel[], current: QualityLevel): QualityLevel | null {
|
|
416
421
|
const currentBitrate = current.bitrate ?? 0;
|
|
417
422
|
|
|
418
423
|
// Sort by bitrate descending
|
|
@@ -431,10 +436,7 @@ export class ABRController {
|
|
|
431
436
|
/**
|
|
432
437
|
* Find a higher quality level
|
|
433
438
|
*/
|
|
434
|
-
private findHigherQuality(
|
|
435
|
-
qualities: QualityLevel[],
|
|
436
|
-
current: QualityLevel
|
|
437
|
-
): QualityLevel | null {
|
|
439
|
+
private findHigherQuality(qualities: QualityLevel[], current: QualityLevel): QualityLevel | null {
|
|
438
440
|
const currentBitrate = current.bitrate ?? 0;
|
|
439
441
|
|
|
440
442
|
// Sort by bitrate ascending
|
|
@@ -466,15 +468,15 @@ export class ABRController {
|
|
|
466
468
|
/**
|
|
467
469
|
* Select a quality level
|
|
468
470
|
*/
|
|
469
|
-
private selectQuality(id: string |
|
|
471
|
+
private selectQuality(id: string | "auto"): void {
|
|
470
472
|
this.currentQualityId = id;
|
|
471
473
|
this.config.selectQuality(id);
|
|
472
474
|
|
|
473
475
|
// Notify callbacks
|
|
474
476
|
const qualities = this.config.getQualities();
|
|
475
|
-
const selected = qualities.find(q => q.id === id);
|
|
477
|
+
const selected = qualities.find((q) => q.id === id);
|
|
476
478
|
if (selected) {
|
|
477
|
-
this.qualityChangeCallbacks.forEach(cb => cb(selected));
|
|
479
|
+
this.qualityChangeCallbacks.forEach((cb) => cb(selected));
|
|
478
480
|
}
|
|
479
481
|
}
|
|
480
482
|
|
|
@@ -494,7 +496,7 @@ export class ABRController {
|
|
|
494
496
|
/**
|
|
495
497
|
* Manually set quality (switches to manual mode temporarily)
|
|
496
498
|
*/
|
|
497
|
-
setQuality(id: string |
|
|
499
|
+
setQuality(id: string | "auto"): void {
|
|
498
500
|
this.selectQuality(id);
|
|
499
501
|
}
|
|
500
502
|
|