@ermis-network/ermis-chat-sdk 2.0.0 → 2.0.1

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 (54) hide show
  1. package/README.md +330 -0
  2. package/dist/encryption/index.browser.cjs +13045 -0
  3. package/dist/encryption/index.browser.cjs.map +1 -0
  4. package/dist/encryption/index.browser.mjs +12959 -0
  5. package/dist/encryption/index.browser.mjs.map +1 -0
  6. package/dist/encryption/index.cjs +13045 -0
  7. package/dist/encryption/index.cjs.map +1 -0
  8. package/dist/encryption/index.d.mts +3 -0
  9. package/dist/encryption/index.d.ts +3 -0
  10. package/dist/encryption/index.mjs +12959 -0
  11. package/dist/encryption/index.mjs.map +1 -0
  12. package/dist/index-CcvHIY5q.d.mts +4988 -0
  13. package/dist/index-CcvHIY5q.d.ts +4988 -0
  14. package/dist/index.browser.cjs +20192 -5766
  15. package/dist/index.browser.cjs.map +1 -1
  16. package/dist/index.browser.full-bundle.min.js +20 -16
  17. package/dist/index.browser.full-bundle.min.js.map +1 -1
  18. package/dist/index.browser.mjs +20106 -5731
  19. package/dist/index.browser.mjs.map +1 -1
  20. package/dist/index.cjs +20191 -5765
  21. package/dist/index.cjs.map +1 -1
  22. package/dist/index.d.mts +15 -1337
  23. package/dist/index.d.ts +15 -1337
  24. package/dist/index.mjs +20106 -5731
  25. package/dist/index.mjs.map +1 -1
  26. package/dist/wasm_worker.worker.mjs +8 -4
  27. package/dist/wasm_worker.worker.mjs.map +1 -1
  28. package/package.json +21 -6
  29. package/public/e2ee-media-stream-worker.js +627 -0
  30. package/public/openmls_wasm_bg.wasm +0 -0
  31. package/src/attachment_utils.ts +0 -148
  32. package/src/auth.ts +0 -352
  33. package/src/channel.ts +0 -1879
  34. package/src/channel_state.ts +0 -612
  35. package/src/client.ts +0 -1759
  36. package/src/client_state.ts +0 -55
  37. package/src/connection.ts +0 -587
  38. package/src/ermis_call_node.ts +0 -1046
  39. package/src/errors.ts +0 -60
  40. package/src/events.ts +0 -46
  41. package/src/hevc_decoder_config.ts +0 -305
  42. package/src/index.ts +0 -17
  43. package/src/media_stream_receiver.ts +0 -593
  44. package/src/media_stream_sender.ts +0 -465
  45. package/src/shims/empty.ts +0 -1
  46. package/src/signal_message.ts +0 -171
  47. package/src/system_message.ts +0 -259
  48. package/src/token_manager.ts +0 -48
  49. package/src/types.ts +0 -594
  50. package/src/utils.ts +0 -553
  51. package/src/wasm/ermis_call_node_wasm.d.ts +0 -156
  52. package/src/wasm/ermis_call_node_wasm.js +0 -1568
  53. package/src/wasm_worker.ts +0 -219
  54. package/src/wasm_worker_proxy.ts +0 -244
