@livepeer-frameworks/player-core 0.0.3

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 (120) hide show
  1. package/dist/cjs/index.js +19493 -0
  2. package/dist/cjs/index.js.map +1 -0
  3. package/dist/esm/index.js +19398 -0
  4. package/dist/esm/index.js.map +1 -0
  5. package/dist/player.css +2140 -0
  6. package/dist/types/core/ABRController.d.ts +164 -0
  7. package/dist/types/core/CodecUtils.d.ts +54 -0
  8. package/dist/types/core/Disposable.d.ts +61 -0
  9. package/dist/types/core/EventEmitter.d.ts +73 -0
  10. package/dist/types/core/GatewayClient.d.ts +144 -0
  11. package/dist/types/core/InteractionController.d.ts +121 -0
  12. package/dist/types/core/LiveDurationProxy.d.ts +102 -0
  13. package/dist/types/core/MetaTrackManager.d.ts +220 -0
  14. package/dist/types/core/MistReporter.d.ts +163 -0
  15. package/dist/types/core/MistSignaling.d.ts +148 -0
  16. package/dist/types/core/PlayerController.d.ts +665 -0
  17. package/dist/types/core/PlayerInterface.d.ts +230 -0
  18. package/dist/types/core/PlayerManager.d.ts +182 -0
  19. package/dist/types/core/PlayerRegistry.d.ts +27 -0
  20. package/dist/types/core/QualityMonitor.d.ts +184 -0
  21. package/dist/types/core/ScreenWakeLockManager.d.ts +70 -0
  22. package/dist/types/core/SeekingUtils.d.ts +142 -0
  23. package/dist/types/core/StreamStateClient.d.ts +108 -0
  24. package/dist/types/core/SubtitleManager.d.ts +111 -0
  25. package/dist/types/core/TelemetryReporter.d.ts +79 -0
  26. package/dist/types/core/TimeFormat.d.ts +97 -0
  27. package/dist/types/core/TimerManager.d.ts +83 -0
  28. package/dist/types/core/UrlUtils.d.ts +81 -0
  29. package/dist/types/core/detector.d.ts +149 -0
  30. package/dist/types/core/index.d.ts +49 -0
  31. package/dist/types/core/scorer.d.ts +167 -0
  32. package/dist/types/core/selector.d.ts +9 -0
  33. package/dist/types/index.d.ts +45 -0
  34. package/dist/types/lib/utils.d.ts +2 -0
  35. package/dist/types/players/DashJsPlayer.d.ts +102 -0
  36. package/dist/types/players/HlsJsPlayer.d.ts +70 -0
  37. package/dist/types/players/MewsWsPlayer/SourceBufferManager.d.ts +119 -0
  38. package/dist/types/players/MewsWsPlayer/WebSocketManager.d.ts +60 -0
  39. package/dist/types/players/MewsWsPlayer/index.d.ts +220 -0
  40. package/dist/types/players/MewsWsPlayer/types.d.ts +89 -0
  41. package/dist/types/players/MistPlayer.d.ts +25 -0
  42. package/dist/types/players/MistWebRTCPlayer/index.d.ts +133 -0
  43. package/dist/types/players/NativePlayer.d.ts +143 -0
  44. package/dist/types/players/VideoJsPlayer.d.ts +59 -0
  45. package/dist/types/players/WebCodecsPlayer/JitterBuffer.d.ts +118 -0
  46. package/dist/types/players/WebCodecsPlayer/LatencyProfiles.d.ts +64 -0
  47. package/dist/types/players/WebCodecsPlayer/RawChunkParser.d.ts +63 -0
  48. package/dist/types/players/WebCodecsPlayer/SyncController.d.ts +174 -0
  49. package/dist/types/players/WebCodecsPlayer/WebSocketController.d.ts +164 -0
  50. package/dist/types/players/WebCodecsPlayer/index.d.ts +149 -0
  51. package/dist/types/players/WebCodecsPlayer/polyfills/MediaStreamTrackGenerator.d.ts +105 -0
  52. package/dist/types/players/WebCodecsPlayer/types.d.ts +395 -0
  53. package/dist/types/players/WebCodecsPlayer/worker/decoder.worker.d.ts +13 -0
  54. package/dist/types/players/WebCodecsPlayer/worker/types.d.ts +197 -0
  55. package/dist/types/players/index.d.ts +14 -0
  56. package/dist/types/styles/index.d.ts +11 -0
  57. package/dist/types/types.d.ts +363 -0
  58. package/dist/types/vanilla/FrameWorksPlayer.d.ts +143 -0
  59. package/dist/types/vanilla/index.d.ts +19 -0
  60. package/dist/workers/decoder.worker.js +989 -0
  61. package/dist/workers/decoder.worker.js.map +1 -0
  62. package/package.json +80 -0
  63. package/src/core/ABRController.ts +550 -0
  64. package/src/core/CodecUtils.ts +257 -0
  65. package/src/core/Disposable.ts +120 -0
  66. package/src/core/EventEmitter.ts +113 -0
  67. package/src/core/GatewayClient.ts +439 -0
  68. package/src/core/InteractionController.ts +712 -0
  69. package/src/core/LiveDurationProxy.ts +270 -0
  70. package/src/core/MetaTrackManager.ts +753 -0
  71. package/src/core/MistReporter.ts +543 -0
  72. package/src/core/MistSignaling.ts +346 -0
  73. package/src/core/PlayerController.ts +2829 -0
  74. package/src/core/PlayerInterface.ts +432 -0
  75. package/src/core/PlayerManager.ts +900 -0
  76. package/src/core/PlayerRegistry.ts +149 -0
  77. package/src/core/QualityMonitor.ts +597 -0
  78. package/src/core/ScreenWakeLockManager.ts +163 -0
  79. package/src/core/SeekingUtils.ts +364 -0
  80. package/src/core/StreamStateClient.ts +457 -0
  81. package/src/core/SubtitleManager.ts +297 -0
  82. package/src/core/TelemetryReporter.ts +308 -0
  83. package/src/core/TimeFormat.ts +205 -0
  84. package/src/core/TimerManager.ts +209 -0
  85. package/src/core/UrlUtils.ts +179 -0
  86. package/src/core/detector.ts +382 -0
  87. package/src/core/index.ts +140 -0
  88. package/src/core/scorer.ts +553 -0
  89. package/src/core/selector.ts +16 -0
  90. package/src/global.d.ts +11 -0
  91. package/src/index.ts +75 -0
  92. package/src/lib/utils.ts +6 -0
  93. package/src/players/DashJsPlayer.ts +642 -0
  94. package/src/players/HlsJsPlayer.ts +483 -0
  95. package/src/players/MewsWsPlayer/SourceBufferManager.ts +572 -0
  96. package/src/players/MewsWsPlayer/WebSocketManager.ts +241 -0
  97. package/src/players/MewsWsPlayer/index.ts +1065 -0
  98. package/src/players/MewsWsPlayer/types.ts +106 -0
  99. package/src/players/MistPlayer.ts +188 -0
  100. package/src/players/MistWebRTCPlayer/index.ts +703 -0
  101. package/src/players/NativePlayer.ts +820 -0
  102. package/src/players/VideoJsPlayer.ts +643 -0
  103. package/src/players/WebCodecsPlayer/JitterBuffer.ts +299 -0
  104. package/src/players/WebCodecsPlayer/LatencyProfiles.ts +151 -0
  105. package/src/players/WebCodecsPlayer/RawChunkParser.ts +151 -0
  106. package/src/players/WebCodecsPlayer/SyncController.ts +456 -0
  107. package/src/players/WebCodecsPlayer/WebSocketController.ts +564 -0
  108. package/src/players/WebCodecsPlayer/index.ts +1650 -0
  109. package/src/players/WebCodecsPlayer/polyfills/MediaStreamTrackGenerator.ts +379 -0
  110. package/src/players/WebCodecsPlayer/types.ts +542 -0
  111. package/src/players/WebCodecsPlayer/worker/decoder.worker.ts +1360 -0
  112. package/src/players/WebCodecsPlayer/worker/types.ts +276 -0
  113. package/src/players/index.ts +22 -0
  114. package/src/styles/animations.css +21 -0
  115. package/src/styles/index.ts +52 -0
  116. package/src/styles/player.css +2126 -0
  117. package/src/styles/tailwind.css +1015 -0
  118. package/src/types.ts +421 -0
  119. package/src/vanilla/FrameWorksPlayer.ts +367 -0
  120. package/src/vanilla/index.ts +22 -0
