@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,400 +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
|
-
|
|
9
|
-
private localStream: MediaStream | null = null;
|
|
10
|
-
|
|
11
|
-
private videoConfig: VideoConfig | null = null;
|
|
12
|
-
private audioConfig: AudioConfig | null = null;
|
|
13
|
-
|
|
14
|
-
private videoConfigSent: boolean = false;
|
|
15
|
-
private audioConfigSent: boolean = false;
|
|
16
|
-
|
|
17
|
-
private hasVideo: boolean = false;
|
|
18
|
-
private hasAudio: boolean = false;
|
|
19
|
-
|
|
20
|
-
private forceKeyFrame: boolean = false;
|
|
21
|
-
|
|
22
|
-
private nodeCall: INodeCall;
|
|
23
|
-
|
|
24
|
-
constructor(nodeCall: INodeCall) {
|
|
25
|
-
this.nodeCall = nodeCall;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Bắt đầu xử lý MediaStream
|
|
30
|
-
*/
|
|
31
|
-
public async connect(address: string): Promise<void> {
|
|
32
|
-
try {
|
|
33
|
-
await this.nodeCall.connect(address);
|
|
34
|
-
await this.sendConnected();
|
|
35
|
-
await this.sendConfigs();
|
|
36
|
-
} catch (error) {
|
|
37
|
-
console.error('Error starting MediaStreamSender:', error);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
public async sendConfigs(): Promise<void> {
|
|
42
|
-
try {
|
|
43
|
-
await this.sendTransceiverState(this.hasAudio, this.hasVideo);
|
|
44
|
-
await this.sendAudioConfig();
|
|
45
|
-
|
|
46
|
-
const videoTrack = this.localStream?.getVideoTracks()[0];
|
|
47
|
-
if (videoTrack) {
|
|
48
|
-
await this.sendVideoConfig();
|
|
49
|
-
}
|
|
50
|
-
} catch (error) {
|
|
51
|
-
console.error('Error sending configs:', error);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Dừng và reset encoders
|
|
57
|
-
*/
|
|
58
|
-
public stop = (): void => {
|
|
59
|
-
if (this.videoReader) {
|
|
60
|
-
try {
|
|
61
|
-
this.videoReader.cancel('Stream stopped').catch(() => {});
|
|
62
|
-
} catch (e) {}
|
|
63
|
-
this.videoReader = null;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (this.videoEncoder) {
|
|
67
|
-
try {
|
|
68
|
-
if (this.videoEncoder.state !== 'closed') {
|
|
69
|
-
this.videoEncoder.reset(); // Xả frame
|
|
70
|
-
this.videoEncoder.close();
|
|
71
|
-
}
|
|
72
|
-
} catch (e) {}
|
|
73
|
-
this.videoEncoder = null;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Reset and close audio encoder
|
|
77
|
-
if (this.audioEncoder) {
|
|
78
|
-
try {
|
|
79
|
-
if (this.audioEncoder.state !== 'closed') {
|
|
80
|
-
this.audioEncoder.reset();
|
|
81
|
-
this.audioEncoder.close();
|
|
82
|
-
}
|
|
83
|
-
} catch (e) {}
|
|
84
|
-
this.audioEncoder = null;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Reset configs and flags
|
|
88
|
-
this.videoConfig = null;
|
|
89
|
-
this.audioConfig = null;
|
|
90
|
-
|
|
91
|
-
this.videoConfigSent = false;
|
|
92
|
-
this.audioConfigSent = false;
|
|
93
|
-
this.hasVideo = false;
|
|
94
|
-
this.hasAudio = false;
|
|
95
|
-
|
|
96
|
-
if (this.localStream) {
|
|
97
|
-
this.localStream.getTracks().forEach((track) => track.stop());
|
|
98
|
-
this.localStream = null;
|
|
99
|
-
}
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
public initAudioEncoder = (audioTrack: MediaStreamTrack): void => {
|
|
103
|
-
this.localStream = new MediaStream([audioTrack]);
|
|
104
|
-
this.audioConfigSent = false;
|
|
105
|
-
this.hasAudio = !!audioTrack;
|
|
106
|
-
|
|
107
|
-
const audioEncoder = new AudioEncoder({
|
|
108
|
-
output: (chunk, metadata) => {
|
|
109
|
-
if (metadata?.decoderConfig && !this.audioConfigSent) {
|
|
110
|
-
let description: string | undefined = undefined;
|
|
111
|
-
if (metadata.decoderConfig.description) {
|
|
112
|
-
description = base64Encode(metadata.decoderConfig.description as ArrayBuffer);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
this.audioConfig = {
|
|
116
|
-
codec: metadata.decoderConfig.codec ?? 'opus',
|
|
117
|
-
sampleRate: metadata.decoderConfig.sampleRate ?? 48000,
|
|
118
|
-
numberOfChannels: metadata.decoderConfig.numberOfChannels ?? 1,
|
|
119
|
-
...(description && { description }),
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (chunk && this.isReadyToSendData('audio')) {
|
|
124
|
-
const data = new ArrayBuffer(chunk.byteLength);
|
|
125
|
-
chunk.copyTo(data);
|
|
126
|
-
// const timestamp = Math.floor(chunk.timestamp / 1000);
|
|
127
|
-
const timestamp = chunk.timestamp;
|
|
128
|
-
|
|
129
|
-
const packet = createPacketWithHeader(data, timestamp, 'audio', null);
|
|
130
|
-
this.sendPacketOrQueue(packet, 'audio', null);
|
|
131
|
-
}
|
|
132
|
-
},
|
|
133
|
-
error: (e) => console.error('AudioEncoder error:', e),
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
audioEncoder.configure({
|
|
137
|
-
codec: 'mp4a.40.2',
|
|
138
|
-
sampleRate: 48000,
|
|
139
|
-
numberOfChannels: 1,
|
|
140
|
-
bitrate: 128000,
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
this.audioEncoder = audioEncoder;
|
|
144
|
-
this.processAudioFrames(audioTrack);
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
public initVideoEncoder(videoTrack: MediaStreamTrack): void {
|
|
148
|
-
if (this.localStream) {
|
|
149
|
-
this.localStream.addTrack(videoTrack);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
this.videoConfigSent = false;
|
|
153
|
-
this.hasVideo = !!videoTrack;
|
|
154
|
-
|
|
155
|
-
const settings = videoTrack.getSettings();
|
|
156
|
-
const videoWidth = settings.width || 1280;
|
|
157
|
-
const videoHeight = settings.height || 720;
|
|
158
|
-
|
|
159
|
-
const videoEncoder = new VideoEncoder({
|
|
160
|
-
output: async (chunk, metadata) => {
|
|
161
|
-
if (metadata?.decoderConfig && !this.videoConfigSent) {
|
|
162
|
-
let description: string | undefined = undefined;
|
|
163
|
-
if (metadata.decoderConfig.description) {
|
|
164
|
-
description = base64Encode(metadata.decoderConfig.description as ArrayBuffer);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
this.videoConfig = {
|
|
168
|
-
codec: metadata.decoderConfig.codec ?? 'hev1.1.6.L93.B0',
|
|
169
|
-
codedWidth: metadata.decoderConfig.codedWidth ?? videoWidth,
|
|
170
|
-
codedHeight: metadata.decoderConfig.codedHeight ?? videoHeight,
|
|
171
|
-
frameRate: 30.0,
|
|
172
|
-
orientation: 0,
|
|
173
|
-
...(description && { description }),
|
|
174
|
-
};
|
|
175
|
-
|
|
176
|
-
await this.sendVideoConfig();
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
if (chunk && this.isReadyToSendData('video')) {
|
|
180
|
-
const data = new ArrayBuffer(chunk.byteLength);
|
|
181
|
-
chunk.copyTo(data);
|
|
182
|
-
const frameType = chunk.type === 'key' ? 'video-key' : 'video-delta';
|
|
183
|
-
// const timestamp = Math.floor(chunk.timestamp / 1000);
|
|
184
|
-
const timestamp = chunk.timestamp;
|
|
185
|
-
|
|
186
|
-
const packet = createPacketWithHeader(data, timestamp, frameType, null);
|
|
187
|
-
this.sendPacketOrQueue(packet, 'video', frameType);
|
|
188
|
-
}
|
|
189
|
-
},
|
|
190
|
-
error: (e) => console.error('VideoEncoder error:', e),
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
videoEncoder.configure({
|
|
194
|
-
codec: 'hev1.1.6.L93.B0',
|
|
195
|
-
width: videoWidth,
|
|
196
|
-
height: videoHeight,
|
|
197
|
-
bitrate: 500_000,
|
|
198
|
-
framerate: 30,
|
|
199
|
-
latencyMode: 'realtime',
|
|
200
|
-
hardwareAcceleration: 'prefer-hardware',
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
this.videoEncoder = videoEncoder;
|
|
204
|
-
this.processVideoFrames(videoTrack);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
public initEncoders = (stream: MediaStream): void => {
|
|
208
|
-
const videoTrack = stream.getVideoTracks()[0];
|
|
209
|
-
const audioTrack = stream.getAudioTracks()[0];
|
|
210
|
-
|
|
211
|
-
if (audioTrack) {
|
|
212
|
-
this.initAudioEncoder(audioTrack);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
if (videoTrack) {
|
|
216
|
-
this.initVideoEncoder(videoTrack);
|
|
217
|
-
}
|
|
218
|
-
};
|
|
219
|
-
|
|
220
|
-
public sendTransceiverState = async (audioEnable: boolean, videoEnable: boolean) => {
|
|
221
|
-
const state: TransceiverState = {
|
|
222
|
-
audio_enable: !!audioEnable,
|
|
223
|
-
video_enable: !!videoEnable,
|
|
224
|
-
};
|
|
225
|
-
const configPacket = createPacketWithHeader(null, null, 'transciverState', state);
|
|
226
|
-
await this.nodeCall.sendControlFrame(configPacket);
|
|
227
|
-
};
|
|
228
|
-
|
|
229
|
-
public async replaceVideoTrack(track: MediaStreamTrack): Promise<void> {
|
|
230
|
-
// 1. Dừng reader của track cũ (quan trọng)
|
|
231
|
-
if (this.videoReader) {
|
|
232
|
-
try {
|
|
233
|
-
// 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
|
|
234
|
-
await this.videoReader.cancel('Replacing track');
|
|
235
|
-
} catch (e) {
|
|
236
|
-
// Bỏ qua lỗi khi cancel
|
|
237
|
-
}
|
|
238
|
-
this.videoReader = null;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
if (track) {
|
|
242
|
-
this.processVideoFrames(track);
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
public async replaceAudioTrack(track: MediaStreamTrack): Promise<void> {
|
|
247
|
-
if (track) {
|
|
248
|
-
this.processAudioFrames(track);
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
* Yêu cầu gửi keyframe ngay lập tức (được gọi khi nhận REQUEST_KEY_FRAME từ receiver)
|
|
254
|
-
*/
|
|
255
|
-
public requestKeyFrame = (): void => {
|
|
256
|
-
console.log('📥 KeyFrame requested');
|
|
257
|
-
this.forceKeyFrame = true;
|
|
258
|
-
};
|
|
259
|
-
|
|
260
|
-
// ================= PRIVATE METHODS =================
|
|
261
|
-
|
|
262
|
-
private processVideoFrames = async (videoTrack: MediaStreamTrack) => {
|
|
263
|
-
try {
|
|
264
|
-
// @ts-ignore: MediaStreamTrackProcessor is explicitly defined in WebCodecs types
|
|
265
|
-
const videoProcessor = new MediaStreamTrackProcessor({ track: videoTrack });
|
|
266
|
-
this.videoReader = videoProcessor.readable.getReader();
|
|
267
|
-
|
|
268
|
-
let frameCounter = 0;
|
|
269
|
-
while (true) {
|
|
270
|
-
if (!this.videoReader) break;
|
|
271
|
-
|
|
272
|
-
const { done, value: frame } = await this.videoReader.read();
|
|
273
|
-
if (done) break;
|
|
274
|
-
|
|
275
|
-
if (!this.videoEncoder) {
|
|
276
|
-
frame?.close();
|
|
277
|
-
break;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
if (frame) {
|
|
281
|
-
frameCounter += 1;
|
|
282
|
-
const keyFrame = frameCounter % 60 === 0 || this.forceKeyFrame;
|
|
283
|
-
if (this.forceKeyFrame) {
|
|
284
|
-
console.log('📤 Sending forced KeyFrame');
|
|
285
|
-
this.forceKeyFrame = false;
|
|
286
|
-
}
|
|
287
|
-
try {
|
|
288
|
-
this.videoEncoder.encode(frame, { keyFrame });
|
|
289
|
-
} catch (err) {
|
|
290
|
-
console.error('Encode error:', err);
|
|
291
|
-
} finally {
|
|
292
|
-
frame.close();
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
} catch (error: any) {
|
|
297
|
-
console.error(`Error processing video frames: ${error.message}`);
|
|
298
|
-
} finally {
|
|
299
|
-
if (this.videoReader) {
|
|
300
|
-
try {
|
|
301
|
-
this.videoReader.releaseLock();
|
|
302
|
-
} catch (e) {}
|
|
303
|
-
// this.videoReader = null;
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
};
|
|
307
|
-
|
|
308
|
-
private processAudioFrames = async (audioTrack: MediaStreamTrack) => {
|
|
309
|
-
// @ts-ignore
|
|
310
|
-
const audioProcessor = new MediaStreamTrackProcessor({ track: audioTrack });
|
|
311
|
-
const audioReader = audioProcessor.readable.getReader();
|
|
312
|
-
|
|
313
|
-
try {
|
|
314
|
-
while (true) {
|
|
315
|
-
const { done, value: frame } = await audioReader.read();
|
|
316
|
-
if (done) break;
|
|
317
|
-
|
|
318
|
-
if (!this.audioEncoder) {
|
|
319
|
-
frame?.close();
|
|
320
|
-
break;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
if (frame) {
|
|
324
|
-
try {
|
|
325
|
-
this.audioEncoder.encode(frame);
|
|
326
|
-
} catch (err) {
|
|
327
|
-
console.error('Audio Encoding error:', err);
|
|
328
|
-
} finally {
|
|
329
|
-
frame.close();
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
} catch (error: any) {
|
|
334
|
-
console.error(`Error processing audio frames: ${error.message}`);
|
|
335
|
-
}
|
|
336
|
-
};
|
|
337
|
-
|
|
338
|
-
private isReadyToSendData = (type: 'video' | 'audio'): boolean => {
|
|
339
|
-
const videoReady = !this.hasVideo || this.videoConfigSent;
|
|
340
|
-
const audioReady = !this.hasAudio || this.audioConfigSent;
|
|
341
|
-
const allConfigsSent = videoReady && audioReady;
|
|
342
|
-
|
|
343
|
-
if (type === 'video') {
|
|
344
|
-
return allConfigsSent && this.videoConfigSent;
|
|
345
|
-
} else if (type === 'audio') {
|
|
346
|
-
return allConfigsSent && this.audioConfigSent;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
return false;
|
|
350
|
-
};
|
|
351
|
-
|
|
352
|
-
private sendVideoConfig = async () => {
|
|
353
|
-
if (this.videoConfig && !this.videoConfigSent) {
|
|
354
|
-
try {
|
|
355
|
-
const configPacket = createPacketWithHeader(null, null, 'videoConfig', this.videoConfig);
|
|
356
|
-
await this.nodeCall.sendControlFrame(configPacket);
|
|
357
|
-
this.videoConfigSent = true;
|
|
358
|
-
} catch (error) {
|
|
359
|
-
console.error('Error sending video config:', error);
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
};
|
|
363
|
-
|
|
364
|
-
private sendAudioConfig = async () => {
|
|
365
|
-
if (this.audioConfig && !this.audioConfigSent) {
|
|
366
|
-
try {
|
|
367
|
-
const configPacket = createPacketWithHeader(null, null, 'audioConfig', this.audioConfig);
|
|
368
|
-
await this.nodeCall.sendControlFrame(configPacket);
|
|
369
|
-
this.audioConfigSent = true;
|
|
370
|
-
} catch (error) {
|
|
371
|
-
console.error('Error sending audio config:', error);
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
};
|
|
375
|
-
|
|
376
|
-
public sendConnected = async () => {
|
|
377
|
-
const configPacket = createPacketWithHeader(null, null, 'connected', null);
|
|
378
|
-
await this.nodeCall.sendControlFrame(configPacket);
|
|
379
|
-
};
|
|
380
|
-
|
|
381
|
-
private sendPacketOrQueue = async (
|
|
382
|
-
packet: Uint8Array,
|
|
383
|
-
type: 'video' | 'audio',
|
|
384
|
-
frameType: 'video-key' | 'video-delta' | null,
|
|
385
|
-
) => {
|
|
386
|
-
if (!this.isReadyToSendData(type)) {
|
|
387
|
-
return;
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
if (type === 'audio') {
|
|
391
|
-
await this.nodeCall.sendAudioFrame(packet);
|
|
392
|
-
} else if (type === 'video') {
|
|
393
|
-
if (frameType === 'video-key') {
|
|
394
|
-
await this.nodeCall.beginWithGop(packet);
|
|
395
|
-
} else if (frameType === 'video-delta') {
|
|
396
|
-
await this.nodeCall.sendFrame(packet);
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
};
|
|
400
|
-
}
|
package/src/shims/empty.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export default null;
|
package/src/signal_message.ts
DELETED
|
@@ -1,146 +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
|
-
* Format duration from milliseconds to "X min, Y sec" format.
|
|
23
|
-
*/
|
|
24
|
-
function formatDuration(durationMs: string): string {
|
|
25
|
-
if (!durationMs) return '';
|
|
26
|
-
const ms = parseInt(durationMs, 10);
|
|
27
|
-
if (isNaN(ms) || ms <= 0) return '';
|
|
28
|
-
const totalSeconds = Math.floor(ms / 1000);
|
|
29
|
-
const minutes = Math.floor(totalSeconds / 60);
|
|
30
|
-
const seconds = totalSeconds % 60;
|
|
31
|
-
return `${minutes} min, ${seconds} sec`;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Parse a raw signal message string into a structured object
|
|
36
|
-
* containing text, duration, call type, and color.
|
|
37
|
-
*
|
|
38
|
-
* Signal messages represent call events. The raw format is:
|
|
39
|
-
* `"<formatId> <callerId> [<enderId> <duration>]"`
|
|
40
|
-
*
|
|
41
|
-
* @param value - Raw signal message string from the server
|
|
42
|
-
* @param myUserId - The current user's ID (from client.userID)
|
|
43
|
-
* @returns Parsed signal message object, or null if input is empty
|
|
44
|
-
*/
|
|
45
|
-
export function parseSignalMessage(
|
|
46
|
-
value: string,
|
|
47
|
-
myUserId: string,
|
|
48
|
-
): SignalMessageResult | null {
|
|
49
|
-
if (!value || typeof value !== 'string') return null;
|
|
50
|
-
|
|
51
|
-
const trimmed = value.trim();
|
|
52
|
-
if (!trimmed) return null;
|
|
53
|
-
|
|
54
|
-
const parts = trimmed.split(' ');
|
|
55
|
-
const number = parseInt(parts[0], 10);
|
|
56
|
-
const callerId = parts[1] ?? '';
|
|
57
|
-
const isMe = myUserId === callerId;
|
|
58
|
-
|
|
59
|
-
let enderId = '';
|
|
60
|
-
let duration = '';
|
|
61
|
-
let callType: CallTypeValue | '' = '';
|
|
62
|
-
let color = '';
|
|
63
|
-
|
|
64
|
-
if (number === 3 || number === 6) {
|
|
65
|
-
enderId = parts[2] ?? '';
|
|
66
|
-
duration = parts[3] === '0' ? '' : (parts[3] ?? '');
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
let text: string;
|
|
70
|
-
switch (number) {
|
|
71
|
-
case 1: // AudioCallStarted
|
|
72
|
-
text = isMe ? 'Calling...' : 'Incoming audio call...';
|
|
73
|
-
callType = CallType.AUDIO;
|
|
74
|
-
color = '#54D62C';
|
|
75
|
-
break;
|
|
76
|
-
case 2: // AudioCallMissed
|
|
77
|
-
text = isMe ? 'Outgoing audio call' : 'You missed audio call';
|
|
78
|
-
callType = CallType.AUDIO;
|
|
79
|
-
color = '#FF4842';
|
|
80
|
-
break;
|
|
81
|
-
case 3: // AudioCallEnded
|
|
82
|
-
if (duration) {
|
|
83
|
-
text = isMe ? 'Outgoing audio call' : 'Incoming audio call';
|
|
84
|
-
color = '#54D62C';
|
|
85
|
-
} else {
|
|
86
|
-
if (enderId === myUserId) {
|
|
87
|
-
text = 'You cancel audio call';
|
|
88
|
-
} else {
|
|
89
|
-
text = 'You missed audio call';
|
|
90
|
-
}
|
|
91
|
-
color = '#FF4842';
|
|
92
|
-
}
|
|
93
|
-
callType = CallType.AUDIO;
|
|
94
|
-
break;
|
|
95
|
-
case 4: // VideoCallStarted
|
|
96
|
-
text = isMe ? 'Calling...' : 'Incoming video call...';
|
|
97
|
-
callType = CallType.VIDEO;
|
|
98
|
-
color = '#54D62C';
|
|
99
|
-
break;
|
|
100
|
-
case 5: // VideoCallMissed
|
|
101
|
-
text = isMe ? 'Outgoing video call' : 'You missed video call';
|
|
102
|
-
callType = CallType.VIDEO;
|
|
103
|
-
color = '#FF4842';
|
|
104
|
-
break;
|
|
105
|
-
case 6: // VideoCallEnded
|
|
106
|
-
if (duration) {
|
|
107
|
-
text = isMe ? 'Outgoing video call' : 'Incoming video call';
|
|
108
|
-
color = '#54D62C';
|
|
109
|
-
} else {
|
|
110
|
-
if (enderId === myUserId) {
|
|
111
|
-
text = 'You cancel video call';
|
|
112
|
-
} else {
|
|
113
|
-
text = 'You missed video call';
|
|
114
|
-
}
|
|
115
|
-
color = '#FF4842';
|
|
116
|
-
}
|
|
117
|
-
callType = CallType.VIDEO;
|
|
118
|
-
break;
|
|
119
|
-
case 7: // AudioCallRejected
|
|
120
|
-
text = isMe ? 'Recipient rejected audio call' : 'You rejected audio call';
|
|
121
|
-
callType = CallType.AUDIO;
|
|
122
|
-
color = '#FF4842';
|
|
123
|
-
break;
|
|
124
|
-
case 8: // VideoCallRejected
|
|
125
|
-
text = isMe ? 'Recipient rejected video call' : 'You rejected video call';
|
|
126
|
-
callType = CallType.VIDEO;
|
|
127
|
-
color = '#FF4842';
|
|
128
|
-
break;
|
|
129
|
-
case 9: // AudioCallBusy
|
|
130
|
-
text = isMe ? 'Recipient was busy' : 'You missed audio call';
|
|
131
|
-
callType = CallType.AUDIO;
|
|
132
|
-
color = '#FF4842';
|
|
133
|
-
break;
|
|
134
|
-
case 10: // VideoCallBusy
|
|
135
|
-
text = isMe ? 'Recipient was busy' : 'You missed video call';
|
|
136
|
-
callType = CallType.VIDEO;
|
|
137
|
-
color = '#FF4842';
|
|
138
|
-
break;
|
|
139
|
-
default:
|
|
140
|
-
text = trimmed;
|
|
141
|
-
callType = '';
|
|
142
|
-
color = '';
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
return { text, duration: formatDuration(duration), callType, color };
|
|
146
|
-
}
|
package/src/system_message.ts
DELETED
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Duration lookup: milliseconds → human-readable text.
|
|
3
|
-
*/
|
|
4
|
-
const DURATION_MAP: Record<string, string> = {
|
|
5
|
-
'10000': '10 seconds',
|
|
6
|
-
'30000': '30 seconds',
|
|
7
|
-
'60000': '1 minute',
|
|
8
|
-
'300000': '5 minutes',
|
|
9
|
-
'900000': '15 minutes',
|
|
10
|
-
'3600000': '60 minutes',
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Resolve a user ID to a display name using the provided map.
|
|
15
|
-
* Falls back to the raw userId if no entry is found.
|
|
16
|
-
*/
|
|
17
|
-
function resolveUser(userId: string, userMap: Record<string, string>): string {
|
|
18
|
-
return userMap[userId] ?? userId;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Parse a raw system message string into a human-readable English sentence.
|
|
23
|
-
*
|
|
24
|
-
* The raw format is: `"<formatId> <userID> [<param1> <param2> ...]"`
|
|
25
|
-
*
|
|
26
|
-
* @param value - Raw system message string from the server
|
|
27
|
-
* @param userMap - Mapping of user IDs → display names
|
|
28
|
-
* @returns Parsed English text, or the original string if the format is unknown
|
|
29
|
-
*/
|
|
30
|
-
export function parseSystemMessage(
|
|
31
|
-
value: string,
|
|
32
|
-
userMap: Record<string, string>,
|
|
33
|
-
): string {
|
|
34
|
-
if (!value || typeof value !== 'string') return value ?? '';
|
|
35
|
-
|
|
36
|
-
const trimmed = value.trim();
|
|
37
|
-
if (!trimmed) return '';
|
|
38
|
-
|
|
39
|
-
const parts = trimmed.split(' ');
|
|
40
|
-
const formatId = parts[0];
|
|
41
|
-
const userId = parts[1] ?? '';
|
|
42
|
-
const userName = userId ? resolveUser(userId, userMap) : 'User';
|
|
43
|
-
|
|
44
|
-
switch (formatId) {
|
|
45
|
-
// 1: userName changed the channel name to channelName (may contain spaces)
|
|
46
|
-
case '1': {
|
|
47
|
-
const channelName = parts.slice(2).join(' ');
|
|
48
|
-
return `${userName} changed the channel name to ${channelName}.`;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// 2–13, 16–17: single-user actions
|
|
52
|
-
case '2':
|
|
53
|
-
return `${userName} changed the channel avatar.`;
|
|
54
|
-
case '3':
|
|
55
|
-
return `${userName} changed the channel description.`;
|
|
56
|
-
case '4':
|
|
57
|
-
return `${userName} was removed from the channel.`;
|
|
58
|
-
case '5':
|
|
59
|
-
return `${userName} was banned.`;
|
|
60
|
-
case '6':
|
|
61
|
-
return `${userName} was unbanned.`;
|
|
62
|
-
case '7':
|
|
63
|
-
return `${userName} was promoted to moderator.`;
|
|
64
|
-
case '8':
|
|
65
|
-
return `${userName} was demoted from moderator.`;
|
|
66
|
-
case '9':
|
|
67
|
-
return `${userName}'s permissions were updated.`;
|
|
68
|
-
case '10':
|
|
69
|
-
return `${userName} joined the channel.`;
|
|
70
|
-
case '11':
|
|
71
|
-
return `${userName} declined the channel invitation.`;
|
|
72
|
-
case '12':
|
|
73
|
-
return `${userName} left the channel.`;
|
|
74
|
-
case '13':
|
|
75
|
-
return `${userName} cleared the chat history.`;
|
|
76
|
-
|
|
77
|
-
// 14: channel type change (true = public, false = private)
|
|
78
|
-
case '14': {
|
|
79
|
-
const rawType = parts[2] ?? '';
|
|
80
|
-
const channelType = rawType === 'true' ? 'public' : 'private';
|
|
81
|
-
return `${userName} changed the channel to ${channelType}.`;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// 15: cooldown toggle / duration
|
|
85
|
-
case '15': {
|
|
86
|
-
const duration = parts[2] ?? '0';
|
|
87
|
-
if (duration === '0') {
|
|
88
|
-
return `${userName} disabled cooldown.`;
|
|
89
|
-
}
|
|
90
|
-
const durationText = DURATION_MAP[duration] ?? `${duration}ms`;
|
|
91
|
-
return `${userName} enabled cooldown for ${durationText}.`;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
case '16':
|
|
95
|
-
return `${userName} updated the banned words.`;
|
|
96
|
-
case '17':
|
|
97
|
-
return `${userName} was added to the channel.`;
|
|
98
|
-
|
|
99
|
-
// 18: admin transfer (two user IDs)
|
|
100
|
-
case '18': {
|
|
101
|
-
const oldUserId = parts[1] ?? '';
|
|
102
|
-
const newUserId = parts[2] ?? '';
|
|
103
|
-
const oldUserName = oldUserId ? resolveUser(oldUserId, userMap) : 'User';
|
|
104
|
-
const newUserName = newUserId ? resolveUser(newUserId, userMap) : 'User';
|
|
105
|
-
return `Admin ${oldUserName} left and assigned ${newUserName} as the new admin.`;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// 19–20: pin / unpin (userId + msgID)
|
|
109
|
-
case '19':
|
|
110
|
-
return `${userName} pinned a message.`;
|
|
111
|
-
case '20':
|
|
112
|
-
return `${userName} unpinned a message.`;
|
|
113
|
-
|
|
114
|
-
default:
|
|
115
|
-
return trimmed;
|
|
116
|
-
}
|
|
117
|
-
}
|
package/src/token_manager.ts
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { ExtendableGenerics, DefaultGenerics, UserResponse } from './types';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* TokenManager
|
|
5
|
-
*
|
|
6
|
-
* Manages token storage and retrieval for the chat client.
|
|
7
|
-
*/
|
|
8
|
-
export class TokenManager<ErmisChatGenerics extends ExtendableGenerics = DefaultGenerics> {
|
|
9
|
-
loadTokenPromise: Promise<string> | null;
|
|
10
|
-
token?: string;
|
|
11
|
-
user?: UserResponse<ErmisChatGenerics>;
|
|
12
|
-
|
|
13
|
-
constructor() {
|
|
14
|
-
this.loadTokenPromise = null;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Set the static string token.
|
|
19
|
-
*/
|
|
20
|
-
setTokenOrProvider = async (tokenOrProvider: string | null, user: UserResponse<ErmisChatGenerics>) => {
|
|
21
|
-
this.user = user;
|
|
22
|
-
|
|
23
|
-
if (typeof tokenOrProvider === 'string') {
|
|
24
|
-
this.token = tokenOrProvider;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
this.loadTokenPromise = Promise.resolve(this.token as string);
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Resets the token manager.
|
|
32
|
-
*/
|
|
33
|
-
reset = () => {
|
|
34
|
-
this.token = undefined;
|
|
35
|
-
this.user = undefined;
|
|
36
|
-
this.loadTokenPromise = null;
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Resolves when token is ready.
|
|
41
|
-
*/
|
|
42
|
-
tokenReady = () => this.loadTokenPromise;
|
|
43
|
-
|
|
44
|
-
/** Returns the current token */
|
|
45
|
-
getToken = () => {
|
|
46
|
-
return this.token;
|
|
47
|
-
};
|
|
48
|
-
}
|