@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,465 +0,0 @@
|
|
|
1
|
-
import { base64Encode, createPacketWithHeader } from './utils';
|
|
2
|
-
import { AudioConfig, INodeCall, TransceiverState, VideoConfig } from './types';
|
|
3
|
-
|
|
4
|
-
export class MediaStreamSender {
|
|
5
|
-
private videoEncoder: VideoEncoder | null = null;
|
|
6
|
-
private audioEncoder: AudioEncoder | null = null;
|
|
7
|
-
private videoReader: ReadableStreamDefaultReader<VideoFrame> | null = null;
|
|
8
|
-
private audioReader: ReadableStreamDefaultReader<AudioData> | null = null;
|
|
9
|
-
|
|
10
|
-
private localStream: MediaStream | null = null;
|
|
11
|
-
|
|
12
|
-
private videoConfig: VideoConfig | null = null;
|
|
13
|
-
private audioConfig: AudioConfig | null = null;
|
|
14
|
-
|
|
15
|
-
private videoConfigSent: boolean = false;
|
|
16
|
-
private audioConfigSent: boolean = false;
|
|
17
|
-
|
|
18
|
-
private hasVideo: boolean = false;
|
|
19
|
-
private hasAudio: boolean = false;
|
|
20
|
-
|
|
21
|
-
private forceKeyFrame: boolean = false;
|
|
22
|
-
private isSendingVideo: boolean = false;
|
|
23
|
-
private isSendingAudio: boolean = false;
|
|
24
|
-
|
|
25
|
-
private nodeCall: INodeCall;
|
|
26
|
-
|
|
27
|
-
private healthCallInterval: ReturnType<typeof setInterval> | null = null;
|
|
28
|
-
|
|
29
|
-
constructor(nodeCall: INodeCall) {
|
|
30
|
-
this.nodeCall = nodeCall;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Bắt đầu xử lý MediaStream
|
|
35
|
-
*/
|
|
36
|
-
public async connect(address: string): Promise<void> {
|
|
37
|
-
try {
|
|
38
|
-
await this.nodeCall.connect(address);
|
|
39
|
-
await this.sendConnected();
|
|
40
|
-
await this.sendConfigs();
|
|
41
|
-
|
|
42
|
-
// Start health call keep-alive (every 5s, matching native SDK)
|
|
43
|
-
this.startHealthCallInterval();
|
|
44
|
-
} catch (error) {
|
|
45
|
-
console.error('Error starting MediaStreamSender:', error);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
public async sendConfigs(): Promise<void> {
|
|
50
|
-
try {
|
|
51
|
-
await this.sendTransceiverState(this.hasAudio, this.hasVideo);
|
|
52
|
-
await this.sendAudioConfig();
|
|
53
|
-
|
|
54
|
-
const videoTrack = this.localStream?.getVideoTracks()[0];
|
|
55
|
-
if (videoTrack) {
|
|
56
|
-
await this.sendVideoConfig();
|
|
57
|
-
}
|
|
58
|
-
} catch (error) {
|
|
59
|
-
console.error('Error sending configs:', error);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Dừng và reset encoders
|
|
65
|
-
*/
|
|
66
|
-
public stop = (): void => {
|
|
67
|
-
// Stop health call keep-alive
|
|
68
|
-
if (this.healthCallInterval) {
|
|
69
|
-
clearInterval(this.healthCallInterval);
|
|
70
|
-
this.healthCallInterval = null;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
if (this.videoReader) {
|
|
74
|
-
try {
|
|
75
|
-
this.videoReader.cancel('Stream stopped').catch(() => {});
|
|
76
|
-
} catch (e) {}
|
|
77
|
-
this.videoReader = null;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (this.videoEncoder) {
|
|
81
|
-
try {
|
|
82
|
-
if (this.videoEncoder.state !== 'closed') {
|
|
83
|
-
this.videoEncoder.reset(); // Xả frame
|
|
84
|
-
this.videoEncoder.close();
|
|
85
|
-
}
|
|
86
|
-
} catch (e) {}
|
|
87
|
-
this.videoEncoder = null;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
if (this.audioEncoder) {
|
|
91
|
-
try {
|
|
92
|
-
if (this.audioEncoder.state !== 'closed') {
|
|
93
|
-
this.audioEncoder.reset();
|
|
94
|
-
this.audioEncoder.close();
|
|
95
|
-
}
|
|
96
|
-
} catch (e) {}
|
|
97
|
-
this.audioEncoder = null;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (this.audioReader) {
|
|
101
|
-
try {
|
|
102
|
-
this.audioReader.cancel('Stream stopped').catch(() => {});
|
|
103
|
-
} catch (e) {}
|
|
104
|
-
this.audioReader = null;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Reset configs and flags
|
|
108
|
-
this.videoConfig = null;
|
|
109
|
-
this.audioConfig = null;
|
|
110
|
-
|
|
111
|
-
this.videoConfigSent = false;
|
|
112
|
-
this.audioConfigSent = false;
|
|
113
|
-
this.hasVideo = false;
|
|
114
|
-
this.hasAudio = false;
|
|
115
|
-
|
|
116
|
-
if (this.localStream) {
|
|
117
|
-
this.localStream.getTracks().forEach((track) => track.stop());
|
|
118
|
-
this.localStream = null;
|
|
119
|
-
}
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
public initAudioEncoder = (audioTrack: MediaStreamTrack): void => {
|
|
123
|
-
this.localStream = new MediaStream([audioTrack]);
|
|
124
|
-
this.audioConfigSent = false;
|
|
125
|
-
this.hasAudio = !!audioTrack;
|
|
126
|
-
|
|
127
|
-
const audioEncoder = new AudioEncoder({
|
|
128
|
-
output: (chunk, metadata) => {
|
|
129
|
-
if (metadata?.decoderConfig && !this.audioConfigSent) {
|
|
130
|
-
let description: string | undefined = undefined;
|
|
131
|
-
if (metadata.decoderConfig.description) {
|
|
132
|
-
description = base64Encode(metadata.decoderConfig.description as ArrayBuffer);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
this.audioConfig = {
|
|
136
|
-
codec: metadata.decoderConfig.codec ?? 'opus',
|
|
137
|
-
sampleRate: metadata.decoderConfig.sampleRate ?? 48000,
|
|
138
|
-
numberOfChannels: metadata.decoderConfig.numberOfChannels ?? 1,
|
|
139
|
-
...(description && { description }),
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
this.sendAudioConfig();
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
if (chunk && this.isReadyToSendData('audio')) {
|
|
146
|
-
if (this.isSendingAudio) return; // Backpressure: drop if network is congested
|
|
147
|
-
|
|
148
|
-
this.isSendingAudio = true;
|
|
149
|
-
const data = new ArrayBuffer(chunk.byteLength);
|
|
150
|
-
chunk.copyTo(data);
|
|
151
|
-
const timestamp = chunk.timestamp;
|
|
152
|
-
|
|
153
|
-
const packet = createPacketWithHeader(data, timestamp, 'audio', null);
|
|
154
|
-
this.sendPacketOrQueue(packet, 'audio', null).finally(() => {
|
|
155
|
-
this.isSendingAudio = false;
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
},
|
|
159
|
-
error: (e) => console.error('AudioEncoder error:', e),
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
audioEncoder.configure({
|
|
163
|
-
codec: 'mp4a.40.2',
|
|
164
|
-
// codec: 'opus',
|
|
165
|
-
sampleRate: 48000,
|
|
166
|
-
numberOfChannels: 1,
|
|
167
|
-
bitrate: 128000,
|
|
168
|
-
// bitrate: 64000,
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
this.audioEncoder = audioEncoder;
|
|
172
|
-
this.processAudioFrames(audioTrack);
|
|
173
|
-
};
|
|
174
|
-
|
|
175
|
-
public initVideoEncoder(videoTrack: MediaStreamTrack): void {
|
|
176
|
-
if (this.localStream) {
|
|
177
|
-
this.localStream.addTrack(videoTrack);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
this.videoConfigSent = false;
|
|
181
|
-
this.hasVideo = !!videoTrack;
|
|
182
|
-
|
|
183
|
-
const settings = videoTrack.getSettings();
|
|
184
|
-
const videoWidth = settings.width || 1280;
|
|
185
|
-
const videoHeight = settings.height || 720;
|
|
186
|
-
|
|
187
|
-
const videoEncoder = new VideoEncoder({
|
|
188
|
-
output: async (chunk, metadata) => {
|
|
189
|
-
if (metadata?.decoderConfig && !this.videoConfigSent) {
|
|
190
|
-
let description: string | undefined = undefined;
|
|
191
|
-
if (metadata.decoderConfig.description) {
|
|
192
|
-
description = base64Encode(metadata.decoderConfig.description as ArrayBuffer);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
this.videoConfig = {
|
|
196
|
-
codec: metadata.decoderConfig.codec ?? 'hev1.1.6.L93.B0',
|
|
197
|
-
codedWidth: metadata.decoderConfig.codedWidth ?? videoWidth,
|
|
198
|
-
codedHeight: metadata.decoderConfig.codedHeight ?? videoHeight,
|
|
199
|
-
frameRate: 30.0,
|
|
200
|
-
orientation: 0,
|
|
201
|
-
...(description && { description }),
|
|
202
|
-
};
|
|
203
|
-
|
|
204
|
-
await this.sendVideoConfig();
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
if (chunk && this.isReadyToSendData('video')) {
|
|
208
|
-
if (this.isSendingVideo) return; // Backpressure: drop if network is congested
|
|
209
|
-
|
|
210
|
-
this.isSendingVideo = true;
|
|
211
|
-
const data = new ArrayBuffer(chunk.byteLength);
|
|
212
|
-
chunk.copyTo(data);
|
|
213
|
-
const frameType = chunk.type === 'key' ? 'video-key' : 'video-delta';
|
|
214
|
-
const timestamp = chunk.timestamp;
|
|
215
|
-
|
|
216
|
-
const packet = createPacketWithHeader(data, timestamp, frameType, null);
|
|
217
|
-
this.sendPacketOrQueue(packet, 'video', frameType).finally(() => {
|
|
218
|
-
this.isSendingVideo = false;
|
|
219
|
-
});
|
|
220
|
-
}
|
|
221
|
-
},
|
|
222
|
-
error: (e) => console.error('VideoEncoder error:', e),
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
videoEncoder.configure({
|
|
226
|
-
codec: 'hev1.1.6.L93.B0',
|
|
227
|
-
width: videoWidth,
|
|
228
|
-
height: videoHeight,
|
|
229
|
-
bitrate: 500_000,
|
|
230
|
-
framerate: 30,
|
|
231
|
-
latencyMode: 'realtime',
|
|
232
|
-
hardwareAcceleration: 'prefer-hardware',
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
this.videoEncoder = videoEncoder;
|
|
236
|
-
this.processVideoFrames(videoTrack);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
public initEncoders = (stream: MediaStream): void => {
|
|
240
|
-
const videoTrack = stream.getVideoTracks()[0];
|
|
241
|
-
const audioTrack = stream.getAudioTracks()[0];
|
|
242
|
-
|
|
243
|
-
if (audioTrack) {
|
|
244
|
-
this.initAudioEncoder(audioTrack);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
if (videoTrack) {
|
|
248
|
-
this.initVideoEncoder(videoTrack);
|
|
249
|
-
}
|
|
250
|
-
};
|
|
251
|
-
|
|
252
|
-
public sendTransceiverState = async (audioEnable: boolean, videoEnable: boolean) => {
|
|
253
|
-
const state: TransceiverState = {
|
|
254
|
-
audio_enable: !!audioEnable,
|
|
255
|
-
video_enable: !!videoEnable,
|
|
256
|
-
};
|
|
257
|
-
const configPacket = createPacketWithHeader(null, null, 'transciverState', state);
|
|
258
|
-
await this.nodeCall.sendControlFrame(configPacket);
|
|
259
|
-
};
|
|
260
|
-
|
|
261
|
-
public async replaceVideoTrack(track: MediaStreamTrack): Promise<void> {
|
|
262
|
-
// 1. Dừng reader của track cũ (quan trọng)
|
|
263
|
-
if (this.videoReader) {
|
|
264
|
-
try {
|
|
265
|
-
// Việc gọi cancel sẽ làm Promise tại dòng await read() bên dưới throw lỗi hoặc trả về done
|
|
266
|
-
await this.videoReader.cancel('Replacing track');
|
|
267
|
-
} catch (e) {
|
|
268
|
-
// Bỏ qua lỗi khi cancel
|
|
269
|
-
}
|
|
270
|
-
this.videoReader = null;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
if (track) {
|
|
274
|
-
this.processVideoFrames(track);
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
public async replaceAudioTrack(track: MediaStreamTrack): Promise<void> {
|
|
279
|
-
if (this.audioReader) {
|
|
280
|
-
try {
|
|
281
|
-
await this.audioReader.cancel('Replacing audio track');
|
|
282
|
-
} catch (e) {}
|
|
283
|
-
this.audioReader = null;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
if (track) {
|
|
287
|
-
this.processAudioFrames(track);
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
/**
|
|
292
|
-
* Yêu cầu gửi keyframe ngay lập tức (được gọi khi nhận REQUEST_KEY_FRAME từ receiver)
|
|
293
|
-
*/
|
|
294
|
-
public requestKeyFrame = (): void => {
|
|
295
|
-
console.log('📥 KeyFrame requested');
|
|
296
|
-
this.forceKeyFrame = true;
|
|
297
|
-
};
|
|
298
|
-
|
|
299
|
-
// ================= PRIVATE METHODS =================
|
|
300
|
-
|
|
301
|
-
private processVideoFrames = async (videoTrack: MediaStreamTrack) => {
|
|
302
|
-
try {
|
|
303
|
-
// @ts-ignore: MediaStreamTrackProcessor is explicitly defined in WebCodecs types
|
|
304
|
-
const videoProcessor = new MediaStreamTrackProcessor({ track: videoTrack });
|
|
305
|
-
this.videoReader = videoProcessor.readable.getReader();
|
|
306
|
-
|
|
307
|
-
let frameCounter = 0;
|
|
308
|
-
while (true) {
|
|
309
|
-
if (!this.videoReader) break;
|
|
310
|
-
|
|
311
|
-
const { done, value: frame } = await this.videoReader.read();
|
|
312
|
-
if (done) break;
|
|
313
|
-
|
|
314
|
-
if (!this.videoEncoder) {
|
|
315
|
-
frame?.close();
|
|
316
|
-
break;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
if (frame) {
|
|
320
|
-
frameCounter += 1;
|
|
321
|
-
const keyFrame = frameCounter % 60 === 0 || this.forceKeyFrame;
|
|
322
|
-
if (this.forceKeyFrame) {
|
|
323
|
-
console.log('📤 Sending forced KeyFrame');
|
|
324
|
-
this.forceKeyFrame = false;
|
|
325
|
-
}
|
|
326
|
-
try {
|
|
327
|
-
this.videoEncoder.encode(frame, { keyFrame });
|
|
328
|
-
} catch (err) {
|
|
329
|
-
console.error('Encode error:', err);
|
|
330
|
-
} finally {
|
|
331
|
-
frame.close();
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
} catch (error: any) {
|
|
336
|
-
console.error(`Error processing video frames: ${error.message}`);
|
|
337
|
-
} finally {
|
|
338
|
-
if (this.videoReader) {
|
|
339
|
-
try {
|
|
340
|
-
this.videoReader.releaseLock();
|
|
341
|
-
} catch (e) {}
|
|
342
|
-
// this.videoReader = null;
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
};
|
|
346
|
-
|
|
347
|
-
private processAudioFrames = async (audioTrack: MediaStreamTrack) => {
|
|
348
|
-
// @ts-ignore
|
|
349
|
-
const audioProcessor = new MediaStreamTrackProcessor({ track: audioTrack });
|
|
350
|
-
this.audioReader = audioProcessor.readable.getReader();
|
|
351
|
-
|
|
352
|
-
try {
|
|
353
|
-
while (true) {
|
|
354
|
-
if (!this.audioReader) break;
|
|
355
|
-
|
|
356
|
-
const { done, value: frame } = await this.audioReader.read();
|
|
357
|
-
if (done) break;
|
|
358
|
-
|
|
359
|
-
if (!this.audioEncoder) {
|
|
360
|
-
frame?.close();
|
|
361
|
-
break;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
if (frame) {
|
|
365
|
-
try {
|
|
366
|
-
this.audioEncoder.encode(frame);
|
|
367
|
-
} catch (err) {
|
|
368
|
-
console.error('Audio Encoding error:', err);
|
|
369
|
-
} finally {
|
|
370
|
-
frame.close();
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
} catch (error: any) {
|
|
375
|
-
console.error(`Error processing audio frames: ${error.message}`);
|
|
376
|
-
} finally {
|
|
377
|
-
if (this.audioReader) {
|
|
378
|
-
try {
|
|
379
|
-
this.audioReader.releaseLock();
|
|
380
|
-
} catch (e) {}
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
};
|
|
384
|
-
|
|
385
|
-
private isReadyToSendData = (type: 'video' | 'audio'): boolean => {
|
|
386
|
-
const videoReady = !this.hasVideo || this.videoConfigSent;
|
|
387
|
-
const audioReady = !this.hasAudio || this.audioConfigSent;
|
|
388
|
-
const allConfigsSent = videoReady && audioReady;
|
|
389
|
-
|
|
390
|
-
if (type === 'video') {
|
|
391
|
-
return allConfigsSent && this.videoConfigSent;
|
|
392
|
-
} else if (type === 'audio') {
|
|
393
|
-
return allConfigsSent && this.audioConfigSent;
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
return false;
|
|
397
|
-
};
|
|
398
|
-
|
|
399
|
-
private sendVideoConfig = async () => {
|
|
400
|
-
if (this.videoConfig && !this.videoConfigSent) {
|
|
401
|
-
try {
|
|
402
|
-
const configPacket = createPacketWithHeader(null, null, 'videoConfig', this.videoConfig);
|
|
403
|
-
await this.nodeCall.sendControlFrame(configPacket);
|
|
404
|
-
this.videoConfigSent = true;
|
|
405
|
-
} catch (error) {
|
|
406
|
-
console.error('Error sending video config:', error);
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
};
|
|
410
|
-
|
|
411
|
-
private sendAudioConfig = async () => {
|
|
412
|
-
if (this.audioConfig && !this.audioConfigSent) {
|
|
413
|
-
try {
|
|
414
|
-
const configPacket = createPacketWithHeader(null, null, 'audioConfig', this.audioConfig);
|
|
415
|
-
await this.nodeCall.sendControlFrame(configPacket);
|
|
416
|
-
this.audioConfigSent = true;
|
|
417
|
-
} catch (error) {
|
|
418
|
-
console.error('Error sending audio config:', error);
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
};
|
|
422
|
-
|
|
423
|
-
public sendConnected = async () => {
|
|
424
|
-
const configPacket = createPacketWithHeader(null, null, 'connected', null);
|
|
425
|
-
await this.nodeCall.sendControlFrame(configPacket);
|
|
426
|
-
};
|
|
427
|
-
|
|
428
|
-
private startHealthCallInterval = (): void => {
|
|
429
|
-
if (this.healthCallInterval) {
|
|
430
|
-
clearInterval(this.healthCallInterval);
|
|
431
|
-
}
|
|
432
|
-
this.healthCallInterval = setInterval(() => {
|
|
433
|
-
this.sendHealthCall().catch(() => {});
|
|
434
|
-
}, 5000);
|
|
435
|
-
};
|
|
436
|
-
|
|
437
|
-
private sendHealthCall = async (): Promise<void> => {
|
|
438
|
-
try {
|
|
439
|
-
const packet = createPacketWithHeader(null, null, 'healthCall', null);
|
|
440
|
-
await this.nodeCall.sendControlFrame(packet);
|
|
441
|
-
} catch (e) {
|
|
442
|
-
// Silently ignore — connection may have closed
|
|
443
|
-
}
|
|
444
|
-
};
|
|
445
|
-
|
|
446
|
-
private sendPacketOrQueue = async (
|
|
447
|
-
packet: Uint8Array,
|
|
448
|
-
type: 'video' | 'audio',
|
|
449
|
-
frameType: 'video-key' | 'video-delta' | null,
|
|
450
|
-
) => {
|
|
451
|
-
if (!this.isReadyToSendData(type)) {
|
|
452
|
-
return;
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
if (type === 'audio') {
|
|
456
|
-
await this.nodeCall.sendAudioFrame(packet);
|
|
457
|
-
} else if (type === 'video') {
|
|
458
|
-
if (frameType === 'video-key') {
|
|
459
|
-
await this.nodeCall.beginWithGop(packet);
|
|
460
|
-
} else if (frameType === 'video-delta') {
|
|
461
|
-
await this.nodeCall.sendFrame(packet);
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
};
|
|
465
|
-
}
|
package/src/shims/empty.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export default null;
|
package/src/signal_message.ts
DELETED
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Call type constants for signal messages.
|
|
3
|
-
*/
|
|
4
|
-
export const CallType = {
|
|
5
|
-
AUDIO: 'audio',
|
|
6
|
-
VIDEO: 'video',
|
|
7
|
-
} as const;
|
|
8
|
-
|
|
9
|
-
export type CallTypeValue = (typeof CallType)[keyof typeof CallType];
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Result of parsing a signal message.
|
|
13
|
-
*/
|
|
14
|
-
export interface SignalMessageResult {
|
|
15
|
-
text: string;
|
|
16
|
-
duration: string;
|
|
17
|
-
callType: CallTypeValue | '';
|
|
18
|
-
color: string;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Translation templates for signal messages.
|
|
23
|
-
*/
|
|
24
|
-
export interface SignalMessageTranslations {
|
|
25
|
-
calling?: string; // "Calling..."
|
|
26
|
-
incomingAudioCall?: string; // "Incoming audio call..."
|
|
27
|
-
incomingVideoCall?: string; // "Incoming video call..."
|
|
28
|
-
outgoingAudioCall?: string; // "Outgoing audio call"
|
|
29
|
-
outgoingVideoCall?: string; // "Outgoing video call"
|
|
30
|
-
missedAudioCall?: string; // "You missed audio call"
|
|
31
|
-
missedVideoCall?: string; // "You missed video call"
|
|
32
|
-
cancelAudioCall?: string; // "You cancel audio call"
|
|
33
|
-
cancelVideoCall?: string; // "You cancel video call"
|
|
34
|
-
rejectedAudioCallRecipient?: string; // "Recipient rejected audio call"
|
|
35
|
-
rejectedAudioCallYou?: string; // "You rejected audio call"
|
|
36
|
-
rejectedVideoCallRecipient?: string; // "Recipient rejected video call"
|
|
37
|
-
rejectedVideoCallYou?: string; // "You rejected video call"
|
|
38
|
-
busyRecipient?: string; // "Recipient was busy"
|
|
39
|
-
durationUnitMin?: string; // "min"
|
|
40
|
-
durationUnitSec?: string; // "sec"
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Format duration from milliseconds to human-readable format.
|
|
45
|
-
*/
|
|
46
|
-
function formatDuration(durationMs: string, translations?: SignalMessageTranslations): string {
|
|
47
|
-
if (!durationMs) return '';
|
|
48
|
-
const ms = parseInt(durationMs, 10);
|
|
49
|
-
if (isNaN(ms) || ms <= 0) return '';
|
|
50
|
-
const totalSeconds = Math.floor(ms / 1000);
|
|
51
|
-
const minutes = Math.floor(totalSeconds / 60);
|
|
52
|
-
const seconds = totalSeconds % 60;
|
|
53
|
-
const minUnit = translations?.durationUnitMin ?? 'min';
|
|
54
|
-
const secUnit = translations?.durationUnitSec ?? 'sec';
|
|
55
|
-
return `${minutes} ${minUnit}, ${seconds} ${secUnit}`;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Parse a raw signal message string into a structured object.
|
|
60
|
-
*
|
|
61
|
-
* Signal messages represent call events. The raw format is:
|
|
62
|
-
* `"<formatId> <callerId> [<enderId> <duration>]"`
|
|
63
|
-
*
|
|
64
|
-
* @param value - Raw signal message string from the server
|
|
65
|
-
* @param myUserId - The current user's ID
|
|
66
|
-
* @param translations - Optional translation templates
|
|
67
|
-
* @returns Parsed signal message object, or null if input is empty
|
|
68
|
-
*/
|
|
69
|
-
export function parseSignalMessage(
|
|
70
|
-
value: string,
|
|
71
|
-
myUserId: string,
|
|
72
|
-
translations?: SignalMessageTranslations,
|
|
73
|
-
): SignalMessageResult | null {
|
|
74
|
-
if (!value || typeof value !== 'string') return null;
|
|
75
|
-
|
|
76
|
-
const trimmed = value.trim();
|
|
77
|
-
if (!trimmed) return null;
|
|
78
|
-
|
|
79
|
-
const parts = trimmed.split(' ');
|
|
80
|
-
const number = parseInt(parts[0], 10);
|
|
81
|
-
const callerId = parts[1] ?? '';
|
|
82
|
-
const isMe = myUserId === callerId;
|
|
83
|
-
|
|
84
|
-
let enderId = '';
|
|
85
|
-
let duration = '';
|
|
86
|
-
let callType: CallTypeValue | '' = '';
|
|
87
|
-
let color = '';
|
|
88
|
-
|
|
89
|
-
if (number === 3 || number === 6) {
|
|
90
|
-
enderId = parts[2] ?? '';
|
|
91
|
-
duration = parts[3] === '0' ? '' : (parts[3] ?? '');
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
let text: string;
|
|
95
|
-
switch (number) {
|
|
96
|
-
case 1: // AudioCallStarted
|
|
97
|
-
text = isMe ? (translations?.calling ?? 'Calling...') : (translations?.incomingAudioCall ?? 'Incoming audio call...');
|
|
98
|
-
callType = CallType.AUDIO;
|
|
99
|
-
color = '#54D62C';
|
|
100
|
-
break;
|
|
101
|
-
case 2: // AudioCallMissed
|
|
102
|
-
text = isMe ? (translations?.outgoingAudioCall ?? 'Outgoing audio call') : (translations?.missedAudioCall ?? 'You missed audio call');
|
|
103
|
-
callType = CallType.AUDIO;
|
|
104
|
-
color = '#FF4842';
|
|
105
|
-
break;
|
|
106
|
-
case 3: // AudioCallEnded
|
|
107
|
-
if (duration) {
|
|
108
|
-
text = isMe ? (translations?.outgoingAudioCall ?? 'Outgoing audio call') : (translations?.incomingAudioCall ?? 'Incoming audio call');
|
|
109
|
-
color = '#54D62C';
|
|
110
|
-
} else {
|
|
111
|
-
if (enderId === myUserId) {
|
|
112
|
-
text = translations?.cancelAudioCall ?? 'You cancel audio call';
|
|
113
|
-
} else {
|
|
114
|
-
text = translations?.missedAudioCall ?? 'You missed audio call';
|
|
115
|
-
}
|
|
116
|
-
color = '#FF4842';
|
|
117
|
-
}
|
|
118
|
-
callType = CallType.AUDIO;
|
|
119
|
-
break;
|
|
120
|
-
case 4: // VideoCallStarted
|
|
121
|
-
text = isMe ? (translations?.calling ?? 'Calling...') : (translations?.incomingVideoCall ?? 'Incoming video call...');
|
|
122
|
-
callType = CallType.VIDEO;
|
|
123
|
-
color = '#54D62C';
|
|
124
|
-
break;
|
|
125
|
-
case 5: // VideoCallMissed
|
|
126
|
-
text = isMe ? (translations?.outgoingVideoCall ?? 'Outgoing video call') : (translations?.missedVideoCall ?? 'You missed video call');
|
|
127
|
-
callType = CallType.VIDEO;
|
|
128
|
-
color = '#FF4842';
|
|
129
|
-
break;
|
|
130
|
-
case 6: // VideoCallEnded
|
|
131
|
-
if (duration) {
|
|
132
|
-
text = isMe ? (translations?.outgoingVideoCall ?? 'Outgoing video call') : (translations?.incomingVideoCall ?? 'Incoming video call');
|
|
133
|
-
color = '#54D62C';
|
|
134
|
-
} else {
|
|
135
|
-
if (enderId === myUserId) {
|
|
136
|
-
text = translations?.cancelVideoCall ?? 'You cancel video call';
|
|
137
|
-
} else {
|
|
138
|
-
text = translations?.missedVideoCall ?? 'You missed video call';
|
|
139
|
-
}
|
|
140
|
-
color = '#FF4842';
|
|
141
|
-
}
|
|
142
|
-
callType = CallType.VIDEO;
|
|
143
|
-
break;
|
|
144
|
-
case 7: // AudioCallRejected
|
|
145
|
-
text = isMe ? (translations?.rejectedAudioCallRecipient ?? 'Recipient rejected audio call') : (translations?.rejectedAudioCallYou ?? 'You rejected audio call');
|
|
146
|
-
callType = CallType.AUDIO;
|
|
147
|
-
color = '#FF4842';
|
|
148
|
-
break;
|
|
149
|
-
case 8: // VideoCallRejected
|
|
150
|
-
text = isMe ? (translations?.rejectedVideoCallRecipient ?? 'Recipient rejected video call') : (translations?.rejectedVideoCallYou ?? 'You rejected video call');
|
|
151
|
-
callType = CallType.VIDEO;
|
|
152
|
-
color = '#FF4842';
|
|
153
|
-
break;
|
|
154
|
-
case 9: // AudioCallBusy
|
|
155
|
-
text = isMe ? (translations?.busyRecipient ?? 'Recipient was busy') : (translations?.missedAudioCall ?? 'You missed audio call');
|
|
156
|
-
callType = CallType.AUDIO;
|
|
157
|
-
color = '#FF4842';
|
|
158
|
-
break;
|
|
159
|
-
case 10: // VideoCallBusy
|
|
160
|
-
text = isMe ? (translations?.busyRecipient ?? 'Recipient was busy') : (translations?.missedVideoCall ?? 'You missed video call');
|
|
161
|
-
callType = CallType.VIDEO;
|
|
162
|
-
color = '#FF4842';
|
|
163
|
-
break;
|
|
164
|
-
default:
|
|
165
|
-
text = trimmed;
|
|
166
|
-
callType = '';
|
|
167
|
-
color = '';
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
return { text, duration: formatDuration(duration, translations), callType, color };
|
|
171
|
-
}
|