@@ -0,0 +1,550 @@
1
+ import type { ABRMode, ABROptions, PlaybackQuality, QualityLevel } from '../types';
2
+ import { TimerManager } from './TimerManager';
3
+
4
+ /**
5
+ * Default ABR options
6
+ */
7
+ const DEFAULT_OPTIONS: Required<ABROptions> = {
8
+ mode: 'auto',
9
+ maxResolution: { width: 1920, height: 1080 },
10
+ maxBitrate: 8000000, // 8 Mbps
11
+ minBufferForUpgrade: 10,
12
+ downgradeThreshold: 60,
13
+ };
14
+
15
+ export interface ABRControllerConfig {
16
+ /** ABR options */
17
+ options?: Partial<ABROptions>;
18
+ /** Callback to get available qualities */
19
+ getQualities: () => QualityLevel[];
20
+ /** Callback to select a quality */
21
+ selectQuality: (id: string | 'auto') => void;
22
+ /** Callback to get current quality */
23
+ getCurrentQuality?: () => QualityLevel | null;
24
+ /** Callback to get bandwidth estimate (bits per second) - typically from player stats */
25
+ getBandwidthEstimate?: () => Promise<number>;
26
+ /** Debug logging */
27
+ debug?: boolean;
28
+ }
29
+
30
+ export type ABRDecision = 'upgrade' | 'downgrade' | 'maintain' | 'none';
31
+
32
+ /**
33
+ * ABRController - Adaptive Bitrate Controller
34
+ *
35
+ * Manages automatic quality selection based on:
36
+ * - ABR_resize: Matches video resolution to viewport size
37
+ * - ABR_bitrate: Switches quality based on playback performance
38
+ * - auto: Combines both modes
39
+ * - manual: No automatic switching
40
+ *
41
+ * @example
42
+ * ```ts
43
+ * const abr = new ABRController({
44
+ * options: { mode: 'auto' },
45
+ * getQualities: () => player.getQualities(),
46
+ * selectQuality: (id) => player.selectQuality(id),
47
+ * });
48
+ *
49
+ * abr.start(videoElement);
50
+ * abr.onQualityChange((quality) => console.log('Quality:', quality.score));
51
+ * ```
52
+ */
53
+ export class ABRController {
54
+ private options: Required<ABROptions>;
55
+ private config: ABRControllerConfig;
56
+ private videoElement: HTMLVideoElement | null = null;
57
+ private currentQualityId: string | 'auto' = 'auto';
58
+ private lastDecision: ABRDecision = 'none';
59
+ private lastDecisionTime = 0;
60
+ private resizeObserver: ResizeObserver | null = null;
61
+ private qualityChangeCallbacks: Array<(level: QualityLevel) => void> = [];
62
+ private debug: boolean;
63
+
64
+ // Active monitoring
65
+ private timers = new TimerManager();
66
+ private bandwidthHistory: number[] = [];
67
+ private static readonly BANDWIDTH_HISTORY_SIZE = 10;
68
+ private static readonly MONITORING_INTERVAL_MS = 1000;
69
+
70
+ // D3: Separate upgrade/downgrade cooldowns
71
+ // Downgrade is immediate (0ms) for quick response to problems
72
+ // Upgrade requires 5s stability to prevent flapping
73
+ private static readonly UPGRADE_COOLDOWN_MS = 5000;
74
+ private static readonly DOWNGRADE_COOLDOWN_MS = 0;
75
+ private lastUpgradeTime = 0;
76
+ private lastDowngradeTime = 0;
77
+
78
+ // D2: Hysteresis bands to prevent oscillation at boundaries
79
+ // Upgrade: must exceed 1.5x to upgrade, stay until drops below 1.2x
80
+ // Downgrade: must drop below 0.8x to downgrade
81
+ private static readonly UPGRADE_HEADROOM = 1.5;
82
+ private static readonly UPGRADE_HOLD_THRESHOLD = 1.2;
83
+ private static readonly DOWNGRADE_THRESHOLD = 0.8;
84
+ private currentQualityBitrate = 0;
85
+
86
+ constructor(config: ABRControllerConfig) {
87
+ this.options = { ...DEFAULT_OPTIONS, ...config.options };
88
+ this.config = config;
89
+ this.debug = config.debug ?? false;
90
+ }
91
+
92
+ /**
93
+ * Start ABR control
94
+ */
95
+ start(videoElement: HTMLVideoElement): void {
96
+ this.stop();
97
+ this.videoElement = videoElement;
98
+
99
+ if (this.options.mode === 'manual') {
100
+ this.log('Manual mode - no automatic ABR');
101
+ return;
102
+ }
103
+
104
+ // Setup resize observer for ABR_resize mode
105
+ if (this.options.mode === 'resize' || this.options.mode === 'auto') {
106
+ this.setupResizeObserver();
107
+ }
108
+
109
+ // Start active bandwidth monitoring for bitrate mode
110
+ if (this.options.mode === 'bitrate' || this.options.mode === 'auto') {
111
+ this.startActiveMonitoring();
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Stop ABR control
117
+ */
118
+ stop(): void {
119
+ if (this.resizeObserver) {
120
+ this.resizeObserver.disconnect();
121
+ this.resizeObserver = null;
122
+ }
123
+ this.timers.destroy();
124
+ this.videoElement = null;
125
+ this.bandwidthHistory = [];
126
+ }
127
+
128
+ /**
129
+ * Start active bandwidth monitoring loop
130
+ * Continuously monitors bandwidth and proactively switches quality
131
+ */
132
+ private startActiveMonitoring(): void {
133
+ this.timers.startInterval(
134
+ () => this.checkBandwidthAndSwitch(),
135
+ ABRController.MONITORING_INTERVAL_MS,
136
+ 'monitoring'
137
+ );
138
+
139
+ // Initial check
140
+ this.checkBandwidthAndSwitch();
141
+ }
142
+
143
+ /**
144
+ * Check current bandwidth and switch quality if needed
145
+ *
146
+ * Uses hysteresis (D2) and separate cooldowns (D3) to prevent oscillation:
147
+ * - Downgrade: immediate response (0ms cooldown), triggers at 0.8x
148
+ * - Upgrade: 5s cooldown, requires 1.5x headroom, holds until 1.2x
149
+ */
150
+ private async checkBandwidthAndSwitch(): Promise<void> {
151
+ if (!this.videoElement) return;
152
+
153
+ const now = Date.now();
154
+
155
+ // Get bandwidth estimate from player stats
156
+ const bandwidth = await this.getBandwidthEstimate();
157
+ if (bandwidth <= 0) return;
158
+
159
+ // Add to history
160
+ this.bandwidthHistory.push(bandwidth);
161
+ if (this.bandwidthHistory.length > ABRController.BANDWIDTH_HISTORY_SIZE) {
162
+ this.bandwidthHistory.shift();
163
+ }
164
+
165
+ // Use smoothed bandwidth (average of history)
166
+ const smoothedBandwidth = this.getSmoothedBandwidth();
167
+ if (smoothedBandwidth <= 0) return;
168
+
169
+ const qualities = this.config.getQualities();
170
+ const currentQuality = this.config.getCurrentQuality?.();
171
+ if (!currentQuality || qualities.length === 0) return;
172
+
173
+ const currentBitrate = currentQuality.bitrate || 0;
174
+
175
+ // Track current quality bitrate for hysteresis
176
+ if (this.currentQualityBitrate !== currentBitrate) {
177
+ this.currentQualityBitrate = currentBitrate;
178
+ }
179
+
180
+ // D3: Check for downgrade (immediate, no cooldown)
181
+ if (now - this.lastDowngradeTime >= ABRController.DOWNGRADE_COOLDOWN_MS) {
182
+ if (smoothedBandwidth < currentBitrate * ABRController.DOWNGRADE_THRESHOLD) {
183
+ const lowerQuality = this.findLowerQuality(qualities, currentQuality);
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';
187
+ this.lastDecisionTime = now;
188
+ this.lastDowngradeTime = now;
189
+ this.selectQuality(lowerQuality.id);
190
+ return;
191
+ }
192
+ }
193
+ }
194
+
195
+ // D3: Check for upgrade (5s cooldown required)
196
+ if (now - this.lastUpgradeTime >= ABRController.UPGRADE_COOLDOWN_MS) {
197
+ const higherQuality = this.findHigherQuality(qualities, currentQuality);
198
+ if (higherQuality && this.isWithinConstraints(higherQuality)) {
199
+ const targetBitrate = higherQuality.bitrate || 0;
200
+
201
+ // D2: Hysteresis - require 1.5x headroom to upgrade
202
+ // Once at a quality level, stay until bandwidth drops below 1.2x (not 1.0x)
203
+ const shouldUpgrade = smoothedBandwidth >= targetBitrate * ABRController.UPGRADE_HEADROOM;
204
+ const canHoldHigher = smoothedBandwidth >= targetBitrate * ABRController.UPGRADE_HOLD_THRESHOLD;
205
+
206
+ 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';
209
+ this.lastDecisionTime = now;
210
+ this.lastUpgradeTime = now;
211
+ this.selectQuality(higherQuality.id);
212
+ return;
213
+ }
214
+ }
215
+ }
216
+ }
217
+
218
+ /**
219
+ * Get bandwidth estimate from player stats
220
+ */
221
+ private async getBandwidthEstimate(): Promise<number> {
222
+ // Try to get bandwidth from player stats
223
+ if (this.config.getBandwidthEstimate) {
224
+ const estimate = await this.config.getBandwidthEstimate();
225
+ if (estimate > 0) return estimate;
226
+ }
227
+
228
+ // Fallback: estimate from buffer growth (rough)
229
+ const video = this.videoElement;
230
+ if (!video || video.buffered.length === 0) return 0;
231
+
232
+ // This is a rough fallback - prefer player-specific estimates
233
+ return 0;
234
+ }
235
+
236
+ /**
237
+ * Get smoothed bandwidth from history
238
+ */
239
+ private getSmoothedBandwidth(): number {
240
+ if (this.bandwidthHistory.length === 0) return 0;
241
+ if (this.bandwidthHistory.length < 3) {
242
+ // Need at least 3 samples for reliable estimate
243
+ return 0;
244
+ }
245
+ const sum = this.bandwidthHistory.reduce((a, b) => a + b, 0);
246
+ return sum / this.bandwidthHistory.length;
247
+ }
248
+
249
+ /**
250
+ * Get current bandwidth estimate (for external use)
251
+ */
252
+ getCurrentBandwidth(): number {
253
+ return this.getSmoothedBandwidth();
254
+ }
255
+
256
+ /**
257
+ * Setup resize observer for viewport-based quality selection
258
+ */
259
+ private setupResizeObserver(): void {
260
+ const video = this.videoElement;
261
+ if (!video) return;
262
+
263
+ this.resizeObserver = new ResizeObserver((entries) => {
264
+ for (const entry of entries) {
265
+ const { width, height } = entry.contentRect;
266
+ this.handleResize(width, height);
267
+ }
268
+ });
269
+
270
+ // Observe the video element's container
271
+ const container = video.parentElement;
272
+ if (container) {
273
+ this.resizeObserver.observe(container);
274
+ }
275
+
276
+ // Initial resize handling
277
+ const rect = video.getBoundingClientRect();
278
+ this.handleResize(rect.width, rect.height);
279
+ }
280
+
281
+ /**
282
+ * Handle viewport resize (ABR_resize mode)
283
+ */
284
+ private handleResize(width: number, height: number): void {
285
+ if (this.options.mode !== 'resize' && this.options.mode !== 'auto') {
286
+ return;
287
+ }
288
+
289
+ const qualities = this.config.getQualities();
290
+ if (qualities.length === 0) return;
291
+
292
+ // Find best quality for viewport size
293
+ const targetWidth = Math.min(width * window.devicePixelRatio, this.options.maxResolution.width);
294
+ const targetHeight = Math.min(height * window.devicePixelRatio, this.options.maxResolution.height);
295
+
296
+ const bestQuality = this.findBestQualityForResolution(qualities, targetWidth, targetHeight);
297
+
298
+ if (bestQuality && bestQuality.id !== this.currentQualityId) {
299
+ this.log(`Resize ABR: ${width}x${height} -> selecting ${bestQuality.label}`);
300
+ this.selectQuality(bestQuality.id);
301
+ }
302
+ }
303
+
304
+ /**
305
+ * Handle quality degradation (ABR_bitrate mode)
306
+ *
307
+ * Called by QualityMonitor when playback quality drops
308
+ */
309
+ handleQualityDegraded(quality: PlaybackQuality): void {
310
+ if (this.options.mode !== 'bitrate' && this.options.mode !== 'auto') {
311
+ return;
312
+ }
313
+
314
+ // D3: Downgrade should be fast (0ms default), but still respects configured cooldown
315
+ const now = Date.now();
316
+ if (now - this.lastDowngradeTime < ABRController.DOWNGRADE_COOLDOWN_MS) {
317
+ return;
318
+ }
319
+
320
+ if (quality.score < this.options.downgradeThreshold) {
321
+ const qualities = this.config.getQualities();
322
+ const currentQuality = this.config.getCurrentQuality?.();
323
+
324
+ if (currentQuality) {
325
+ // Find a lower quality level
326
+ const lowerQuality = this.findLowerQuality(qualities, currentQuality);
327
+
328
+ if (lowerQuality) {
329
+ this.log(`Bitrate ABR: score ${quality.score} -> downgrading to ${lowerQuality.label}`);
330
+ this.lastDecision = 'downgrade';
331
+ this.lastDecisionTime = now;
332
+ this.lastDowngradeTime = now;
333
+ this.selectQuality(lowerQuality.id);
334
+ }
335
+ }
336
+ }
337
+ }
338
+
339
+ /**
340
+ * Handle quality improvement opportunity
341
+ *
342
+ * Called when conditions are good enough to try higher quality
343
+ */
344
+ handleQualityImproved(quality: PlaybackQuality): void {
345
+ if (this.options.mode !== 'bitrate' && this.options.mode !== 'auto') {
346
+ return;
347
+ }
348
+
349
+ // D3: Upgrade requires sustained stability (5s default) to prevent flapping
350
+ const now = Date.now();
351
+ if (now - this.lastUpgradeTime < ABRController.UPGRADE_COOLDOWN_MS) {
352
+ return;
353
+ }
354
+
355
+ // Only upgrade if buffer is healthy and quality is good
356
+ if (quality.score >= 90 && quality.bufferedAhead >= this.options.minBufferForUpgrade) {
357
+ const qualities = this.config.getQualities();
358
+ const currentQuality = this.config.getCurrentQuality?.();
359
+
360
+ if (currentQuality) {
361
+ // Find a higher quality level
362
+ const higherQuality = this.findHigherQuality(qualities, currentQuality);
363
+
364
+ if (higherQuality && this.isWithinConstraints(higherQuality)) {
365
+ this.log(`Bitrate ABR: score ${quality.score} -> upgrading to ${higherQuality.label}`);
366
+ this.lastDecision = 'upgrade';
367
+ this.lastDecisionTime = now;
368
+ this.lastUpgradeTime = now;
369
+ this.selectQuality(higherQuality.id);
370
+ }
371
+ }
372
+ }
373
+ }
374
+
375
+ /**
376
+ * Find best quality level for given resolution
377
+ */
378
+ private findBestQualityForResolution(
379
+ qualities: QualityLevel[],
380
+ targetWidth: number,
381
+ targetHeight: number
382
+ ): QualityLevel | null {
383
+ // Filter out qualities that exceed constraints
384
+ const validQualities = qualities.filter(q => this.isWithinConstraints(q));
385
+
386
+ if (validQualities.length === 0) return null;
387
+
388
+ // Sort by resolution (ascending)
389
+ const sorted = [...validQualities].sort((a, b) => {
390
+ const aPixels = (a.width ?? 0) * (a.height ?? 0);
391
+ const bPixels = (b.width ?? 0) * (b.height ?? 0);
392
+ return aPixels - bPixels;
393
+ });
394
+
395
+ // Find smallest quality that is >= target resolution
396
+ for (const q of sorted) {
397
+ const qWidth = q.width ?? 0;
398
+ const qHeight = q.height ?? 0;
399
+
400
+ if (qWidth >= targetWidth && qHeight >= targetHeight) {
401
+ return q;
402
+ }
403
+ }
404
+
405
+ // If no quality is large enough, return the highest available
406
+ return sorted[sorted.length - 1];
407
+ }
408
+
409
+ /**
410
+ * Find a lower quality level
411
+ */
412
+ private findLowerQuality(
413
+ qualities: QualityLevel[],
414
+ current: QualityLevel
415
+ ): QualityLevel | null {
416
+ const currentBitrate = current.bitrate ?? 0;
417
+
418
+ // Sort by bitrate descending
419
+ const sorted = [...qualities].sort((a, b) => (b.bitrate ?? 0) - (a.bitrate ?? 0));
420
+
421
+ // Find next lower bitrate
422
+ for (const q of sorted) {
423
+ if ((q.bitrate ?? 0) < currentBitrate) {
424
+ return q;
425
+ }
426
+ }
427
+
428
+ return null;
429
+ }
430
+
431
+ /**
432
+ * Find a higher quality level
433
+ */
434
+ private findHigherQuality(
435
+ qualities: QualityLevel[],
436
+ current: QualityLevel
437
+ ): QualityLevel | null {
438
+ const currentBitrate = current.bitrate ?? 0;
439
+
440
+ // Sort by bitrate ascending
441
+ const sorted = [...qualities].sort((a, b) => (a.bitrate ?? 0) - (b.bitrate ?? 0));
442
+
443
+ // Find next higher bitrate
444
+ for (const q of sorted) {
445
+ if ((q.bitrate ?? 0) > currentBitrate) {
446
+ return q;
447
+ }
448
+ }
449
+
450
+ return null;
451
+ }
452
+
453
+ /**
454
+ * Check if quality is within configured constraints
455
+ */
456
+ private isWithinConstraints(quality: QualityLevel): boolean {
457
+ const { maxResolution, maxBitrate } = this.options;
458
+
459
+ if (quality.width && quality.width > maxResolution.width) return false;
460
+ if (quality.height && quality.height > maxResolution.height) return false;
461
+ if (quality.bitrate && quality.bitrate > maxBitrate) return false;
462
+
463
+ return true;
464
+ }
465
+
466
+ /**
467
+ * Select a quality level
468
+ */
469
+ private selectQuality(id: string | 'auto'): void {
470
+ this.currentQualityId = id;
471
+ this.config.selectQuality(id);
472
+
473
+ // Notify callbacks
474
+ const qualities = this.config.getQualities();
475
+ const selected = qualities.find(q => q.id === id);
476
+ if (selected) {
477
+ this.qualityChangeCallbacks.forEach(cb => cb(selected));
478
+ }
479
+ }
480
+
481
+ /**
482
+ * Register callback for quality changes
483
+ */
484
+ onQualityChange(callback: (level: QualityLevel) => void): () => void {
485
+ this.qualityChangeCallbacks.push(callback);
486
+ return () => {
487
+ const idx = this.qualityChangeCallbacks.indexOf(callback);
488
+ if (idx >= 0) {
489
+ this.qualityChangeCallbacks.splice(idx, 1);
490
+ }
491
+ };
492
+ }
493
+
494
+ /**
495
+ * Manually set quality (switches to manual mode temporarily)
496
+ */
497
+ setQuality(id: string | 'auto'): void {
498
+ this.selectQuality(id);
499
+ }
500
+
501
+ /**
502
+ * Get current ABR mode
503
+ */
504
+ getMode(): ABRMode {
505
+ return this.options.mode;
506
+ }
507
+
508
+ /**
509
+ * Set ABR mode at runtime.
510
+ * Restarts monitoring if video element is attached.
511
+ */
512
+ setMode(mode: ABRMode): void {
513
+ if (this.options.mode === mode) return;
514
+
515
+ this.options.mode = mode;
516
+ this.log(`Mode changed to: ${mode}`);
517
+
518
+ // Restart with new mode if we have a video element
519
+ if (this.videoElement) {
520
+ const video = this.videoElement;
521
+ this.stop();
522
+ this.start(video);
523
+ }
524
+ }
525
+
526
+ /**
527
+ * Update ABR options
528
+ */
529
+ updateOptions(options: Partial<ABROptions>): void {
530
+ this.options = { ...this.options, ...options };
531
+ }
532
+
533
+ /**
534
+ * Get last ABR decision
535
+ */
536
+ getLastDecision(): ABRDecision {
537
+ return this.lastDecision;
538
+ }
539
+
540
+ /**
541
+ * Debug logging
542
+ */
543
+ private log(message: string): void {
544
+ if (this.debug) {
545
+ console.debug(`[ABRController] ${message}`);
546
+ }
547
+ }
548
+ }
549
+
550
+ export default ABRController;