@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.
Files changed (54) hide show
  1. package/README.md +330 -0
  2. package/bin/init-call.js +9 -0
  3. package/dist/encryption/index.browser.cjs +13045 -0
  4. package/dist/encryption/index.browser.cjs.map +1 -0
  5. package/dist/encryption/index.browser.mjs +12959 -0
  6. package/dist/encryption/index.browser.mjs.map +1 -0
  7. package/dist/encryption/index.cjs +13045 -0
  8. package/dist/encryption/index.cjs.map +1 -0
  9. package/dist/encryption/index.d.mts +3 -0
  10. package/dist/encryption/index.d.ts +3 -0
  11. package/dist/encryption/index.mjs +12959 -0
  12. package/dist/encryption/index.mjs.map +1 -0
  13. package/dist/index-CcvHIY5q.d.mts +4988 -0
  14. package/dist/index-CcvHIY5q.d.ts +4988 -0
  15. package/dist/index.browser.cjs +20399 -6823
  16. package/dist/index.browser.cjs.map +1 -1
  17. package/dist/index.browser.full-bundle.min.js +20 -18
  18. package/dist/index.browser.full-bundle.min.js.map +1 -1
  19. package/dist/index.browser.mjs +20315 -6790
  20. package/dist/index.browser.mjs.map +1 -1
  21. package/dist/index.cjs +20400 -6824
  22. package/dist/index.cjs.map +1 -1
  23. package/dist/index.d.mts +167 -1356
  24. package/dist/index.d.ts +167 -1356
  25. package/dist/index.mjs +20312 -6787
  26. package/dist/index.mjs.map +1 -1
  27. package/dist/wasm_worker.worker.mjs +1600 -0
  28. package/dist/wasm_worker.worker.mjs.map +1 -0
  29. package/package.json +22 -7
  30. package/public/e2ee-media-stream-worker.js +627 -0
  31. package/public/ermis_call_node_wasm_bg.wasm +0 -0
  32. package/public/openmls_wasm_bg.wasm +0 -0
  33. package/src/attachment_utils.ts +0 -148
  34. package/src/auth.ts +0 -352
  35. package/src/channel.ts +0 -1806
  36. package/src/channel_state.ts +0 -607
  37. package/src/client.ts +0 -1617
  38. package/src/client_state.ts +0 -55
  39. package/src/connection.ts +0 -587
  40. package/src/ermis_call_node.ts +0 -978
  41. package/src/errors.ts +0 -60
  42. package/src/events.ts +0 -46
  43. package/src/hevc_decoder_config.ts +0 -305
  44. package/src/index.ts +0 -16
  45. package/src/media_stream_receiver.ts +0 -525
  46. package/src/media_stream_sender.ts +0 -400
  47. package/src/shims/empty.ts +0 -1
  48. package/src/signal_message.ts +0 -146
  49. package/src/system_message.ts +0 -117
  50. package/src/token_manager.ts +0 -48
  51. package/src/types.ts +0 -581
  52. package/src/utils.ts +0 -534
  53. package/src/wasm/ermis_call_node_wasm.d.ts +0 -154
  54. package/src/wasm/ermis_call_node_wasm.js +0 -1498
