@livepeer-frameworks/player-core 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/dist/cjs/index.js +19493 -0
  2. package/dist/cjs/index.js.map +1 -0
  3. package/dist/esm/index.js +19398 -0
  4. package/dist/esm/index.js.map +1 -0
  5. package/dist/player.css +2140 -0
  6. package/dist/types/core/ABRController.d.ts +164 -0
  7. package/dist/types/core/CodecUtils.d.ts +54 -0
  8. package/dist/types/core/Disposable.d.ts +61 -0
  9. package/dist/types/core/EventEmitter.d.ts +73 -0
  10. package/dist/types/core/GatewayClient.d.ts +144 -0
  11. package/dist/types/core/InteractionController.d.ts +121 -0
  12. package/dist/types/core/LiveDurationProxy.d.ts +102 -0
  13. package/dist/types/core/MetaTrackManager.d.ts +220 -0
  14. package/dist/types/core/MistReporter.d.ts +163 -0
  15. package/dist/types/core/MistSignaling.d.ts +148 -0
  16. package/dist/types/core/PlayerController.d.ts +665 -0
  17. package/dist/types/core/PlayerInterface.d.ts +230 -0
  18. package/dist/types/core/PlayerManager.d.ts +182 -0
  19. package/dist/types/core/PlayerRegistry.d.ts +27 -0
  20. package/dist/types/core/QualityMonitor.d.ts +184 -0
  21. package/dist/types/core/ScreenWakeLockManager.d.ts +70 -0
  22. package/dist/types/core/SeekingUtils.d.ts +142 -0
  23. package/dist/types/core/StreamStateClient.d.ts +108 -0
  24. package/dist/types/core/SubtitleManager.d.ts +111 -0
  25. package/dist/types/core/TelemetryReporter.d.ts +79 -0
  26. package/dist/types/core/TimeFormat.d.ts +97 -0
  27. package/dist/types/core/TimerManager.d.ts +83 -0
  28. package/dist/types/core/UrlUtils.d.ts +81 -0
  29. package/dist/types/core/detector.d.ts +149 -0
  30. package/dist/types/core/index.d.ts +49 -0
  31. package/dist/types/core/scorer.d.ts +167 -0
  32. package/dist/types/core/selector.d.ts +9 -0
  33. package/dist/types/index.d.ts +45 -0
  34. package/dist/types/lib/utils.d.ts +2 -0
  35. package/dist/types/players/DashJsPlayer.d.ts +102 -0
  36. package/dist/types/players/HlsJsPlayer.d.ts +70 -0
  37. package/dist/types/players/MewsWsPlayer/SourceBufferManager.d.ts +119 -0
  38. package/dist/types/players/MewsWsPlayer/WebSocketManager.d.ts +60 -0
  39. package/dist/types/players/MewsWsPlayer/index.d.ts +220 -0
  40. package/dist/types/players/MewsWsPlayer/types.d.ts +89 -0
  41. package/dist/types/players/MistPlayer.d.ts +25 -0
  42. package/dist/types/players/MistWebRTCPlayer/index.d.ts +133 -0
  43. package/dist/types/players/NativePlayer.d.ts +143 -0
  44. package/dist/types/players/VideoJsPlayer.d.ts +59 -0
  45. package/dist/types/players/WebCodecsPlayer/JitterBuffer.d.ts +118 -0
  46. package/dist/types/players/WebCodecsPlayer/LatencyProfiles.d.ts +64 -0
  47. package/dist/types/players/WebCodecsPlayer/RawChunkParser.d.ts +63 -0
  48. package/dist/types/players/WebCodecsPlayer/SyncController.d.ts +174 -0
  49. package/dist/types/players/WebCodecsPlayer/WebSocketController.d.ts +164 -0
  50. package/dist/types/players/WebCodecsPlayer/index.d.ts +149 -0
  51. package/dist/types/players/WebCodecsPlayer/polyfills/MediaStreamTrackGenerator.d.ts +105 -0
  52. package/dist/types/players/WebCodecsPlayer/types.d.ts +395 -0
  53. package/dist/types/players/WebCodecsPlayer/worker/decoder.worker.d.ts +13 -0
  54. package/dist/types/players/WebCodecsPlayer/worker/types.d.ts +197 -0
  55. package/dist/types/players/index.d.ts +14 -0
  56. package/dist/types/styles/index.d.ts +11 -0
  57. package/dist/types/types.d.ts +363 -0
  58. package/dist/types/vanilla/FrameWorksPlayer.d.ts +143 -0
  59. package/dist/types/vanilla/index.d.ts +19 -0
  60. package/dist/workers/decoder.worker.js +989 -0
  61. package/dist/workers/decoder.worker.js.map +1 -0
  62. package/package.json +80 -0
  63. package/src/core/ABRController.ts +550 -0
  64. package/src/core/CodecUtils.ts +257 -0
  65. package/src/core/Disposable.ts +120 -0
  66. package/src/core/EventEmitter.ts +113 -0
  67. package/src/core/GatewayClient.ts +439 -0
  68. package/src/core/InteractionController.ts +712 -0
  69. package/src/core/LiveDurationProxy.ts +270 -0
  70. package/src/core/MetaTrackManager.ts +753 -0
  71. package/src/core/MistReporter.ts +543 -0
  72. package/src/core/MistSignaling.ts +346 -0
  73. package/src/core/PlayerController.ts +2829 -0
  74. package/src/core/PlayerInterface.ts +432 -0
  75. package/src/core/PlayerManager.ts +900 -0
  76. package/src/core/PlayerRegistry.ts +149 -0
  77. package/src/core/QualityMonitor.ts +597 -0
  78. package/src/core/ScreenWakeLockManager.ts +163 -0
  79. package/src/core/SeekingUtils.ts +364 -0
  80. package/src/core/StreamStateClient.ts +457 -0
  81. package/src/core/SubtitleManager.ts +297 -0
  82. package/src/core/TelemetryReporter.ts +308 -0
  83. package/src/core/TimeFormat.ts +205 -0
  84. package/src/core/TimerManager.ts +209 -0
  85. package/src/core/UrlUtils.ts +179 -0
  86. package/src/core/detector.ts +382 -0
  87. package/src/core/index.ts +140 -0
  88. package/src/core/scorer.ts +553 -0
  89. package/src/core/selector.ts +16 -0
  90. package/src/global.d.ts +11 -0
  91. package/src/index.ts +75 -0
  92. package/src/lib/utils.ts +6 -0
  93. package/src/players/DashJsPlayer.ts +642 -0
  94. package/src/players/HlsJsPlayer.ts +483 -0
  95. package/src/players/MewsWsPlayer/SourceBufferManager.ts +572 -0
  96. package/src/players/MewsWsPlayer/WebSocketManager.ts +241 -0
  97. package/src/players/MewsWsPlayer/index.ts +1065 -0
  98. package/src/players/MewsWsPlayer/types.ts +106 -0
  99. package/src/players/MistPlayer.ts +188 -0
  100. package/src/players/MistWebRTCPlayer/index.ts +703 -0
  101. package/src/players/NativePlayer.ts +820 -0
  102. package/src/players/VideoJsPlayer.ts +643 -0
  103. package/src/players/WebCodecsPlayer/JitterBuffer.ts +299 -0
  104. package/src/players/WebCodecsPlayer/LatencyProfiles.ts +151 -0
  105. package/src/players/WebCodecsPlayer/RawChunkParser.ts +151 -0
  106. package/src/players/WebCodecsPlayer/SyncController.ts +456 -0
  107. package/src/players/WebCodecsPlayer/WebSocketController.ts +564 -0
  108. package/src/players/WebCodecsPlayer/index.ts +1650 -0
  109. package/src/players/WebCodecsPlayer/polyfills/MediaStreamTrackGenerator.ts +379 -0
  110. package/src/players/WebCodecsPlayer/types.ts +542 -0
  111. package/src/players/WebCodecsPlayer/worker/decoder.worker.ts +1360 -0
  112. package/src/players/WebCodecsPlayer/worker/types.ts +276 -0
  113. package/src/players/index.ts +22 -0
  114. package/src/styles/animations.css +21 -0
  115. package/src/styles/index.ts +52 -0
  116. package/src/styles/player.css +2126 -0
  117. package/src/styles/tailwind.css +1015 -0
  118. package/src/types.ts +421 -0
  119. package/src/vanilla/FrameWorksPlayer.ts +367 -0
  120. package/src/vanilla/index.ts +22 -0
@@ -0,0 +1,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
+ }