@@ -1,593 +0,0 @@
1
- import { replaceCodecNumber } from './utils';
2
- import { FRAME_TYPE, IMediaReceiverEvents, INodeCall, VideoConfig } from './types';
3
- import { HEVCDecoderConfigurationRecord } from './hevc_decoder_config';
4
-
5
- const MAX_AUDIO_LATENCY = 0.5; // 500ms
6
- const MIN_BUFFER_AHEAD = 0.05; // 50ms
7
-
8
- export class MediaStreamReceiver {
9
- private videoDecoder: VideoDecoder | null = null;
10
- private audioDecoder: AudioDecoder | null = null;
11
-
12
- // WritableStreamDefaultWriter<VideoFrame> là type chuẩn của WebCodecs
13
- private videoWriter: WritableStreamDefaultWriter<VideoFrame> | null = null;
14
-
15
- private audioContext: AudioContext | null = null;
16
- private mediaDestination: MediaStreamAudioDestinationNode | null = null;
17
- private scheduledAudioNodes: AudioBufferSourceNode[] = [];
18
-
19
- private isWaitingForKeyFrame: boolean = true;
20
- private nextStartTime: number = 0;
21
- private lastVideoConfig: VideoConfig | null = null;
22
- private lastVideoConfigStr: string = '';
23
- private lastAudioConfigStr: string = '';
24
-
25
- private nodeCall: INodeCall;
26
- private events: IMediaReceiverEvents;
27
-
28
- private generatedStream: MediaStream | null = null;
29
-
30
- constructor(nodeCall: INodeCall, events: IMediaReceiverEvents = {}) {
31
- this.nodeCall = nodeCall;
32
- this.events = events;
33
- }
34
-
35
- public async acceptConnection(): Promise<void> {
36
- try {
37
- await this.nodeCall.acceptConnection();
38
- } catch (error) {
39
- console.error('❌ Error starting MediaStreamReceiver:', error);
40
- }
41
- }
42
-
43
- public getRemoteStream = (): MediaStream | null => {
44
- return this.generatedStream;
45
- };
46
-
47
- /**
48
- * Dừng toàn bộ quá trình và giải phóng tài nguyên
49
- */
50
- public stop = (): void => {
51
- this.resetDecoders();
52
- };
53
-
54
- // ================= PRIVATE METHODS =================
55
-
56
- private initAudioContext = async (): Promise<void> => {
57
- if (!this.audioContext || this.audioContext.state === 'closed') {
58
- const AudioContextClass = window.AudioContext || (window as any).webkitAudioContext;
59
- this.audioContext = new AudioContextClass({
60
- sampleRate: 48000,
61
- latencyHint: 'interactive',
62
- });
63
-
64
- this.mediaDestination = this.audioContext.createMediaStreamDestination();
65
-
66
- if (this.audioContext.state === 'suspended') {
67
- await this.audioContext.resume();
68
- }
69
- this.nextStartTime = this.audioContext.currentTime + MIN_BUFFER_AHEAD;
70
- }
71
- };
72
-
73
- public initDecoders = (callType: string): void => {
74
- // 1. Audio Decoder Setup
75
- if (this.audioDecoder) this.audioDecoder.close();
76
-
77
- // 2. Setup Audio Context & Streams
78
- this.initAudioContext();
79
-
80
- // Đảm bảo mediaDestination đã được tạo trong initAudioContext
81
- if (this.mediaDestination) {
82
- const audioTrack = this.mediaDestination.stream.getAudioTracks()[0];
83
-
84
- // Khởi tạo stream với 1 track audio
85
- this.generatedStream = new MediaStream([audioTrack]);
86
- }
87
-
88
- this.isWaitingForKeyFrame = true;
89
-
90
- // 3. Init AudioDecoder
91
- this.audioDecoder = new AudioDecoder({
92
- output: (audioData) => this.playDecodedAudio(audioData),
93
- error: (err) => console.error('AudioDecoder error:', err),
94
- });
95
-
96
- if (callType === 'video') {
97
- // 4. Setup VideoDecoder
98
- this.setupVideoDecoder();
99
- }
100
-
101
- this.receiveLoop();
102
- };
103
-
104
- public setupVideoDecoder = (): void => {
105
- if (!this.videoWriter) return;
106
-
107
- if (this.videoDecoder) {
108
- try {
109
- if (this.videoDecoder.state !== 'closed') this.videoDecoder.close();
110
- } catch (e) {
111
- /* ignore */
112
- }
113
- }
114
-
115
- const videoDecoder = new VideoDecoder({
116
- output: async (frame) => {
117
- try {
118
- if (!this.videoWriter) {
119
- frame.close();
120
- return;
121
- }
122
-
123
- // Backpressure check: Nếu writer đang bận, drop frame để tránh overflow
124
- if (this.videoWriter.desiredSize! <= 0) {
125
- frame.close();
126
- return;
127
- }
128
-
129
- await this.videoWriter.write(frame);
130
- } catch (err) {
131
- frame.close();
132
- // console.error('Frame write error:', err);
133
- } finally {
134
- frame.close();
135
- }
136
- },
137
- error: (err) => {
138
- console.error('❌ VideoDecoder CRASHED:', err);
139
- this.isWaitingForKeyFrame = true;
140
-
141
- if (this.videoWriter) {
142
- // Tránh hồi sinh ngay lập tức gây vòng lặp vô hạn (Infinite Loop) nếu stream bị hỏng nặng
143
- console.log('♻️ Scheduled VideoDecoder respawn in 1000ms...');
144
- setTimeout(() => {
145
- if (!this.videoWriter) return;
146
- this.setupVideoDecoder();
147
- if (this.lastVideoConfig && this.videoDecoder) {
148
- try {
149
- this.videoDecoder.configure(this.lastVideoConfig);
150
- } catch (configErr) { }
151
- }
152
- }, 1000);
153
- }
154
- },
155
- });
156
-
157
- this.videoDecoder = videoDecoder;
158
- };
159
-
160
- private playDecodedAudio = (audioData: AudioData): void => {
161
- try {
162
- if (!this.audioContext || !this.mediaDestination) {
163
- audioData.close();
164
- return;
165
- }
166
-
167
- const { numberOfChannels, numberOfFrames, sampleRate } = audioData;
168
- const duration = numberOfFrames / sampleRate;
169
- const currentTime = this.audioContext.currentTime;
170
-
171
- // --- XỬ LÝ LATENCY & SYNC ---
172
- if (this.nextStartTime < currentTime) {
173
- this.nextStartTime = currentTime + MIN_BUFFER_AHEAD;
174
- } else if (this.nextStartTime > currentTime + MAX_AUDIO_LATENCY) {
175
- // Nếu buffer quá lớn (latency cao), reset về thời điểm hiện tại cộng khoảng đệm
176
- this.nextStartTime = currentTime + MIN_BUFFER_AHEAD;
177
-
178
- // Dọn dẹp các audio node cũ đang được lên lịch để tránh phát đè (overlap) gây nổ/méo tiếng
179
- this.scheduledAudioNodes.forEach((node) => {
180
- try {
181
- node.stop();
182
- } catch (e) {
183
- /* ignore */
184
- }
185
- });
186
- this.scheduledAudioNodes = [];
187
- }
188
-
189
- const audioBuffer = this.audioContext.createBuffer(numberOfChannels, numberOfFrames, sampleRate);
190
-
191
- // Lấy data đúng cho từng channel đối với âm thanh stereo/đa kênh
192
- for (let ch = 0; ch < numberOfChannels; ch++) {
193
- const channelData = new Float32Array(numberOfFrames);
194
- audioData.copyTo(channelData, { planeIndex: ch, format: 'f32-planar' });
195
- audioBuffer.copyToChannel(channelData, ch);
196
- }
197
-
198
- const source = this.audioContext.createBufferSource();
199
- source.buffer = audioBuffer;
200
- source.connect(this.mediaDestination);
201
-
202
- this.scheduledAudioNodes.push(source);
203
-
204
- // Quick Fix: Cap the buffer size to prevent memory/CPU accumulation on jittery networks (e.g. 3G)
205
- // If we have too many scheduled nodes, it means the network is jittery or the clock is drifting.
206
- // Dropping oldest nodes prevents a total UI freeze after ~1 minute of instability.
207
- if (this.scheduledAudioNodes.length > 100) {
208
- const oldestNode = this.scheduledAudioNodes.shift();
209
- try {
210
- oldestNode?.stop();
211
- oldestNode?.disconnect();
212
- } catch (e) { /* ignore */ }
213
- }
214
-
215
- source.onended = () => {
216
- this.scheduledAudioNodes = this.scheduledAudioNodes.filter((n) => n !== source);
217
- };
218
-
219
- source.start(this.nextStartTime);
220
- this.nextStartTime += duration;
221
-
222
- audioData.close();
223
- } catch (err) {
224
- console.error('Error in playDecodedAudio:', err);
225
- audioData?.close();
226
- }
227
- };
228
-
229
- private newCodecFromDescription = (buffer: ArrayBuffer, receivedCodec: string): string => {
230
- // Chỉ check nếu codec là HEVC (hvc1 hoặc hev1)
231
- if (!receivedCodec.toLowerCase().includes('hvc') && !receivedCodec.toLowerCase().includes('hev')) return '';
232
-
233
- try {
234
- const record = HEVCDecoderConfigurationRecord.demux(buffer);
235
- return record.toCodecString();
236
- } catch (error) {
237
- return '';
238
- }
239
- };
240
-
241
- // Vòng lặp chính xử lý dữ liệu
242
- public receiveLoop = async (): Promise<void> => {
243
- const textDecoder = new TextDecoder();
244
- let lastYieldTime = Date.now();
245
-
246
- while (true) {
247
- // Force a macro-task yield every 16ms (roughly every frame) to prevent Main Thread starvation.
248
- // This is crucial because if nodeCall.asyncRecv() returns data from a local buffer,
249
- // the 'await' might resolve as a micro-task, which blocks UI painting/interaction.
250
- if (Date.now() - lastYieldTime > 16) {
251
- await new Promise((resolve) => setTimeout(resolve, 0));
252
- lastYieldTime = Date.now();
253
- }
254
-
255
- try {
256
- if (!this.nodeCall) break;
257
-
258
- // Gọi hàm nhận dữ liệu async
259
- const data = await this.nodeCall.asyncRecv();
260
-
261
- const dataView = new DataView(data.buffer, data.byteOffset, data.byteLength);
262
- const frameType = dataView.getUint8(0);
263
-
264
- // const frameTypeName =
265
- // {
266
- // [FRAME_TYPE.VIDEO_CONFIG]: 'VIDEO_CONFIG',
267
- // [FRAME_TYPE.AUDIO_CONFIG]: 'AUDIO_CONFIG',
268
- // [FRAME_TYPE.VIDEO_KEY]: 'VIDEO_KEY',
269
- // [FRAME_TYPE.VIDEO_DELTA]: 'VIDEO_DELTA',
270
- // [FRAME_TYPE.AUDIO]: 'AUDIO',
271
- // [FRAME_TYPE.CONNECTED]: 'CONNECTED',
272
- // [FRAME_TYPE.TRANSCEIVER_STATE]: 'TRANSCEIVER_STATE',
273
- // [FRAME_TYPE.ORIENTATION]: 'ORIENTATION',
274
- // [FRAME_TYPE.REQUEST_CONFIG]: 'REQUEST_CONFIG',
275
- // [FRAME_TYPE.REQUEST_KEY_FRAME]: 'REQUEST_KEY_FRAME',
276
- // [FRAME_TYPE.END_CALL]: 'END_CALL',
277
- // }[frameType] || 'UNKNOWN';
278
-
279
- // console.log(`----frameType ${frameTypeName}----`, frameType);
280
-
281
- const payloadOffset = (
282
- [
283
- FRAME_TYPE.VIDEO_CONFIG,
284
- FRAME_TYPE.AUDIO_CONFIG,
285
- FRAME_TYPE.CONNECTED,
286
- FRAME_TYPE.TRANSCEIVER_STATE,
287
- FRAME_TYPE.ORIENTATION,
288
- FRAME_TYPE.REQUEST_CONFIG,
289
- FRAME_TYPE.REQUEST_KEY_FRAME,
290
- FRAME_TYPE.END_CALL,
291
- FRAME_TYPE.HEALTH_CALL,
292
- ] as number[]
293
- ).includes(frameType)
294
- ? 1
295
- : 9; // 1 byte type + 8 bytes timestamp
296
-
297
- const payload = new Uint8Array(data.buffer, data.byteOffset + payloadOffset, data.byteLength - payloadOffset);
298
-
299
- switch (frameType) {
300
- // --- VIDEO CONFIG ---
301
- case FRAME_TYPE.VIDEO_CONFIG: {
302
- try {
303
- const videoConfigStr = textDecoder.decode(payload);
304
-
305
- // Deduplicate: Don't re-configure if config hasn't changed (saves CPU on unstable networks)
306
- if (this.lastVideoConfigStr === videoConfigStr && this.videoDecoder?.state === 'configured') {
307
- break;
308
- }
309
- this.lastVideoConfigStr = videoConfigStr;
310
- const videoConfig = JSON.parse(videoConfigStr);
311
-
312
- console.log('videoConfig', videoConfig);
313
-
314
- // Setup Video Track Writer & Combine Streams
315
- if (!this.videoWriter) {
316
- // @ts-ignore: MediaStreamTrackGenerator types
317
- const videoTrackGenerator = new MediaStreamTrackGenerator({ kind: 'video' });
318
- this.videoWriter = videoTrackGenerator.writable.getWriter();
319
-
320
- // MERGE: Add Video Track vào Stream Audio đã có sẵn
321
- if (this.generatedStream) {
322
- this.generatedStream.addTrack(videoTrackGenerator);
323
- }
324
- }
325
-
326
- if (!this.videoDecoder) {
327
- this.setupVideoDecoder();
328
- }
329
-
330
- this.isWaitingForKeyFrame = true;
331
-
332
- let descriptionBuffer: ArrayBuffer | undefined = undefined;
333
- if (videoConfig.description) {
334
- try {
335
- const binaryString = atob(videoConfig.description);
336
- const desc = new Uint8Array(binaryString.length);
337
- for (let i = 0; i < binaryString.length; i++) {
338
- desc[i] = binaryString.charCodeAt(i);
339
- }
340
- descriptionBuffer = desc.buffer;
341
- } catch (e) {}
342
- }
343
-
344
- // const newCodec = replaceCodecNumber(videoConfig.codec);
345
-
346
- let newCodec: string = '';
347
- if (descriptionBuffer) {
348
- newCodec =
349
- this.newCodecFromDescription(descriptionBuffer, videoConfig.codec) ||
350
- replaceCodecNumber(videoConfig.codec);
351
- }
352
-
353
- const decoderConfig: VideoConfig = {
354
- codec: newCodec,
355
- frameRate: videoConfig.frameRate,
356
- codedWidth: videoConfig.codedWidth,
357
- codedHeight: videoConfig.codedHeight,
358
- ...(videoConfig.orientation && { rotation: videoConfig.orientation }),
359
- ...(descriptionBuffer && { description: descriptionBuffer }),
360
- };
361
-
362
- this.lastVideoConfig = decoderConfig;
363
-
364
- if (this.videoDecoder && this.videoDecoder.state !== 'closed') {
365
- const support = await VideoDecoder.isConfigSupported(decoderConfig);
366
- if (support.supported) {
367
- this.videoDecoder.configure(decoderConfig);
368
- this.isWaitingForKeyFrame = true;
369
- } else {
370
- console.error('❌ Browser does not support this video config:', decoderConfig);
371
- }
372
- }
373
- } catch (error) {
374
- console.error('❌ Error processing VIDEO_CONFIG:', error);
375
- }
376
- break;
377
- }
378
-
379
- // --- AUDIO CONFIG ---
380
- case FRAME_TYPE.AUDIO_CONFIG: {
381
- try {
382
- const audioConfigStr = textDecoder.decode(payload);
383
-
384
- // Deduplicate audio config
385
- if (this.lastAudioConfigStr === audioConfigStr && this.audioDecoder?.state === 'configured') {
386
- break;
387
- }
388
- this.lastAudioConfigStr = audioConfigStr;
389
- const audioConfig = JSON.parse(audioConfigStr);
390
-
391
- console.log('audioConfig', audioConfig);
392
-
393
- if (this.audioDecoder?.state !== 'closed') {
394
- this.audioDecoder?.configure({
395
- codec: audioConfig.codec,
396
- sampleRate: audioConfig.sampleRate,
397
- numberOfChannels: audioConfig.numberOfChannels,
398
- });
399
- }
400
- } catch (e) {
401
- console.error('❌ Error processing AUDIO_CONFIG:', e);
402
- }
403
- break;
404
- }
405
-
406
- // --- VIDEO DATA ---
407
- case FRAME_TYPE.VIDEO_KEY:
408
- case FRAME_TYPE.VIDEO_DELTA: {
409
- if (!this.videoDecoder || this.videoDecoder.state !== 'configured') break;
410
- const isKeyFrame = frameType === FRAME_TYPE.VIDEO_KEY;
411
-
412
- if (this.isWaitingForKeyFrame) {
413
- if (!isKeyFrame) break;
414
- // console.log('✅ Resumed decoding at KeyFrame');
415
- this.isWaitingForKeyFrame = false;
416
- }
417
-
418
- // Stricter Backpressure: Nếu queue > 5, drop ngay lập tức các frame không quan trọng
419
- if (!isKeyFrame && this.videoDecoder.decodeQueueSize > 5) {
420
- this.isWaitingForKeyFrame = true;
421
- break;
422
- }
423
-
424
- // const timestamp = dataView.getUint32(1, false);
425
- const timestampBigInt = dataView.getBigUint64(1, false);
426
- const timestamp = Number(timestampBigInt);
427
-
428
- try {
429
- this.videoDecoder.decode(
430
- new EncodedVideoChunk({
431
- type: isKeyFrame ? 'key' : 'delta',
432
- timestamp: timestamp,
433
- data: payload,
434
- }),
435
- );
436
- } catch (decodeErr) {
437
- console.error('Video decode failed:', decodeErr);
438
- if ((this.videoDecoder as VideoDecoder).state === 'closed') {
439
- // this.setupVideoDecoder();
440
- // if (this.lastVideoConfig) {
441
- // this.videoDecoder?.configure(this.lastVideoConfig);
442
- // }
443
- this.isWaitingForKeyFrame = true;
444
- }
445
- }
446
- break;
447
- }
448
-
449
- // --- AUDIO DATA ---
450
- case FRAME_TYPE.AUDIO: {
451
- if (this.audioDecoder?.state === 'configured') {
452
- // const timestamp = dataView.getUint32(1, false);
453
- const timestampBigInt = dataView.getBigUint64(1, false);
454
- const timestamp = Number(timestampBigInt);
455
- this.audioDecoder.decode(
456
- new EncodedAudioChunk({
457
- type: 'key',
458
- timestamp: timestamp,
459
- data: payload,
460
- }),
461
- );
462
- }
463
- break;
464
- }
465
-
466
- case FRAME_TYPE.CONNECTED:
467
- if (this.events.onConnected) {
468
- this.events.onConnected();
469
- }
470
- break;
471
-
472
- case FRAME_TYPE.TRANSCEIVER_STATE:
473
- const transceiverState = JSON.parse(textDecoder.decode(payload));
474
- if (this.events.onTransceiverState) {
475
- this.events.onTransceiverState(transceiverState);
476
- }
477
- break;
478
-
479
- case FRAME_TYPE.ORIENTATION: {
480
- const orientation = dataView.getInt32(1, false);
481
- if (this.videoDecoder && this.lastVideoConfig && this.lastVideoConfig.rotation !== orientation) {
482
- this.lastVideoConfig.rotation = orientation;
483
- try {
484
- // 1. Configure lại với rotation mới
485
- this.videoDecoder.configure(this.lastVideoConfig);
486
-
487
- // 2. QUAN TRỌNG: Phải chờ KeyFrame mới để tránh lỗi decode Delta frame sau khi reset
488
- this.isWaitingForKeyFrame = true;
489
-
490
- console.log('🔄 Reconfigured rotation to', orientation, '- Waiting for next KeyFrame');
491
- } catch (configErr) {
492
- console.error('Error reconfiguring VideoDecoder with new orientation:', configErr);
493
- }
494
- }
495
- break;
496
- }
497
-
498
- case FRAME_TYPE.REQUEST_CONFIG:
499
- console.log('📥 Received REQUEST_CONFIG');
500
- if (this.events.onRequestConfig) {
501
- this.events.onRequestConfig();
502
- }
503
- break;
504
-
505
- case FRAME_TYPE.REQUEST_KEY_FRAME:
506
- console.log('📥 Received REQUEST_KEY_FRAME');
507
- if (this.events.onRequestKeyFrame) {
508
- this.events.onRequestKeyFrame();
509
- }
510
- break;
511
-
512
- case FRAME_TYPE.END_CALL:
513
- console.log('📥 Received END_CALL');
514
- if (this.events.onEndCall) {
515
- this.events.onEndCall();
516
- }
517
- break;
518
-
519
- case FRAME_TYPE.HEALTH_CALL:
520
- // Keep-alive ping from peer — silently acknowledge
521
- break;
522
-
523
- default:
524
- // Mute unknown frame logs to save CPU and RAM
525
- break;
526
- }
527
- } catch (error) {
528
- // console.error('Stream loop error', error);
529
- await new Promise((r) => setTimeout(r, 200));
530
- }
531
- }
532
- };
533
-
534
- private resetDecoders = (): void => {
535
- if (this.videoWriter) {
536
- try {
537
- this.videoWriter.abort('Stream stopped').catch(() => {});
538
- this.videoWriter.releaseLock();
539
- } catch (e) {
540
- console.warn('Error closing video writer:', e);
541
- }
542
- this.videoWriter = null;
543
- }
544
-
545
- if (this.videoDecoder) {
546
- try {
547
- if (this.videoDecoder.state !== 'closed') {
548
- this.videoDecoder.reset();
549
- this.videoDecoder.close();
550
- }
551
- } catch (e) {}
552
- this.videoDecoder = null;
553
- }
554
-
555
- if (this.audioDecoder) {
556
- try {
557
- if (this.audioDecoder.state !== 'closed') {
558
- this.audioDecoder.reset();
559
- this.audioDecoder.close();
560
- }
561
- } catch (e) {}
562
- this.audioDecoder = null;
563
- }
564
-
565
- // Đóng AudioContext
566
- if (this.audioContext) {
567
- try {
568
- this.audioContext.close();
569
- } catch (e) {
570
- console.warn('Error closing audio context:', e);
571
- }
572
- this.audioContext = null;
573
- }
574
-
575
- this.scheduledAudioNodes.forEach((node) => {
576
- try {
577
- node.stop();
578
- } catch (e) {}
579
- });
580
- this.scheduledAudioNodes = [];
581
-
582
- // Reset các biến
583
- this.isWaitingForKeyFrame = true;
584
- this.mediaDestination = null;
585
- this.nextStartTime = 0;
586
- this.lastVideoConfig = null;
587
-
588
- if (this.generatedStream) {
589
- this.generatedStream.getTracks().forEach((track) => track.stop());
590
- this.generatedStream = null;
591
- }
592
- };
593
- }