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.
- package/.editorconfig +4 -0
- package/.eslintrc.js +22 -0
- package/README.md +100 -0
- package/dist/index.js +1 -0
- package/package.json +31 -0
- package/src/classes/CallParticipantStateMachine/index.ts +141 -0
- package/src/classes/RTCConfig/index.ts +10 -0
- package/src/classes/audioTrack/index.ts +17 -0
- package/src/classes/call/index.ts +345 -0
- package/src/classes/call-client/index.ts +201 -0
- package/src/classes/internal-call-handler/index.ts +764 -0
- package/src/classes/participant/index.ts +123 -0
- package/src/classes/signaling/index.ts +38 -0
- package/src/classes/videoTrack/index.ts +17 -0
- package/src/classes/webrtcClient/index.ts +243 -0
- package/src/constants/index.ts +35 -0
- package/src/enum/index.ts +5 -0
- package/src/helpers/index.ts +22 -0
- package/src/index.ts +2 -0
- package/src/internal.ts +1 -0
- package/src/types/index.ts +160 -0
- package/src/utils/logger.ts +20 -0
- package/tsconfig.json +112 -0
- package/webpack.config.js +40 -0
- package/yarn-error.log +1677 -0
|
@@ -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
|
+
}
|