@ermis-network/ermis-chat-sdk 1.0.9 → 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/bin/init-call.js +9 -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 +20399 -6823
- package/dist/index.browser.cjs.map +1 -1
- package/dist/index.browser.full-bundle.min.js +20 -18
- package/dist/index.browser.full-bundle.min.js.map +1 -1
- package/dist/index.browser.mjs +20315 -6790
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.cjs +20400 -6824
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +167 -1356
- package/dist/index.d.ts +167 -1356
- package/dist/index.mjs +20312 -6787
- package/dist/index.mjs.map +1 -1
- package/dist/wasm_worker.worker.mjs +1600 -0
- package/dist/wasm_worker.worker.mjs.map +1 -0
- package/package.json +22 -7
- package/public/e2ee-media-stream-worker.js +627 -0
- package/public/ermis_call_node_wasm_bg.wasm +0 -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 -1806
- package/src/channel_state.ts +0 -607
- package/src/client.ts +0 -1617
- package/src/client_state.ts +0 -55
- package/src/connection.ts +0 -587
- package/src/ermis_call_node.ts +0 -978
- 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 -16
- package/src/media_stream_receiver.ts +0 -525
- package/src/media_stream_sender.ts +0 -400
- package/src/shims/empty.ts +0 -1
- package/src/signal_message.ts +0 -146
- package/src/system_message.ts +0 -117
- package/src/token_manager.ts +0 -48
- package/src/types.ts +0 -581
- package/src/utils.ts +0 -534
- package/src/wasm/ermis_call_node_wasm.d.ts +0 -154
- package/src/wasm/ermis_call_node_wasm.js +0 -1498
|
@@ -1,525 +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
|
-
// Các biến cấu hình Audio Sync
|
|
6
|
-
const MAX_AUDIO_LATENCY = 0.1; // 100ms
|
|
7
|
-
const MIN_BUFFER_AHEAD = 0.02; // 20ms
|
|
8
|
-
|
|
9
|
-
export class MediaStreamReceiver {
|
|
10
|
-
private videoDecoder: VideoDecoder | null = null;
|
|
11
|
-
private audioDecoder: AudioDecoder | null = null;
|
|
12
|
-
|
|
13
|
-
// WritableStreamDefaultWriter<VideoFrame> là type chuẩn của WebCodecs
|
|
14
|
-
private videoWriter: WritableStreamDefaultWriter<VideoFrame> | null = null;
|
|
15
|
-
|
|
16
|
-
private audioContext: AudioContext | null = null;
|
|
17
|
-
private mediaDestination: MediaStreamAudioDestinationNode | null = null;
|
|
18
|
-
|
|
19
|
-
private isWaitingForKeyFrame: boolean = true;
|
|
20
|
-
private nextStartTime: number = 0;
|
|
21
|
-
private lastVideoConfig: VideoConfig | null = null;
|
|
22
|
-
|
|
23
|
-
private nodeCall: INodeCall;
|
|
24
|
-
private events: IMediaReceiverEvents;
|
|
25
|
-
|
|
26
|
-
private generatedStream: MediaStream | null = null;
|
|
27
|
-
|
|
28
|
-
constructor(nodeCall: INodeCall, events: IMediaReceiverEvents = {}) {
|
|
29
|
-
this.nodeCall = nodeCall;
|
|
30
|
-
this.events = events;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
public async acceptConnection(): Promise<void> {
|
|
34
|
-
try {
|
|
35
|
-
await this.nodeCall.acceptConnection();
|
|
36
|
-
} catch (error) {
|
|
37
|
-
console.error('❌ Error starting MediaStreamReceiver:', error);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
public getRemoteStream = (): MediaStream | null => {
|
|
42
|
-
return this.generatedStream;
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Dừng toàn bộ quá trình và giải phóng tài nguyên
|
|
47
|
-
*/
|
|
48
|
-
public stop = (): void => {
|
|
49
|
-
this.resetDecoders();
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
// ================= PRIVATE METHODS =================
|
|
53
|
-
|
|
54
|
-
private initAudioContext = async (): Promise<void> => {
|
|
55
|
-
if (!this.audioContext || this.audioContext.state === 'closed') {
|
|
56
|
-
const AudioContextClass = window.AudioContext || (window as any).webkitAudioContext;
|
|
57
|
-
this.audioContext = new AudioContextClass({
|
|
58
|
-
sampleRate: 48000,
|
|
59
|
-
latencyHint: 'interactive',
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
this.mediaDestination = this.audioContext.createMediaStreamDestination();
|
|
63
|
-
|
|
64
|
-
if (this.audioContext.state === 'suspended') {
|
|
65
|
-
await this.audioContext.resume();
|
|
66
|
-
}
|
|
67
|
-
this.nextStartTime = this.audioContext.currentTime + MIN_BUFFER_AHEAD;
|
|
68
|
-
}
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
public initDecoders = (callType: string): void => {
|
|
72
|
-
// 1. Audio Decoder Setup
|
|
73
|
-
if (this.audioDecoder) this.audioDecoder.close();
|
|
74
|
-
|
|
75
|
-
// 2. Setup Audio Context & Streams
|
|
76
|
-
this.initAudioContext();
|
|
77
|
-
|
|
78
|
-
// Đảm bảo mediaDestination đã được tạo trong initAudioContext
|
|
79
|
-
if (this.mediaDestination) {
|
|
80
|
-
const audioTrack = this.mediaDestination.stream.getAudioTracks()[0];
|
|
81
|
-
|
|
82
|
-
// Khởi tạo stream với 1 track audio
|
|
83
|
-
this.generatedStream = new MediaStream([audioTrack]);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
this.isWaitingForKeyFrame = true;
|
|
87
|
-
|
|
88
|
-
// 3. Init AudioDecoder
|
|
89
|
-
this.audioDecoder = new AudioDecoder({
|
|
90
|
-
output: (audioData) => this.playDecodedAudio(audioData),
|
|
91
|
-
error: (err) => console.error('AudioDecoder error:', err),
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
if (callType === 'video') {
|
|
95
|
-
// 4. Setup VideoDecoder
|
|
96
|
-
this.setupVideoDecoder();
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
this.receiveLoop();
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
public setupVideoDecoder = (): void => {
|
|
103
|
-
if (!this.videoWriter) return;
|
|
104
|
-
|
|
105
|
-
if (this.videoDecoder) {
|
|
106
|
-
try {
|
|
107
|
-
if (this.videoDecoder.state !== 'closed') this.videoDecoder.close();
|
|
108
|
-
} catch (e) {
|
|
109
|
-
/* ignore */
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const videoDecoder = new VideoDecoder({
|
|
114
|
-
output: async (frame) => {
|
|
115
|
-
try {
|
|
116
|
-
if (!this.videoWriter) {
|
|
117
|
-
frame.close();
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Backpressure check: Nếu writer đang bận, drop frame để tránh overflow
|
|
122
|
-
if (this.videoWriter.desiredSize! <= 0) {
|
|
123
|
-
frame.close();
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
await this.videoWriter.write(frame);
|
|
128
|
-
} catch (err) {
|
|
129
|
-
frame.close();
|
|
130
|
-
// console.error('Frame write error:', err);
|
|
131
|
-
} finally {
|
|
132
|
-
frame.close();
|
|
133
|
-
}
|
|
134
|
-
},
|
|
135
|
-
error: (err) => {
|
|
136
|
-
console.error('❌ VideoDecoder CRASHED:', err);
|
|
137
|
-
this.isWaitingForKeyFrame = true;
|
|
138
|
-
|
|
139
|
-
if (this.videoWriter) {
|
|
140
|
-
// Chỉ hồi sinh nếu Writer vẫn còn sống
|
|
141
|
-
console.log('♻️ Attempting to respawn VideoDecoder...');
|
|
142
|
-
this.setupVideoDecoder();
|
|
143
|
-
if (this.lastVideoConfig && this.videoDecoder) {
|
|
144
|
-
try {
|
|
145
|
-
this.videoDecoder.configure(this.lastVideoConfig);
|
|
146
|
-
} catch (configErr) {}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
},
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
this.videoDecoder = videoDecoder;
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
private playDecodedAudio = (audioData: AudioData): void => {
|
|
156
|
-
try {
|
|
157
|
-
if (!this.audioContext || !this.mediaDestination) {
|
|
158
|
-
audioData.close();
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const { numberOfChannels, numberOfFrames, sampleRate } = audioData;
|
|
163
|
-
const duration = numberOfFrames / sampleRate;
|
|
164
|
-
const currentTime = this.audioContext.currentTime;
|
|
165
|
-
|
|
166
|
-
// --- XỬ LÝ LATENCY & SYNC ---
|
|
167
|
-
if (this.nextStartTime < currentTime) {
|
|
168
|
-
this.nextStartTime = currentTime;
|
|
169
|
-
} else if (this.nextStartTime > currentTime + MAX_AUDIO_LATENCY) {
|
|
170
|
-
// Nếu buffer quá lớn (latency cao), reset về thời điểm hiện tại
|
|
171
|
-
this.nextStartTime = currentTime + MIN_BUFFER_AHEAD;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const audioBuffer = this.audioContext.createBuffer(numberOfChannels, numberOfFrames, sampleRate);
|
|
175
|
-
const size = numberOfChannels * numberOfFrames;
|
|
176
|
-
const tempBuffer = new Float32Array(size);
|
|
177
|
-
|
|
178
|
-
audioData.copyTo(tempBuffer, { planeIndex: 0, format: 'f32-planar' });
|
|
179
|
-
|
|
180
|
-
for (let ch = 0; ch < numberOfChannels; ch++) {
|
|
181
|
-
const channelData = tempBuffer.subarray(ch * numberOfFrames, (ch + 1) * numberOfFrames);
|
|
182
|
-
audioBuffer.copyToChannel(channelData, ch);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const source = this.audioContext.createBufferSource();
|
|
186
|
-
source.buffer = audioBuffer;
|
|
187
|
-
source.connect(this.mediaDestination);
|
|
188
|
-
|
|
189
|
-
source.start(this.nextStartTime);
|
|
190
|
-
this.nextStartTime += duration;
|
|
191
|
-
|
|
192
|
-
audioData.close();
|
|
193
|
-
} catch (err) {
|
|
194
|
-
console.error('Error in playDecodedAudio:', err);
|
|
195
|
-
audioData?.close();
|
|
196
|
-
}
|
|
197
|
-
};
|
|
198
|
-
|
|
199
|
-
private newCodecFromDescription = (buffer: ArrayBuffer, receivedCodec: string): string => {
|
|
200
|
-
// Chỉ check nếu codec là HEVC (hvc1 hoặc hev1)
|
|
201
|
-
if (!receivedCodec.toLowerCase().includes('hvc') && !receivedCodec.toLowerCase().includes('hev')) return '';
|
|
202
|
-
|
|
203
|
-
try {
|
|
204
|
-
const record = HEVCDecoderConfigurationRecord.demux(buffer);
|
|
205
|
-
return record.toCodecString();
|
|
206
|
-
} catch (error) {
|
|
207
|
-
return '';
|
|
208
|
-
}
|
|
209
|
-
};
|
|
210
|
-
|
|
211
|
-
// Vòng lặp chính xử lý dữ liệu
|
|
212
|
-
public receiveLoop = async (): Promise<void> => {
|
|
213
|
-
const textDecoder = new TextDecoder();
|
|
214
|
-
|
|
215
|
-
while (true) {
|
|
216
|
-
try {
|
|
217
|
-
if (!this.nodeCall) break;
|
|
218
|
-
|
|
219
|
-
// Gọi hàm nhận dữ liệu async
|
|
220
|
-
const data = await this.nodeCall.asyncRecv();
|
|
221
|
-
|
|
222
|
-
const dataView = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
223
|
-
const frameType = dataView.getUint8(0);
|
|
224
|
-
|
|
225
|
-
// const frameTypeName =
|
|
226
|
-
// {
|
|
227
|
-
// [FRAME_TYPE.VIDEO_CONFIG]: 'VIDEO_CONFIG',
|
|
228
|
-
// [FRAME_TYPE.AUDIO_CONFIG]: 'AUDIO_CONFIG',
|
|
229
|
-
// [FRAME_TYPE.VIDEO_KEY]: 'VIDEO_KEY',
|
|
230
|
-
// [FRAME_TYPE.VIDEO_DELTA]: 'VIDEO_DELTA',
|
|
231
|
-
// [FRAME_TYPE.AUDIO]: 'AUDIO',
|
|
232
|
-
// [FRAME_TYPE.CONNECTED]: 'CONNECTED',
|
|
233
|
-
// [FRAME_TYPE.TRANSCEIVER_STATE]: 'TRANSCEIVER_STATE',
|
|
234
|
-
// [FRAME_TYPE.ORIENTATION]: 'ORIENTATION',
|
|
235
|
-
// [FRAME_TYPE.REQUEST_CONFIG]: 'REQUEST_CONFIG',
|
|
236
|
-
// [FRAME_TYPE.REQUEST_KEY_FRAME]: 'REQUEST_KEY_FRAME',
|
|
237
|
-
// [FRAME_TYPE.END_CALL]: 'END_CALL',
|
|
238
|
-
// }[frameType] || 'UNKNOWN';
|
|
239
|
-
|
|
240
|
-
// console.log(`----frameType ${frameTypeName}----`, frameType);
|
|
241
|
-
|
|
242
|
-
const payloadOffset = (
|
|
243
|
-
[
|
|
244
|
-
FRAME_TYPE.VIDEO_CONFIG,
|
|
245
|
-
FRAME_TYPE.AUDIO_CONFIG,
|
|
246
|
-
FRAME_TYPE.CONNECTED,
|
|
247
|
-
FRAME_TYPE.TRANSCEIVER_STATE,
|
|
248
|
-
FRAME_TYPE.ORIENTATION,
|
|
249
|
-
FRAME_TYPE.REQUEST_CONFIG,
|
|
250
|
-
FRAME_TYPE.REQUEST_KEY_FRAME,
|
|
251
|
-
FRAME_TYPE.END_CALL,
|
|
252
|
-
] as number[]
|
|
253
|
-
).includes(frameType)
|
|
254
|
-
? 1
|
|
255
|
-
: 9; // 1 byte type + 8 bytes timestamp
|
|
256
|
-
|
|
257
|
-
const payload = new Uint8Array(data.buffer, data.byteOffset + payloadOffset, data.byteLength - payloadOffset);
|
|
258
|
-
|
|
259
|
-
switch (frameType) {
|
|
260
|
-
// --- VIDEO CONFIG ---
|
|
261
|
-
case FRAME_TYPE.VIDEO_CONFIG: {
|
|
262
|
-
try {
|
|
263
|
-
const videoConfigStr = textDecoder.decode(payload);
|
|
264
|
-
const videoConfig = JSON.parse(videoConfigStr);
|
|
265
|
-
console.log('--videoConfig--', videoConfig);
|
|
266
|
-
|
|
267
|
-
// Setup Video Track Writer & Combine Streams
|
|
268
|
-
if (!this.videoWriter) {
|
|
269
|
-
// @ts-ignore: MediaStreamTrackGenerator types
|
|
270
|
-
const videoTrackGenerator = new MediaStreamTrackGenerator({ kind: 'video' });
|
|
271
|
-
this.videoWriter = videoTrackGenerator.writable.getWriter();
|
|
272
|
-
|
|
273
|
-
// MERGE: Add Video Track vào Stream Audio đã có sẵn
|
|
274
|
-
if (this.generatedStream) {
|
|
275
|
-
this.generatedStream.addTrack(videoTrackGenerator);
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
if (!this.videoDecoder) {
|
|
280
|
-
this.setupVideoDecoder();
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
this.isWaitingForKeyFrame = true;
|
|
284
|
-
|
|
285
|
-
let descriptionBuffer: ArrayBuffer | undefined = undefined;
|
|
286
|
-
if (videoConfig.description) {
|
|
287
|
-
try {
|
|
288
|
-
const binaryString = atob(videoConfig.description);
|
|
289
|
-
const desc = new Uint8Array(binaryString.length);
|
|
290
|
-
for (let i = 0; i < binaryString.length; i++) {
|
|
291
|
-
desc[i] = binaryString.charCodeAt(i);
|
|
292
|
-
}
|
|
293
|
-
descriptionBuffer = desc.buffer;
|
|
294
|
-
} catch (e) {}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
// const newCodec = replaceCodecNumber(videoConfig.codec);
|
|
298
|
-
|
|
299
|
-
let newCodec: string = '';
|
|
300
|
-
if (descriptionBuffer) {
|
|
301
|
-
newCodec =
|
|
302
|
-
this.newCodecFromDescription(descriptionBuffer, videoConfig.codec) ||
|
|
303
|
-
replaceCodecNumber(videoConfig.codec);
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
const decoderConfig: VideoConfig = {
|
|
307
|
-
codec: newCodec,
|
|
308
|
-
frameRate: videoConfig.frameRate,
|
|
309
|
-
codedWidth: videoConfig.codedWidth,
|
|
310
|
-
codedHeight: videoConfig.codedHeight,
|
|
311
|
-
...(videoConfig.orientation && { rotation: videoConfig.orientation }),
|
|
312
|
-
...(descriptionBuffer && { description: descriptionBuffer }),
|
|
313
|
-
};
|
|
314
|
-
|
|
315
|
-
this.lastVideoConfig = decoderConfig;
|
|
316
|
-
|
|
317
|
-
if (this.videoDecoder && this.videoDecoder.state !== 'closed') {
|
|
318
|
-
const support = await VideoDecoder.isConfigSupported(decoderConfig);
|
|
319
|
-
if (support.supported) {
|
|
320
|
-
this.videoDecoder.configure(decoderConfig);
|
|
321
|
-
this.isWaitingForKeyFrame = true;
|
|
322
|
-
} else {
|
|
323
|
-
console.error('❌ Browser does not support this video config:', decoderConfig);
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
} catch (error) {
|
|
327
|
-
console.error('❌ Error processing VIDEO_CONFIG:', error);
|
|
328
|
-
}
|
|
329
|
-
break;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
// --- AUDIO CONFIG ---
|
|
333
|
-
case FRAME_TYPE.AUDIO_CONFIG: {
|
|
334
|
-
const audioConfig = JSON.parse(textDecoder.decode(payload));
|
|
335
|
-
console.log('--audioConfig--', audioConfig);
|
|
336
|
-
|
|
337
|
-
if (this.audioDecoder?.state !== 'closed') {
|
|
338
|
-
this.audioDecoder?.configure({
|
|
339
|
-
codec: audioConfig.codec,
|
|
340
|
-
sampleRate: audioConfig.sampleRate,
|
|
341
|
-
numberOfChannels: audioConfig.numberOfChannels,
|
|
342
|
-
});
|
|
343
|
-
}
|
|
344
|
-
break;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
// --- VIDEO DATA ---
|
|
348
|
-
case FRAME_TYPE.VIDEO_KEY:
|
|
349
|
-
case FRAME_TYPE.VIDEO_DELTA: {
|
|
350
|
-
if (!this.videoDecoder || this.videoDecoder.state !== 'configured') break;
|
|
351
|
-
const isKeyFrame = frameType === FRAME_TYPE.VIDEO_KEY;
|
|
352
|
-
|
|
353
|
-
if (this.isWaitingForKeyFrame) {
|
|
354
|
-
if (!isKeyFrame) break;
|
|
355
|
-
console.log('✅ Resumed decoding at KeyFrame');
|
|
356
|
-
this.isWaitingForKeyFrame = false;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
if (!isKeyFrame && this.videoDecoder.decodeQueueSize > 15) {
|
|
360
|
-
console.warn('⚠️ Queue > 15. Dropping & Waiting for KeyFrame...');
|
|
361
|
-
// Nếu drop bất kỳ frame nào, ta phải chờ Key Frame tiếp theo mới decode được
|
|
362
|
-
this.isWaitingForKeyFrame = true;
|
|
363
|
-
break;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
// const timestamp = dataView.getUint32(1, false);
|
|
367
|
-
const timestampBigInt = dataView.getBigUint64(1, false);
|
|
368
|
-
const timestamp = Number(timestampBigInt);
|
|
369
|
-
|
|
370
|
-
try {
|
|
371
|
-
this.videoDecoder.decode(
|
|
372
|
-
new EncodedVideoChunk({
|
|
373
|
-
type: isKeyFrame ? 'key' : 'delta',
|
|
374
|
-
timestamp: timestamp,
|
|
375
|
-
data: payload,
|
|
376
|
-
}),
|
|
377
|
-
);
|
|
378
|
-
} catch (decodeErr) {
|
|
379
|
-
console.error('Video decode failed:', decodeErr);
|
|
380
|
-
if ((this.videoDecoder as VideoDecoder).state === 'closed') {
|
|
381
|
-
// this.setupVideoDecoder();
|
|
382
|
-
// if (this.lastVideoConfig) {
|
|
383
|
-
// this.videoDecoder?.configure(this.lastVideoConfig);
|
|
384
|
-
// }
|
|
385
|
-
this.isWaitingForKeyFrame = true;
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
break;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
// --- AUDIO DATA ---
|
|
392
|
-
case FRAME_TYPE.AUDIO: {
|
|
393
|
-
if (this.audioDecoder?.state === 'configured') {
|
|
394
|
-
// const timestamp = dataView.getUint32(1, false);
|
|
395
|
-
const timestampBigInt = dataView.getBigUint64(1, false);
|
|
396
|
-
const timestamp = Number(timestampBigInt);
|
|
397
|
-
this.audioDecoder.decode(
|
|
398
|
-
new EncodedAudioChunk({
|
|
399
|
-
type: 'key',
|
|
400
|
-
timestamp: timestamp,
|
|
401
|
-
data: payload,
|
|
402
|
-
}),
|
|
403
|
-
);
|
|
404
|
-
}
|
|
405
|
-
break;
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
case FRAME_TYPE.CONNECTED:
|
|
409
|
-
if (this.events.onConnected) {
|
|
410
|
-
this.events.onConnected();
|
|
411
|
-
}
|
|
412
|
-
break;
|
|
413
|
-
|
|
414
|
-
case FRAME_TYPE.TRANSCEIVER_STATE:
|
|
415
|
-
const transceiverState = JSON.parse(textDecoder.decode(payload));
|
|
416
|
-
if (this.events.onTransceiverState) {
|
|
417
|
-
this.events.onTransceiverState(transceiverState);
|
|
418
|
-
}
|
|
419
|
-
break;
|
|
420
|
-
|
|
421
|
-
case FRAME_TYPE.ORIENTATION: {
|
|
422
|
-
const orientation = dataView.getInt32(1, false);
|
|
423
|
-
if (this.videoDecoder && this.lastVideoConfig && this.lastVideoConfig.rotation !== orientation) {
|
|
424
|
-
this.lastVideoConfig.rotation = orientation;
|
|
425
|
-
try {
|
|
426
|
-
// 1. Configure lại với rotation mới
|
|
427
|
-
this.videoDecoder.configure(this.lastVideoConfig);
|
|
428
|
-
|
|
429
|
-
// 2. QUAN TRỌNG: Phải chờ KeyFrame mới để tránh lỗi decode Delta frame sau khi reset
|
|
430
|
-
this.isWaitingForKeyFrame = true;
|
|
431
|
-
|
|
432
|
-
console.log('🔄 Reconfigured rotation to', orientation, '- Waiting for next KeyFrame');
|
|
433
|
-
} catch (configErr) {
|
|
434
|
-
console.error('Error reconfiguring VideoDecoder with new orientation:', configErr);
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
break;
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
case FRAME_TYPE.REQUEST_CONFIG:
|
|
441
|
-
console.log('📥 Received REQUEST_CONFIG');
|
|
442
|
-
if (this.events.onRequestConfig) {
|
|
443
|
-
this.events.onRequestConfig();
|
|
444
|
-
}
|
|
445
|
-
break;
|
|
446
|
-
|
|
447
|
-
case FRAME_TYPE.REQUEST_KEY_FRAME:
|
|
448
|
-
console.log('📥 Received REQUEST_KEY_FRAME');
|
|
449
|
-
if (this.events.onRequestKeyFrame) {
|
|
450
|
-
this.events.onRequestKeyFrame();
|
|
451
|
-
}
|
|
452
|
-
break;
|
|
453
|
-
|
|
454
|
-
case FRAME_TYPE.END_CALL:
|
|
455
|
-
console.log('📥 Received END_CALL');
|
|
456
|
-
if (this.events.onEndCall) {
|
|
457
|
-
this.events.onEndCall();
|
|
458
|
-
}
|
|
459
|
-
break;
|
|
460
|
-
|
|
461
|
-
default:
|
|
462
|
-
console.warn('❓ Unknown frame type received:', frameType);
|
|
463
|
-
break;
|
|
464
|
-
}
|
|
465
|
-
} catch (error) {
|
|
466
|
-
console.error('Stream loop error', error);
|
|
467
|
-
// Có thể thêm delay nhỏ ở đây để tránh spam error nếu loop lỗi liên tục
|
|
468
|
-
await new Promise((r) => setTimeout(r, 200));
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
};
|
|
472
|
-
|
|
473
|
-
private resetDecoders = (): void => {
|
|
474
|
-
if (this.videoWriter) {
|
|
475
|
-
try {
|
|
476
|
-
this.videoWriter.abort('Stream stopped').catch(() => {});
|
|
477
|
-
this.videoWriter.releaseLock();
|
|
478
|
-
} catch (e) {
|
|
479
|
-
console.warn('Error closing video writer:', e);
|
|
480
|
-
}
|
|
481
|
-
this.videoWriter = null;
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
if (this.videoDecoder) {
|
|
485
|
-
try {
|
|
486
|
-
if (this.videoDecoder.state !== 'closed') {
|
|
487
|
-
this.videoDecoder.reset();
|
|
488
|
-
this.videoDecoder.close();
|
|
489
|
-
}
|
|
490
|
-
} catch (e) {}
|
|
491
|
-
this.videoDecoder = null;
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
if (this.audioDecoder) {
|
|
495
|
-
try {
|
|
496
|
-
if (this.audioDecoder.state !== 'closed') {
|
|
497
|
-
this.audioDecoder.reset();
|
|
498
|
-
this.audioDecoder.close();
|
|
499
|
-
}
|
|
500
|
-
} catch (e) {}
|
|
501
|
-
this.audioDecoder = null;
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
// Đóng AudioContext
|
|
505
|
-
if (this.audioContext) {
|
|
506
|
-
try {
|
|
507
|
-
this.audioContext.close();
|
|
508
|
-
} catch (e) {
|
|
509
|
-
console.warn('Error closing audio context:', e);
|
|
510
|
-
}
|
|
511
|
-
this.audioContext = null;
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
// Reset các biến
|
|
515
|
-
this.isWaitingForKeyFrame = true;
|
|
516
|
-
this.mediaDestination = null;
|
|
517
|
-
this.nextStartTime = 0;
|
|
518
|
-
this.lastVideoConfig = null;
|
|
519
|
-
|
|
520
|
-
if (this.generatedStream) {
|
|
521
|
-
this.generatedStream.getTracks().forEach((track) => track.stop());
|
|
522
|
-
this.generatedStream = null;
|
|
523
|
-
}
|
|
524
|
-
};
|
|
525
|
-
}
|