@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,597 @@
|
|
|
1
|
+
import type { PlaybackQuality, QualityThresholds } from '../types';
|
|
2
|
+
import { TimerManager } from './TimerManager';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Default quality thresholds
|
|
6
|
+
*/
|
|
7
|
+
const DEFAULT_THRESHOLDS: QualityThresholds = {
|
|
8
|
+
minScore: 60,
|
|
9
|
+
maxStalls: 3,
|
|
10
|
+
minBuffer: 2,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Rolling average window size
|
|
15
|
+
*/
|
|
16
|
+
const ROLLING_WINDOW_SIZE = 20;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Playback score history entry (for MistPlayer-style 0-2.0 score)
|
|
20
|
+
*/
|
|
21
|
+
interface PlaybackScoreEntry {
|
|
22
|
+
clock: number; // Wall clock time in seconds
|
|
23
|
+
video: number; // Video currentTime
|
|
24
|
+
score: number; // Calculated score for this sample
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Protocol type for threshold selection */
|
|
28
|
+
export type PlayerProtocol = 'webrtc' | 'hls' | 'dash' | 'html5' | 'unknown';
|
|
29
|
+
|
|
30
|
+
/** Protocol-specific playback score thresholds (MistMetaPlayer reference) */
|
|
31
|
+
export const PROTOCOL_THRESHOLDS: Record<PlayerProtocol, number> = {
|
|
32
|
+
webrtc: 0.95, // Very strict for low-latency
|
|
33
|
+
hls: 0.75, // More lenient for adaptive streaming
|
|
34
|
+
dash: 0.75, // More lenient for adaptive streaming
|
|
35
|
+
html5: 0.75, // Standard threshold
|
|
36
|
+
unknown: 0.75, // Default
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export interface QualityMonitorOptions {
|
|
40
|
+
/** Sample interval in ms */
|
|
41
|
+
sampleInterval?: number;
|
|
42
|
+
/** Quality thresholds */
|
|
43
|
+
thresholds?: Partial<QualityThresholds>;
|
|
44
|
+
/** Callback when quality degrades */
|
|
45
|
+
onQualityDegraded?: (quality: PlaybackQuality) => void;
|
|
46
|
+
/** Callback on every sample */
|
|
47
|
+
onSample?: (quality: PlaybackQuality) => void;
|
|
48
|
+
/** Current player protocol for threshold selection */
|
|
49
|
+
protocol?: PlayerProtocol;
|
|
50
|
+
/** Custom playback score threshold (overrides protocol default) */
|
|
51
|
+
playbackScoreThreshold?: number;
|
|
52
|
+
/**
|
|
53
|
+
* Callback when sustained poor quality triggers a fallback request
|
|
54
|
+
* Reference: player.js:654-665 - "nextCombo" action
|
|
55
|
+
*/
|
|
56
|
+
onFallbackRequest?: (reason: { score: number; consecutivePoorSamples: number }) => void;
|
|
57
|
+
/**
|
|
58
|
+
* Number of consecutive poor samples before requesting fallback
|
|
59
|
+
* Default: 5 (2.5 seconds at 500ms sample interval)
|
|
60
|
+
*/
|
|
61
|
+
poorSamplesBeforeFallback?: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface QualityMonitorState {
|
|
65
|
+
isMonitoring: boolean;
|
|
66
|
+
quality: PlaybackQuality | null;
|
|
67
|
+
history: PlaybackQuality[];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* QualityMonitor - Tracks playback quality metrics
|
|
72
|
+
*
|
|
73
|
+
* Monitors:
|
|
74
|
+
* - Buffer health (seconds ahead)
|
|
75
|
+
* - Stall count (waiting events)
|
|
76
|
+
* - Frame drop rate (via video.getVideoPlaybackQuality())
|
|
77
|
+
* - Estimated bitrate
|
|
78
|
+
* - Latency (for live streams)
|
|
79
|
+
*
|
|
80
|
+
* Calculates a composite quality score (0-100) and triggers
|
|
81
|
+
* callbacks when quality degrades below thresholds.
|
|
82
|
+
*/
|
|
83
|
+
export class QualityMonitor {
|
|
84
|
+
private videoElement: HTMLVideoElement | null = null;
|
|
85
|
+
private options: Required<Omit<QualityMonitorOptions, 'protocol' | 'playbackScoreThreshold'>> & {
|
|
86
|
+
protocol: PlayerProtocol;
|
|
87
|
+
playbackScoreThreshold: number | null;
|
|
88
|
+
};
|
|
89
|
+
private thresholds: QualityThresholds;
|
|
90
|
+
private timers = new TimerManager();
|
|
91
|
+
private stallCount = 0;
|
|
92
|
+
private lastStallTime = 0;
|
|
93
|
+
private totalStallMs = 0;
|
|
94
|
+
private history: PlaybackQuality[] = [];
|
|
95
|
+
private lastBytesLoaded = 0;
|
|
96
|
+
private lastBytesTime = 0;
|
|
97
|
+
private listeners: Array<() => void> = [];
|
|
98
|
+
|
|
99
|
+
// MistPlayer-style playback score (0-2.0 scale)
|
|
100
|
+
private playbackScoreHistory: PlaybackScoreEntry[] = [];
|
|
101
|
+
private playbackScore = 1.0;
|
|
102
|
+
private readonly PLAYBACK_SCORE_AVERAGING_STEPS = 10;
|
|
103
|
+
|
|
104
|
+
// Automatic fallback tracking
|
|
105
|
+
// Reference: player.js:654-665 - triggers "nextCombo" after sustained poor quality
|
|
106
|
+
private consecutivePoorSamples = 0;
|
|
107
|
+
private fallbackTriggered = false;
|
|
108
|
+
|
|
109
|
+
constructor(options: QualityMonitorOptions = {}) {
|
|
110
|
+
this.options = {
|
|
111
|
+
sampleInterval: options.sampleInterval ?? 500,
|
|
112
|
+
thresholds: options.thresholds ?? {},
|
|
113
|
+
onQualityDegraded: options.onQualityDegraded ?? (() => {}),
|
|
114
|
+
onSample: options.onSample ?? (() => {}),
|
|
115
|
+
protocol: options.protocol ?? 'unknown',
|
|
116
|
+
playbackScoreThreshold: options.playbackScoreThreshold ?? null,
|
|
117
|
+
onFallbackRequest: options.onFallbackRequest ?? (() => {}),
|
|
118
|
+
poorSamplesBeforeFallback: options.poorSamplesBeforeFallback ?? 5,
|
|
119
|
+
};
|
|
120
|
+
this.thresholds = { ...DEFAULT_THRESHOLDS, ...options.thresholds };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Set the current player protocol for threshold selection
|
|
125
|
+
*/
|
|
126
|
+
setProtocol(protocol: PlayerProtocol): void {
|
|
127
|
+
this.options.protocol = protocol;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Get the current player protocol
|
|
132
|
+
*/
|
|
133
|
+
getProtocol(): PlayerProtocol {
|
|
134
|
+
return this.options.protocol;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Get the playback score threshold for the current protocol
|
|
139
|
+
*/
|
|
140
|
+
getPlaybackScoreThreshold(): number {
|
|
141
|
+
// Custom threshold takes precedence
|
|
142
|
+
if (this.options.playbackScoreThreshold !== null) {
|
|
143
|
+
return this.options.playbackScoreThreshold;
|
|
144
|
+
}
|
|
145
|
+
return PROTOCOL_THRESHOLDS[this.options.protocol];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Set a custom playback score threshold (overrides protocol default)
|
|
150
|
+
*/
|
|
151
|
+
setPlaybackScoreThreshold(threshold: number | null): void {
|
|
152
|
+
this.options.playbackScoreThreshold = threshold;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Start monitoring a video element
|
|
157
|
+
*/
|
|
158
|
+
start(videoElement: HTMLVideoElement): void {
|
|
159
|
+
this.stop();
|
|
160
|
+
|
|
161
|
+
this.videoElement = videoElement;
|
|
162
|
+
this.stallCount = 0;
|
|
163
|
+
this.totalStallMs = 0;
|
|
164
|
+
this.lastStallTime = 0;
|
|
165
|
+
this.history = [];
|
|
166
|
+
this.lastBytesLoaded = 0;
|
|
167
|
+
this.lastBytesTime = 0;
|
|
168
|
+
this.consecutivePoorSamples = 0;
|
|
169
|
+
this.fallbackTriggered = false;
|
|
170
|
+
this.playbackScoreHistory = [];
|
|
171
|
+
this.playbackScore = 1.0;
|
|
172
|
+
|
|
173
|
+
// Listen for stall events
|
|
174
|
+
const onWaiting = () => {
|
|
175
|
+
this.stallCount++;
|
|
176
|
+
this.lastStallTime = performance.now();
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const onPlaying = () => {
|
|
180
|
+
if (this.lastStallTime > 0) {
|
|
181
|
+
this.totalStallMs += performance.now() - this.lastStallTime;
|
|
182
|
+
this.lastStallTime = 0;
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const onCanPlay = () => {
|
|
187
|
+
if (this.lastStallTime > 0) {
|
|
188
|
+
this.totalStallMs += performance.now() - this.lastStallTime;
|
|
189
|
+
this.lastStallTime = 0;
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
videoElement.addEventListener('waiting', onWaiting);
|
|
194
|
+
videoElement.addEventListener('playing', onPlaying);
|
|
195
|
+
videoElement.addEventListener('canplay', onCanPlay);
|
|
196
|
+
|
|
197
|
+
this.listeners = [
|
|
198
|
+
() => videoElement.removeEventListener('waiting', onWaiting),
|
|
199
|
+
() => videoElement.removeEventListener('playing', onPlaying),
|
|
200
|
+
() => videoElement.removeEventListener('canplay', onCanPlay),
|
|
201
|
+
];
|
|
202
|
+
|
|
203
|
+
// Start sampling interval
|
|
204
|
+
this.timers.startInterval(() => this.sample(), this.options.sampleInterval, 'sampling');
|
|
205
|
+
|
|
206
|
+
// Take initial sample
|
|
207
|
+
this.sample();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Stop monitoring
|
|
212
|
+
*/
|
|
213
|
+
stop(): void {
|
|
214
|
+
this.timers.destroy();
|
|
215
|
+
|
|
216
|
+
this.listeners.forEach(cleanup => cleanup());
|
|
217
|
+
this.listeners = [];
|
|
218
|
+
|
|
219
|
+
this.videoElement = null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Take a quality sample
|
|
224
|
+
*/
|
|
225
|
+
private sample(): void {
|
|
226
|
+
const video = this.videoElement;
|
|
227
|
+
if (!video) return;
|
|
228
|
+
|
|
229
|
+
// Update MistPlayer-style playback score
|
|
230
|
+
this.updatePlaybackScore();
|
|
231
|
+
|
|
232
|
+
const quality = this.calculateQuality(video);
|
|
233
|
+
this.history.push(quality);
|
|
234
|
+
|
|
235
|
+
// Keep rolling window
|
|
236
|
+
if (this.history.length > ROLLING_WINDOW_SIZE) {
|
|
237
|
+
this.history.shift();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Notify listeners
|
|
241
|
+
this.options.onSample(quality);
|
|
242
|
+
|
|
243
|
+
// Check for quality degradation
|
|
244
|
+
if (quality.score < this.thresholds.minScore ||
|
|
245
|
+
quality.stallCount > this.thresholds.maxStalls ||
|
|
246
|
+
quality.bufferedAhead < this.thresholds.minBuffer) {
|
|
247
|
+
this.options.onQualityDegraded(quality);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Track sustained poor quality for automatic fallback
|
|
251
|
+
// Reference: player.js:654-665 - "nextCombo" after sustained poor playback
|
|
252
|
+
if (this.isPlaybackPoor()) {
|
|
253
|
+
this.consecutivePoorSamples++;
|
|
254
|
+
|
|
255
|
+
// Trigger fallback after sustained poor quality
|
|
256
|
+
// Only trigger once until quality improves or reset
|
|
257
|
+
if (!this.fallbackTriggered &&
|
|
258
|
+
this.consecutivePoorSamples >= this.options.poorSamplesBeforeFallback) {
|
|
259
|
+
this.fallbackTriggered = true;
|
|
260
|
+
console.warn(
|
|
261
|
+
`[QualityMonitor] Poor playback detected: ${Math.round(this.playbackScore * 100)}% ` +
|
|
262
|
+
`(threshold: ${Math.round(this.getPlaybackScoreThreshold() * 100)}%, ` +
|
|
263
|
+
`protocol: ${this.options.protocol})`
|
|
264
|
+
);
|
|
265
|
+
this.options.onFallbackRequest({
|
|
266
|
+
score: this.playbackScore,
|
|
267
|
+
consecutivePoorSamples: this.consecutivePoorSamples,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
} else {
|
|
271
|
+
// Quality recovered - reset counters
|
|
272
|
+
this.consecutivePoorSamples = 0;
|
|
273
|
+
this.fallbackTriggered = false;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Calculate current quality metrics
|
|
279
|
+
*/
|
|
280
|
+
private calculateQuality(video: HTMLVideoElement): PlaybackQuality {
|
|
281
|
+
const now = Date.now();
|
|
282
|
+
|
|
283
|
+
// Calculate buffered ahead
|
|
284
|
+
let bufferedAhead = 0;
|
|
285
|
+
if (video.buffered.length > 0) {
|
|
286
|
+
for (let i = 0; i < video.buffered.length; i++) {
|
|
287
|
+
if (video.buffered.start(i) <= video.currentTime && video.buffered.end(i) > video.currentTime) {
|
|
288
|
+
bufferedAhead = video.buffered.end(i) - video.currentTime;
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Get frame stats if available
|
|
295
|
+
let framesDecoded = 0;
|
|
296
|
+
let framesDropped = 0;
|
|
297
|
+
let frameDropRate = 0;
|
|
298
|
+
|
|
299
|
+
if ('getVideoPlaybackQuality' in video) {
|
|
300
|
+
const stats = video.getVideoPlaybackQuality();
|
|
301
|
+
framesDecoded = stats.totalVideoFrames;
|
|
302
|
+
framesDropped = stats.droppedVideoFrames;
|
|
303
|
+
frameDropRate = framesDecoded > 0 ? (framesDropped / framesDecoded) * 100 : 0;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Estimate bitrate from buffer loading
|
|
307
|
+
let bitrate = 0;
|
|
308
|
+
if (video.buffered.length > 0 && this.lastBytesTime > 0) {
|
|
309
|
+
const timeElapsed = (now - this.lastBytesTime) / 1000;
|
|
310
|
+
if (timeElapsed > 0) {
|
|
311
|
+
// Estimate from buffer growth
|
|
312
|
+
// This is a rough approximation - real bitrate tracking would use MSE
|
|
313
|
+
const bufferEnd = video.buffered.end(video.buffered.length - 1);
|
|
314
|
+
const bufferDuration = bufferEnd - video.currentTime;
|
|
315
|
+
// Assume average bitrate based on buffer size
|
|
316
|
+
bitrate = bufferDuration > 0 ? Math.round((bufferDuration * 1000000) / timeElapsed) : 0;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
this.lastBytesTime = now;
|
|
320
|
+
|
|
321
|
+
// Calculate latency for live streams
|
|
322
|
+
let latency = 0;
|
|
323
|
+
if (video.duration === Infinity || !isFinite(video.duration)) {
|
|
324
|
+
// Live stream - estimate latency from buffer
|
|
325
|
+
if (video.buffered.length > 0) {
|
|
326
|
+
const liveEdge = video.buffered.end(video.buffered.length - 1);
|
|
327
|
+
latency = (liveEdge - video.currentTime) * 1000;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Calculate composite quality score (0-100) with duration-weighted stalls
|
|
332
|
+
const score = this.calculateScore({
|
|
333
|
+
bufferedAhead,
|
|
334
|
+
stallCount: this.stallCount,
|
|
335
|
+
stallDurationMs: this.totalStallMs,
|
|
336
|
+
frameDropRate,
|
|
337
|
+
latency,
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
return {
|
|
341
|
+
score,
|
|
342
|
+
bitrate,
|
|
343
|
+
bufferedAhead,
|
|
344
|
+
stallCount: this.stallCount,
|
|
345
|
+
frameDropRate,
|
|
346
|
+
latency,
|
|
347
|
+
timestamp: now,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Calculate composite quality score
|
|
353
|
+
*
|
|
354
|
+
* D4: Duration-weighted stall tracking - stall penalty considers both
|
|
355
|
+
* count AND duration. 10x 0.1s stalls (1s total) weighs less than 1x 1s stall.
|
|
356
|
+
*/
|
|
357
|
+
private calculateScore(metrics: {
|
|
358
|
+
bufferedAhead: number;
|
|
359
|
+
stallCount: number;
|
|
360
|
+
stallDurationMs: number;
|
|
361
|
+
frameDropRate: number;
|
|
362
|
+
latency: number;
|
|
363
|
+
}): number {
|
|
364
|
+
let score = 100;
|
|
365
|
+
|
|
366
|
+
// Buffer penalty (max -40 points)
|
|
367
|
+
if (metrics.bufferedAhead < this.thresholds.minBuffer) {
|
|
368
|
+
const bufferPenalty = Math.min(40, (this.thresholds.minBuffer - metrics.bufferedAhead) * 20);
|
|
369
|
+
score -= bufferPenalty;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// D4: Duration-weighted stall penalty (max -30 points)
|
|
373
|
+
// Base: 5 points per stall + 2 points per second of total stall time
|
|
374
|
+
// This weights duration: 1x 2s stall = 5 + 4 = 9 points
|
|
375
|
+
// 10x 0.2s stalls = 50 + 4 = 54 points (capped at 30)
|
|
376
|
+
// So many short stalls are penalized more than few long stalls of same duration
|
|
377
|
+
const countPenalty = metrics.stallCount * 5;
|
|
378
|
+
const durationPenalty = (metrics.stallDurationMs / 1000) * 2;
|
|
379
|
+
const stallPenalty = Math.min(30, countPenalty + durationPenalty);
|
|
380
|
+
score -= stallPenalty;
|
|
381
|
+
|
|
382
|
+
// Frame drop penalty (max -20 points)
|
|
383
|
+
const framePenalty = Math.min(20, metrics.frameDropRate * 2);
|
|
384
|
+
score -= framePenalty;
|
|
385
|
+
|
|
386
|
+
// Latency penalty for live streams (max -10 points)
|
|
387
|
+
if (metrics.latency > 5000) {
|
|
388
|
+
const latencyPenalty = Math.min(10, (metrics.latency - 5000) / 1000);
|
|
389
|
+
score -= latencyPenalty;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return Math.max(0, Math.round(score));
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Get current quality metrics
|
|
397
|
+
*/
|
|
398
|
+
getCurrentQuality(): PlaybackQuality | null {
|
|
399
|
+
return this.history.length > 0 ? this.history[this.history.length - 1] : null;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Get rolling average quality
|
|
404
|
+
*/
|
|
405
|
+
getAverageQuality(): PlaybackQuality | null {
|
|
406
|
+
if (this.history.length === 0) return null;
|
|
407
|
+
|
|
408
|
+
const avg: PlaybackQuality = {
|
|
409
|
+
score: 0,
|
|
410
|
+
bitrate: 0,
|
|
411
|
+
bufferedAhead: 0,
|
|
412
|
+
stallCount: this.stallCount,
|
|
413
|
+
frameDropRate: 0,
|
|
414
|
+
latency: 0,
|
|
415
|
+
timestamp: Date.now(),
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
for (const q of this.history) {
|
|
419
|
+
avg.score += q.score;
|
|
420
|
+
avg.bitrate += q.bitrate;
|
|
421
|
+
avg.bufferedAhead += q.bufferedAhead;
|
|
422
|
+
avg.frameDropRate += q.frameDropRate;
|
|
423
|
+
avg.latency += q.latency;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const len = this.history.length;
|
|
427
|
+
avg.score = Math.round(avg.score / len);
|
|
428
|
+
avg.bitrate = Math.round(avg.bitrate / len);
|
|
429
|
+
avg.bufferedAhead = avg.bufferedAhead / len;
|
|
430
|
+
avg.frameDropRate = avg.frameDropRate / len;
|
|
431
|
+
avg.latency = avg.latency / len;
|
|
432
|
+
|
|
433
|
+
return avg;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Get quality history
|
|
438
|
+
*/
|
|
439
|
+
getHistory(): PlaybackQuality[] {
|
|
440
|
+
return [...this.history];
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Reset stall counters
|
|
445
|
+
*/
|
|
446
|
+
resetStallCounters(): void {
|
|
447
|
+
this.stallCount = 0;
|
|
448
|
+
this.totalStallMs = 0;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Get total stall time in ms
|
|
453
|
+
*/
|
|
454
|
+
getTotalStallMs(): number {
|
|
455
|
+
return this.totalStallMs;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Check if currently monitoring
|
|
460
|
+
*/
|
|
461
|
+
isMonitoring(): boolean {
|
|
462
|
+
return this.videoElement !== null && this.timers.activeCount > 0;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// ========================================
|
|
466
|
+
// MistPlayer-style Playback Score (0-2.0)
|
|
467
|
+
// ========================================
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Calculate playback score entry value
|
|
471
|
+
* Compares video time progress vs wall clock time
|
|
472
|
+
*/
|
|
473
|
+
private getPlaybackScoreValue(): PlaybackScoreEntry {
|
|
474
|
+
const video = this.videoElement;
|
|
475
|
+
const clock = performance.now() / 1000;
|
|
476
|
+
const videoTime = video?.currentTime ?? 0;
|
|
477
|
+
|
|
478
|
+
const result: PlaybackScoreEntry = {
|
|
479
|
+
clock,
|
|
480
|
+
video: videoTime,
|
|
481
|
+
score: 1.0,
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
if (this.playbackScoreHistory.length > 0) {
|
|
485
|
+
const prev = this.playbackScoreHistory[this.playbackScoreHistory.length - 1];
|
|
486
|
+
result.score = this.calculatePlaybackScoreFromEntries(prev, result);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
return result;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Calculate score between two entries
|
|
494
|
+
* Returns 1.0 for normal playback, >1.0 if faster, <1.0 if stalled, <0 if backwards
|
|
495
|
+
*/
|
|
496
|
+
private calculatePlaybackScoreFromEntries(a: PlaybackScoreEntry, b: PlaybackScoreEntry): number {
|
|
497
|
+
const video = this.videoElement;
|
|
498
|
+
let rate = 1;
|
|
499
|
+
if (video) {
|
|
500
|
+
rate = video.playbackRate || 1;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const clockDelta = b.clock - a.clock;
|
|
504
|
+
const videoDelta = b.video - a.video;
|
|
505
|
+
|
|
506
|
+
if (clockDelta <= 0) return 1.0;
|
|
507
|
+
|
|
508
|
+
return (videoDelta / clockDelta) / rate;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Calculate and update the playback score
|
|
513
|
+
* Like MistPlayer's calcScore function
|
|
514
|
+
*/
|
|
515
|
+
private updatePlaybackScore(): number {
|
|
516
|
+
const entry = this.getPlaybackScoreValue();
|
|
517
|
+
this.playbackScoreHistory.push(entry);
|
|
518
|
+
|
|
519
|
+
if (this.playbackScoreHistory.length <= 1) {
|
|
520
|
+
return 1.0;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Calculate score from oldest to newest
|
|
524
|
+
const first = this.playbackScoreHistory[0];
|
|
525
|
+
const last = this.playbackScoreHistory[this.playbackScoreHistory.length - 1];
|
|
526
|
+
let score = this.calculatePlaybackScoreFromEntries(first, last);
|
|
527
|
+
|
|
528
|
+
// Trim history
|
|
529
|
+
if (this.playbackScoreHistory.length > this.PLAYBACK_SCORE_AVERAGING_STEPS) {
|
|
530
|
+
this.playbackScoreHistory.shift();
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Final score is max of averaged and current
|
|
534
|
+
score = Math.max(score, entry.score);
|
|
535
|
+
this.playbackScore = score;
|
|
536
|
+
|
|
537
|
+
return score;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Get current playback score (MistPlayer-style 0-2.0 scale)
|
|
542
|
+
*
|
|
543
|
+
* - 1.0 = normal playback (video progresses at expected rate)
|
|
544
|
+
* - > 1.0 = faster than expected (catching up)
|
|
545
|
+
* - < 1.0 = slower than expected (stalling/buffering)
|
|
546
|
+
* - < 0 = video went backwards
|
|
547
|
+
*
|
|
548
|
+
* Threshold recommendations:
|
|
549
|
+
* - WebRTC: warn below 0.95
|
|
550
|
+
* - HLS/DASH: warn below 0.75
|
|
551
|
+
*/
|
|
552
|
+
getPlaybackScore(): number {
|
|
553
|
+
return this.playbackScore;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Check if playback quality is poor based on score
|
|
558
|
+
* Uses protocol-specific thresholds (MistPlayer-style)
|
|
559
|
+
* WebRTC: 0.95 (strict), HLS/DASH/HTML5: 0.75 (lenient)
|
|
560
|
+
*/
|
|
561
|
+
isPlaybackPoor(): boolean {
|
|
562
|
+
return this.playbackScore < this.getPlaybackScoreThreshold();
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Reset playback score tracking
|
|
567
|
+
*/
|
|
568
|
+
resetPlaybackScore(): void {
|
|
569
|
+
this.playbackScoreHistory = [];
|
|
570
|
+
this.playbackScore = 1.0;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Reset fallback state
|
|
575
|
+
* Call after a player switch to allow fallback to trigger again
|
|
576
|
+
*/
|
|
577
|
+
resetFallbackState(): void {
|
|
578
|
+
this.consecutivePoorSamples = 0;
|
|
579
|
+
this.fallbackTriggered = false;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Get consecutive poor sample count (for debugging)
|
|
584
|
+
*/
|
|
585
|
+
getConsecutivePoorSamples(): number {
|
|
586
|
+
return this.consecutivePoorSamples;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Check if fallback has been triggered (for debugging)
|
|
591
|
+
*/
|
|
592
|
+
hasFallbackTriggered(): boolean {
|
|
593
|
+
return this.fallbackTriggered;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
export default QualityMonitor;
|