@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,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JitterBuffer - Network Jitter Estimation
|
|
3
|
+
*
|
|
4
|
+
* Tracks network jitter to inform buffer sizing decisions.
|
|
5
|
+
* Ported from legacy rawws.js JitterTracker with improvements:
|
|
6
|
+
* - Per-track jitter tracking (audio/video can differ)
|
|
7
|
+
* - TypeScript types
|
|
8
|
+
* - Better edge case handling
|
|
9
|
+
*
|
|
10
|
+
* Algorithm:
|
|
11
|
+
* 1. Track arrival time vs media time for last N chunks
|
|
12
|
+
* 2. Calculate jitter = (mediaTimePassed / speed) - clockTimePassed
|
|
13
|
+
* 3. Maintain sliding window of peak jitter per second
|
|
14
|
+
* 4. Weighted average: (avgPeak + maxPeak * 2) / 3 + 1ms
|
|
15
|
+
* 5. Limit lowering rate to prevent oscillation
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { JitterState } from './types';
|
|
19
|
+
|
|
20
|
+
/** Default sliding window size for chunk tracking */
|
|
21
|
+
const DEFAULT_CHUNK_WINDOW = 8;
|
|
22
|
+
|
|
23
|
+
/** Default sliding window size for peak tracking */
|
|
24
|
+
const DEFAULT_PEAK_WINDOW = 8;
|
|
25
|
+
|
|
26
|
+
/** Interval between peak calculations (ms) */
|
|
27
|
+
const PEAK_INTERVAL_MS = 1000;
|
|
28
|
+
|
|
29
|
+
/** Maximum jitter decrease per interval (ms) */
|
|
30
|
+
const MAX_JITTER_DECREASE = 500;
|
|
31
|
+
|
|
32
|
+
/** Initial jitter estimate (ms) */
|
|
33
|
+
const INITIAL_JITTER = 120;
|
|
34
|
+
|
|
35
|
+
/** Minimum jitter floor (ms) */
|
|
36
|
+
const MIN_JITTER = 1;
|
|
37
|
+
|
|
38
|
+
interface ChunkTiming {
|
|
39
|
+
/** Wall clock time when chunk arrived (performance.now()) */
|
|
40
|
+
receiveTime: number;
|
|
41
|
+
/** Media timestamp from chunk (ms) */
|
|
42
|
+
mediaTime: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface JitterTrackerOptions {
|
|
46
|
+
/** Initial jitter estimate (ms) */
|
|
47
|
+
initialJitter?: number;
|
|
48
|
+
/** Sliding window size for chunks */
|
|
49
|
+
chunkWindowSize?: number;
|
|
50
|
+
/** Sliding window size for peaks */
|
|
51
|
+
peakWindowSize?: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* JitterTracker - Estimates network jitter for a single track
|
|
56
|
+
*/
|
|
57
|
+
export class JitterTracker {
|
|
58
|
+
/** Sliding window of chunk timings */
|
|
59
|
+
private chunks: ChunkTiming[] = [];
|
|
60
|
+
|
|
61
|
+
/** Current playback speed (1 = realtime) */
|
|
62
|
+
private speed = 1;
|
|
63
|
+
|
|
64
|
+
/** Last time a peak was recorded */
|
|
65
|
+
private lastPeakTime = 0;
|
|
66
|
+
|
|
67
|
+
/** Maximum jitter observed in current interval */
|
|
68
|
+
private currentPeak = 0;
|
|
69
|
+
|
|
70
|
+
/** Sliding window of peak jitter values */
|
|
71
|
+
private peaks: number[] = [];
|
|
72
|
+
|
|
73
|
+
/** Weighted average jitter estimate */
|
|
74
|
+
private maxJitter: number;
|
|
75
|
+
|
|
76
|
+
/** Configuration */
|
|
77
|
+
private readonly chunkWindowSize: number;
|
|
78
|
+
private readonly peakWindowSize: number;
|
|
79
|
+
|
|
80
|
+
constructor(options: JitterTrackerOptions = {}) {
|
|
81
|
+
this.maxJitter = options.initialJitter ?? INITIAL_JITTER;
|
|
82
|
+
this.chunkWindowSize = options.chunkWindowSize ?? DEFAULT_CHUNK_WINDOW;
|
|
83
|
+
this.peakWindowSize = options.peakWindowSize ?? DEFAULT_PEAK_WINDOW;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Add a received chunk to jitter calculation
|
|
88
|
+
*
|
|
89
|
+
* @param mediaTime - Media timestamp from chunk (ms)
|
|
90
|
+
* @param receiveTime - Wall clock time (performance.now())
|
|
91
|
+
*/
|
|
92
|
+
addChunk(mediaTime: number, receiveTime: number = performance.now()): void {
|
|
93
|
+
// Add to sliding window
|
|
94
|
+
this.chunks.push({ receiveTime, mediaTime });
|
|
95
|
+
if (this.chunks.length > this.chunkWindowSize) {
|
|
96
|
+
this.chunks.shift();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Calculate instantaneous jitter
|
|
100
|
+
const jitter = this.calculateJitter();
|
|
101
|
+
if (jitter > this.currentPeak) {
|
|
102
|
+
this.currentPeak = jitter;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Update peaks every second
|
|
106
|
+
const now = performance.now();
|
|
107
|
+
if (now > this.lastPeakTime + PEAK_INTERVAL_MS) {
|
|
108
|
+
this.recordPeak();
|
|
109
|
+
this.lastPeakTime = now;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Calculate current instantaneous jitter
|
|
115
|
+
*/
|
|
116
|
+
private calculateJitter(): number {
|
|
117
|
+
if (this.chunks.length <= 1) {
|
|
118
|
+
return 0;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Skip calculation during fast-forward
|
|
122
|
+
if (this.speed === 0 || !isFinite(this.speed)) {
|
|
123
|
+
return 0;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const oldest = this.chunks[0];
|
|
127
|
+
const newest = this.chunks[this.chunks.length - 1];
|
|
128
|
+
|
|
129
|
+
// Time passed on wall clock
|
|
130
|
+
const clockTimePassed = newest.receiveTime - oldest.receiveTime;
|
|
131
|
+
|
|
132
|
+
// Time that should have passed based on media timestamps
|
|
133
|
+
const mediaTimePassed = newest.mediaTime - oldest.mediaTime;
|
|
134
|
+
|
|
135
|
+
// Jitter = expected - actual
|
|
136
|
+
// Positive jitter means chunks arriving faster than expected (buffering)
|
|
137
|
+
// Negative jitter means chunks arriving slower than expected (starving)
|
|
138
|
+
const jitter = mediaTimePassed / this.speed - clockTimePassed;
|
|
139
|
+
|
|
140
|
+
return Math.max(0, jitter);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Record current peak and update weighted average
|
|
145
|
+
*/
|
|
146
|
+
private recordPeak(): void {
|
|
147
|
+
// Add current peak to sliding window
|
|
148
|
+
this.peaks.push(this.currentPeak);
|
|
149
|
+
if (this.peaks.length > this.peakWindowSize) {
|
|
150
|
+
this.peaks.shift();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Reset for next interval
|
|
154
|
+
this.currentPeak = 0;
|
|
155
|
+
|
|
156
|
+
// Calculate new weighted average
|
|
157
|
+
if (this.peaks.length > 0) {
|
|
158
|
+
const maxPeak = Math.max(...this.peaks);
|
|
159
|
+
const avgPeak =
|
|
160
|
+
this.peaks.reduce((sum, p) => sum + p, 0) / this.peaks.length;
|
|
161
|
+
|
|
162
|
+
// Weighted: emphasize max peak for safety
|
|
163
|
+
let weighted = (avgPeak + maxPeak * 2) / 3 + MIN_JITTER;
|
|
164
|
+
|
|
165
|
+
// Limit rate of decrease to prevent oscillation
|
|
166
|
+
if (this.maxJitter > weighted + MAX_JITTER_DECREASE) {
|
|
167
|
+
weighted = this.maxJitter - MAX_JITTER_DECREASE;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Smooth transition
|
|
171
|
+
this.maxJitter = (this.maxJitter + weighted) / 2;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Get current jitter estimate (ms)
|
|
177
|
+
*/
|
|
178
|
+
get(): number {
|
|
179
|
+
return this.maxJitter;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Get detailed jitter state
|
|
184
|
+
*/
|
|
185
|
+
getState(): JitterState {
|
|
186
|
+
return {
|
|
187
|
+
current: this.calculateJitter(),
|
|
188
|
+
peak: this.currentPeak,
|
|
189
|
+
weighted: this.maxJitter,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Set playback speed for jitter calculation
|
|
195
|
+
*/
|
|
196
|
+
setSpeed(speed: number | 'auto'): void {
|
|
197
|
+
const newSpeed = speed === 'auto' ? 1 : speed;
|
|
198
|
+
if (newSpeed !== this.speed) {
|
|
199
|
+
this.speed = newSpeed;
|
|
200
|
+
this.reset();
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Reset jitter tracking (e.g., after seek)
|
|
206
|
+
*/
|
|
207
|
+
reset(): void {
|
|
208
|
+
this.chunks = [];
|
|
209
|
+
this.currentPeak = 0;
|
|
210
|
+
// Don't reset maxJitter - keep the learned estimate
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Full reset including learned jitter estimate
|
|
215
|
+
*/
|
|
216
|
+
fullReset(): void {
|
|
217
|
+
this.reset();
|
|
218
|
+
this.peaks = [];
|
|
219
|
+
this.maxJitter = INITIAL_JITTER;
|
|
220
|
+
this.lastPeakTime = 0;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* MultiTrackJitterTracker - Manages jitter tracking for multiple tracks
|
|
226
|
+
*/
|
|
227
|
+
export class MultiTrackJitterTracker {
|
|
228
|
+
private trackers = new Map<number, JitterTracker>();
|
|
229
|
+
private globalSpeed = 1;
|
|
230
|
+
private options: JitterTrackerOptions;
|
|
231
|
+
|
|
232
|
+
constructor(options: JitterTrackerOptions = {}) {
|
|
233
|
+
this.options = options;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Add a chunk for a specific track
|
|
238
|
+
*/
|
|
239
|
+
addChunk(trackIndex: number, mediaTime: number, receiveTime?: number): void {
|
|
240
|
+
let tracker = this.trackers.get(trackIndex);
|
|
241
|
+
if (!tracker) {
|
|
242
|
+
tracker = new JitterTracker(this.options);
|
|
243
|
+
tracker.setSpeed(this.globalSpeed);
|
|
244
|
+
this.trackers.set(trackIndex, tracker);
|
|
245
|
+
}
|
|
246
|
+
tracker.addChunk(mediaTime, receiveTime);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Get maximum jitter across all tracks
|
|
251
|
+
*/
|
|
252
|
+
getMax(): number {
|
|
253
|
+
let max = 0;
|
|
254
|
+
for (const tracker of this.trackers.values()) {
|
|
255
|
+
max = Math.max(max, tracker.get());
|
|
256
|
+
}
|
|
257
|
+
return max;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Get jitter for a specific track
|
|
262
|
+
*/
|
|
263
|
+
getForTrack(trackIndex: number): number {
|
|
264
|
+
return this.trackers.get(trackIndex)?.get() ?? INITIAL_JITTER;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Set playback speed for all trackers
|
|
269
|
+
*/
|
|
270
|
+
setSpeed(speed: number | 'auto'): void {
|
|
271
|
+
this.globalSpeed = speed === 'auto' ? 1 : speed;
|
|
272
|
+
for (const tracker of this.trackers.values()) {
|
|
273
|
+
tracker.setSpeed(speed);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Reset all trackers
|
|
279
|
+
*/
|
|
280
|
+
reset(): void {
|
|
281
|
+
for (const tracker of this.trackers.values()) {
|
|
282
|
+
tracker.reset();
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Remove a track's tracker
|
|
288
|
+
*/
|
|
289
|
+
removeTrack(trackIndex: number): void {
|
|
290
|
+
this.trackers.delete(trackIndex);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Clear all trackers
|
|
295
|
+
*/
|
|
296
|
+
clear(): void {
|
|
297
|
+
this.trackers.clear();
|
|
298
|
+
}
|
|
299
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Latency Profiles for WebCodecs Player
|
|
3
|
+
*
|
|
4
|
+
* Presets for trading off latency vs stability.
|
|
5
|
+
*
|
|
6
|
+
* Buffer calculation: desiredBuffer = keepAway + serverDelay + (jitter * jitterMultiplier)
|
|
7
|
+
*
|
|
8
|
+
* Speed tweaking:
|
|
9
|
+
* - If buffer > desired * speedUpThreshold → speed up to maxSpeedUp
|
|
10
|
+
* - If buffer < desired * speedDownThreshold → slow down to minSpeedDown
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { LatencyProfile, LatencyProfileName } from './types';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Ultra-low latency profile
|
|
17
|
+
* Target: <200ms end-to-end latency
|
|
18
|
+
* Use case: Real-time conferencing, remote control
|
|
19
|
+
* Trade-offs: May stutter on poor networks
|
|
20
|
+
*/
|
|
21
|
+
export const ULTRA_LOW_PROFILE: LatencyProfile = {
|
|
22
|
+
name: 'Ultra Low Latency',
|
|
23
|
+
keepAway: 50, // 50ms base buffer
|
|
24
|
+
jitterMultiplier: 1.0, // Full jitter protection (no extra margin)
|
|
25
|
+
speedUpThreshold: 1.5, // Speed up when buffer > 150% of desired
|
|
26
|
+
speedDownThreshold: 0.5, // Slow down when buffer < 50% of desired
|
|
27
|
+
maxSpeedUp: 1.08, // 8% speed up max
|
|
28
|
+
minSpeedDown: 0.95, // 5% slow down max
|
|
29
|
+
audioBufferMs: 100, // 100ms audio ring buffer
|
|
30
|
+
optimizeForLatency: true, // Tell decoders to optimize for latency
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Low latency profile
|
|
35
|
+
* Target: ~300-500ms end-to-end latency
|
|
36
|
+
* Use case: Live sports, gaming streams
|
|
37
|
+
* Trade-offs: Balanced latency/stability
|
|
38
|
+
*/
|
|
39
|
+
export const LOW_PROFILE: LatencyProfile = {
|
|
40
|
+
name: 'Low Latency',
|
|
41
|
+
keepAway: 100, // 100ms base buffer
|
|
42
|
+
jitterMultiplier: 1.2, // 20% extra jitter protection
|
|
43
|
+
speedUpThreshold: 2.0, // Speed up when buffer > 200% of desired
|
|
44
|
+
speedDownThreshold: 0.6, // Slow down when buffer < 60% of desired
|
|
45
|
+
maxSpeedUp: 1.05, // 5% speed up max (legacy default)
|
|
46
|
+
minSpeedDown: 0.98, // 2% slow down max (legacy default)
|
|
47
|
+
audioBufferMs: 150, // 150ms audio ring buffer
|
|
48
|
+
optimizeForLatency: true,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Balanced profile
|
|
53
|
+
* Target: ~500-1000ms end-to-end latency
|
|
54
|
+
* Use case: General live streaming
|
|
55
|
+
* Trade-offs: Prioritizes stability over latency
|
|
56
|
+
*/
|
|
57
|
+
export const BALANCED_PROFILE: LatencyProfile = {
|
|
58
|
+
name: 'Balanced',
|
|
59
|
+
keepAway: 200, // 200ms base buffer
|
|
60
|
+
jitterMultiplier: 1.5, // 50% extra jitter protection
|
|
61
|
+
speedUpThreshold: 2.5, // Speed up when buffer > 250% of desired
|
|
62
|
+
speedDownThreshold: 0.5, // Slow down when buffer < 50% of desired
|
|
63
|
+
maxSpeedUp: 1.03, // 3% speed up max
|
|
64
|
+
minSpeedDown: 0.97, // 3% slow down max
|
|
65
|
+
audioBufferMs: 200, // 200ms audio ring buffer
|
|
66
|
+
optimizeForLatency: false, // Let decoders optimize for quality
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Quality priority profile
|
|
71
|
+
* Target: ~1-2s end-to-end latency
|
|
72
|
+
* Use case: VOD, recorded content, poor networks
|
|
73
|
+
* Trade-offs: Maximum stability, higher latency
|
|
74
|
+
*/
|
|
75
|
+
export const QUALITY_PROFILE: LatencyProfile = {
|
|
76
|
+
name: 'Quality Priority',
|
|
77
|
+
keepAway: 500, // 500ms base buffer (legacy VOD default)
|
|
78
|
+
jitterMultiplier: 2.0, // Double jitter protection
|
|
79
|
+
speedUpThreshold: 3.0, // Speed up when buffer > 300% of desired
|
|
80
|
+
speedDownThreshold: 0.4, // Slow down when buffer < 40% of desired
|
|
81
|
+
maxSpeedUp: 1.02, // 2% speed up max
|
|
82
|
+
minSpeedDown: 0.98, // 2% slow down max
|
|
83
|
+
audioBufferMs: 300, // 300ms audio ring buffer
|
|
84
|
+
optimizeForLatency: false,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* All available latency profiles
|
|
89
|
+
*/
|
|
90
|
+
export const LATENCY_PROFILES: Record<LatencyProfileName, LatencyProfile> = {
|
|
91
|
+
'ultra-low': ULTRA_LOW_PROFILE,
|
|
92
|
+
'low': LOW_PROFILE,
|
|
93
|
+
'balanced': BALANCED_PROFILE,
|
|
94
|
+
'quality': QUALITY_PROFILE,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Get a latency profile by name
|
|
99
|
+
* @param name - Profile name
|
|
100
|
+
* @returns The profile, or 'low' as default
|
|
101
|
+
*/
|
|
102
|
+
export function getLatencyProfile(name?: LatencyProfileName): LatencyProfile {
|
|
103
|
+
if (name && name in LATENCY_PROFILES) {
|
|
104
|
+
return LATENCY_PROFILES[name];
|
|
105
|
+
}
|
|
106
|
+
return LATENCY_PROFILES['low'];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Merge a custom partial profile with a base profile
|
|
111
|
+
* @param base - Base profile name or profile object
|
|
112
|
+
* @param custom - Partial overrides
|
|
113
|
+
* @returns Merged profile
|
|
114
|
+
*/
|
|
115
|
+
export function mergeLatencyProfile(
|
|
116
|
+
base: LatencyProfileName | LatencyProfile,
|
|
117
|
+
custom?: Partial<LatencyProfile>
|
|
118
|
+
): LatencyProfile {
|
|
119
|
+
const baseProfile = typeof base === 'string' ? getLatencyProfile(base) : base;
|
|
120
|
+
|
|
121
|
+
if (!custom) {
|
|
122
|
+
return baseProfile;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
...baseProfile,
|
|
127
|
+
...custom,
|
|
128
|
+
name: custom.name ?? `${baseProfile.name} (Custom)`,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Select appropriate profile based on stream type
|
|
134
|
+
* @param isLive - Whether the stream is live
|
|
135
|
+
* @param preferLowLatency - Whether to prefer low latency (e.g., WebRTC source)
|
|
136
|
+
* @returns Recommended profile name
|
|
137
|
+
*/
|
|
138
|
+
export function selectDefaultProfile(
|
|
139
|
+
isLive: boolean,
|
|
140
|
+
preferLowLatency = false
|
|
141
|
+
): LatencyProfileName {
|
|
142
|
+
if (!isLive) {
|
|
143
|
+
return 'quality';
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (preferLowLatency) {
|
|
147
|
+
return 'low';
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return 'balanced';
|
|
151
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RawChunkParser - Binary Frame Header Parser
|
|
3
|
+
*
|
|
4
|
+
* Parses the 12-byte binary header from MistServer's raw WebSocket stream.
|
|
5
|
+
*
|
|
6
|
+
* Header format:
|
|
7
|
+
* Byte 0: Track index (uint8)
|
|
8
|
+
* Byte 1: Chunk type: 0=delta, 1=key, 2=init
|
|
9
|
+
* Bytes 2-9: Timestamp in milliseconds (uint64 big-endian)
|
|
10
|
+
* Bytes 10-11: Offset in milliseconds (int16 big-endian, signed)
|
|
11
|
+
*
|
|
12
|
+
* The offset is server-calculated and used for A/V synchronization.
|
|
13
|
+
* Combined presentation time = timestamp + offset
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { RawChunk, ChunkType } from './types';
|
|
17
|
+
|
|
18
|
+
const HEADER_LENGTH = 12;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Parse chunk type byte to string
|
|
22
|
+
*/
|
|
23
|
+
function parseChunkType(typeByte: number): ChunkType {
|
|
24
|
+
switch (typeByte) {
|
|
25
|
+
case 1:
|
|
26
|
+
return 'key';
|
|
27
|
+
case 2:
|
|
28
|
+
return 'init';
|
|
29
|
+
default:
|
|
30
|
+
return 'delta';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Parse a raw binary chunk from MistServer
|
|
36
|
+
*
|
|
37
|
+
* @param data - ArrayBuffer containing header + frame data
|
|
38
|
+
* @returns Parsed RawChunk object
|
|
39
|
+
* @throws Error if data is too short
|
|
40
|
+
*/
|
|
41
|
+
export function parseRawChunk(data: ArrayBuffer): RawChunk {
|
|
42
|
+
if (data.byteLength < HEADER_LENGTH) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`Invalid chunk: expected at least ${HEADER_LENGTH} bytes, got ${data.byteLength}`
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const headerView = new DataView(data, 0, HEADER_LENGTH);
|
|
49
|
+
|
|
50
|
+
// Byte 0: track index
|
|
51
|
+
const trackIndex = headerView.getUint8(0);
|
|
52
|
+
|
|
53
|
+
// Byte 1: chunk type
|
|
54
|
+
const type = parseChunkType(headerView.getUint8(1));
|
|
55
|
+
|
|
56
|
+
// Bytes 2-9: timestamp (uint64 big-endian)
|
|
57
|
+
// getBigUint64 returns BigInt, convert to Number (safe for values < 2^53)
|
|
58
|
+
const timestampBig = headerView.getBigUint64(2, false); // false = big-endian
|
|
59
|
+
const timestamp = Number(timestampBig);
|
|
60
|
+
|
|
61
|
+
// Bytes 10-11: offset (int16 big-endian, signed)
|
|
62
|
+
const offset = headerView.getInt16(10, false); // false = big-endian
|
|
63
|
+
|
|
64
|
+
// Remaining bytes: actual frame data
|
|
65
|
+
// Use slice() to create a copy (like reference rawws.js line 462)
|
|
66
|
+
// This ensures the data survives buffer transfers
|
|
67
|
+
const frameData = new Uint8Array(data.slice(HEADER_LENGTH));
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
trackIndex,
|
|
71
|
+
type,
|
|
72
|
+
timestamp,
|
|
73
|
+
offset,
|
|
74
|
+
data: frameData,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Calculate the presentation timestamp for a chunk
|
|
80
|
+
* This combines the server timestamp with the sync offset
|
|
81
|
+
*
|
|
82
|
+
* @param chunk - Parsed raw chunk
|
|
83
|
+
* @returns Presentation timestamp in microseconds (for WebCodecs API)
|
|
84
|
+
*/
|
|
85
|
+
export function getPresentationTimestamp(chunk: RawChunk): number {
|
|
86
|
+
// timestamp and offset are in milliseconds
|
|
87
|
+
// WebCodecs expects microseconds
|
|
88
|
+
return (chunk.timestamp + chunk.offset) * 1000;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Check if this chunk is a keyframe
|
|
93
|
+
*/
|
|
94
|
+
export function isKeyframe(chunk: RawChunk): boolean {
|
|
95
|
+
return chunk.type === 'key';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Check if this chunk contains codec initialization data
|
|
100
|
+
*/
|
|
101
|
+
export function isInitData(chunk: RawChunk): boolean {
|
|
102
|
+
return chunk.type === 'init';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Format chunk for debug logging
|
|
107
|
+
*/
|
|
108
|
+
export function formatChunkForLog(chunk: RawChunk): string {
|
|
109
|
+
const pts = (chunk.timestamp + chunk.offset) / 1000; // seconds
|
|
110
|
+
const ptsFormatted = pts.toFixed(3);
|
|
111
|
+
return `[Track ${chunk.trackIndex}] ${chunk.type.toUpperCase()} @ ${ptsFormatted}s (${chunk.data.byteLength} bytes)`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* RawChunkParser class for stateful parsing with validation
|
|
116
|
+
*/
|
|
117
|
+
export class RawChunkParser {
|
|
118
|
+
private debug: boolean;
|
|
119
|
+
|
|
120
|
+
constructor(options: { debug?: boolean } = {}) {
|
|
121
|
+
this.debug = options.debug ?? false;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Parse binary data from WebSocket
|
|
126
|
+
*
|
|
127
|
+
* @param data - ArrayBuffer from WebSocket message
|
|
128
|
+
* @returns Parsed chunk or null if invalid
|
|
129
|
+
*/
|
|
130
|
+
parse(data: ArrayBuffer): RawChunk | null {
|
|
131
|
+
try {
|
|
132
|
+
const chunk = parseRawChunk(data);
|
|
133
|
+
|
|
134
|
+
if (this.debug) {
|
|
135
|
+
console.log('▶️', formatChunkForLog(chunk));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return chunk;
|
|
139
|
+
} catch (err) {
|
|
140
|
+
console.error('▶️ Failed to parse chunk:', err);
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Set debug mode
|
|
147
|
+
*/
|
|
148
|
+
setDebug(enabled: boolean | 'verbose'): void {
|
|
149
|
+
this.debug = enabled === 'verbose' || enabled === true;
|
|
150
|
+
}
|
|
151
|
+
}
|