@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.
- package/README.md +330 -0
- package/dist/encryption/index.browser.cjs +13045 -0
- package/dist/encryption/index.browser.cjs.map +1 -0
- package/dist/encryption/index.browser.mjs +12959 -0
- package/dist/encryption/index.browser.mjs.map +1 -0
- package/dist/encryption/index.cjs +13045 -0
- package/dist/encryption/index.cjs.map +1 -0
- package/dist/encryption/index.d.mts +3 -0
- package/dist/encryption/index.d.ts +3 -0
- package/dist/encryption/index.mjs +12959 -0
- package/dist/encryption/index.mjs.map +1 -0
- package/dist/index-CcvHIY5q.d.mts +4988 -0
- package/dist/index-CcvHIY5q.d.ts +4988 -0
- package/dist/index.browser.cjs +20192 -5766
- package/dist/index.browser.cjs.map +1 -1
- package/dist/index.browser.full-bundle.min.js +20 -16
- package/dist/index.browser.full-bundle.min.js.map +1 -1
- package/dist/index.browser.mjs +20106 -5731
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.cjs +20191 -5765
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +15 -1337
- package/dist/index.d.ts +15 -1337
- package/dist/index.mjs +20106 -5731
- package/dist/index.mjs.map +1 -1
- package/dist/wasm_worker.worker.mjs +8 -4
- package/dist/wasm_worker.worker.mjs.map +1 -1
- package/package.json +21 -6
- package/public/e2ee-media-stream-worker.js +627 -0
- package/public/openmls_wasm_bg.wasm +0 -0
- package/src/attachment_utils.ts +0 -148
- package/src/auth.ts +0 -352
- package/src/channel.ts +0 -1879
- package/src/channel_state.ts +0 -612
- package/src/client.ts +0 -1759
- package/src/client_state.ts +0 -55
- package/src/connection.ts +0 -587
- package/src/ermis_call_node.ts +0 -1046
- package/src/errors.ts +0 -60
- package/src/events.ts +0 -46
- package/src/hevc_decoder_config.ts +0 -305
- package/src/index.ts +0 -17
- package/src/media_stream_receiver.ts +0 -593
- package/src/media_stream_sender.ts +0 -465
- package/src/shims/empty.ts +0 -1
- package/src/signal_message.ts +0 -171
- package/src/system_message.ts +0 -259
- package/src/token_manager.ts +0 -48
- package/src/types.ts +0 -594
- package/src/utils.ts +0 -553
- package/src/wasm/ermis_call_node_wasm.d.ts +0 -156
- package/src/wasm/ermis_call_node_wasm.js +0 -1568
- package/src/wasm_worker.ts +0 -219
- 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
|
-
}
|