@@ -1,978 +0,0 @@
1
- import { ErmisChat } from './client';
2
- import init, { ErmisCall } from './wasm/ermis_call_node_wasm';
3
- import {
4
- CallAction,
5
- CallEventData,
6
- CallStatus,
7
- DefaultGenerics,
8
- Event,
9
- ExtendableGenerics,
10
- Metadata,
11
- SignalData,
12
- UserCallInfo,
13
- } from './types';
14
- import { MediaStreamSender } from './media_stream_sender';
15
- import { MediaStreamReceiver } from './media_stream_receiver';
16
-
17
- export class ErmisCallNode<ErmisChatGenerics extends ExtendableGenerics = DefaultGenerics> {
18
- wasmPath: string;
19
-
20
- relayUrl = 'https://test-iroh.ermis.network.:8443';
21
-
22
- /** Reference to the Ermis Chat client instance */
23
- _client: ErmisChat<ErmisChatGenerics>;
24
-
25
- /** Unique identifier for the current call session */
26
- sessionID: string;
27
-
28
- /** Channel ID for communication between users */
29
- cid?: string;
30
-
31
- /** Type of call: 'audio' or 'video' */
32
- callType?: string;
33
-
34
- /** ID of the current user */
35
- userID?: string | undefined;
36
-
37
- /** Current status of the call */
38
- callStatus? = '';
39
-
40
- metadata?: Metadata;
41
-
42
- callNode: ErmisCall | null = null;
43
-
44
- /** Local media stream from user's camera/microphone */
45
- localStream?: MediaStream | null = null;
46
-
47
- /** Remote media stream from the other participant */
48
- remoteStream?: MediaStream | null = null;
49
-
50
- /** Information about the caller */
51
- callerInfo?: UserCallInfo;
52
-
53
- /** Information about the call receiver */
54
- receiverInfo?: UserCallInfo;
55
-
56
- /** Callback triggered when call events occur (incoming/outgoing) */
57
- onCallEvent?: (data: CallEventData) => void;
58
-
59
- /** Callback triggered when local stream is available */
60
- onLocalStream?: (stream: MediaStream) => void;
61
-
62
- /** Callback triggered when remote stream is available */
63
- onRemoteStream?: (stream: MediaStream) => void;
64
-
65
- /** Callback for connection status message changes */
66
- onConnectionMessageChange?: (message: string | null) => void;
67
-
68
- /** Callback for call status changes */
69
- onCallStatus?: (status: string | null) => void;
70
-
71
- /** Callback for messages received through WebRTC data channel */
72
- onDataChannelMessage?: (data: any) => void;
73
-
74
- /** Callback for when a call is upgraded (e.g., audio to video) */
75
- onUpgradeCall?: (upgraderInfo: UserCallInfo) => void;
76
-
77
- /** Callback for screen sharing status changes */
78
- onScreenShareChange?: (isSharing: boolean) => void;
79
-
80
- /** Callback for error handling */
81
- onError?: (error: string) => void;
82
-
83
- /** Callback for device list changes */
84
- onDeviceChange?: (audioDevices: MediaDeviceInfo[], videoDevices: MediaDeviceInfo[]) => void;
85
-
86
- /** Available audio input devices */
87
- private availableAudioDevices: MediaDeviceInfo[] = [];
88
-
89
- /** Available video input devices */
90
- private availableVideoDevices: MediaDeviceInfo[] = [];
91
-
92
- /** Currently selected audio device ID */
93
- private selectedAudioDeviceId?: string;
94
-
95
- /** Currently selected video device ID */
96
- private selectedVideoDeviceId?: string;
97
-
98
- /** Timeout for ending call if not answered after a period */
99
- private missCallTimeout: ReturnType<typeof setTimeout> | null = null;
100
-
101
- /** Interval for sending health check via WebRTC */
102
- private healthCallInterval: ReturnType<typeof setInterval> | null = null;
103
-
104
- /** Interval for sending health check via server */
105
- private healthCallServerInterval: ReturnType<typeof setInterval> | null = null;
106
-
107
- /** Timeout for detecting if remote peer has disconnected */
108
- private healthCallTimeout: ReturnType<typeof setTimeout> | null = null;
109
-
110
- /** Timeout for showing warning when connection becomes unstable */
111
- private healthCallWarningTimeout: ReturnType<typeof setTimeout> | null = null;
112
-
113
- /** Handler for signal events */
114
- private signalHandler: any;
115
-
116
- /** Handler for connection change events */
117
- private connectionChangedHandler: any;
118
-
119
- /** Handler for message updated events */
120
- private messageUpdatedHandler: any;
121
-
122
- /** Flag indicating if the user is offline */
123
- private isOffline: boolean = false;
124
-
125
- /**
126
- * True if this call instance is destroyed (e.g., when another device accepts the call).
127
- * When true, SIGNAL_CALL events will be ignored.
128
- */
129
- private isDestroyed = false;
130
-
131
- public mediaSender: MediaStreamSender | null = null;
132
- public mediaReceiver: MediaStreamReceiver | null = null;
133
-
134
- constructor(client: ErmisChat<ErmisChatGenerics>, sessionID: string, wasmPath: string, relayUrl: string) {
135
- this._client = client;
136
- this.cid = '';
137
- this.callType = '';
138
- this.sessionID = sessionID;
139
- this.userID = client.userID;
140
- this.metadata = {};
141
- this.wasmPath = wasmPath;
142
- this.relayUrl = relayUrl;
143
-
144
- this.listenSocketEvents();
145
- this.setupDeviceChangeListener();
146
- this.loadWasm();
147
- }
148
-
149
- private async loadWasm(): Promise<void> {
150
- try {
151
- await init(this.wasmPath);
152
- } catch (error) {
153
- console.error('Failed to load ErmisCall WASM module:', error);
154
- throw error;
155
- }
156
- }
157
-
158
- private async initialize(): Promise<ErmisCall> {
159
- try {
160
- const node = new ErmisCall();
161
- await node.spawn([this.relayUrl]);
162
- this.callNode = node;
163
-
164
- // 1. Init Sender
165
- this.mediaSender = new MediaStreamSender(node as any);
166
-
167
- // 2. Init Receiver
168
- this.mediaReceiver = new MediaStreamReceiver(node as any, {
169
- onConnected: () => {
170
- this.setCallStatus(CallStatus.CONNECTED);
171
- this.connectCall();
172
- if (this.missCallTimeout) {
173
- clearTimeout(this.missCallTimeout);
174
- this.missCallTimeout = null;
175
- }
176
- if (this.healthCallServerInterval) clearInterval(this.healthCallServerInterval);
177
- this.healthCallServerInterval = setInterval(() => {
178
- this.healthCall();
179
- }, 10000);
180
-
181
- const remoteStream = this.mediaReceiver?.getRemoteStream();
182
-
183
- if (remoteStream && this.onRemoteStream) {
184
- this.onRemoteStream(remoteStream);
185
- }
186
- },
187
-
188
- onTransceiverState: (state) => {
189
- if (typeof this.onDataChannelMessage === 'function') {
190
- this.onDataChannelMessage(state);
191
- }
192
- },
193
-
194
- onRequestConfig: () => {
195
- console.log('📤 Responding to REQUEST_CONFIG by sending configs');
196
- this.mediaSender?.sendConfigs();
197
- },
198
-
199
- onRequestKeyFrame: () => {
200
- console.log('📤 Responding to REQUEST_KEY_FRAME by forcing key frame');
201
- this.mediaSender?.requestKeyFrame();
202
- },
203
-
204
- onEndCall: () => {
205
- console.log('📥 Received END_CALL from remote peer');
206
- this.destroy();
207
- },
208
- });
209
-
210
- return node;
211
- } catch (error) {
212
- console.error('Failed to initialize Ermis SDK:', error);
213
- throw error;
214
- }
215
- }
216
-
217
- public async getLocalEndpointAddr(): Promise<string | null> {
218
- try {
219
- await this.initialize();
220
-
221
- if (!this.callNode) {
222
- console.error('ErmisCall is not initialized.');
223
- return null;
224
- }
225
-
226
- const address = await this.callNode.getLocalEndpointAddr();
227
- if (this.metadata) {
228
- this.metadata.address = address;
229
- }
230
- return address;
231
- } catch (error) {
232
- console.error('Failed to get address from ErmisCall:', error);
233
- return null;
234
- }
235
- }
236
-
237
- private getClient(): ErmisChat<ErmisChatGenerics> {
238
- return this._client;
239
- }
240
-
241
- private async _sendSignal(payload: SignalData) {
242
- try {
243
- return await this.getClient().post(this.getClient().baseURL + '/signal', {
244
- ...payload,
245
- cid: this.cid || payload.cid,
246
- is_video: this.callType === 'video' || payload.is_video,
247
- ios: false,
248
- session_id: this.sessionID,
249
- });
250
- } catch (error: any) {
251
- const action = payload.action;
252
-
253
- // Skip error message for HEALTH_CALL action
254
- if (action === CallAction.HEALTH_CALL) {
255
- return;
256
- }
257
-
258
- if (typeof this.onError === 'function') {
259
- if (error.code === 'ERR_NETWORK') {
260
- if (action === CallAction.CREATE_CALL) {
261
- this.onError('Unable to make the call. Please check your network connection');
262
- }
263
- } else {
264
- if (error.response.data.ermis_code === 20) {
265
- this.onError('Recipient was busy');
266
- } else {
267
- const errMsg = error.response.data?.message ? error.response.data?.message : 'Call failed';
268
- this.onError(errMsg);
269
- }
270
- }
271
- }
272
- }
273
- }
274
-
275
- private async getAvailableDevices(): Promise<{ audioDevices: MediaDeviceInfo[]; videoDevices: MediaDeviceInfo[] }> {
276
- try {
277
- const devices = await navigator.mediaDevices.enumerateDevices();
278
-
279
- const audioDevices = devices.filter((device) => device.kind === 'audioinput');
280
- const videoDevices = devices.filter((device) => device.kind === 'videoinput');
281
-
282
- this.availableAudioDevices = audioDevices;
283
- this.availableVideoDevices = videoDevices;
284
-
285
- return { audioDevices, videoDevices };
286
- } catch (error) {
287
- console.error('Error enumerating devices:', error);
288
- return { audioDevices: [], videoDevices: [] };
289
- }
290
- }
291
-
292
- private async getMediaConstraints() {
293
- // Get available devices first
294
- const { audioDevices, videoDevices } = await this.getAvailableDevices();
295
-
296
- // Notify UI about available devices
297
- if (this.onDeviceChange) {
298
- this.onDeviceChange(audioDevices, videoDevices);
299
- }
300
-
301
- // Auto-select default devices if none selected
302
- if (!this.selectedAudioDeviceId && audioDevices.length > 0) {
303
- this.selectedAudioDeviceId = audioDevices[0].deviceId;
304
- }
305
- if (!this.selectedVideoDeviceId && videoDevices.length > 0) {
306
- this.selectedVideoDeviceId = videoDevices[0].deviceId;
307
- }
308
-
309
- // Build constraints with specific device IDs if selected
310
- const audioConstraints = {
311
- deviceId: this.selectedAudioDeviceId ? { exact: this.selectedAudioDeviceId } : undefined,
312
- echoCancellation: true,
313
- noiseSuppression: true,
314
- sampleRate: 48000,
315
- };
316
-
317
- const videoConstraints =
318
- this.callType === 'video'
319
- ? {
320
- deviceId: this.selectedVideoDeviceId ? { exact: this.selectedVideoDeviceId } : undefined,
321
- width: 640,
322
- height: 360,
323
- }
324
- : false;
325
-
326
- const finalConstraints: MediaStreamConstraints = {
327
- audio: audioConstraints,
328
- video: videoConstraints,
329
- };
330
-
331
- return finalConstraints;
332
- }
333
-
334
- public async startLocalStream() {
335
- const mediaConstraints = await this.getMediaConstraints();
336
-
337
- try {
338
- const stream = await navigator.mediaDevices.getUserMedia(mediaConstraints);
339
- return this.applyLocalStream(stream);
340
- } catch (error: any) {
341
- console.warn('Error getting user media:', error?.message);
342
-
343
- // Video call: try fallback to audio-only (camera not available)
344
- if (this.callType === 'video' && mediaConstraints.video) {
345
- try {
346
- const audioOnlyStream = await navigator.mediaDevices.getUserMedia({
347
- audio: mediaConstraints.audio,
348
- video: false,
349
- });
350
- this.setConnectionMessage('Camera not available, using audio only');
351
- return this.applyLocalStream(audioOnlyStream);
352
- } catch {
353
- // Audio fallback also failed
354
- }
355
- }
356
-
357
- // No device found at all — report error
358
- if (typeof this.onError === 'function') {
359
- this.onError('No microphone or camera found. Please check your device.');
360
- }
361
- return null;
362
- }
363
- }
364
-
365
- private applyLocalStream(stream: MediaStream) {
366
- if (this.callStatus === CallStatus.ENDED) {
367
- stream.getTracks().forEach((track) => track.stop());
368
- this.destroy();
369
- return;
370
- }
371
- if (this.onLocalStream) {
372
- this.onLocalStream(stream);
373
- }
374
- this.localStream = stream;
375
- return stream;
376
- }
377
-
378
- private setConnectionMessage(message: string | null) {
379
- if (typeof this.onConnectionMessageChange === 'function') {
380
- this.onConnectionMessageChange(message);
381
- }
382
- }
383
-
384
- private setCallStatus(status: CallStatus) {
385
- this.callStatus = status;
386
- if (typeof this.onCallStatus === 'function') {
387
- this.onCallStatus(status);
388
- }
389
- }
390
-
391
- private setUserInfo(cid: string | undefined, eventUserId: string | undefined) {
392
- if (!cid || !eventUserId) return;
393
-
394
- // Get caller and receiver userId from activeChannels
395
- const channel = cid ? this.getClient().activeChannels[cid] : undefined;
396
- const members = channel?.state?.members || {};
397
- const memberIds = Object.keys(members);
398
-
399
- // callerId is eventUserId, receiverId is the other user in the channel
400
- const callerId = eventUserId || '';
401
- const receiverId = memberIds.find((id) => id !== callerId) || '';
402
-
403
- // Get user info from client.state.users
404
- const callerUser = this.getClient().state.users[callerId];
405
- const receiverUser = this.getClient().state.users[receiverId];
406
-
407
- this.callerInfo = {
408
- id: callerId,
409
- name: callerUser?.name,
410
- avatar: callerUser?.avatar || '',
411
- };
412
- this.receiverInfo = {
413
- id: receiverId,
414
- name: receiverUser?.name,
415
- avatar: receiverUser?.avatar || '',
416
- };
417
- }
418
-
419
- private listenSocketEvents() {
420
- this.signalHandler = async (event: Event<ErmisChatGenerics>) => {
421
- const { action, user_id: eventUserId, session_id: eventSessionId, cid, is_video, signal, metadata } = event;
422
-
423
- switch (action) {
424
- case CallAction.CREATE_CALL:
425
- if (eventUserId === this.userID && eventSessionId !== this.sessionID) {
426
- // If the event is triggered by the current user but the session ID is different,
427
- // it means another device (or tab) of the same user has started a call.
428
- // In this case, mark this call instance as destroyed and ignore further events.
429
- this.isDestroyed = true;
430
- this.destroy();
431
- return;
432
- }
433
- this.isDestroyed = false;
434
- this.callStatus = '';
435
- this.callType = is_video ? 'video' : 'audio';
436
-
437
- this.setUserInfo(cid, eventUserId);
438
- this.setCallStatus(CallStatus.RINGING);
439
- this.cid = cid || '';
440
- this.metadata = metadata || {};
441
-
442
- if (typeof this.onCallEvent === 'function') {
443
- this.onCallEvent({
444
- type: eventUserId !== this.userID ? 'incoming' : 'outgoing',
445
- callType: is_video ? 'video' : 'audio',
446
- cid: cid || '',
447
- callerInfo: this.callerInfo,
448
- receiverInfo: this.receiverInfo,
449
- metadata: this.metadata,
450
- });
451
- }
452
-
453
- await this.startLocalStream();
454
- if (this.callStatus === CallStatus.ENDED) return;
455
-
456
- if (eventUserId !== this.userID) {
457
- await this.initialize();
458
- }
459
-
460
- if (this.localStream && this.mediaSender && this.mediaReceiver) {
461
- this.mediaSender?.initEncoders(this.localStream);
462
- this.mediaReceiver?.initDecoders(this.callType);
463
- }
464
-
465
- if (eventUserId === this.userID) {
466
- // Set missCall timeout if no connection after 60s
467
- if (this.missCallTimeout) clearTimeout(this.missCallTimeout);
468
- this.missCallTimeout = setTimeout(async () => {
469
- await this.missCall();
470
- }, 60000);
471
- }
472
- break;
473
-
474
- case CallAction.ACCEPT_CALL:
475
- if (eventUserId === this.userID && eventSessionId !== this.sessionID) {
476
- this.isDestroyed = true;
477
- this.destroy();
478
- return;
479
- }
480
-
481
- if (eventUserId !== this.userID && !this.isDestroyed) {
482
- if (this.mediaReceiver && this.mediaSender) {
483
- await this.mediaReceiver.acceptConnection();
484
- await this.mediaSender.sendConnected();
485
- await this.mediaSender.sendConfigs();
486
- }
487
- }
488
- break;
489
-
490
- case CallAction.END_CALL:
491
- case CallAction.REJECT_CALL:
492
- case CallAction.MISS_CALL:
493
- // this.setCallStatus(CallStatus.ENDED);
494
- this.destroy();
495
- break;
496
- }
497
- };
498
-
499
- this.connectionChangedHandler = (event: Event<ErmisChatGenerics>) => {
500
- const online = event.online;
501
- this.isOffline = !online;
502
- if (!online) {
503
- this.setConnectionMessage('Your network connection is unstable');
504
-
505
- // Clear health_call intervals when offline
506
- if (this.healthCallInterval) {
507
- clearInterval(this.healthCallInterval);
508
- this.healthCallInterval = null;
509
- }
510
- if (this.healthCallServerInterval) {
511
- clearInterval(this.healthCallServerInterval);
512
- this.healthCallServerInterval = null;
513
- }
514
- } else {
515
- this.setConnectionMessage(null);
516
-
517
- // When back online, if CONNECTED, set up health_call intervals again
518
- if (this.callStatus === CallStatus.CONNECTED) {
519
- if (!this.healthCallServerInterval) {
520
- this.healthCallServerInterval = setInterval(() => {
521
- this.healthCall();
522
- }, 10000);
523
- }
524
- }
525
- }
526
- };
527
-
528
- this.messageUpdatedHandler = (event: Event<ErmisChatGenerics>) => {
529
- if (this.callStatus === CallStatus.CONNECTED && event.cid === this.cid) {
530
- const upgradeUserId = event.user?.id;
531
-
532
- let upgraderInfo: UserCallInfo | undefined;
533
-
534
- if (upgradeUserId === this.callerInfo?.id) {
535
- upgraderInfo = this.callerInfo;
536
- } else if (upgradeUserId === this.receiverInfo?.id) {
537
- upgraderInfo = this.receiverInfo;
538
- }
539
-
540
- if (upgraderInfo && typeof this.onUpgradeCall === 'function') {
541
- this.onUpgradeCall(upgraderInfo);
542
- }
543
- }
544
- };
545
-
546
- this._client.on('signal', this.signalHandler);
547
- this._client.on('connection.changed', this.connectionChangedHandler);
548
- this._client.on('message.updated', this.messageUpdatedHandler);
549
- }
550
-
551
- private cleanupCall() {
552
- if (this.mediaSender) {
553
- this.mediaSender?.stop();
554
- this.mediaSender = null;
555
- }
556
- if (this.mediaReceiver) {
557
- this.mediaReceiver.stop();
558
- this.mediaReceiver = null;
559
- }
560
-
561
- if (this.callNode) {
562
- this.callNode?.closeEndpoint();
563
-
564
- if (this.callStatus === CallStatus.CONNECTED) {
565
- this.callNode?.closeConnection();
566
- }
567
-
568
- this.callNode = null;
569
- }
570
-
571
- // Clear all timeouts and intervals
572
- if (this.missCallTimeout) {
573
- clearTimeout(this.missCallTimeout);
574
- this.missCallTimeout = null;
575
- }
576
-
577
- if (this.healthCallInterval) {
578
- clearInterval(this.healthCallInterval);
579
- this.healthCallInterval = null;
580
- }
581
-
582
- if (this.healthCallServerInterval) {
583
- clearInterval(this.healthCallServerInterval);
584
- this.healthCallServerInterval = null;
585
- }
586
-
587
- if (this.healthCallTimeout) {
588
- clearTimeout(this.healthCallTimeout);
589
- this.healthCallTimeout = null;
590
- }
591
-
592
- if (this.healthCallWarningTimeout) {
593
- clearTimeout(this.healthCallWarningTimeout);
594
- this.healthCallWarningTimeout = null;
595
- }
596
-
597
- // this.setCallStatus(CallStatus.ENDED);
598
- this.setConnectionMessage(null);
599
- this.cid = '';
600
- this.callType = '';
601
- this.metadata = {};
602
-
603
- if (this.localStream) {
604
- this.localStream.getTracks().forEach((track) => track.stop());
605
- this.localStream = null;
606
- }
607
-
608
- if (this.remoteStream) {
609
- this.remoteStream.getTracks().forEach((track) => track.stop());
610
- this.remoteStream = null;
611
- }
612
-
613
- this.setCallStatus(CallStatus.ENDED);
614
- }
615
-
616
- private destroy() {
617
- // if (this.signalHandler) this._client.off('signal', this.signalHandler);
618
- // if (this.connectionChangedHandler) this._client.off('connection.changed', this.connectionChangedHandler);
619
- // if (this.messageUpdatedHandler) this._client.off('message.updated', this.messageUpdatedHandler);
620
- this.cleanupCall();
621
- }
622
-
623
- public async getDevices(): Promise<{ audioDevices: MediaDeviceInfo[]; videoDevices: MediaDeviceInfo[] }> {
624
- // Return cached devices if available, otherwise fetch new ones
625
- if (this.availableAudioDevices.length > 0 || this.availableVideoDevices.length > 0) {
626
- return {
627
- audioDevices: this.availableAudioDevices,
628
- videoDevices: this.availableVideoDevices,
629
- };
630
- }
631
- return await this.getAvailableDevices();
632
- }
633
-
634
- // Get current selected devices info
635
- public getSelectedDevices(): { audioDevice?: MediaDeviceInfo; videoDevice?: MediaDeviceInfo } {
636
- const audioDevice = this.selectedAudioDeviceId
637
- ? this.availableAudioDevices.find((device) => device.deviceId === this.selectedAudioDeviceId)
638
- : undefined;
639
-
640
- const videoDevice = this.selectedVideoDeviceId
641
- ? this.availableVideoDevices.find((device) => device.deviceId === this.selectedVideoDeviceId)
642
- : undefined;
643
-
644
- return { audioDevice, videoDevice };
645
- }
646
-
647
- // Get default devices (first available device)
648
- public getDefaultDevices(): { audioDevice?: MediaDeviceInfo; videoDevice?: MediaDeviceInfo } {
649
- return {
650
- audioDevice: this.availableAudioDevices[0],
651
- videoDevice: this.availableVideoDevices[0],
652
- };
653
- }
654
-
655
- public async createCall(callType: string, cid: string) {
656
- try {
657
- this.cid = cid;
658
-
659
- const address = await this.getLocalEndpointAddr();
660
-
661
- await this._sendSignal({
662
- action: CallAction.CREATE_CALL,
663
- cid,
664
- is_video: callType === 'video',
665
- metadata: { address },
666
- });
667
- } catch (error) {
668
- console.error('Failed to create call:', error);
669
- throw error;
670
- }
671
- }
672
-
673
- public async acceptCall() {
674
- try {
675
- await this._sendSignal({ action: CallAction.ACCEPT_CALL });
676
-
677
- if (this.mediaSender) {
678
- const address = this.metadata?.address || '';
679
- await this.mediaSender.connect(address);
680
- }
681
- } catch (error) {
682
- console.error('Failed to accept call:', error);
683
- throw error;
684
- }
685
- }
686
-
687
- public async endCall() {
688
- await this._sendSignal({ action: CallAction.END_CALL });
689
- this.destroy();
690
- }
691
-
692
- public async rejectCall() {
693
- await this._sendSignal({ action: CallAction.REJECT_CALL });
694
- this.destroy();
695
- }
696
-
697
- private async missCall() {
698
- await this._sendSignal({ action: CallAction.MISS_CALL });
699
- this.destroy();
700
- }
701
-
702
- private async connectCall() {
703
- return await this._sendSignal({ action: CallAction.CONNECT_CALL });
704
- }
705
-
706
- private async healthCall() {
707
- return await this._sendSignal({ action: CallAction.HEALTH_CALL });
708
- }
709
-
710
- private async addVideoTrackToLocalStream() {
711
- const mediaConstraints = await this.getMediaConstraints();
712
- const stream = await navigator.mediaDevices.getUserMedia(mediaConstraints);
713
- const newVideoTrack = stream.getVideoTracks()[0];
714
- if (this.localStream) {
715
- this.localStream.addTrack(newVideoTrack);
716
-
717
- if (this.onLocalStream) {
718
- this.onLocalStream(this.localStream);
719
- }
720
- } else {
721
- this.localStream = stream;
722
- if (this.onLocalStream) {
723
- this.onLocalStream(this.localStream);
724
- }
725
- }
726
- }
727
-
728
- public async upgradeCall() {
729
- try {
730
- this.callType = 'video';
731
- await this.addVideoTrackToLocalStream();
732
- await this._sendSignal({ action: CallAction.UPGRADE_CALL });
733
-
734
- if (this.localStream) {
735
- this.mediaSender?.initVideoEncoder(this.localStream?.getVideoTracks()[0]);
736
- const audioEnable = !!this.localStream?.getAudioTracks().some((track) => track.enabled);
737
- const videoEnable = !!this.localStream?.getVideoTracks().some((track) => track.enabled);
738
- await this.mediaSender?.sendTransceiverState(audioEnable, videoEnable);
739
- }
740
- } catch (error) {
741
- console.error('Failed to upgrade call:', error);
742
- throw error;
743
- }
744
- }
745
-
746
- public async requestUpgradeCall(enabled: boolean) {
747
- if (enabled) {
748
- this.callType = 'video';
749
- await this.addVideoTrackToLocalStream();
750
-
751
- if (this.localStream) {
752
- this.mediaSender?.initVideoEncoder(this.localStream?.getVideoTracks()[0]);
753
- const audioEnable = !!this.localStream?.getAudioTracks().some((track) => track.enabled);
754
- const videoEnable = !!this.localStream?.getVideoTracks().some((track) => track.enabled);
755
- await this.mediaSender?.sendTransceiverState(audioEnable, videoEnable);
756
- }
757
- }
758
- }
759
-
760
- public async startScreenShare() {
761
- // @ts-ignore
762
- if (!navigator.mediaDevices.getDisplayMedia) {
763
- throw new Error('Screen sharing is not supported in this browser.');
764
- }
765
-
766
- // @ts-ignore
767
- const screenStream = await navigator.mediaDevices.getDisplayMedia({ video: true });
768
- const screenTrack = screenStream.getVideoTracks()[0];
769
-
770
- // Replace video track in localStream
771
- if (this.localStream) {
772
- // Stop old track
773
- this.localStream.getVideoTracks().forEach((track) => track.stop());
774
- // Add new track to localStream
775
- this.localStream.removeTrack(this.localStream.getVideoTracks()[0]);
776
- this.localStream.addTrack(screenTrack);
777
- } else {
778
- // If no localStream, create new one
779
- this.localStream = screenStream;
780
- }
781
-
782
- // When screen sharing stops, automatically switch back to camera
783
- screenTrack.onended = () => {
784
- this.stopScreenShare();
785
- };
786
-
787
- // Call callback if UI needs to update
788
- if (this.onLocalStream) {
789
- // @ts-ignore
790
- this.onLocalStream(this.localStream);
791
-
792
- this.mediaSender?.replaceVideoTrack(this.localStream.getVideoTracks()[0]);
793
- }
794
-
795
- // Call callback when screen sharing starts
796
- if (typeof this.onScreenShareChange === 'function') {
797
- this.onScreenShareChange(true);
798
- }
799
- }
800
-
801
- public async stopScreenShare() {
802
- const mediaConstraints = await this.getMediaConstraints();
803
-
804
- try {
805
- // Only request video; we already have an active audio track in localStream
806
- const cameraStream = await navigator.mediaDevices.getUserMedia({
807
- video: mediaConstraints.video,
808
- audio: false,
809
- });
810
- const cameraTrack = cameraStream.getVideoTracks()[0];
811
-
812
- // Replace video track in localStream
813
- if (this.localStream) {
814
- // Stop old screen tracks
815
- this.localStream.getVideoTracks().forEach((track) => {
816
- track.stop();
817
- this.localStream?.removeTrack(track);
818
- });
819
-
820
- // Add new camera track
821
- this.localStream.addTrack(cameraTrack);
822
- } else {
823
- this.localStream = cameraStream;
824
- }
825
-
826
- // Call callback if UI needs to update
827
- if (this.onLocalStream) {
828
- this.onLocalStream(this.localStream);
829
- this.mediaSender?.replaceVideoTrack(this.localStream.getVideoTracks()[0]);
830
- }
831
-
832
- // Call callback when screen sharing stops
833
- if (typeof this.onScreenShareChange === 'function') {
834
- this.onScreenShareChange(false);
835
- }
836
- } catch (error) {
837
- console.error('Error stopping screen share and reverting to camera:', error);
838
- }
839
- }
840
-
841
- public async toggleMic(enabled: boolean) {
842
- if (this.localStream) {
843
- this.localStream.getAudioTracks().forEach((track) => {
844
- track.enabled = enabled;
845
- });
846
-
847
- const audioEnable = enabled;
848
- const videoEnable = this.localStream.getVideoTracks().some((track) => track.enabled);
849
- await this.mediaSender?.sendTransceiverState(audioEnable, videoEnable);
850
- }
851
- }
852
-
853
- public async toggleCamera(enabled: boolean) {
854
- if (this.localStream) {
855
- this.localStream.getVideoTracks().forEach((track) => {
856
- track.enabled = enabled;
857
- });
858
-
859
- const audioEnable = this.localStream.getAudioTracks().some((track) => track.enabled);
860
- const videoEnable = enabled;
861
- await this.mediaSender?.sendTransceiverState(audioEnable, videoEnable);
862
- }
863
- }
864
-
865
- // Public method to switch audio device
866
- public async switchAudioDevice(deviceId: string): Promise<boolean> {
867
- try {
868
- // Validate device exists in available devices
869
- const targetDevice = this.availableAudioDevices.find((device) => device.deviceId === deviceId);
870
- if (!targetDevice) {
871
- console.error('Audio device not found:', deviceId);
872
- if (this.onError) {
873
- this.onError('Selected microphone not found');
874
- }
875
- return false;
876
- }
877
-
878
- this.selectedAudioDeviceId = deviceId;
879
-
880
- if (!this.localStream) return false;
881
-
882
- // Get new audio stream with selected device
883
- const newStream = await navigator.mediaDevices.getUserMedia({
884
- audio: { deviceId: { exact: deviceId } },
885
- video: false,
886
- });
887
-
888
- const newAudioTrack = newStream.getAudioTracks()[0];
889
- const oldAudioTrack = this.localStream.getAudioTracks()[0];
890
-
891
- // Replace audio track in peer connection
892
- // if (this.peer && oldAudioTrack) {
893
- // const sender = this.peer.getSenders().find((s) => s.track === oldAudioTrack);
894
- // if (sender) {
895
- // await sender.replaceTrack(newAudioTrack);
896
- // }
897
- // }
898
-
899
- // Replace audio track in local stream
900
- if (oldAudioTrack) {
901
- this.localStream.removeTrack(oldAudioTrack);
902
- oldAudioTrack.stop();
903
- }
904
- this.localStream.addTrack(newAudioTrack);
905
-
906
- // Update UI
907
- if (this.onLocalStream) {
908
- this.onLocalStream(this.localStream);
909
- }
910
-
911
- return true;
912
- } catch (error) {
913
- console.error('Error switching audio device:', error);
914
- if (this.onError) {
915
- this.onError('Failed to switch microphone');
916
- }
917
- return false;
918
- }
919
- }
920
-
921
- // Public method to switch video device
922
- public async switchVideoDevice(deviceId: string): Promise<boolean> {
923
- try {
924
- // Validate device exists in available devices
925
- const targetDevice = this.availableVideoDevices.find((device) => device.deviceId === deviceId);
926
- if (!targetDevice) {
927
- console.error('Video device not found:', deviceId);
928
- if (this.onError) {
929
- this.onError('Selected camera not found');
930
- }
931
- return false;
932
- }
933
-
934
- this.selectedVideoDeviceId = deviceId;
935
-
936
- if (!this.localStream) return false;
937
-
938
- // Get new video stream with selected device
939
- const newStream = await navigator.mediaDevices.getUserMedia({
940
- audio: false,
941
- video: { deviceId: { exact: deviceId } },
942
- });
943
-
944
- const newVideoTrack = newStream.getVideoTracks()[0];
945
- const oldVideoTrack = this.localStream.getVideoTracks()[0];
946
-
947
- // Replace video track in local stream
948
- if (oldVideoTrack) {
949
- this.localStream.removeTrack(oldVideoTrack);
950
- oldVideoTrack.stop();
951
- }
952
- this.localStream.addTrack(newVideoTrack);
953
-
954
- // Update UI
955
- if (this.onLocalStream) {
956
- this.onLocalStream(this.localStream);
957
- }
958
-
959
- return true;
960
- } catch (error) {
961
- console.error('Error switching video device:', error);
962
- if (this.onError) {
963
- this.onError('Failed to switch camera');
964
- }
965
- return false;
966
- }
967
- }
968
-
969
- // Listen for device changes
970
- private setupDeviceChangeListener() {
971
- navigator.mediaDevices.addEventListener('devicechange', async () => {
972
- const { audioDevices, videoDevices } = await this.getAvailableDevices();
973
- if (this.onDeviceChange) {
974
- this.onDeviceChange(audioDevices, videoDevices);
975
- }
976
- });
977
- }
978
- }