@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,564 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocketController - Control Channel & Binary Frame Routing
|
|
3
|
+
*
|
|
4
|
+
* Manages WebSocket connection to MistServer for raw frame streaming.
|
|
5
|
+
* Handles:
|
|
6
|
+
* - JSON control messages (play, hold, seek, etc.)
|
|
7
|
+
* - Binary frame routing to chunk parser
|
|
8
|
+
* - Reconnection with exponential backoff
|
|
9
|
+
* - Server delay estimation
|
|
10
|
+
*
|
|
11
|
+
* Based on MewsWsPlayer WebSocketManager with raw frame protocol support.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type {
|
|
15
|
+
ControlMessage,
|
|
16
|
+
ControlCommand,
|
|
17
|
+
CodecDataMessage,
|
|
18
|
+
InfoMessage,
|
|
19
|
+
OnTimeMessage,
|
|
20
|
+
RawChunk,
|
|
21
|
+
TrackInfo,
|
|
22
|
+
} from './types';
|
|
23
|
+
import { parseRawChunk, formatChunkForLog } from './RawChunkParser';
|
|
24
|
+
|
|
25
|
+
/** Connection states */
|
|
26
|
+
export type ConnectionState = 'disconnected' | 'connecting' | 'connected' | 'reconnecting' | 'error';
|
|
27
|
+
|
|
28
|
+
/** Event types emitted by WebSocketController */
|
|
29
|
+
export interface WebSocketControllerEvents {
|
|
30
|
+
statechange: ConnectionState;
|
|
31
|
+
codecdata: CodecDataMessage;
|
|
32
|
+
info: InfoMessage;
|
|
33
|
+
ontime: OnTimeMessage;
|
|
34
|
+
tracks: TrackInfo[];
|
|
35
|
+
chunk: RawChunk;
|
|
36
|
+
stop: void;
|
|
37
|
+
error: Error;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
type EventListener<K extends keyof WebSocketControllerEvents> = (
|
|
41
|
+
data: WebSocketControllerEvents[K]
|
|
42
|
+
) => void;
|
|
43
|
+
|
|
44
|
+
/** Options for WebSocketController */
|
|
45
|
+
export interface WebSocketControllerOptions {
|
|
46
|
+
/** Enable debug logging */
|
|
47
|
+
debug?: boolean;
|
|
48
|
+
/** Maximum reconnection attempts (0 = unlimited) */
|
|
49
|
+
maxReconnectAttempts?: number;
|
|
50
|
+
/** Initial reconnection delay (ms) */
|
|
51
|
+
reconnectDelayMs?: number;
|
|
52
|
+
/** Maximum reconnection delay (ms) */
|
|
53
|
+
maxReconnectDelayMs?: number;
|
|
54
|
+
/** Connection timeout (ms) */
|
|
55
|
+
connectionTimeoutMs?: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Default options */
|
|
59
|
+
const DEFAULTS: Required<WebSocketControllerOptions> = {
|
|
60
|
+
debug: false,
|
|
61
|
+
maxReconnectAttempts: 5,
|
|
62
|
+
reconnectDelayMs: 1000,
|
|
63
|
+
maxReconnectDelayMs: 30000,
|
|
64
|
+
connectionTimeoutMs: 5000,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Server delay tracker for estimating round-trip time
|
|
69
|
+
*/
|
|
70
|
+
class ServerDelayTracker {
|
|
71
|
+
private delays: number[] = [];
|
|
72
|
+
private pending = new Map<string, number>();
|
|
73
|
+
private maxSamples = 3;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Start timing a request
|
|
77
|
+
*/
|
|
78
|
+
startTiming(requestType: string): void {
|
|
79
|
+
this.pending.set(requestType, performance.now());
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Complete timing and record delay
|
|
84
|
+
*/
|
|
85
|
+
completeTiming(requestType: string): number | null {
|
|
86
|
+
const startTime = this.pending.get(requestType);
|
|
87
|
+
if (startTime === undefined) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
this.pending.delete(requestType);
|
|
92
|
+
const delay = performance.now() - startTime;
|
|
93
|
+
|
|
94
|
+
this.delays.push(delay);
|
|
95
|
+
if (this.delays.length > this.maxSamples) {
|
|
96
|
+
this.delays.shift();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return delay;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Get average server delay
|
|
104
|
+
*/
|
|
105
|
+
getAverageDelay(): number {
|
|
106
|
+
if (this.delays.length === 0) {
|
|
107
|
+
return 0;
|
|
108
|
+
}
|
|
109
|
+
return this.delays.reduce((sum, d) => sum + d, 0) / this.delays.length;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Clear all pending timings
|
|
114
|
+
*/
|
|
115
|
+
clear(): void {
|
|
116
|
+
this.pending.clear();
|
|
117
|
+
this.delays = [];
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* WebSocketController - Manages raw frame WebSocket connection
|
|
123
|
+
*/
|
|
124
|
+
export class WebSocketController {
|
|
125
|
+
private ws: WebSocket | null = null;
|
|
126
|
+
private url: string;
|
|
127
|
+
private options: Required<WebSocketControllerOptions>;
|
|
128
|
+
private state: ConnectionState = 'disconnected';
|
|
129
|
+
private reconnectAttempts = 0;
|
|
130
|
+
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
|
131
|
+
private connectionTimer: ReturnType<typeof setTimeout> | null = null;
|
|
132
|
+
private listeners = new Map<keyof WebSocketControllerEvents, Set<Function>>();
|
|
133
|
+
private serverDelay = new ServerDelayTracker();
|
|
134
|
+
private intentionalClose = false;
|
|
135
|
+
|
|
136
|
+
constructor(url: string, options: WebSocketControllerOptions = {}) {
|
|
137
|
+
this.url = url;
|
|
138
|
+
this.options = { ...DEFAULTS, ...options };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Connect to WebSocket server
|
|
143
|
+
*/
|
|
144
|
+
connect(): Promise<void> {
|
|
145
|
+
return new Promise((resolve, reject) => {
|
|
146
|
+
if (this.ws && this.state === 'connected') {
|
|
147
|
+
resolve();
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
this.intentionalClose = false;
|
|
152
|
+
this.setState('connecting');
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
this.ws = new WebSocket(this.url);
|
|
156
|
+
this.ws.binaryType = 'arraybuffer';
|
|
157
|
+
|
|
158
|
+
// Connection timeout
|
|
159
|
+
this.connectionTimer = setTimeout(() => {
|
|
160
|
+
if (this.state === 'connecting') {
|
|
161
|
+
this.log('Connection timeout');
|
|
162
|
+
this.ws?.close();
|
|
163
|
+
reject(new Error('Connection timeout'));
|
|
164
|
+
}
|
|
165
|
+
}, this.options.connectionTimeoutMs);
|
|
166
|
+
|
|
167
|
+
this.ws.onopen = () => {
|
|
168
|
+
this.clearConnectionTimer();
|
|
169
|
+
this.setState('connected');
|
|
170
|
+
this.reconnectAttempts = 0;
|
|
171
|
+
this.log('Connected');
|
|
172
|
+
resolve();
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
this.ws.onclose = (event) => {
|
|
176
|
+
this.clearConnectionTimer();
|
|
177
|
+
this.log(`Disconnected: ${event.code} ${event.reason}`);
|
|
178
|
+
|
|
179
|
+
if (!this.intentionalClose && this.shouldReconnect()) {
|
|
180
|
+
this.scheduleReconnect();
|
|
181
|
+
} else {
|
|
182
|
+
this.setState('disconnected');
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
this.ws.onerror = (event) => {
|
|
187
|
+
this.log('WebSocket error');
|
|
188
|
+
this.emit('error', new Error('WebSocket error'));
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
this.ws.onmessage = (event) => {
|
|
192
|
+
this.handleMessage(event);
|
|
193
|
+
};
|
|
194
|
+
} catch (err) {
|
|
195
|
+
this.setState('error');
|
|
196
|
+
reject(err);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Disconnect from WebSocket server
|
|
203
|
+
*/
|
|
204
|
+
disconnect(): void {
|
|
205
|
+
this.intentionalClose = true;
|
|
206
|
+
this.clearReconnectTimer();
|
|
207
|
+
this.clearConnectionTimer();
|
|
208
|
+
|
|
209
|
+
if (this.ws) {
|
|
210
|
+
// Send hold command before closing
|
|
211
|
+
try {
|
|
212
|
+
this.send({ type: 'hold' });
|
|
213
|
+
} catch {
|
|
214
|
+
// Ignore send errors during close
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
this.ws.close();
|
|
218
|
+
this.ws = null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
this.serverDelay.clear();
|
|
222
|
+
this.setState('disconnected');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Send a control command
|
|
227
|
+
*/
|
|
228
|
+
send(command: ControlCommand): boolean {
|
|
229
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
230
|
+
this.log(`Cannot send ${command.type}: not connected`);
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Track timing for certain commands
|
|
235
|
+
const timedCommands = ['seek', 'set_speed', 'request_codec_data'];
|
|
236
|
+
if (timedCommands.includes(command.type)) {
|
|
237
|
+
this.serverDelay.startTiming(command.type);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const message = JSON.stringify(command);
|
|
241
|
+
this.log(`Sending: ${message}`);
|
|
242
|
+
this.ws.send(message);
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Request playback start
|
|
248
|
+
*/
|
|
249
|
+
play(): boolean {
|
|
250
|
+
return this.send({ type: 'play' });
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Request playback pause
|
|
255
|
+
*/
|
|
256
|
+
hold(): boolean {
|
|
257
|
+
return this.send({ type: 'hold' });
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Seek to position
|
|
262
|
+
* @param timeMs - Target time in milliseconds
|
|
263
|
+
* @param fastForwardMs - Additional buffer to request (ms)
|
|
264
|
+
*/
|
|
265
|
+
seek(timeMs: number, fastForwardMs?: number): boolean {
|
|
266
|
+
const cmd: ControlCommand = {
|
|
267
|
+
type: 'seek',
|
|
268
|
+
seek_time: Math.round(timeMs),
|
|
269
|
+
};
|
|
270
|
+
if (fastForwardMs !== undefined) {
|
|
271
|
+
cmd.ff_add = Math.round(fastForwardMs);
|
|
272
|
+
}
|
|
273
|
+
return this.send(cmd);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Set playback speed
|
|
278
|
+
*/
|
|
279
|
+
setSpeed(rate: number | 'auto'): boolean {
|
|
280
|
+
return this.send({ type: 'set_speed', play_rate: rate });
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Request codec initialization data
|
|
285
|
+
* @param supportedCombinations - Array of codec combinations we can play
|
|
286
|
+
* Format: [[ ["H264"], ["AAC"] ]] means "H264 video AND AAC audio"
|
|
287
|
+
* Per MistServer rawws.js line 1544
|
|
288
|
+
*/
|
|
289
|
+
requestCodecData(supportedCombinations?: string[][][]): boolean {
|
|
290
|
+
if (supportedCombinations && supportedCombinations.length > 0) {
|
|
291
|
+
return this.send({
|
|
292
|
+
type: 'request_codec_data',
|
|
293
|
+
supported_combinations: supportedCombinations,
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
return this.send({ type: 'request_codec_data' });
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Request additional data (fast-forward for buffer recovery)
|
|
301
|
+
*/
|
|
302
|
+
fastForward(ms: number): boolean {
|
|
303
|
+
return this.send({ type: 'fast_forward', ff_add: Math.round(ms) });
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Get current connection state
|
|
308
|
+
*/
|
|
309
|
+
getState(): ConnectionState {
|
|
310
|
+
return this.state;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Get estimated server delay
|
|
315
|
+
*/
|
|
316
|
+
getServerDelay(): number {
|
|
317
|
+
return this.serverDelay.getAverageDelay();
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Check if connected
|
|
322
|
+
*/
|
|
323
|
+
isConnected(): boolean {
|
|
324
|
+
return this.state === 'connected' && this.ws?.readyState === WebSocket.OPEN;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Add event listener
|
|
329
|
+
*/
|
|
330
|
+
on<K extends keyof WebSocketControllerEvents>(
|
|
331
|
+
event: K,
|
|
332
|
+
listener: EventListener<K>
|
|
333
|
+
): void {
|
|
334
|
+
if (!this.listeners.has(event)) {
|
|
335
|
+
this.listeners.set(event, new Set());
|
|
336
|
+
}
|
|
337
|
+
this.listeners.get(event)!.add(listener);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Remove event listener
|
|
342
|
+
*/
|
|
343
|
+
off<K extends keyof WebSocketControllerEvents>(
|
|
344
|
+
event: K,
|
|
345
|
+
listener: EventListener<K>
|
|
346
|
+
): void {
|
|
347
|
+
this.listeners.get(event)?.delete(listener);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Emit event to listeners
|
|
352
|
+
*/
|
|
353
|
+
private emit<K extends keyof WebSocketControllerEvents>(
|
|
354
|
+
event: K,
|
|
355
|
+
data: WebSocketControllerEvents[K]
|
|
356
|
+
): void {
|
|
357
|
+
const eventListeners = this.listeners.get(event);
|
|
358
|
+
if (eventListeners) {
|
|
359
|
+
for (const listener of eventListeners) {
|
|
360
|
+
try {
|
|
361
|
+
listener(data);
|
|
362
|
+
} catch (err) {
|
|
363
|
+
console.error(`Error in ${event} listener:`, err);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Handle incoming WebSocket message
|
|
371
|
+
*/
|
|
372
|
+
private handleMessage(event: MessageEvent): void {
|
|
373
|
+
if (event.data instanceof ArrayBuffer) {
|
|
374
|
+
// Binary data - parse as raw chunk
|
|
375
|
+
this.handleBinaryMessage(event.data);
|
|
376
|
+
} else if (typeof event.data === 'string') {
|
|
377
|
+
// JSON control message
|
|
378
|
+
this.handleControlMessage(event.data);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Rate limit delta frame logging (log at most once per 5 seconds per track)
|
|
383
|
+
private lastDeltaLogTime: Record<number, number> = {};
|
|
384
|
+
private deltaLogInterval = 5000; // ms
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Handle binary frame data
|
|
388
|
+
*/
|
|
389
|
+
private handleBinaryMessage(data: ArrayBuffer): void {
|
|
390
|
+
try {
|
|
391
|
+
const chunk = parseRawChunk(data);
|
|
392
|
+
if (this.options.debug) {
|
|
393
|
+
// Only log KEY/INIT frames, rate-limit DELTA logs
|
|
394
|
+
if (chunk.type !== 'delta') {
|
|
395
|
+
this.log(formatChunkForLog(chunk));
|
|
396
|
+
} else {
|
|
397
|
+
const now = performance.now();
|
|
398
|
+
const lastLog = this.lastDeltaLogTime[chunk.trackIndex] || 0;
|
|
399
|
+
if (now - lastLog > this.deltaLogInterval) {
|
|
400
|
+
this.lastDeltaLogTime[chunk.trackIndex] = now;
|
|
401
|
+
// Don't log delta frames at all - too spammy
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
this.emit('chunk', chunk);
|
|
406
|
+
} catch (err) {
|
|
407
|
+
this.log(`Failed to parse binary chunk: ${err}`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Handle JSON control message
|
|
413
|
+
*
|
|
414
|
+
* Per MistServer util.js line 1301, we need to unwrap the `data` field:
|
|
415
|
+
* MistServer sends: { type: "codec_data", data: { codecs: [...], tracks: [...] } }
|
|
416
|
+
* We need to emit: { type: "codec_data", codecs: [...], tracks: [...] }
|
|
417
|
+
*/
|
|
418
|
+
private handleControlMessage(data: string): void {
|
|
419
|
+
try {
|
|
420
|
+
const raw = JSON.parse(data);
|
|
421
|
+
|
|
422
|
+
// Match MistServer util.js line 1301: unwrap data field if present
|
|
423
|
+
// Some messages (like on_answer_sdp) don't have the data key
|
|
424
|
+
const payload = 'data' in raw ? raw.data : raw;
|
|
425
|
+
const message: ControlMessage = { type: raw.type, ...payload };
|
|
426
|
+
|
|
427
|
+
this.log(`Received: ${message.type}`);
|
|
428
|
+
|
|
429
|
+
// Complete timing for responses
|
|
430
|
+
if (message.type === 'codec_data') {
|
|
431
|
+
this.serverDelay.completeTiming('request_codec_data');
|
|
432
|
+
} else if (message.type === 'on_time') {
|
|
433
|
+
this.serverDelay.completeTiming('seek');
|
|
434
|
+
this.serverDelay.completeTiming('set_speed');
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Route to appropriate handler
|
|
438
|
+
switch (message.type) {
|
|
439
|
+
case 'codec_data':
|
|
440
|
+
this.emit('codecdata', message as CodecDataMessage);
|
|
441
|
+
break;
|
|
442
|
+
|
|
443
|
+
case 'info':
|
|
444
|
+
this.log(`Info message tracks: ${JSON.stringify(Object.keys((message as InfoMessage).meta?.tracks ?? {}))}`);
|
|
445
|
+
this.emit('info', message as InfoMessage);
|
|
446
|
+
break;
|
|
447
|
+
|
|
448
|
+
case 'on_time':
|
|
449
|
+
if ((message as OnTimeMessage).tracks?.length) {
|
|
450
|
+
this.log(`on_time tracks: ${JSON.stringify((message as OnTimeMessage).tracks)}`);
|
|
451
|
+
}
|
|
452
|
+
this.emit('ontime', message as OnTimeMessage);
|
|
453
|
+
break;
|
|
454
|
+
|
|
455
|
+
case 'tracks':
|
|
456
|
+
this.emit('tracks', (message as any).tracks);
|
|
457
|
+
break;
|
|
458
|
+
|
|
459
|
+
case 'on_stop':
|
|
460
|
+
this.emit('stop', undefined);
|
|
461
|
+
break;
|
|
462
|
+
|
|
463
|
+
case 'error':
|
|
464
|
+
this.emit('error', new Error((message as any).message));
|
|
465
|
+
break;
|
|
466
|
+
|
|
467
|
+
case 'pause':
|
|
468
|
+
case 'set_speed':
|
|
469
|
+
// Acknowledgments - no action needed
|
|
470
|
+
break;
|
|
471
|
+
|
|
472
|
+
default:
|
|
473
|
+
this.log(`Unknown message type: ${(message as any).type}`);
|
|
474
|
+
}
|
|
475
|
+
} catch (err) {
|
|
476
|
+
this.log(`Failed to parse control message: ${err}`);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Set connection state and emit event
|
|
482
|
+
*/
|
|
483
|
+
private setState(state: ConnectionState): void {
|
|
484
|
+
if (this.state !== state) {
|
|
485
|
+
this.state = state;
|
|
486
|
+
this.emit('statechange', state);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Check if should attempt reconnection
|
|
492
|
+
*/
|
|
493
|
+
private shouldReconnect(): boolean {
|
|
494
|
+
if (this.intentionalClose) {
|
|
495
|
+
return false;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (this.options.maxReconnectAttempts === 0) {
|
|
499
|
+
return true; // Unlimited retries
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
return this.reconnectAttempts < this.options.maxReconnectAttempts;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Schedule reconnection with exponential backoff
|
|
507
|
+
*/
|
|
508
|
+
private scheduleReconnect(): void {
|
|
509
|
+
this.setState('reconnecting');
|
|
510
|
+
this.reconnectAttempts++;
|
|
511
|
+
|
|
512
|
+
// Exponential backoff with jitter
|
|
513
|
+
const baseDelay = this.options.reconnectDelayMs;
|
|
514
|
+
const maxDelay = this.options.maxReconnectDelayMs;
|
|
515
|
+
const delay = Math.min(
|
|
516
|
+
baseDelay * Math.pow(2, this.reconnectAttempts - 1),
|
|
517
|
+
maxDelay
|
|
518
|
+
);
|
|
519
|
+
const jitter = delay * 0.2 * Math.random(); // +/- 20% jitter
|
|
520
|
+
|
|
521
|
+
this.log(`Reconnecting in ${Math.round(delay + jitter)}ms (attempt ${this.reconnectAttempts})`);
|
|
522
|
+
|
|
523
|
+
this.reconnectTimer = setTimeout(() => {
|
|
524
|
+
this.connect().catch((err) => {
|
|
525
|
+
this.log(`Reconnect failed: ${err.message}`);
|
|
526
|
+
if (this.shouldReconnect()) {
|
|
527
|
+
this.scheduleReconnect();
|
|
528
|
+
} else {
|
|
529
|
+
this.setState('error');
|
|
530
|
+
this.emit('error', new Error('Max reconnection attempts exceeded'));
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
}, delay + jitter);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Clear reconnect timer
|
|
538
|
+
*/
|
|
539
|
+
private clearReconnectTimer(): void {
|
|
540
|
+
if (this.reconnectTimer) {
|
|
541
|
+
clearTimeout(this.reconnectTimer);
|
|
542
|
+
this.reconnectTimer = null;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Clear connection timer
|
|
548
|
+
*/
|
|
549
|
+
private clearConnectionTimer(): void {
|
|
550
|
+
if (this.connectionTimer) {
|
|
551
|
+
clearTimeout(this.connectionTimer);
|
|
552
|
+
this.connectionTimer = null;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Log message (if debug enabled)
|
|
558
|
+
*/
|
|
559
|
+
private log(message: string): void {
|
|
560
|
+
if (this.options.debug) {
|
|
561
|
+
console.log(`[WebCodecsWS] ${message}`);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|