@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,1065 @@
1
+ /**
2
+ * MEWS WebSocket Player Implementation
3
+ *
4
+ * Low-latency WebSocket MP4 streaming using MediaSource Extensions.
5
+ * Protocol: Custom MEWS (MistServer Extended WebSocket)
6
+ *
7
+ * Ported from reference: mews.js (MistMetaPlayer)
8
+ */
9
+
10
+ import { BasePlayer } from '../../core/PlayerInterface';
11
+ import type { StreamSource, StreamInfo, PlayerOptions, PlayerCapability } from '../../core/PlayerInterface';
12
+ import { WebSocketManager } from './WebSocketManager';
13
+ import { SourceBufferManager } from './SourceBufferManager';
14
+ import { translateCodec } from '../../core/CodecUtils';
15
+ import type { MewsMessage, AnalyticsConfig, OnTimeMessage, MewsMessageListener } from './types';
16
+
17
+ export class MewsWsPlayerImpl extends BasePlayer {
18
+ readonly capability: PlayerCapability = {
19
+ name: "MEWS WebSocket Player",
20
+ shortname: "mews",
21
+ priority: 2, // High priority - low latency protocol
22
+ mimes: ["ws/video/mp4", "wss/video/mp4", "ws/video/webm", "wss/video/webm"]
23
+ };
24
+
25
+ private wsManager: WebSocketManager | null = null;
26
+ private sbManager: SourceBufferManager | null = null;
27
+ private mediaSource: MediaSource | null = null;
28
+ private objectUrl: string | null = null;
29
+ private container: HTMLElement | null = null;
30
+ private isDestroyed = false;
31
+ private debugging = false;
32
+
33
+ // Server delay estimation (ported from mews.js:833-882)
34
+ private serverDelays: number[] = [];
35
+ private pendingDelayTypes: Record<string, number> = {};
36
+
37
+ // Supported codecs (short names for MistServer protocol)
38
+ private supportedCodecs: string[] = [];
39
+
40
+ // Ready state - true after codec_data received and SourceBuffer initialized
41
+ private isReady = false;
42
+ private readyResolvers: Array<() => void> = [];
43
+
44
+ // Duration tracking (ported from mews.js:1113)
45
+ private lastDuration = Infinity;
46
+
47
+ // Live vs VoD detection (ported from mews.js:105-107, 508)
48
+ private streamType: 'live' | 'vod' | 'unknown' = 'unknown';
49
+
50
+ // Current tracks for change detection (ported from mews.js:455, 593-619)
51
+ private currentTracks: string[] = [];
52
+
53
+ // Last codecs for track switch comparison (ported from mews.js:687)
54
+ private lastCodecs: string[] | null = null;
55
+
56
+ // Playback rate tuning (ported from mews.js:453, 509-545)
57
+ private requestedRate = 1;
58
+
59
+ // ABR state (ported from mews.js:1266-1314)
60
+ private bitCounter: number[] = [];
61
+ private bitsSince: number[] = [];
62
+ private currentBps: number | null = null;
63
+ private nWaiting = 0;
64
+ private nWaitingThreshold = 3;
65
+
66
+ // Seeking state (ported from mews.js:1169-1175)
67
+ private seeking = false;
68
+
69
+ // Analytics
70
+ private analyticsConfig: AnalyticsConfig = { enabled: false, endpoint: null };
71
+ private analyticsTimer: ReturnType<typeof setInterval> | null = null;
72
+
73
+ isMimeSupported(mimetype: string): boolean {
74
+ return this.capability.mimes.includes(mimetype);
75
+ }
76
+
77
+ isBrowserSupported(mimetype: string, source: StreamSource, streamInfo: StreamInfo): boolean | string[] {
78
+ // Basic requirements check (mews.js:10)
79
+ if (!('WebSocket' in window) || !('MediaSource' in window) || !('Promise' in window)) {
80
+ return false;
81
+ }
82
+
83
+ // MacOS exemption (reference mews.js behavior)
84
+ // MediaSource has bugs on Safari/MacOS - prefer HLS
85
+ const isMac = /Mac OS X/.test(navigator.userAgent);
86
+ if (isMac) {
87
+ return false;
88
+ }
89
+
90
+ // Check codec compatibility using ACTUAL stream codecs (mews.js:45-83)
91
+ const container = mimetype.split('/')[2] || 'mp4';
92
+ const playableTracks: Record<string, number> = {};
93
+ let hasSubtitles = false;
94
+
95
+ // Test actual stream codecs against MediaSource
96
+ this.supportedCodecs = [];
97
+ for (const track of streamInfo.meta.tracks) {
98
+ if (track.type === 'meta') {
99
+ if (track.codec === 'subtitle') hasSubtitles = true;
100
+ continue;
101
+ }
102
+
103
+ const codecString = translateCodec(track as any);
104
+ const testMime = `video/${container};codecs="${codecString}"`;
105
+
106
+ if (MediaSource.isTypeSupported(testMime)) {
107
+ this.supportedCodecs.push(track.codec);
108
+ playableTracks[track.type] = 1;
109
+ }
110
+ }
111
+
112
+ // Check for subtitle source (mews.js:73-80)
113
+ if (hasSubtitles) {
114
+ const hasVttSource = streamInfo.source?.some(s => s.type === 'html5/text/vtt');
115
+ if (hasVttSource) {
116
+ playableTracks['subtitle'] = 1;
117
+ }
118
+ }
119
+
120
+ if (Object.keys(playableTracks).length === 0) return false;
121
+ return Object.keys(playableTracks);
122
+ }
123
+
124
+ async initialize(container: HTMLElement, source: StreamSource, options: PlayerOptions): Promise<HTMLVideoElement> {
125
+ this.container = container;
126
+ container.classList.add('fw-player-container');
127
+
128
+ const video = document.createElement('video');
129
+ video.classList.add('fw-player-video');
130
+ video.setAttribute('playsinline', ''); // iphones (mews.js:92)
131
+ video.setAttribute('crossorigin', 'anonymous'); // mews.js:111
132
+
133
+ // Apply options (mews.js:95-110)
134
+ if (options.autoplay) video.autoplay = true;
135
+ if (options.muted) video.muted = true;
136
+ video.controls = options.controls === true;
137
+ if (options.loop) video.loop = true;
138
+ if (options.poster) video.poster = options.poster;
139
+
140
+ // Live streams don't loop (mews.js:105-107)
141
+ if (this.streamType === 'live') {
142
+ video.loop = false;
143
+ }
144
+
145
+ this.videoElement = video;
146
+ container.appendChild(video);
147
+ this.setupVideoEventListeners(video, options);
148
+
149
+ // Analytics configuration
150
+ const anyOpts = options as any;
151
+ this.analyticsConfig = {
152
+ enabled: !!anyOpts.analytics?.enabled,
153
+ endpoint: anyOpts.analytics?.endpoint || null
154
+ };
155
+
156
+ // Get stream type from options if available
157
+ if ((source as any).type === 'live') {
158
+ this.streamType = 'live';
159
+ } else if ((source as any).type === 'vod') {
160
+ this.streamType = 'vod';
161
+ }
162
+
163
+ try {
164
+ // Initialize MediaSource (mews.js:138-196)
165
+ this.mediaSource = new MediaSource();
166
+
167
+ // Set up MediaSource event handlers (mews.js:143-195)
168
+ this.mediaSource.addEventListener('sourceopen', () => this.handleSourceOpen(source));
169
+ this.mediaSource.addEventListener('sourceclose', () => this.handleSourceClose());
170
+ this.mediaSource.addEventListener('sourceended', () => this.handleSourceEnded());
171
+
172
+ this.objectUrl = URL.createObjectURL(this.mediaSource);
173
+ video.src = this.objectUrl;
174
+ this.isDestroyed = false;
175
+ this.startTelemetry();
176
+ return video;
177
+ } catch (error: any) {
178
+ this.emit('error', error.message || String(error));
179
+ throw error;
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Handle MediaSource sourceopen event.
185
+ * Ported from mews.js:143-148, 198-204, 885-902
186
+ */
187
+ private handleSourceOpen(source: StreamSource): void {
188
+ if (!this.mediaSource || !this.videoElement) return;
189
+
190
+ // Create SourceBufferManager
191
+ this.sbManager = new SourceBufferManager({
192
+ mediaSource: this.mediaSource,
193
+ videoElement: this.videoElement,
194
+ onError: (msg) => this.emit('error', msg)
195
+ });
196
+
197
+ // Install browser event handlers
198
+ this.installWaitingHandler();
199
+ this.installSeekingHandler();
200
+ this.installPauseHandler();
201
+ this.installLoopHandler();
202
+
203
+ // Create WebSocketManager with listener support
204
+ this.wsManager = new WebSocketManager({
205
+ url: source.url,
206
+ maxReconnectAttempts: 5,
207
+ onMessage: (data) => this.handleMessage(data),
208
+ onOpen: () => this.handleWsOpen(),
209
+ onClose: () => this.handleWsClose(),
210
+ onError: (msg) => this.emit('error', msg)
211
+ });
212
+
213
+ this.wsManager.connect();
214
+ }
215
+
216
+ /**
217
+ * Handle MediaSource sourceclose event.
218
+ * Ported from mews.js:150-153
219
+ */
220
+ private handleSourceClose(): void {
221
+ if (this.debugging) console.log('MEWS: MediaSource closed');
222
+ this.send({ type: 'stop' });
223
+ }
224
+
225
+ /**
226
+ * Handle MediaSource sourceended event.
227
+ * Ported from mews.js:154-194
228
+ */
229
+ private handleSourceEnded(): void {
230
+ if (this.debugging) console.log('MEWS: MediaSource ended');
231
+ this.send({ type: 'stop' });
232
+ }
233
+
234
+ /**
235
+ * Handle WebSocket open event.
236
+ * Ported from mews.js:401-403, 885-902
237
+ */
238
+ private handleWsOpen(): void {
239
+ // Request codec data (mews.js:885-902)
240
+ const listener: MewsMessageListener = (msg) => {
241
+ // Got codec data, set up source buffer
242
+ if (this.mediaSource?.readyState === 'open') {
243
+ const codecs = msg.data?.codecs || [];
244
+ const initialized = this.sbManager?.initWithCodecs(codecs);
245
+
246
+ if (initialized && !this.isReady) {
247
+ this.isReady = true;
248
+ // Resolve any waiting play() calls
249
+ for (const resolve of this.readyResolvers) {
250
+ resolve();
251
+ }
252
+ this.readyResolvers = [];
253
+ }
254
+ }
255
+ this.wsManager?.removeListener('codec_data', listener);
256
+ };
257
+
258
+ this.wsManager?.addListener('codec_data', listener);
259
+ this.logDelay('codec_data');
260
+
261
+ // Send request with SHORT codec names (mews.js:901)
262
+ // CRITICAL: MistServer expects short names like "H264", not browser codec strings
263
+ this.send({ type: 'request_codec_data', supported_codecs: this.supportedCodecs });
264
+ }
265
+
266
+ /**
267
+ * Handle WebSocket close event with reconnection logic.
268
+ * Ported from mews.js:408-431
269
+ */
270
+ private handleWsClose(): void {
271
+ if (this.debugging) console.log('MEWS: WebSocket closed');
272
+ // Reconnection is handled by WebSocketManager
273
+ }
274
+
275
+ /**
276
+ * Handle incoming WebSocket message.
277
+ * Routes to binary append or JSON control message handler.
278
+ * Ported from mews.js:456-830
279
+ */
280
+ private handleMessage(data: ArrayBuffer | string): void {
281
+ if (typeof data === 'string') {
282
+ try {
283
+ const msg = JSON.parse(data) as MewsMessage;
284
+ this.handleControlMessage(msg);
285
+ // Notify listeners (mews.js:795-799)
286
+ this.wsManager?.notifyListeners(msg);
287
+ } catch (e) {
288
+ if (this.debugging) console.error('MEWS: Failed to parse message', e);
289
+ }
290
+ return;
291
+ }
292
+
293
+ // Binary data - MP4 segment (mews.js:802-829)
294
+ const bytes = new Uint8Array(data);
295
+ this.sbManager?.append(bytes);
296
+ this.trackBits(data);
297
+ }
298
+
299
+ /**
300
+ * Handle JSON control messages.
301
+ * Ported from mews.js:461-799
302
+ */
303
+ private handleControlMessage(msg: MewsMessage): void {
304
+ if (this.debugging && msg.type !== 'on_time') {
305
+ console.log('MEWS: message', msg);
306
+ }
307
+
308
+ switch (msg.type) {
309
+ case 'on_stop':
310
+ this.handleOnStop();
311
+ break;
312
+
313
+ case 'on_time':
314
+ this.handleOnTime(msg as OnTimeMessage);
315
+ break;
316
+
317
+ case 'tracks':
318
+ this.handleTracks(msg);
319
+ break;
320
+
321
+ case 'pause':
322
+ this.handlePause();
323
+ break;
324
+
325
+ case 'codec_data':
326
+ this.resolveDelay('codec_data');
327
+ break;
328
+
329
+ case 'seek':
330
+ this.resolveDelay('seek');
331
+ break;
332
+
333
+ case 'set_speed':
334
+ this.resolveDelay('set_speed');
335
+ break;
336
+ }
337
+ }
338
+
339
+ /**
340
+ * Handle on_stop message - stream ended (VoD).
341
+ * Ported from mews.js:462-471
342
+ */
343
+ private handleOnStop(): void {
344
+ // Mark as VoD (stream ended)
345
+ this.streamType = 'vod';
346
+
347
+ // Wait for buffer to finish playing (mews.js:465-469)
348
+ const onWaiting = () => {
349
+ if (this.sbManager) {
350
+ this.sbManager.paused = true;
351
+ }
352
+ this.emit('ended', undefined);
353
+ this.videoElement?.removeEventListener('waiting', onWaiting);
354
+ };
355
+ this.videoElement?.addEventListener('waiting', onWaiting);
356
+ }
357
+
358
+ /**
359
+ * Handle on_time message - playback time sync.
360
+ * Ported from mews.js:473-621
361
+ */
362
+ private handleOnTime(msg: OnTimeMessage): void {
363
+ const data = msg.data;
364
+ if (!data || !this.videoElement) return;
365
+
366
+ const currentMs = data.current;
367
+ const endMs = data.end;
368
+ const jitter = data.jitter || 0;
369
+
370
+ // Buffer calculation (mews.js:474)
371
+ const buffer = currentMs - this.videoElement.currentTime * 1000;
372
+ const serverDelay = this.getServerDelay();
373
+ // Chrome needs larger base buffer (mews.js:482)
374
+ const isChrome = /Chrome/.test(navigator.userAgent) && !/Edge|Edg/.test(navigator.userAgent);
375
+ const baseBuffer = isChrome ? 1000 : 100;
376
+ const desiredBuffer = Math.max(baseBuffer + serverDelay, serverDelay * 2);
377
+ const desiredBufferWithJitter = desiredBuffer + jitter;
378
+
379
+ // VoD gets extra buffer (mews.js:480)
380
+ const actualDesiredBuffer = this.streamType !== 'live' ? desiredBuffer + 2000 : desiredBuffer;
381
+
382
+ if (this.debugging) {
383
+ console.log(
384
+ 'MEWS: on_time',
385
+ 'current:', currentMs / 1000,
386
+ 'video:', this.videoElement.currentTime,
387
+ 'rate:', this.requestedRate + 'x',
388
+ 'buffer:', Math.round(buffer), '/', Math.round(desiredBuffer),
389
+ this.streamType === 'live' ? 'latency:' + Math.round((endMs || 0) - this.videoElement.currentTime * 1000) + 'ms' : ''
390
+ );
391
+ }
392
+
393
+ if (!this.sbManager) {
394
+ if (this.debugging) console.log('MEWS: on_time but no sourceBuffer');
395
+ return;
396
+ }
397
+
398
+ // Update duration (mews.js:501-504)
399
+ if (endMs !== undefined && this.lastDuration !== endMs / 1000) {
400
+ this.lastDuration = endMs / 1000;
401
+ // Duration is updated via native video element durationchange event
402
+ }
403
+
404
+ // Mark source buffer as not paused
405
+ this.sbManager.paused = false;
406
+
407
+ // Playback rate tuning for LIVE streams (mews.js:508-545)
408
+ if (this.streamType === 'live') {
409
+ this.tuneLivePlaybackRate(buffer, desiredBufferWithJitter, data.play_rate_curr);
410
+ } else {
411
+ // VoD - adjust server delivery speed (mews.js:547-586)
412
+ this.tuneVodDeliverySpeed(buffer, actualDesiredBuffer, data.play_rate_curr);
413
+ }
414
+
415
+ // Track change detection (mews.js:593-619)
416
+ if (data.tracks && this.currentTracks.join(',') !== data.tracks.join(',')) {
417
+ if (this.debugging) {
418
+ for (const trackId of data.tracks) {
419
+ if (!this.currentTracks.includes(trackId)) {
420
+ console.log('MEWS: track changed', trackId);
421
+ }
422
+ }
423
+ }
424
+ this.currentTracks = data.tracks;
425
+ }
426
+ }
427
+
428
+ /**
429
+ * Tune playback rate for live streams.
430
+ * Ported from mews.js:508-545
431
+ *
432
+ * Fixed: Use direct assignment instead of multiplication to prevent
433
+ * compounding rate adjustments on each on_time message.
434
+ */
435
+ private tuneLivePlaybackRate(buffer: number, desiredBuffer: number, playRateCurr?: 'auto' | number): void {
436
+ if (!this.videoElement) return;
437
+
438
+ if (this.requestedRate === 1) {
439
+ if (playRateCurr === 'auto' && this.videoElement.currentTime > 0) {
440
+ // Assume we want to be as live as possible
441
+ if (buffer > desiredBuffer * 2) {
442
+ // Buffer too big, speed up (mews.js:513-516)
443
+ this.requestedRate = 1 + Math.min(1, (buffer - desiredBuffer) / desiredBuffer) * 0.08;
444
+ this.videoElement.playbackRate = this.requestedRate;
445
+ if (this.debugging) console.log('MEWS: speeding up to', this.requestedRate);
446
+ } else if (buffer < 0) {
447
+ // Negative buffer, slow down (mews.js:518-521)
448
+ this.requestedRate = 0.8;
449
+ this.videoElement.playbackRate = this.requestedRate;
450
+ if (this.debugging) console.log('MEWS: slowing down to', this.requestedRate);
451
+ } else if (buffer < desiredBuffer / 2) {
452
+ // Buffer too small, slow down (mews.js:523-526)
453
+ this.requestedRate = 1 + Math.min(1, (buffer - desiredBuffer) / desiredBuffer) * 0.08;
454
+ this.videoElement.playbackRate = this.requestedRate;
455
+ if (this.debugging) console.log('MEWS: adjusting to', this.requestedRate);
456
+ }
457
+ }
458
+ } else if (this.requestedRate > 1) {
459
+ // Return to normal when buffer is small enough (mews.js:531-536)
460
+ if (buffer < desiredBuffer) {
461
+ this.videoElement.playbackRate = 1;
462
+ this.requestedRate = 1;
463
+ if (this.debugging) console.log('MEWS: returning to normal rate');
464
+ }
465
+ } else {
466
+ // requestedRate < 1, return to normal when buffer is big enough (mews.js:538-544)
467
+ if (buffer > desiredBuffer) {
468
+ this.videoElement.playbackRate = 1;
469
+ this.requestedRate = 1;
470
+ if (this.debugging) console.log('MEWS: returning to normal rate');
471
+ }
472
+ }
473
+ }
474
+
475
+ /**
476
+ * Tune server delivery speed for VoD.
477
+ * Ported from mews.js:547-586
478
+ */
479
+ private tuneVodDeliverySpeed(buffer: number, desiredBuffer: number, playRateCurr?: 'auto' | number): void {
480
+ if (this.requestedRate === 1) {
481
+ if (playRateCurr === 'auto') {
482
+ if (buffer < desiredBuffer / 2) {
483
+ if (buffer < -10000) {
484
+ // Way behind, seek to current position (mews.js:553-554)
485
+ this.send({ type: 'seek', seek_time: Math.round((this.videoElement?.currentTime || 0) * 1000) });
486
+ } else {
487
+ // Request faster delivery (mews.js:557-560)
488
+ this.requestedRate = 2;
489
+ this.send({ type: 'set_speed', play_rate: this.requestedRate });
490
+ if (this.debugging) console.log('MEWS: requesting faster delivery');
491
+ }
492
+ } else if (buffer - desiredBuffer > desiredBuffer) {
493
+ // Too much buffer, slow down (mews.js:563-566)
494
+ this.requestedRate = 0.5;
495
+ this.send({ type: 'set_speed', play_rate: this.requestedRate });
496
+ if (this.debugging) console.log('MEWS: requesting slower delivery');
497
+ }
498
+ }
499
+ } else if (this.requestedRate > 1) {
500
+ if (buffer > desiredBuffer) {
501
+ // Enough buffer, return to realtime (mews.js:571-575)
502
+ this.send({ type: 'set_speed', play_rate: 'auto' });
503
+ this.requestedRate = 1;
504
+ if (this.debugging) console.log('MEWS: returning to realtime delivery');
505
+ }
506
+ } else {
507
+ if (buffer < desiredBuffer) {
508
+ // Buffer small enough, return to realtime (mews.js:579-583)
509
+ this.send({ type: 'set_speed', play_rate: 'auto' });
510
+ this.requestedRate = 1;
511
+ if (this.debugging) console.log('MEWS: returning to realtime delivery');
512
+ }
513
+ }
514
+ }
515
+
516
+ /**
517
+ * Handle tracks message - codec switch.
518
+ * Ported from mews.js:623-788
519
+ */
520
+ private handleTracks(msg: MewsMessage): void {
521
+ const codecs: string[] = msg.data?.codecs || [];
522
+ const switchPointMs = msg.data?.current;
523
+
524
+ if (!codecs.length) {
525
+ this.emit('error', 'Track switch contains no codecs');
526
+ return;
527
+ }
528
+
529
+ // Check if codecs are same as before (mews.js:676)
530
+ const prevCodecs = this.lastCodecs || this.sbManager?.getCodecs() || [];
531
+ if (this.codecsEqual(prevCodecs, codecs)) {
532
+ if (this.debugging) console.log('MEWS: keeping buffer, codecs same');
533
+ // If at position 0 and switch point is not 0, seek to switch point (mews.js:678-679)
534
+ if (this.videoElement?.currentTime === 0 && switchPointMs && switchPointMs !== 0) {
535
+ this.setSeekingPosition(switchPointMs / 1000);
536
+ }
537
+ return;
538
+ }
539
+
540
+ // Different codecs, save for next comparison (mews.js:687)
541
+ this.lastCodecs = codecs;
542
+
543
+ // Change codecs (will handle msgqueue internally)
544
+ this.sbManager?.changeCodecs(codecs, switchPointMs);
545
+ }
546
+
547
+ /**
548
+ * Handle pause message.
549
+ * Ported from mews.js:790-792
550
+ */
551
+ private handlePause(): void {
552
+ if (this.sbManager) {
553
+ this.sbManager.paused = true;
554
+ }
555
+ }
556
+
557
+ /**
558
+ * Set video currentTime with retry logic.
559
+ * Ported from mews.js:635-672
560
+ */
561
+ private setSeekingPosition(tSec: number): void {
562
+ if (!this.videoElement || !this.sbManager) return;
563
+
564
+ const currPos = this.videoElement.currentTime;
565
+ if (currPos > tSec) {
566
+ // Don't seek backwards (mews.js:637-639)
567
+ tSec = currPos;
568
+ }
569
+
570
+ const buffered = this.videoElement.buffered;
571
+ if (!buffered.length || buffered.end(buffered.length - 1) < tSec) {
572
+ // Desired position not in buffer yet, wait for more data (mews.js:641-644)
573
+ this.sbManager.scheduleAfterUpdate(() => this.setSeekingPosition(tSec));
574
+ return;
575
+ }
576
+
577
+ this.videoElement.currentTime = tSec;
578
+
579
+ if (this.videoElement.currentTime < tSec - 0.001) {
580
+ // Didn't reach target, retry (mews.js:648-651)
581
+ this.sbManager.scheduleAfterUpdate(() => this.setSeekingPosition(tSec));
582
+ }
583
+ }
584
+
585
+ /**
586
+ * Check if two codec arrays are equivalent (order-independent)
587
+ */
588
+ private codecsEqual(arr1: string[], arr2: string[]): boolean {
589
+ if (arr1.length !== arr2.length) return false;
590
+ for (const codec of arr1) {
591
+ if (!arr2.includes(codec)) return false;
592
+ }
593
+ return true;
594
+ }
595
+
596
+ // ========== PUBLIC API ==========
597
+
598
+ /**
599
+ * Play with optional skip to live edge.
600
+ * Ported from mews.js:959-1023
601
+ */
602
+ async play(): Promise<void> {
603
+ const v = this.videoElement;
604
+ if (!v) return;
605
+
606
+ // If already playing, nothing to do (mews.js:961-964)
607
+ if (!v.paused) return;
608
+
609
+ // Wait for ready state (codec_data received) with timeout
610
+ if (!this.isReady) {
611
+ await new Promise<void>((resolve, reject) => {
612
+ const timeout = setTimeout(() => {
613
+ reject(new Error('MEWS: Timeout waiting for codec data'));
614
+ }, 5000);
615
+ this.readyResolvers.push(() => {
616
+ clearTimeout(timeout);
617
+ resolve();
618
+ });
619
+ });
620
+ }
621
+
622
+ // Use listener to wait for on_time before playing (mews.js:973-1017)
623
+ return new Promise((resolve, reject) => {
624
+ // Flag to prevent race condition where multiple on_time messages
625
+ // could trigger seek before the first completes
626
+ let handled = false;
627
+
628
+ const onTime: MewsMessageListener = (msg) => {
629
+ // Remove listener immediately to prevent race condition (single-use pattern)
630
+ if (handled) return;
631
+ handled = true;
632
+ this.wsManager?.removeListener('on_time', onTime);
633
+
634
+ if (!this.sbManager) {
635
+ if (this.debugging) console.log('MEWS: play waiting for sourceBuffer');
636
+ handled = false; // Allow retry
637
+ this.wsManager?.addListener('on_time', onTime);
638
+ return;
639
+ }
640
+
641
+ const data = (msg as OnTimeMessage).data;
642
+
643
+ if (this.streamType === 'live') {
644
+ // Live stream - wait for buffer then seek to live edge (mews.js:978-998)
645
+ const waitForBuffer = () => {
646
+ if (!v.buffered.length) return;
647
+
648
+ const bufferIdx = this.sbManager?.findBufferIndex(data.current / 1000);
649
+ if (typeof bufferIdx === 'number') {
650
+ // Check if current position is in buffer
651
+ if (v.buffered.start(bufferIdx) > v.currentTime || v.buffered.end(bufferIdx) < v.currentTime) {
652
+ v.currentTime = data.current / 1000;
653
+ if (this.debugging) console.log('MEWS: seeking to live position', v.currentTime);
654
+ }
655
+
656
+ v.play().then(resolve).catch((err) => {
657
+ this.pause();
658
+ reject(err);
659
+ });
660
+
661
+ this.sbManager!.paused = false;
662
+ }
663
+ };
664
+
665
+ // Wait for buffer via updateend
666
+ this.sbManager?.scheduleAfterUpdate(waitForBuffer);
667
+ } else {
668
+ // VoD - just play when we have data (mews.js:1010-1016)
669
+ this.sbManager!.paused = false;
670
+ if (v.buffered.length && v.buffered.start(0) > v.currentTime) {
671
+ v.currentTime = v.buffered.start(0);
672
+ }
673
+ v.play().then(resolve).catch(reject);
674
+ }
675
+ };
676
+
677
+ this.wsManager?.addListener('on_time', onTime);
678
+
679
+ // Send play command (mews.js:1020-1022)
680
+ const skipToLive = this.streamType === 'live' && v.currentTime === 0;
681
+ if (skipToLive) {
682
+ this.send({ type: 'play', seek_time: 'live' });
683
+ } else {
684
+ this.send({ type: 'play' });
685
+ }
686
+ });
687
+ }
688
+
689
+ /**
690
+ * Pause playback and server delivery.
691
+ * Ported from mews.js:1025-1029
692
+ */
693
+ pause(): void {
694
+ this.videoElement?.pause();
695
+ this.send({ type: 'hold' });
696
+ if (this.sbManager) {
697
+ this.sbManager.paused = true;
698
+ }
699
+ }
700
+
701
+ /**
702
+ * Seek to position with server sync.
703
+ * Ported from mews.js:1071-1111
704
+ */
705
+ seek(time: number): void {
706
+ if (!this.videoElement || isNaN(time) || time < 0) return;
707
+
708
+ // Calculate seek time with server delay compensation (mews.js:1082)
709
+ const seekMs = Math.round(Math.max(0, time * 1000 - (250 + this.getServerDelay())));
710
+
711
+ this.logDelay('seek');
712
+ this.send({ type: 'seek', seek_time: seekMs });
713
+
714
+ // Wait for seek acknowledgment then on_time (mews.js:1084-1108)
715
+ const onSeek: MewsMessageListener = () => {
716
+ this.wsManager?.removeListener('seek', onSeek);
717
+
718
+ const onTime: MewsMessageListener = (msg) => {
719
+ this.wsManager?.removeListener('on_time', onTime);
720
+
721
+ // Use server's actual position (mews.js:1089)
722
+ const actualTime = (msg as OnTimeMessage).data.current / 1000;
723
+ this.trySetCurrentTime(actualTime);
724
+ };
725
+
726
+ this.wsManager?.addListener('on_time', onTime);
727
+ };
728
+
729
+ this.wsManager?.addListener('seek', onSeek);
730
+
731
+ // Also set directly as fallback
732
+ this.videoElement.currentTime = time;
733
+ if (this.debugging) console.log('MEWS: seeking to', time);
734
+ }
735
+
736
+ /**
737
+ * Try to set currentTime with retry logic.
738
+ * Ported from mews.js:1092-1103
739
+ */
740
+ private trySetCurrentTime(tSec: number, retries = 10): void {
741
+ const v = this.videoElement;
742
+ if (!v) return;
743
+
744
+ v.currentTime = tSec;
745
+
746
+ if (v.currentTime < tSec - 0.001 && retries > 0) {
747
+ // Failed to seek, retry (mews.js:1095-1100)
748
+ this.sbManager?.scheduleAfterUpdate(() => this.trySetCurrentTime(tSec, retries - 1));
749
+ }
750
+ }
751
+
752
+ getCurrentTime(): number {
753
+ return this.videoElement?.currentTime ?? 0;
754
+ }
755
+
756
+ getDuration(): number {
757
+ return isFinite(this.lastDuration) ? this.lastDuration : super.getDuration();
758
+ }
759
+
760
+ /**
761
+ * Set playback rate.
762
+ * Ported from mews.js:1119-1129
763
+ */
764
+ setPlaybackRate(rate: number): void {
765
+ super.setPlaybackRate(rate);
766
+ const playRate = rate === 1 ? 'auto' : rate;
767
+ this.logDelay('set_speed');
768
+ this.send({ type: 'set_speed', play_rate: playRate });
769
+ }
770
+
771
+ getQualities(): Array<{ id: string; label: string; isAuto?: boolean; active?: boolean }> {
772
+ return [{ id: 'auto', label: 'Auto', isAuto: true, active: true }];
773
+ }
774
+
775
+ selectQuality(id: string): void {
776
+ if (id === 'auto') {
777
+ this.send({ type: 'set_speed', play_rate: 'auto' });
778
+ }
779
+ }
780
+
781
+ /**
782
+ * Set tracks for ABR or quality selection.
783
+ * Ported from mews.js:1030-1037
784
+ */
785
+ setTracks(obj: { video?: string; audio?: string; subtitle?: string }): void {
786
+ if (!Object.keys(obj).length) return;
787
+ this.send({ type: 'tracks', ...obj });
788
+ }
789
+
790
+ /**
791
+ * Select a subtitle track.
792
+ */
793
+ selectTextTrack(id: string | null): void {
794
+ if (id === null) {
795
+ this.send({ type: 'tracks', subtitle: 'none' });
796
+ } else {
797
+ this.send({ type: 'tracks', subtitle: id });
798
+ }
799
+ }
800
+
801
+ isLive(): boolean {
802
+ return this.streamType === 'live';
803
+ }
804
+
805
+ /**
806
+ * Jump to live edge.
807
+ */
808
+ jumpToLive(): void {
809
+ if (this.streamType !== 'live' || !this.wsManager) return;
810
+ this.send({ type: 'play', seek_time: 'live' });
811
+ this.videoElement?.play().catch(() => {});
812
+ }
813
+
814
+ async getStats(): Promise<any> {
815
+ return {
816
+ currentBps: this.currentBps,
817
+ waitingEvents: this.nWaiting,
818
+ isLive: this.streamType === 'live',
819
+ serverDelay: this.getServerDelay()
820
+ };
821
+ }
822
+
823
+ // ========== EVENT HANDLERS ==========
824
+
825
+ /**
826
+ * Install waiting event handler.
827
+ * Handles buffer gaps and ABR.
828
+ * Ported from mews.js:1177-1186, 1272-1278
829
+ */
830
+ private installWaitingHandler(): void {
831
+ if (!this.videoElement) return;
832
+
833
+ this.videoElement.addEventListener('waiting', () => {
834
+ if (this.seeking) return;
835
+
836
+ const v = this.videoElement!;
837
+ if (!v.buffered || !v.buffered.length) return;
838
+
839
+ // Check for buffer gap and jump it (mews.js:1180-1186)
840
+ const bufferIdx = this.sbManager?.findBufferIndex(v.currentTime);
841
+ if (bufferIdx !== false && typeof bufferIdx === 'number') {
842
+ if (bufferIdx + 1 < v.buffered.length) {
843
+ const nextStart = v.buffered.start(bufferIdx + 1);
844
+ if (nextStart - v.currentTime < 10) {
845
+ if (this.debugging) console.log('MEWS: skipping buffer gap to', nextStart);
846
+ v.currentTime = nextStart;
847
+ }
848
+ }
849
+ }
850
+
851
+ // ABR trigger (mews.js:1272-1278)
852
+ this.nWaiting++;
853
+ if (this.nWaiting >= this.nWaitingThreshold && this.currentBps) {
854
+ this.nWaiting = 0;
855
+ if (this.debugging) console.log('MEWS: ABR triggered, requesting lower bitrate');
856
+ this.setTracks({ video: `<${Math.round(this.currentBps)}bps,minbps` });
857
+ }
858
+ });
859
+ }
860
+
861
+ /**
862
+ * Install seeking event handlers.
863
+ * Ported from mews.js:1169-1175
864
+ */
865
+ private installSeekingHandler(): void {
866
+ if (!this.videoElement) return;
867
+
868
+ this.videoElement.addEventListener('seeking', () => {
869
+ this.seeking = true;
870
+ });
871
+
872
+ this.videoElement.addEventListener('seeked', () => {
873
+ this.seeking = false;
874
+ });
875
+ }
876
+
877
+ /**
878
+ * Install pause event handler for browser pause detection.
879
+ * Ported from mews.js:1188-1200
880
+ */
881
+ private installPauseHandler(): void {
882
+ if (!this.videoElement) return;
883
+
884
+ this.videoElement.addEventListener('pause', () => {
885
+ if (this.sbManager && !this.sbManager.paused) {
886
+ // Browser paused (probably tab hidden) - pause download (mews.js:1189-1192)
887
+ if (this.debugging) console.log('MEWS: browser paused, pausing download');
888
+ this.send({ type: 'hold' });
889
+ this.sbManager.paused = true;
890
+
891
+ // Resume on play (mews.js:1193-1197)
892
+ const onPlay = () => {
893
+ if (this.sbManager?.paused) {
894
+ this.send({ type: 'play' });
895
+ }
896
+ this.videoElement?.removeEventListener('play', onPlay);
897
+ };
898
+ this.videoElement?.addEventListener('play', onPlay);
899
+ }
900
+ });
901
+ }
902
+
903
+ /**
904
+ * Install loop handler for VoD content.
905
+ * Ported from mews.js:1157-1167
906
+ */
907
+ private installLoopHandler(): void {
908
+ if (!this.videoElement) return;
909
+
910
+ this.videoElement.addEventListener('ended', () => {
911
+ const v = this.videoElement;
912
+ if (!v) return;
913
+
914
+ if (v.loop && this.streamType !== 'live') {
915
+ // Loop VoD content (mews.js:1159-1166)
916
+ this.seek(0);
917
+ this.sbManager?._do(() => {
918
+ try {
919
+ // Clear buffer for clean loop
920
+ } catch (e) {}
921
+ });
922
+ }
923
+ });
924
+ }
925
+
926
+ // ========== UTILITIES ==========
927
+
928
+ /**
929
+ * Send command to server with retry.
930
+ * Ported from mews.js:904-944
931
+ */
932
+ private send(cmd: object): void {
933
+ if (this.wsManager) {
934
+ this.wsManager.send(cmd);
935
+ }
936
+ }
937
+
938
+ /**
939
+ * Log delay for server RTT estimation.
940
+ * Ported from mews.js:835-862
941
+ */
942
+ private logDelay(type: string): void {
943
+ this.pendingDelayTypes[type] = Date.now();
944
+ }
945
+
946
+ /**
947
+ * Resolve delay measurement.
948
+ * Ported from mews.js:855-861, 863-867
949
+ */
950
+ private resolveDelay(type: string): void {
951
+ const start = this.pendingDelayTypes[type];
952
+ if (start) {
953
+ const delay = Date.now() - start;
954
+ this.serverDelays.unshift(delay);
955
+ if (this.serverDelays.length > 5) {
956
+ this.serverDelays.pop();
957
+ }
958
+ delete this.pendingDelayTypes[type];
959
+ }
960
+ }
961
+
962
+ /**
963
+ * Get average server delay.
964
+ * Ported from mews.js:869-881
965
+ */
966
+ private getServerDelay(): number {
967
+ if (!this.serverDelays.length) return 500;
968
+ const n = Math.min(3, this.serverDelays.length);
969
+ let sum = 0;
970
+ for (let i = 0; i < n; i++) {
971
+ sum += this.serverDelays[i];
972
+ }
973
+ return sum / n;
974
+ }
975
+
976
+ /**
977
+ * Track bandwidth for ABR.
978
+ * Ported from mews.js:1280-1303
979
+ */
980
+ private trackBits(buf: ArrayBuffer): void {
981
+ this.bitCounter.push(buf.byteLength * 8);
982
+ this.bitsSince.push(Date.now());
983
+
984
+ // Keep window size of 5 samples
985
+ if (this.bitCounter.length > 5) {
986
+ this.bitCounter.shift();
987
+ this.bitsSince.shift();
988
+ }
989
+
990
+ // Calculate current bitrate
991
+ if (this.bitCounter.length >= 2) {
992
+ const bits = this.bitCounter[this.bitCounter.length - 1];
993
+ const dt = (this.bitsSince[this.bitsSince.length - 1] - this.bitsSince[0]) / 1000;
994
+ if (dt > 0) {
995
+ this.currentBps = Math.round(bits / dt);
996
+ }
997
+ }
998
+ }
999
+
1000
+ private startTelemetry(): void {
1001
+ if (!this.analyticsConfig.enabled || !this.analyticsConfig.endpoint) return;
1002
+
1003
+ const endpoint = this.analyticsConfig.endpoint;
1004
+
1005
+ this.analyticsTimer = setInterval(async () => {
1006
+ if (!this.videoElement) return;
1007
+
1008
+ const stats = await this.getStats();
1009
+ const payload = {
1010
+ t: Date.now(),
1011
+ bps: stats.currentBps || 0,
1012
+ waiting: stats.waitingEvents || 0
1013
+ };
1014
+
1015
+ try {
1016
+ await fetch(endpoint, {
1017
+ method: 'POST',
1018
+ headers: { 'Content-Type': 'application/json' },
1019
+ body: JSON.stringify(payload)
1020
+ });
1021
+ } catch {}
1022
+ }, 5000);
1023
+ }
1024
+
1025
+ async destroy(): Promise<void> {
1026
+ console.debug('[MEWS] destroy() called');
1027
+ this.isDestroyed = true;
1028
+ this.isReady = false;
1029
+ this.readyResolvers = [];
1030
+
1031
+ if (this.analyticsTimer) {
1032
+ clearInterval(this.analyticsTimer);
1033
+ this.analyticsTimer = null;
1034
+ }
1035
+
1036
+ // CRITICAL: Close WebSocket FIRST to stop all network activity immediately
1037
+ // Don't send 'stop' message - it can trigger retry logic and delay cleanup
1038
+ this.wsManager?.destroy();
1039
+ this.wsManager = null;
1040
+
1041
+ this.sbManager?.destroy();
1042
+ this.sbManager = null;
1043
+
1044
+ if (this.mediaSource?.readyState === 'open') {
1045
+ try {
1046
+ this.mediaSource.endOfStream();
1047
+ } catch {}
1048
+ }
1049
+
1050
+ if (this.objectUrl) {
1051
+ URL.revokeObjectURL(this.objectUrl);
1052
+ this.objectUrl = null;
1053
+ }
1054
+
1055
+ if (this.videoElement && this.container) {
1056
+ try { this.container.removeChild(this.videoElement); } catch {}
1057
+ }
1058
+
1059
+ this.videoElement = null;
1060
+ this.container = null;
1061
+ this.mediaSource = null;
1062
+ this.listeners.clear();
1063
+ console.debug('[MEWS] destroy() completed');
1064
+ }
1065
+ }