@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.
Files changed (59) hide show
  1. package/README.md +11 -9
  2. package/dist/player.css +182 -42
  3. package/package.json +1 -1
  4. package/src/core/ABRController.ts +38 -36
  5. package/src/core/CodecUtils.ts +49 -46
  6. package/src/core/Disposable.ts +4 -4
  7. package/src/core/EventEmitter.ts +1 -1
  8. package/src/core/GatewayClient.ts +41 -39
  9. package/src/core/InteractionController.ts +89 -82
  10. package/src/core/LiveDurationProxy.ts +14 -15
  11. package/src/core/MetaTrackManager.ts +73 -65
  12. package/src/core/MistReporter.ts +72 -45
  13. package/src/core/MistSignaling.ts +59 -56
  14. package/src/core/PlayerController.ts +527 -384
  15. package/src/core/PlayerInterface.ts +83 -59
  16. package/src/core/PlayerManager.ts +79 -133
  17. package/src/core/PlayerRegistry.ts +59 -42
  18. package/src/core/QualityMonitor.ts +38 -31
  19. package/src/core/ScreenWakeLockManager.ts +8 -9
  20. package/src/core/SeekingUtils.ts +31 -22
  21. package/src/core/StreamStateClient.ts +74 -68
  22. package/src/core/SubtitleManager.ts +24 -22
  23. package/src/core/TelemetryReporter.ts +34 -31
  24. package/src/core/TimeFormat.ts +13 -17
  25. package/src/core/TimerManager.ts +24 -8
  26. package/src/core/UrlUtils.ts +20 -17
  27. package/src/core/detector.ts +44 -44
  28. package/src/core/index.ts +57 -48
  29. package/src/core/scorer.ts +136 -141
  30. package/src/core/selector.ts +2 -6
  31. package/src/global.d.ts +1 -1
  32. package/src/index.ts +46 -35
  33. package/src/players/DashJsPlayer.ts +164 -115
  34. package/src/players/HlsJsPlayer.ts +132 -78
  35. package/src/players/MewsWsPlayer/SourceBufferManager.ts +41 -36
  36. package/src/players/MewsWsPlayer/WebSocketManager.ts +9 -9
  37. package/src/players/MewsWsPlayer/index.ts +192 -152
  38. package/src/players/MewsWsPlayer/types.ts +21 -21
  39. package/src/players/MistPlayer.ts +45 -26
  40. package/src/players/MistWebRTCPlayer/index.ts +175 -129
  41. package/src/players/NativePlayer.ts +203 -143
  42. package/src/players/VideoJsPlayer.ts +170 -118
  43. package/src/players/WebCodecsPlayer/JitterBuffer.ts +6 -7
  44. package/src/players/WebCodecsPlayer/LatencyProfiles.ts +43 -43
  45. package/src/players/WebCodecsPlayer/RawChunkParser.ts +10 -10
  46. package/src/players/WebCodecsPlayer/SyncController.ts +45 -53
  47. package/src/players/WebCodecsPlayer/WebSocketController.ts +66 -68
  48. package/src/players/WebCodecsPlayer/index.ts +263 -221
  49. package/src/players/WebCodecsPlayer/polyfills/MediaStreamTrackGenerator.ts +12 -17
  50. package/src/players/WebCodecsPlayer/types.ts +56 -56
  51. package/src/players/WebCodecsPlayer/worker/decoder.worker.ts +238 -182
  52. package/src/players/WebCodecsPlayer/worker/types.ts +31 -31
  53. package/src/players/index.ts +8 -8
  54. package/src/styles/animations.css +2 -1
  55. package/src/styles/player.css +182 -42
  56. package/src/styles/tailwind.css +473 -159
  57. package/src/types.ts +43 -43
  58. package/src/vanilla/FrameWorksPlayer.ts +29 -14
  59. 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 '@livepeer-frameworks/player-core';
18
+ import { PlayerController } from "@livepeer-frameworks/player-core";
19
19
 
