@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.
- package/dist/cjs/index.js +19493 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/esm/index.js +19398 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/player.css +2140 -0
- package/dist/types/core/ABRController.d.ts +164 -0
- package/dist/types/core/CodecUtils.d.ts +54 -0
- package/dist/types/core/Disposable.d.ts +61 -0
- package/dist/types/core/EventEmitter.d.ts +73 -0
- package/dist/types/core/GatewayClient.d.ts +144 -0
- package/dist/types/core/InteractionController.d.ts +121 -0
- package/dist/types/core/LiveDurationProxy.d.ts +102 -0
- package/dist/types/core/MetaTrackManager.d.ts +220 -0
- package/dist/types/core/MistReporter.d.ts +163 -0
- package/dist/types/core/MistSignaling.d.ts +148 -0
- package/dist/types/core/PlayerController.d.ts +665 -0
- package/dist/types/core/PlayerInterface.d.ts +230 -0
- package/dist/types/core/PlayerManager.d.ts +182 -0
- package/dist/types/core/PlayerRegistry.d.ts +27 -0
- package/dist/types/core/QualityMonitor.d.ts +184 -0
- package/dist/types/core/ScreenWakeLockManager.d.ts +70 -0
- package/dist/types/core/SeekingUtils.d.ts +142 -0
- package/dist/types/core/StreamStateClient.d.ts +108 -0
- package/dist/types/core/SubtitleManager.d.ts +111 -0
- package/dist/types/core/TelemetryReporter.d.ts +79 -0
- package/dist/types/core/TimeFormat.d.ts +97 -0
- package/dist/types/core/TimerManager.d.ts +83 -0
- package/dist/types/core/UrlUtils.d.ts +81 -0
- package/dist/types/core/detector.d.ts +149 -0
- package/dist/types/core/index.d.ts +49 -0
- package/dist/types/core/scorer.d.ts +167 -0
- package/dist/types/core/selector.d.ts +9 -0
- package/dist/types/index.d.ts +45 -0
- package/dist/types/lib/utils.d.ts +2 -0
- package/dist/types/players/DashJsPlayer.d.ts +102 -0
- package/dist/types/players/HlsJsPlayer.d.ts +70 -0
- package/dist/types/players/MewsWsPlayer/SourceBufferManager.d.ts +119 -0
- package/dist/types/players/MewsWsPlayer/WebSocketManager.d.ts +60 -0
- package/dist/types/players/MewsWsPlayer/index.d.ts +220 -0
- package/dist/types/players/MewsWsPlayer/types.d.ts +89 -0
- package/dist/types/players/MistPlayer.d.ts +25 -0
- package/dist/types/players/MistWebRTCPlayer/index.d.ts +133 -0
- package/dist/types/players/NativePlayer.d.ts +143 -0
- package/dist/types/players/VideoJsPlayer.d.ts +59 -0
- package/dist/types/players/WebCodecsPlayer/JitterBuffer.d.ts +118 -0
- package/dist/types/players/WebCodecsPlayer/LatencyProfiles.d.ts +64 -0
- package/dist/types/players/WebCodecsPlayer/RawChunkParser.d.ts +63 -0
- package/dist/types/players/WebCodecsPlayer/SyncController.d.ts +174 -0
- package/dist/types/players/WebCodecsPlayer/WebSocketController.d.ts +164 -0
- package/dist/types/players/WebCodecsPlayer/index.d.ts +149 -0
- package/dist/types/players/WebCodecsPlayer/polyfills/MediaStreamTrackGenerator.d.ts +105 -0
- package/dist/types/players/WebCodecsPlayer/types.d.ts +395 -0
- package/dist/types/players/WebCodecsPlayer/worker/decoder.worker.d.ts +13 -0
- package/dist/types/players/WebCodecsPlayer/worker/types.d.ts +197 -0
- package/dist/types/players/index.d.ts +14 -0
- package/dist/types/styles/index.d.ts +11 -0
- package/dist/types/types.d.ts +363 -0
- package/dist/types/vanilla/FrameWorksPlayer.d.ts +143 -0
- package/dist/types/vanilla/index.d.ts +19 -0
- package/dist/workers/decoder.worker.js +989 -0
- package/dist/workers/decoder.worker.js.map +1 -0
- package/package.json +80 -0
- package/src/core/ABRController.ts +550 -0
- package/src/core/CodecUtils.ts +257 -0
- package/src/core/Disposable.ts +120 -0
- package/src/core/EventEmitter.ts +113 -0
- package/src/core/GatewayClient.ts +439 -0
- package/src/core/InteractionController.ts +712 -0
- package/src/core/LiveDurationProxy.ts +270 -0
- package/src/core/MetaTrackManager.ts +753 -0
- package/src/core/MistReporter.ts +543 -0
- package/src/core/MistSignaling.ts +346 -0
- package/src/core/PlayerController.ts +2829 -0
- package/src/core/PlayerInterface.ts +432 -0
- package/src/core/PlayerManager.ts +900 -0
- package/src/core/PlayerRegistry.ts +149 -0
- package/src/core/QualityMonitor.ts +597 -0
- package/src/core/ScreenWakeLockManager.ts +163 -0
- package/src/core/SeekingUtils.ts +364 -0
- package/src/core/StreamStateClient.ts +457 -0
- package/src/core/SubtitleManager.ts +297 -0
- package/src/core/TelemetryReporter.ts +308 -0
- package/src/core/TimeFormat.ts +205 -0
- package/src/core/TimerManager.ts +209 -0
- package/src/core/UrlUtils.ts +179 -0
- package/src/core/detector.ts +382 -0
- package/src/core/index.ts +140 -0
- package/src/core/scorer.ts +553 -0
- package/src/core/selector.ts +16 -0
- package/src/global.d.ts +11 -0
- package/src/index.ts +75 -0
- package/src/lib/utils.ts +6 -0
- package/src/players/DashJsPlayer.ts +642 -0
- package/src/players/HlsJsPlayer.ts +483 -0
- package/src/players/MewsWsPlayer/SourceBufferManager.ts +572 -0
- package/src/players/MewsWsPlayer/WebSocketManager.ts +241 -0
- package/src/players/MewsWsPlayer/index.ts +1065 -0
- package/src/players/MewsWsPlayer/types.ts +106 -0
- package/src/players/MistPlayer.ts +188 -0
- package/src/players/MistWebRTCPlayer/index.ts +703 -0
- package/src/players/NativePlayer.ts +820 -0
- package/src/players/VideoJsPlayer.ts +643 -0
- package/src/players/WebCodecsPlayer/JitterBuffer.ts +299 -0
- package/src/players/WebCodecsPlayer/LatencyProfiles.ts +151 -0
- package/src/players/WebCodecsPlayer/RawChunkParser.ts +151 -0
- package/src/players/WebCodecsPlayer/SyncController.ts +456 -0
- package/src/players/WebCodecsPlayer/WebSocketController.ts +564 -0
- package/src/players/WebCodecsPlayer/index.ts +1650 -0
- package/src/players/WebCodecsPlayer/polyfills/MediaStreamTrackGenerator.ts +379 -0
- package/src/players/WebCodecsPlayer/types.ts +542 -0
- package/src/players/WebCodecsPlayer/worker/decoder.worker.ts +1360 -0
- package/src/players/WebCodecsPlayer/worker/types.ts +276 -0
- package/src/players/index.ts +22 -0
- package/src/styles/animations.css +21 -0
- package/src/styles/index.ts +52 -0
- package/src/styles/player.css +2126 -0
- package/src/styles/tailwind.css +1015 -0
- package/src/types.ts +421 -0
- package/src/vanilla/FrameWorksPlayer.ts +367 -0
- 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;
|