sceyt-call 1.0.0

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.
@@ -0,0 +1,764 @@
1
+ import {
2
+ ConnectionState,
3
+ ICallEvents,
4
+ ICallEventType,
5
+ ICallParticipantsRTCMap,
6
+ IJoinSignalMessage,
7
+ IOfferSignalMessage,
8
+ ISignalMessageResponse,
9
+ PresenceState
10
+ } from "../../types";
11
+ import { Participant } from "../participant";
12
+ import { WebRTCClient } from "../webrtcClient";
13
+ import { Signaling } from "../signaling";
14
+ import { INFO_MESSAGE, SIGNAL } from "../../constants";
15
+ import { CALL_TYPE } from "../../enum";
16
+ import { Call } from "../call";
17
+ import { getEmptyVideoTrack, makeFirstById } from "../../helpers";
18
+ import logger from "../../utils/logger";
19
+
20
+ export class InternalCallHandler {
21
+ private static instance: InternalCallHandler | null = null;
22
+ private readonly chatClient: any;
23
+ private rtcConfig?: RTCConfiguration;
24
+ private callParticipantsRTCMap: ICallParticipantsRTCMap = {};
25
+ private readonly signalingClient: Signaling;
26
+ activeCalls: Call[] = [];
27
+ private callEvents: ICallEvents = {};
28
+ private readonly user: any;
29
+
30
+ constructor(chatClient: any) {
31
+ if (!chatClient) {
32
+ logger.info('ChatClient is required for InternalCallHandler initialization');
33
+ }
34
+ this.chatClient = chatClient;
35
+ this.user = chatClient.user;
36
+ this.signalingClient = new Signaling(chatClient, this.handleSignalMessage.bind(this));
37
+ }
38
+
39
+ public static initialize(chatClient: any): InternalCallHandler {
40
+ if (!InternalCallHandler.instance) {
41
+ InternalCallHandler.instance = new InternalCallHandler(chatClient);
42
+ }
43
+ return InternalCallHandler.instance;
44
+ }
45
+
46
+ public static getInstance(): InternalCallHandler | null {
47
+ if (!InternalCallHandler.instance) {
48
+ logger.info('InternalCallHandler not initialized. Call initialize() with chatClient first.');
49
+ return null;
50
+ }
51
+ return InternalCallHandler.instance;
52
+ }
53
+
54
+ public static hasInstance(): boolean {
55
+ return InternalCallHandler.instance !== null;
56
+ }
57
+
58
+ public static reset(): void {
59
+ InternalCallHandler.instance = null;
60
+ }
61
+
62
+ public setCallEvent(eventType: ICallEventType, callback: (...args: any) => void): void {
63
+ this.callEvents[eventType] = callback;
64
+ }
65
+
66
+ private async handleSignalMessage(message: ISignalMessageResponse): Promise<void> {
67
+ if (!this.activeCalls) return;
68
+
69
+ const call = this.activeCalls.find((call) => call.id === message.roomId);
70
+ if (!call) return;
71
+
72
+ try {
73
+ switch (message.signal) {
74
+ case SIGNAL.OFFER:
75
+ await this.handleOffer(call, message);
76
+ break;
77
+ case SIGNAL.ANSWER:
78
+ await this.handleAnswer(call, message);
79
+ break;
80
+ case SIGNAL.USER_JOINED:
81
+ await this.handleUserJoined(call, message);
82
+ break;
83
+ case SIGNAL.LEAVE:
84
+ await this.handleLeave(call, message);
85
+ break;
86
+ case SIGNAL.INVITE:
87
+ await this.handleInvite(message);
88
+ break;
89
+ default:
90
+ logger.warn(`Unhandled signal type: ${message.signal}`);
91
+ }
92
+ } catch (error) {
93
+ logger.error(`Error handling signal ${message.signal}:`, error);
94
+ this.handleSignalingError(call, error);
95
+ }
96
+ }
97
+
98
+ private async handleOffer(call: Call, message: ISignalMessageResponse): Promise<void> {
99
+ if (call.state !== "connected") return;
100
+
101
+ const participant = call.participants.find((p) => p.id === message.from);
102
+ if (!participant) return;
103
+
104
+ try {
105
+ if (call.mediaFlow === "p2p") {
106
+ await this.addTracksToPeerConnection(call, participant.id);
107
+ }
108
+
109
+ if (participant.shouldResetPC) {
110
+ await this.resetPeerConnection(call, participant);
111
+ }
112
+
113
+ await this.setRemoteDescription(call.id, participant.id, {
114
+ sdp: message.sdp,
115
+ type: "offer"
116
+ });
117
+
118
+ const answer = await this.callParticipantsRTCMap[call.id][participant.id].createAnswer();
119
+ await this.setLocalDescription(call.id, participant.id, answer);
120
+
121
+ await this.signalingClient.sendSignalMessage({
122
+ to: message.from,
123
+ callType: message.type,
124
+ roomId: call.id,
125
+ signal: SIGNAL.ANSWER,
126
+ sdp: answer.sdp,
127
+ sessionId: message.sessionId
128
+ });
129
+ } catch (error) {
130
+ logger.error('Error handling offer:', error);
131
+ participant.handleEvent("CALL_DISCONNECTED");
132
+ throw error;
133
+ }
134
+ }
135
+
136
+ private async resetPeerConnection(call: Call, participant: Participant): Promise<void> {
137
+ this.closePeerConnections(call.id, participant.id);
138
+ this.addParticipantToRTCMap(call, participant);
139
+ participant.setShouldResetPC(false);
140
+ }
141
+
142
+ private handleSignalingError(call: Call, error: any): void {
143
+ logger.error('Signaling error:', error);
144
+ // Implement appropriate error recovery logic
145
+ if (call.state === "connected") {
146
+ call.setState("reconnecting");
147
+ // Attempt to recover the connection
148
+ this.attemptConnectionRecovery(call).catch(e =>
149
+ logger.error('Failed to recover connection:', e)
150
+ );
151
+ }
152
+ }
153
+
154
+ private async attemptConnectionRecovery(call: Call): Promise<void> {
155
+ // Wait a bit before attempting recovery
156
+ await new Promise(resolve => setTimeout(resolve, 2000));
157
+
158
+ try {
159
+ // Attempt to re-establish connections for all participants
160
+ for (const participant of call.participants) {
161
+ if (participant.id !== this.user.id) {
162
+ await this.resetPeerConnection(call, participant);
163
+ // Create new offer for the participant
164
+ const desc = await this.callParticipantsRTCMap[call.id][participant.id].createOffer();
165
+ await this.setLocalDescription(call.id, participant.id, desc);
166
+ await this.sendOffer({
167
+ callType: CALL_TYPE.group,
168
+ callId: call.id,
169
+ description: desc,
170
+ to: participant.id,
171
+ sessionId: call.sessionId,
172
+ });
173
+ }
174
+ }
175
+ } catch (error) {
176
+ logger.error('Recovery attempt failed:', error);
177
+ call.setState("idle"); // Reset to idle state on complete failure
178
+ // Notify about the failed recovery
179
+ call.onCallStateChanged(call, "Failed to recover connection");
180
+ }
181
+ }
182
+
183
+ private async handleAnswer(call: Call, message: ISignalMessageResponse): Promise<void> {
184
+ if (call.state !== "connected") return;
185
+
186
+ try {
187
+ let participant;
188
+ if (call.serverParticipant && call.serverParticipant.id === message.from) {
189
+ participant = call.serverParticipant;
190
+
191
+ // For group calls, when server answers, set local participant to connected
192
+ if (call.mediaFlow !== "p2p") {
193
+ logger.info("Setting local participant to connected in group call");
194
+ call.localParticipant.handleEvent("CALL_CONNECTED");
195
+ }
196
+ } else {
197
+ participant = call.participants.find((p) => p.id === message.from);
198
+ }
199
+
200
+ if (participant) {
201
+ await this.setRemoteDescription(call.id, participant.id, {
202
+ sdp: message.sdp,
203
+ type: "answer"
204
+ });
205
+
206
+ if (call.mediaFlow !== "p2p") {
207
+ message.participants?.forEach((p) => {
208
+ if (p.id !== this.user.id) {
209
+ logger.info("Send CONNECT to participant: ", p.id);
210
+ this.signalingClient.sendSignalMessage({
211
+ to: p.id,
212
+ callType: message.type,
213
+ roomId: call.id,
214
+ signal: SIGNAL.CONNECT,
215
+ sessionId: call.sessionId
216
+ });
217
+ }
218
+ });
219
+
220
+ // Set server participant to connected state
221
+ participant.handleEvent("CALL_CONNECTED");
222
+ }
223
+
224
+ call.setState("connected");
225
+ }
226
+ } catch (error) {
227
+ logger.error('Error handling answer:', error);
228
+ }
229
+ }
230
+
231
+ private async handleUserJoined(call: Call, message: ISignalMessageResponse): Promise<void> {
232
+ logger.info("handle user joined", message);
233
+ const participant = call.participants.find((p) => p.id === message.from);
234
+ if (participant) {
235
+ // Follow the correct state transition flow
236
+ participant.handleEvent("CALL_INVITED"); // Pending -> Invited
237
+ participant.handleEvent("CALL_RINGING"); // Invited -> Ringing
238
+ participant.handleEvent("CALL_ACCEPTED"); // Ringing -> Joined
239
+
240
+ if (message.videoEnabled) {
241
+ participant.setVideoEnabled(true);
242
+ call.setVideoEnabled(true);
243
+ }
244
+
245
+ if (call.mediaFlow === "p2p") {
246
+ this.addParticipantToRTCMap(call, participant);
247
+ this.addTracksToPeerConnection(call, participant.id);
248
+ const desc = await this.createOffer(call.id, participant.id);
249
+ await this.setLocalDescription(call.id, participant.id, desc);
250
+ await this.sendOffer({
251
+ callType: CALL_TYPE.p2p,
252
+ callId: call.id,
253
+ to: participant.id,
254
+ description: desc,
255
+ sessionId: call.sessionId,
256
+ });
257
+ }
258
+ }
259
+ }
260
+
261
+ private async handleLeave(call: Call, message: ISignalMessageResponse): Promise<void> {
262
+ logger.info("handle leave", message);
263
+ const participant = call.participants.find((p) => p.id === message.from);
264
+ if (participant) {
265
+ participant.setPresenceState(PresenceState.Left);
266
+ participant.setConnectionState(ConnectionState.Disconnected);
267
+ call.onParticipantStateChanged(call, participant, ConnectionState.Disconnected);
268
+ call.kickParticipantFromList(participant);
269
+ call.onParticipantLeft(call, participant);
270
+ this.closePeerConnections(call.id, participant.id);
271
+ }
272
+ }
273
+
274
+ private async handleInvite(message: ISignalMessageResponse): Promise<void> {
275
+ logger.info("handle invite", message);
276
+ if (!message.participants) {
277
+ logger.error("No participants in invite message");
278
+ return;
279
+ }
280
+
281
+ const call = new Call({
282
+ id: message.roomId,
283
+ sessionId: message.sessionId,
284
+ mediaFlow: message.type === CALL_TYPE.group ? "sfu" : "p2p",
285
+ originatorId: message.from,
286
+ localParticipant: new Participant(this.user.id),
287
+ participants: message.participants.map((p: any) => new Participant(p.id)),
288
+ });
289
+
290
+ // Set initial states for all participants using events
291
+ call.participants.forEach(participant => {
292
+ if (participant.id === message.from) {
293
+ // Originator follows Pending -> Invited -> Ringing -> Joined
294
+ participant.handleEvent("CALL_INVITED");
295
+ participant.handleEvent("CALL_RINGING");
296
+ participant.handleEvent("CALL_ACCEPTED");
297
+ } else if (participant.id === this.user.id) {
298
+ // Local user is being invited: Pending -> Invited
299
+ participant.handleEvent("CALL_INVITED");
300
+ } else {
301
+ // Other participants start in Pending state
302
+ // They will receive CALL_INVITED when they get the invite signal
303
+ }
304
+ });
305
+
306
+ this.activeCalls.push(call);
307
+ if (this.callEvents.onInvitedToCall) {
308
+ this.callEvents.onInvitedToCall(call);
309
+ }
310
+
311
+ // Send ringing signal for the local participant
312
+ await this.signalingClient.sendSignalMessage({
313
+ callType: CALL_TYPE[call.mediaFlow === "sfu" ? "group" : "p2p"],
314
+ roomId: call.id,
315
+ signal: SIGNAL.RINGING,
316
+ sessionId: call.sessionId,
317
+ participantIds: [message.from] // Send ringing only to the originator
318
+ });
319
+
320
+ // Update local participant state to ringing
321
+ call.localParticipant.handleEvent("CALL_RINGING");
322
+ }
323
+
324
+ setRTCConfig(config: RTCConfiguration) {
325
+ this.rtcConfig = config;
326
+ }
327
+
328
+ private onIceCandidate = (participant: Participant, e: RTCPeerConnectionIceEvent, call: Call) => {
329
+ if (e.candidate) {
330
+ try {
331
+ this.signalingClient.sendSignalMessage({
332
+ to: participant.id,
333
+ signal: SIGNAL.ICE,
334
+ callType: CALL_TYPE[call.mediaFlow === "sfu" ? "group" : "p2p"],
335
+ roomId: call.id,
336
+ sessionId: call.sessionId,
337
+ ice: {
338
+ candidate: e.candidate.candidate,
339
+ sdpMid: e.candidate.sdpMid || "",
340
+ sdpMLineIndex: e.candidate.sdpMLineIndex || 0,
341
+ },
342
+ });
343
+ } catch (e) {
344
+ logger.info("Failed to send ice: ", e);
345
+ }
346
+ /* .then((res: any) => {
347
+ logger.info("ice res: ", res);
348
+ })
349
+ .catch((e: any) => {
350
+ logger.info("error ice:", e);
351
+ });*/
352
+ }
353
+ };
354
+
355
+ private onTrack = (call: Call, participant: Participant, e: any) => {
356
+ if (e.track.kind === "audio") {
357
+ participant.setAudioTracks([e.track]);
358
+ call.onParticipantAudioTrackAdded(call, participant, e.track);
359
+ } else if (e.track.kind === "video") {
360
+ participant.setVideoTracks([e.track]);
361
+ if (call.videoEnabled && !participant.videoEnabled) {
362
+ participant.setVideoEnabled(true);
363
+ call.setVideoEnabled(true);
364
+ }
365
+ call.onParticipantVideoTrackAdded(call, participant, e.track);
366
+ }
367
+ };
368
+
369
+ onIceCandidateListener(participant: Participant, call: Call) {
370
+ return (e: any) => {
371
+ this.onIceCandidate(participant, e, call);
372
+ };
373
+ }
374
+
375
+ onSignalingStateChangeListener(participant: Participant) {
376
+ return () => {
377
+ logger.info(`PC signalingstatechange, participant: ${participant.id}`);
378
+ };
379
+ }
380
+
381
+ onIceConnectionStateChangeListener(participant: Participant, call: Call, client: WebRTCClient) {
382
+ return () => {
383
+ const state = client.peerConnection.iceConnectionState;
384
+ logger.info("Ice connection state changed to: ", state);
385
+ switch (state) {
386
+ case "checking":
387
+ participant.setConnectionState(ConnectionState.Connecting);
388
+ call.onParticipantStateChanged(call, participant, ConnectionState.Connecting);
389
+ break;
390
+ case "connected":
391
+ participant.setConnectionState(ConnectionState.Connected);
392
+ call.onParticipantStateChanged(call, participant, ConnectionState.Connected);
393
+ break;
394
+ case "disconnected":
395
+ participant.setConnectionState(ConnectionState.Reconnecting);
396
+ call.onParticipantStateChanged(call, participant, ConnectionState.Reconnecting);
397
+ break;
398
+ case "failed":
399
+ case "closed":
400
+ participant.setConnectionState(ConnectionState.Disconnected);
401
+ call.onParticipantStateChanged(call, participant, ConnectionState.Disconnected);
402
+ break;
403
+ default:
404
+ break;
405
+ }
406
+ };
407
+ }
408
+
409
+ onTrackListener(call: Call, participant: Participant) {
410
+ return (event: any) => {
411
+ logger.info(`PC track, participant: ${participant.id}`);
412
+ this.onTrack(call, participant, event);
413
+ };
414
+ }
415
+
416
+ private closePeerConnections(callId: string, participantId: string) {
417
+ logger.info("close peer connection >>.... callId ,, ", callId, "participantId: ", participantId);
418
+ logger.info("this.callParticipantsRTCMap >>.... ", this.callParticipantsRTCMap);
419
+ logger.info("this.callParticipantsRTCMap[callId] >>.... ", this.callParticipantsRTCMap[callId]);
420
+ const webrtcClient = this.callParticipantsRTCMap[callId] && this.callParticipantsRTCMap[callId][participantId];
421
+ logger.info("webrtcClient. . . . ", webrtcClient);
422
+ if (webrtcClient) {
423
+ if (webrtcClient.listeners) {
424
+ logger.info("webrtcClient.listeners: ", webrtcClient.listeners);
425
+ logger.info("Object.keys(webrtcClient.listeners): ", Object.keys(webrtcClient.listeners));
426
+ Object.keys(webrtcClient.listeners).forEach((listener) => {
427
+ logger.info("close listener: ", listener);
428
+ webrtcClient.removeEventListener(listener, webrtcClient.listeners[listener]);
429
+ });
430
+ }
431
+ webrtcClient.close();
432
+ // @ts-ignore
433
+ this.callParticipantsRTCMap[callId][participantId] = null;
434
+ delete this.callParticipantsRTCMap[callId][participantId];
435
+ }
436
+ }
437
+
438
+ addParticipantToRTCMap(call: Call, participant: Participant) {
439
+ if (!this.callParticipantsRTCMap[call.id]) {
440
+ this.callParticipantsRTCMap[call.id] = {};
441
+ }
442
+ // this.callParticipantsRTCMap[call.id][participant.id] = new WebrtcClient(this.rtcConfig);
443
+ const webrtcClient = new WebRTCClient(this.rtcConfig);
444
+ this.callParticipantsRTCMap[call.id][participant.id] = webrtcClient;
445
+ const iceCandidateListener = this.onIceCandidateListener(participant, call);
446
+ const signalingStateChangeListener = this.onSignalingStateChangeListener(participant);
447
+ const iceConnectionStateChangeListener = this.onIceConnectionStateChangeListener(participant, call, webrtcClient);
448
+ const trackListener = this.onTrackListener(call, participant);
449
+ webrtcClient.addEventListener("icecandidate", iceCandidateListener);
450
+ webrtcClient.addEventListener("signalingstatechange", signalingStateChangeListener);
451
+ webrtcClient.addEventListener("connectionstatechange", iceConnectionStateChangeListener);
452
+ webrtcClient.addEventListener("track", trackListener);
453
+ webrtcClient.setListeners({
454
+ iceCandidateListener,
455
+ signalingStateChangeListener,
456
+ iceConnectionStateChangeListener,
457
+ trackListener,
458
+ });
459
+
460
+ logger.info("webrtcClient: set to callParticipantsRTCMap", call.id, " participant.id. .. ", participant.id);
461
+ logger.info("webrtcClient: ", webrtcClient);
462
+
463
+ /* this.callParticipantsRTCMap[call.id][participant.id].addEventListener(
464
+ "icecandidate",
465
+ (e: RTCPeerConnectionIceEvent) => this.onIceCandidate(participant, e, call),
466
+ );
467
+ this.callParticipantsRTCMap[call.id][participant.id].addEventListener("signalingstatechange", (event: any) => {
468
+ logger.info(`PC signalingstatechange: ${event}, participant: ${participant.id}`);
469
+ });
470
+
471
+ this.callParticipantsRTCMap[call.id][participant.id].addEventListener("connectionstatechange", (event: any) => {
472
+ logger.info(
473
+ "PC connectionstatechange:",
474
+ event.currentTarget.connectionState,
475
+ " participant: .. ",
476
+ participant.id,
477
+ );
478
+ participant.setState(event.currentTarget.connectionState);
479
+ call.onParticipantStateChanged(call, participant, event.currentTarget.connectionState);
480
+ });
481
+
482
+ this.callParticipantsRTCMap[call.id][participant.id].addEventListener("track", (event: any) => {
483
+ logger.info(`PC track: ${event}, participant: ${participant.id}`);
484
+ this.onTrack(call!, participant, event);
485
+ });*/
486
+ return participant;
487
+ }
488
+
489
+ joinToCall(options: IJoinSignalMessage) {
490
+ return this.signalingClient.sendSignalMessage({
491
+ callType: options.callType,
492
+ roomId: options.roomId,
493
+ ...(options.sessionId && { sessionId: options.sessionId }),
494
+ signal: SIGNAL.JOIN,
495
+ participantIds: options.participantIds,
496
+ videoEnabled: options.videoEnabled,
497
+ });
498
+ }
499
+
500
+ sendRinging(call: Call) {
501
+ return this.signalingClient.sendSignalMessage({
502
+ callType: CALL_TYPE[call.mediaFlow === "sfu" ? "group" : "p2p"],
503
+ roomId: call.id,
504
+ sessionId: call.sessionId,
505
+ signal: SIGNAL.RINGING,
506
+ participantIds: call.participants.map((participant) => participant.id),
507
+ });
508
+ }
509
+
510
+ rejectCall(call: Call, reason?: string) {
511
+ call.localParticipant.handleEvent("CALL_DECLINED");
512
+ this.activeCalls = this.activeCalls.filter((activeCall) => activeCall.id !== call.id);
513
+ return this.signalingClient.sendSignalMessage({
514
+ callType: CALL_TYPE[call.mediaFlow === "sfu" ? "group" : "p2p"],
515
+ roomId: call.id,
516
+ sessionId: call.sessionId,
517
+ signal: SIGNAL.DECLINE,
518
+ participantIds: call.participants.map((participant) => participant.id),
519
+ message: reason || "",
520
+ });
521
+ }
522
+
523
+ private closeCall(call: Call) {
524
+ logger.info('Handle close call. .... ')
525
+ logger.info('call.localAudioTracks. .... ', call.localAudioTracks)
526
+ call.localAudioTracks.forEach((track) => {
527
+ track.stop();
528
+ });
529
+ logger.info('call.localVideoTracks. .. ', call.localVideoTracks)
530
+ call.localVideoTracks.forEach((track) => {
531
+ track.stop();
532
+ });
533
+ call.participants.forEach((participant) => {
534
+ this.closePeerConnections(call.id, participant.id);
535
+ });
536
+
537
+ const callIndex = this.activeCalls.findIndex((call) => call.id === call.id);
538
+ this.activeCalls.splice(callIndex, 1);
539
+ delete this.callParticipantsRTCMap[call.id];
540
+ }
541
+
542
+ leaveCall(call: Call) {
543
+ if (call) {
544
+ call.localParticipant.handleEvent("CALL_LEFT");
545
+ this.closeCall(call);
546
+ return this.signalingClient.sendSignalMessage({
547
+ callType: CALL_TYPE[call.mediaFlow === "sfu" ? "group" : "p2p"],
548
+ roomId: call.id,
549
+ sessionId: call.sessionId,
550
+ signal: SIGNAL.LEAVE,
551
+ participantIds: call.participants.map((participant) => participant.id),
552
+ });
553
+ }
554
+ }
555
+
556
+ sendOffer(offerOptions: IOfferSignalMessage) {
557
+ logger.info(`Send signal: ${SIGNAL.OFFER}, to participant: ${offerOptions.to}`);
558
+ return this.signalingClient.sendSignalMessage({
559
+ callType: offerOptions.callType,
560
+ roomId: offerOptions.callId,
561
+ signal: SIGNAL.OFFER,
562
+ sdp: offerOptions.description.sdp,
563
+ ...(offerOptions.to && { to: offerOptions.to }),
564
+ sessionId: offerOptions.sessionId,
565
+ });
566
+ }
567
+
568
+ addParticipantsToCall(participantsIds: string[], call: Call) {
569
+ participantsIds.forEach((participantId) => {
570
+ const participant = new Participant(participantId);
571
+ call.addParticipantToList(participant);
572
+ call.onParticipantJoined(call, participant);
573
+ this.addParticipantToRTCMap(call, participant);
574
+ });
575
+
576
+ return this.signalingClient.sendSignalMessage({
577
+ callType: CALL_TYPE[call.mediaFlow === "sfu" ? "group" : "p2p"],
578
+ roomId: call.id,
579
+ sessionId: call.sessionId,
580
+ signal: SIGNAL.USER_ADDED,
581
+ participantIds: participantsIds,
582
+ videoEnabled: call.videoEnabled,
583
+ });
584
+ }
585
+
586
+ createAnswer(callId: string, participantId: string) {
587
+ const webrtcClient = this.callParticipantsRTCMap[callId][participantId];
588
+ return webrtcClient.createAnswer();
589
+ }
590
+
591
+ createOffer(callId: string, participantId: string) {
592
+ const webrtcClient = this.callParticipantsRTCMap[callId][participantId];
593
+ return webrtcClient.createOffer();
594
+ }
595
+
596
+ setLocalDescription(callId: string, participantId: string, sdp: RTCSessionDescriptionInit) {
597
+ const webrtcClient = this.callParticipantsRTCMap[callId][participantId];
598
+ return webrtcClient.setLocalDescription(sdp);
599
+ }
600
+
601
+ setRemoteDescription(callId: string, participantId: string, sdp: RTCSessionDescriptionInit) {
602
+ const webrtcClient = this.callParticipantsRTCMap[callId][participantId];
603
+ return webrtcClient.setRemoteDescription(sdp);
604
+ }
605
+
606
+ sendVideoEnabled(call: Call, videoEnabled: boolean, videoTrack: MediaStreamTrack) {
607
+ if (call.mediaFlow === "p2p") {
608
+ call.participants.forEach((participant) => {
609
+ if (participant.id !== this.user.id) {
610
+ this.callParticipantsRTCMap[call.id][participant.id].enableVideoOnPeerConnection(videoEnabled, videoTrack);
611
+ }
612
+ });
613
+ } else {
614
+ this.callParticipantsRTCMap[call.id][call.serverParticipant!.id].enableVideoOnPeerConnection(
615
+ videoEnabled,
616
+ videoTrack,
617
+ );
618
+ }
619
+ call.localParticipant.videoTracks.forEach((track) => {
620
+ track.enabled = videoEnabled;
621
+ });
622
+
623
+ call.onParticipantEvent(call, call.localParticipant, videoEnabled ? "VideoEnabled" : "VideoDisabled");
624
+
625
+ return this.signalingClient.sendSignalMessage({
626
+ callType: CALL_TYPE[call.mediaFlow === "sfu" ? "group" : "p2p"],
627
+ roomId: call.id,
628
+ signal: SIGNAL.INFO,
629
+ sessionId: call.sessionId,
630
+ message: videoEnabled ? INFO_MESSAGE.VIDEO_ENABLED : INFO_MESSAGE.VIDEO_DISABLED,
631
+ participantIds: call.participants.map((participant) => participant.id),
632
+ });
633
+ }
634
+
635
+ getStats(call: Call) {
636
+ const participant = call.participants.find((participant) => {
637
+ if (participant.id !== this.user.id) {
638
+ return participant;
639
+ }
640
+ });
641
+
642
+ if (participant) {
643
+ return this.callParticipantsRTCMap[call.id][participant?.id].getConnectionStats(
644
+ call.id,
645
+ call.sessionId,
646
+ call.localParticipant.id,
647
+ );
648
+ }
649
+ }
650
+
651
+ sendScreenShare(call: Call, startSharing: boolean, videoTrack: MediaStreamTrack) {
652
+ if (call.mediaFlow === "p2p") {
653
+ call.participants.forEach((participant) => {
654
+ if (participant.id !== this.user.id) {
655
+ this.callParticipantsRTCMap[call.id][participant.id].enableVideoOnPeerConnection(startSharing, videoTrack);
656
+ }
657
+ });
658
+ } else {
659
+ this.callParticipantsRTCMap[call.id][call.serverParticipant!.id].enableVideoOnPeerConnection(
660
+ startSharing,
661
+ videoTrack,
662
+ );
663
+ }
664
+ call.localParticipant.videoTracks.forEach((track) => {
665
+ track.enabled = startSharing;
666
+ });
667
+
668
+ call.onParticipantEvent(
669
+ call,
670
+ call.localParticipant,
671
+ startSharing ? "ScreenSharingStarted" : "ScreenSharingStopped",
672
+ );
673
+
674
+ return this.sendSignalScreenShare(call, startSharing);
675
+ }
676
+
677
+ sendSignalScreenShare = (call: Call, startSharing: boolean) => {
678
+ return this.signalingClient.sendSignalMessage({
679
+ callType: CALL_TYPE[call.mediaFlow === "sfu" ? "group" : "p2p"],
680
+ roomId: call.id,
681
+ signal: SIGNAL.INFO,
682
+ sessionId: call.sessionId,
683
+ message: startSharing ? INFO_MESSAGE.START_SCREEN_SHARE : INFO_MESSAGE.STOP_SCREEN_SHARE,
684
+ participantIds: call.participants.map((participant) => participant.id),
685
+ });
686
+ };
687
+
688
+ sendAudioEnable(call: Call, mute: boolean) {
689
+ if (call.mediaFlow === "p2p") {
690
+ call.participants.forEach((participant) => {
691
+ if (participant.id !== this.user.id) {
692
+ this.callParticipantsRTCMap[call.id][participant.id].enableAudioPeerConnection(mute);
693
+ }
694
+ });
695
+ } else {
696
+ this.callParticipantsRTCMap[call.id][call.serverParticipant!.id].enableAudioPeerConnection(mute);
697
+ }
698
+ call.localParticipant.audioTracks.forEach((track) => {
699
+ track.enabled = !mute;
700
+ });
701
+
702
+ call.onParticipantEvent(call, call.localParticipant, mute ? "Mute" : "Unmute");
703
+
704
+ return this.signalingClient.sendSignalMessage({
705
+ callType: CALL_TYPE[call.mediaFlow === "sfu" ? "group" : "p2p"],
706
+ roomId: call.id,
707
+ signal: SIGNAL.INFO,
708
+ sessionId: call.sessionId,
709
+ message: mute ? INFO_MESSAGE.MUTE : INFO_MESSAGE.UNMUTE,
710
+ participantIds: call.participants.map((participant) => participant.id),
711
+ });
712
+ }
713
+
714
+ addTracksToPeerConnection = (call: Call, participantId: string) => {
715
+ let localVideoTracks = call.localVideoTracks;
716
+ const localAudioTrack = call.localAudioTracks;
717
+ const mediaStream = new MediaStream();
718
+ if (localAudioTrack && localAudioTrack.length > 0) {
719
+ mediaStream.addTrack(localAudioTrack[0]);
720
+ }
721
+ this.callParticipantsRTCMap[call.id][participantId].addTrackToPeerConnection(localAudioTrack[0], mediaStream);
722
+
723
+ if (!(localVideoTracks && localVideoTracks.length > 0)) {
724
+ localVideoTracks = getEmptyVideoTrack();
725
+ }
726
+ mediaStream.addTrack(localVideoTracks[0]);
727
+ this.callParticipantsRTCMap[call.id][participantId].addTrackToPeerConnection(localVideoTracks[0], mediaStream);
728
+ };
729
+
730
+ async switchCallToSfu(call: Call) {
731
+ try {
732
+ const switchRes = await this.signalingClient.sendSignalMessage({
733
+ callType: CALL_TYPE.group,
734
+ roomId: call.id,
735
+ signal: SIGNAL.SWITCH_CALL_TYPE,
736
+ sessionId: call.sessionId,
737
+ });
738
+
739
+ logger.info("switchRes: ", switchRes);
740
+
741
+ const serverParticipant = new Participant(call.id);
742
+ this.addParticipantToRTCMap(call!, serverParticipant);
743
+ this.addTracksToPeerConnection(call, serverParticipant.id);
744
+ const desc = await this.createOffer(call.id, serverParticipant.id);
745
+ await this.setLocalDescription(call.id, serverParticipant.id, desc);
746
+
747
+ call.setServerParticipant(serverParticipant);
748
+
749
+ call.participants.forEach((participant) => {
750
+ participant.setShouldResetPC(true);
751
+ });
752
+ await this.sendOffer({
753
+ callType: CALL_TYPE.group,
754
+ callId: call.id,
755
+ description: desc,
756
+ sessionId: call.sessionId,
757
+ });
758
+ } catch (e) {
759
+ logger.info("Failed to switch sfu");
760
+ return false;
761
+ }
762
+ return true;
763
+ }
764
+ }