20
20
  const controller = new PlayerController({
21
- contentId: 'pk_...', // playbackId
22
- contentType: 'live',
23
- gatewayUrl: 'https://your-bridge/graphql',
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('player')!;
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: 'pk_...',
39
- contentType: 'live',
40
- mistUrl: 'https://edge.example.com',
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 '@livepeer-frameworks/player-core/player.css';
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%; /* #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 */
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%; /* #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 */
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%; /* #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 */
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%, 100% {
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: background-color 0.15s, color 0.15s;
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 { color: hsl(var(--tn-green)); }
205
- .fw-status-offline { color: hsl(var(--tn-red)); }
206
- .fw-status-warning { color: hsl(var(--tn-yellow)); }
207
- .fw-status-info { color: hsl(var(--tn-cyan)); }
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; /* Slabs don't have rounded corners */
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: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
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: background-color 0.15s, color 0.15s;
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: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
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: background-color 0.15s, color 0.15s;
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: background-color 0.15s, color 0.15s;
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: background-color 0.15s, color 0.15s;
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.25rem;
526
- transition: background-color 0.2s, width 0.2s;
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: rgb(255 255 255 / 0.1);
556
+ background: hsl(var(--tn-bg-visual) / 0.5);
532
557
  }
533
558
 
534
559
  .fw-volume-group:hover {
535
- background: rgb(255 255 255 / 0.05);
560
+ background: hsl(var(--tn-bg-visual) / 0.5);
536
561
  }
537
562
 
538
563
  .fw-volume-group--expanded:hover {
539
- background: rgb(255 255 255 / 0.1);
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: width 0.2s ease-out, opacity 0.2s ease-out;
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: opacity 0.15s ease, transform 0.15s ease;
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: background-color 0.15s ease, color 0.15s ease;
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: background-color 0.15s ease, color 0.15s ease;
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: background-color 0.15s, color 0.15s;
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: background-color 0.15s, color 0.15s;
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: background-color 0.15s, color 0.15s;
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: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@livepeer-frameworks/player-core",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "type": "module",
5
5
  "description": "Core player logic for FrameWorks streaming - framework agnostic",
6
6
  "main": "dist/cjs/index.js",
@@ -1,11 +1,11 @@
1
- import type { ABRMode, ABROptions, PlaybackQuality, QualityLevel } from '../types';
2
- import { TimerManager } from './TimerManager';
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: 'auto',
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 | 'auto') => void;
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 = 'upgrade' | 'downgrade' | 'maintain' | 'none';
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 | 'auto' = 'auto';
58
- private lastDecision: ABRDecision = 'none';
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 === 'manual') {
100
- this.log('Manual mode - no automatic ABR');
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 === 'resize' || this.options.mode === 'auto') {
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 === 'bitrate' || this.options.mode === 'auto') {
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
- 'monitoring'
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(`ABR: bandwidth ${Math.round(smoothedBandwidth / 1000)}kbps < ${Math.round(currentBitrate * ABRController.DOWNGRADE_THRESHOLD / 1000)}kbps threshold -> downgrading to ${lowerQuality.label}`);
186
- this.lastDecision = 'downgrade';
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 = smoothedBandwidth >= targetBitrate * ABRController.UPGRADE_HOLD_THRESHOLD;
206
+ const _canHoldHigher =
207
+ smoothedBandwidth >= targetBitrate * ABRController.UPGRADE_HOLD_THRESHOLD;
205
208
 
206
209
  if (shouldUpgrade) {
207
- this.log(`ABR: bandwidth ${Math.round(smoothedBandwidth / 1000)}kbps >= ${Math.round(targetBitrate * ABRController.UPGRADE_HEADROOM / 1000)}kbps headroom -> upgrading to ${higherQuality.label}`);
208
- this.lastDecision = 'upgrade';
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 !== 'resize' && this.options.mode !== 'auto') {
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(height * window.devicePixelRatio, this.options.maxResolution.height);
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 !== 'bitrate' && this.options.mode !== 'auto') {
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 = 'downgrade';
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 !== 'bitrate' && this.options.mode !== 'auto') {
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 = 'upgrade';
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 | 'auto'): void {
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 | 'auto'): void {
499
+ setQuality(id: string | "auto"): void {
498
500
  this.selectQuality(id);
499
501
  }
500
502