@wazo/sdk 1.0.0-alpha.0 → 1.0.0-alpha.2
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/dist/wazo-sdk.js +6 -6
- package/esm/__tests__/__voice__/call.test.js +44 -2
- package/esm/__tests__/api-client.test.js +20 -1
- package/esm/api/auth.js +13 -2
- package/esm/domain/Phone/WebRTCPhone.js +6 -7
- package/esm/index.js +2 -0
- package/esm/simple/Auth.js +6 -12
- package/esm/simple/Phone.js +1 -0
- package/esm/simple/__tests__/Auth.test.js +8 -64
- package/esm/state-machine/call-state-machine.js +1 -1
- package/esm/types/api/auth.d.ts.map +1 -1
- package/esm/types/domain/Phone/WebRTCPhone.d.ts.map +1 -1
- package/esm/types/index.d.ts +2 -0
- package/esm/types/index.d.ts.map +1 -1
- package/esm/types/simple/Auth.d.ts.map +1 -1
- package/esm/types/simple/Phone.d.ts.map +1 -1
- package/esm/types/state-machine/utils.d.ts +1 -1
- package/esm/types/state-machine/utils.d.ts.map +1 -1
- package/esm/types/utils/api-requester.d.ts.map +1 -1
- package/esm/types/utils/sdp.d.ts +2 -0
- package/esm/types/utils/sdp.d.ts.map +1 -1
- package/esm/types/utils/webrtc.d.ts +1 -0
- package/esm/types/utils/webrtc.d.ts.map +1 -1
- package/esm/types/voice/call.d.ts +19 -11
- package/esm/types/voice/call.d.ts.map +1 -1
- package/esm/types/voice/softphone.d.ts +18 -13
- package/esm/types/voice/softphone.d.ts.map +1 -1
- package/esm/utils/__tests__/api-requester.test.js +17 -0
- package/esm/utils/api-requester.js +9 -1
- package/esm/utils/sdp.js +13 -0
- package/esm/utils/webrtc.js +21 -1
- package/esm/voice/call.js +90 -30
- package/esm/voice/softphone.js +94 -23
- package/lib/__tests__/__voice__/call.test.js +43 -1
- package/lib/__tests__/api-client.test.js +20 -1
- package/lib/api/auth.js +13 -2
- package/lib/domain/Phone/WebRTCPhone.js +5 -6
- package/lib/index.js +6 -2
- package/lib/simple/Auth.js +6 -12
- package/lib/simple/Phone.js +1 -0
- package/lib/simple/__tests__/Auth.test.js +11 -64
- package/lib/state-machine/call-state-machine.js +1 -1
- package/lib/types/api/auth.d.ts.map +1 -1
- package/lib/types/domain/Phone/WebRTCPhone.d.ts.map +1 -1
- package/lib/types/index.d.ts +2 -0
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/simple/Auth.d.ts.map +1 -1
- package/lib/types/simple/Phone.d.ts.map +1 -1
- package/lib/types/state-machine/utils.d.ts +1 -1
- package/lib/types/state-machine/utils.d.ts.map +1 -1
- package/lib/types/utils/api-requester.d.ts.map +1 -1
- package/lib/types/utils/sdp.d.ts +2 -0
- package/lib/types/utils/sdp.d.ts.map +1 -1
- package/lib/types/utils/webrtc.d.ts +1 -0
- package/lib/types/utils/webrtc.d.ts.map +1 -1
- package/lib/types/voice/call.d.ts +19 -11
- package/lib/types/voice/call.d.ts.map +1 -1
- package/lib/types/voice/softphone.d.ts +18 -13
- package/lib/types/voice/softphone.d.ts.map +1 -1
- package/lib/utils/__tests__/api-requester.test.js +17 -0
- package/lib/utils/api-requester.js +9 -1
- package/lib/utils/sdp.js +16 -1
- package/lib/utils/webrtc.js +23 -2
- package/lib/voice/call.js +87 -28
- package/lib/voice/softphone.js +95 -23
- package/package.json +1 -1
- package/src/__tests__/__voice__/call.test.ts +47 -2
- package/src/__tests__/api-client.test.ts +22 -1
- package/src/api/auth.ts +25 -3
- package/src/domain/Phone/WebRTCPhone.ts +9 -9
- package/src/index.ts +2 -0
- package/src/simple/Auth.ts +6 -13
- package/src/simple/Phone.ts +1 -0
- package/src/simple/__tests__/Auth.test.ts +9 -87
- package/src/state-machine/call-state-machine.ts +1 -1
- package/src/state-machine/utils.ts +2 -2
- package/src/utils/__tests__/api-requester.test.ts +20 -0
- package/src/utils/api-requester.ts +9 -1
- package/src/utils/sdp.ts +16 -0
- package/src/utils/webrtc.ts +24 -2
- package/src/voice/call.ts +118 -40
- package/src/voice/softphone.ts +116 -30
- package/src/web-rtc-client.ts +1 -1
package/lib/voice/call.js
CHANGED
|
@@ -35,7 +35,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
35
35
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
36
36
|
};
|
|
37
37
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
38
|
-
exports.MESSAGE_TRACK_UPDATED = exports.MESSAGE_TYPE_SIGNAL = exports.MESSAGE_TYPE_CHAT = exports.
|
|
38
|
+
exports.MESSAGE_TRACK_UPDATED = exports.MESSAGE_TYPE_SIGNAL = exports.MESSAGE_TYPE_CHAT = exports.EVENT_CAMERA_DISABLED = exports.EVENT_CAMERA_RESUMED = exports.EVENT_WEB_RTC_STATS = exports.EVENT_SHARE_SCREEN_ENDED = exports.EVENT_SHARE_SCREEN_STARTED = exports.EVENT_SHARE_SCREEN_ENDING = exports.EVENT_REMOVE_STREAM = exports.EVENT_VIDEO_STREAM = exports.EVENT_AUDIO_STREAM = exports.EVENT_TRACK = exports.EVENT_REINVITE = exports.EVENT_CALL_RESUMED = exports.EVENT_CALL_HELD = exports.EVENT_CALL_UN_MUTED = exports.EVENT_CALL_MUTED = exports.EVENT_CALL_ENDED = exports.EVENT_CALL_TERMINATING = exports.EVENT_CALL_REJECTED = exports.EVENT_CALL_ERROR = exports.EVENT_EARLY_MEDIA = exports.EVENT_PROGRESS = exports.EVENT_CALL_CANCELED = exports.EVENT_CALL_ACCEPTED = void 0;
|
|
39
39
|
/* eslint-disable no-underscore-dangle */
|
|
40
40
|
const events_1 = require("events");
|
|
41
41
|
const xstate_1 = require("xstate");
|
|
@@ -43,6 +43,7 @@ const invitation_1 = require("sip.js/lib/api/invitation");
|
|
|
43
43
|
const session_state_1 = require("sip.js/lib/api/session-state");
|
|
44
44
|
const call_state_machine_1 = __importStar(require("../state-machine/call-state-machine"));
|
|
45
45
|
const IssueReporter_1 = __importDefault(require("../service/IssueReporter"));
|
|
46
|
+
const softphone_1 = __importStar(require("./softphone"));
|
|
46
47
|
const sdp_1 = require("../utils/sdp");
|
|
47
48
|
const utils_1 = require("../state-machine/utils");
|
|
48
49
|
const webrtc_1 = require("../utils/webrtc");
|
|
@@ -71,19 +72,63 @@ exports.EVENT_SHARE_SCREEN_ENDED = 'onScreenShareEnded';
|
|
|
71
72
|
exports.EVENT_WEB_RTC_STATS = 'webRtcStats';
|
|
72
73
|
exports.EVENT_CAMERA_RESUMED = 'onCameraResumed';
|
|
73
74
|
exports.EVENT_CAMERA_DISABLED = 'onCameraDisabled';
|
|
74
|
-
exports.EVENT_TERMINATE_SOUND = 'terminateSound';
|
|
75
75
|
exports.MESSAGE_TYPE_CHAT = 'message/TYPE_CHAT';
|
|
76
76
|
exports.MESSAGE_TYPE_SIGNAL = 'message/TYPE_SIGNAL';
|
|
77
77
|
exports.MESSAGE_TRACK_UPDATED = 'onTrackUpdated';
|
|
78
78
|
class Call extends events_1.EventEmitter {
|
|
79
|
+
static parseCall(apiCall) {
|
|
80
|
+
const sipCall = {
|
|
81
|
+
id: apiCall.sipCallId,
|
|
82
|
+
stateChange: {
|
|
83
|
+
addListener: () => { },
|
|
84
|
+
},
|
|
85
|
+
remoteIdentity: {
|
|
86
|
+
uri: {
|
|
87
|
+
_normal: {
|
|
88
|
+
user: apiCall.calleeNumber,
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
// Mock sdk so `hasLocalVideo` will return true.
|
|
93
|
+
sessionDescriptionHandler: {
|
|
94
|
+
localMediaStream: {
|
|
95
|
+
getTracks: () => (apiCall.isVideo ? [{ kind: 'video', readyState: 'live' }] : []),
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
const call = new Call(sipCall, softphone_1.default);
|
|
100
|
+
// call._sendAction(Actions.ES);
|
|
101
|
+
call.apiId = apiCall.id;
|
|
102
|
+
call.answerTime = new Date(+apiCall.startingTime);
|
|
103
|
+
// Update call state
|
|
104
|
+
const snapshot = call.callActor.getSnapshot();
|
|
105
|
+
if (apiCall.isUp()) {
|
|
106
|
+
snapshot.value = { [call_state_machine_1.States.ESTABLISHED]: call_state_machine_1.EstablishedStates.ONGOING };
|
|
107
|
+
}
|
|
108
|
+
if (apiCall.muted) {
|
|
109
|
+
snapshot.value = { [call_state_machine_1.States.ESTABLISHED]: call_state_machine_1.EstablishedStates.MUTED };
|
|
110
|
+
}
|
|
111
|
+
if (apiCall.isRinging()) {
|
|
112
|
+
snapshot.value = call_state_machine_1.States.RINGING;
|
|
113
|
+
}
|
|
114
|
+
if (apiCall.isRecording()) {
|
|
115
|
+
call.recording = true;
|
|
116
|
+
}
|
|
117
|
+
// @ts-ignore
|
|
118
|
+
call.callActor.update(snapshot);
|
|
119
|
+
return call;
|
|
120
|
+
}
|
|
79
121
|
constructor(sipCall, phone) {
|
|
80
122
|
super();
|
|
81
123
|
this.shouldSendReinvite = false;
|
|
124
|
+
this.recording = false;
|
|
82
125
|
this.callActor = (0, xstate_1.createActor)(call_state_machine_1.default);
|
|
83
126
|
this.callActor.start();
|
|
84
127
|
this.creationTime = new Date();
|
|
85
128
|
this.phone = phone;
|
|
86
129
|
this.sipCall = sipCall;
|
|
130
|
+
this.number = (0, sdp_1.getCallNumber)(sipCall);
|
|
131
|
+
this.displayName = (0, sdp_1.getCallDisplayName)(sipCall);
|
|
87
132
|
this._bindEvents();
|
|
88
133
|
}
|
|
89
134
|
accept() {
|
|
@@ -108,7 +153,7 @@ class Call extends events_1.EventEmitter {
|
|
|
108
153
|
return __awaiter(this, void 0, void 0, function* () {
|
|
109
154
|
(0, utils_1.assertCan)(this.callActor, call_state_machine_1.Actions.REJECT);
|
|
110
155
|
logger.info('call - reject', { id: this.id });
|
|
111
|
-
this.
|
|
156
|
+
this.phone.emit(softphone_1.EVENT_TERMINATE_SOUND, 'call rejected locally');
|
|
112
157
|
this.shouldSendReinvite = false;
|
|
113
158
|
return this.hangup();
|
|
114
159
|
});
|
|
@@ -119,7 +164,7 @@ class Call extends events_1.EventEmitter {
|
|
|
119
164
|
logger.info('call - hangup', { id: this.id });
|
|
120
165
|
yield this.phone.client.hangup(this.sipCall);
|
|
121
166
|
// Remove this call from softphone's calls
|
|
122
|
-
this.phone.
|
|
167
|
+
this.phone.onCallTerminated(this);
|
|
123
168
|
return true;
|
|
124
169
|
});
|
|
125
170
|
}
|
|
@@ -132,7 +177,7 @@ class Call extends events_1.EventEmitter {
|
|
|
132
177
|
logger.info('call - force cancel, error', e);
|
|
133
178
|
}
|
|
134
179
|
}
|
|
135
|
-
startConference(withCalls) {
|
|
180
|
+
startConference( /* withCalls: Call[] */) {
|
|
136
181
|
// @TODO
|
|
137
182
|
// const participants = [this, ...withCalls].reduce((acc: Record<string, any>, call: Call) => {
|
|
138
183
|
// acc[call.talkingToIds[0]] = call;
|
|
@@ -150,14 +195,14 @@ class Call extends events_1.EventEmitter {
|
|
|
150
195
|
logger.info('call - mute', { id: this.id });
|
|
151
196
|
this._sendAction(call_state_machine_1.EstablishedActions.MUTE);
|
|
152
197
|
this.phone.client.mute(this.sipCall);
|
|
153
|
-
this.
|
|
198
|
+
this.emit(exports.EVENT_CALL_MUTED);
|
|
154
199
|
}
|
|
155
200
|
unMute() {
|
|
156
201
|
(0, utils_1.assertCan)(this.callActor, call_state_machine_1.EstablishedActions.UN_MUTE);
|
|
157
202
|
logger.info('call - un-mute', { id: this.id });
|
|
158
203
|
this._sendAction(call_state_machine_1.EstablishedActions.UN_MUTE);
|
|
159
204
|
this.phone.client.unmute(this.sipCall);
|
|
160
|
-
this.
|
|
205
|
+
this.emit(exports.EVENT_CALL_UN_MUTED);
|
|
161
206
|
}
|
|
162
207
|
hold() {
|
|
163
208
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -174,7 +219,7 @@ class Call extends events_1.EventEmitter {
|
|
|
174
219
|
yield (0, webrtc_1.downgradeToAudio)(this.sipCall);
|
|
175
220
|
}
|
|
176
221
|
const promise = this.phone.client.hold(this.sipCall, this.isConference(), hasVideo);
|
|
177
|
-
this.
|
|
222
|
+
this.emit(exports.EVENT_CALL_HELD);
|
|
178
223
|
return promise;
|
|
179
224
|
});
|
|
180
225
|
}
|
|
@@ -203,7 +248,7 @@ class Call extends events_1.EventEmitter {
|
|
|
203
248
|
this._onScreenSharing(stream, hadVideo, wasDesktop);
|
|
204
249
|
}
|
|
205
250
|
};
|
|
206
|
-
this.
|
|
251
|
+
this.emit(exports.EVENT_CALL_RESUMED);
|
|
207
252
|
return promise.then(() => {
|
|
208
253
|
const stream = this.getLocalVideoStream();
|
|
209
254
|
if (wasScreensharing) {
|
|
@@ -217,19 +262,25 @@ class Call extends events_1.EventEmitter {
|
|
|
217
262
|
(0, utils_1.assertState)(this.callActor, call_state_machine_1.States.ESTABLISHED);
|
|
218
263
|
logger.info('call - turn camera on', { id: this.id });
|
|
219
264
|
(0, webrtc_1.toggleVideo)(this.sipCall, true);
|
|
220
|
-
this.
|
|
265
|
+
this.emit(exports.EVENT_CAMERA_RESUMED);
|
|
221
266
|
}
|
|
222
267
|
turnCameraOff() {
|
|
223
268
|
(0, utils_1.assertState)(this.callActor, call_state_machine_1.States.ESTABLISHED);
|
|
224
269
|
logger.info('call - turn camera off', { id: this.id });
|
|
225
270
|
(0, webrtc_1.toggleVideo)(this.sipCall, false);
|
|
226
|
-
this.
|
|
271
|
+
this.emit(exports.EVENT_CAMERA_DISABLED);
|
|
272
|
+
}
|
|
273
|
+
isMuted() {
|
|
274
|
+
return this._hasEstablishedState(call_state_machine_1.EstablishedStates.MUTED);
|
|
275
|
+
}
|
|
276
|
+
isCameraMuted() {
|
|
277
|
+
return (0, webrtc_1.isVideoMuted)(this.sipCall);
|
|
227
278
|
}
|
|
228
279
|
isHeld() {
|
|
229
|
-
return
|
|
280
|
+
return this._hasEstablishedState(call_state_machine_1.EstablishedStates.HELD);
|
|
230
281
|
}
|
|
231
282
|
isEstablished() {
|
|
232
|
-
return
|
|
283
|
+
return call_state_machine_1.States.ESTABLISHED in this.state;
|
|
233
284
|
}
|
|
234
285
|
isConference() {
|
|
235
286
|
var _a;
|
|
@@ -366,7 +417,7 @@ class Call extends events_1.EventEmitter {
|
|
|
366
417
|
catch (e) {
|
|
367
418
|
console.warn(e);
|
|
368
419
|
}
|
|
369
|
-
this.
|
|
420
|
+
this.emit(exports.EVENT_SHARE_SCREEN_ENDED);
|
|
370
421
|
this.currentScreenShare = null;
|
|
371
422
|
return reinvited;
|
|
372
423
|
});
|
|
@@ -392,10 +443,13 @@ class Call extends events_1.EventEmitter {
|
|
|
392
443
|
onError(error) {
|
|
393
444
|
this.emit(exports.EVENT_CALL_ERROR, error);
|
|
394
445
|
}
|
|
446
|
+
onCallIncoming() {
|
|
447
|
+
this._sendAction(call_state_machine_1.Actions.INCOMING_CALL);
|
|
448
|
+
}
|
|
395
449
|
onRejected() {
|
|
396
450
|
this.endTime = new Date();
|
|
397
451
|
this.emit(exports.EVENT_CALL_REJECTED);
|
|
398
|
-
this.phone.
|
|
452
|
+
this.phone.onCallTerminated(this);
|
|
399
453
|
}
|
|
400
454
|
onNetworkStats(stats, previousStats) {
|
|
401
455
|
this.emit(exports.EVENT_WEB_RTC_STATS, stats, previousStats);
|
|
@@ -424,16 +478,21 @@ class Call extends events_1.EventEmitter {
|
|
|
424
478
|
isVideoRemotelyHeld() {
|
|
425
479
|
return (0, webrtc_1.isVideoRemotelyHeld)(this.sipCall);
|
|
426
480
|
}
|
|
481
|
+
isRinging() {
|
|
482
|
+
return this.state === call_state_machine_1.States.RINGING;
|
|
483
|
+
}
|
|
427
484
|
onReinvite(request, updatedCalleeName, updatedNumber, hadRemoteVideo) {
|
|
428
485
|
this.emit(exports.EVENT_REINVITE, request, updatedCalleeName, updatedNumber, hadRemoteVideo);
|
|
429
486
|
}
|
|
430
487
|
get id() {
|
|
431
488
|
return (0, sdp_1.getSipSessionId)(this.sipCall);
|
|
432
489
|
}
|
|
433
|
-
get
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
490
|
+
get state() {
|
|
491
|
+
return (0, utils_1.getState)(this.callActor);
|
|
492
|
+
}
|
|
493
|
+
_hasEstablishedState(establishedState) {
|
|
494
|
+
const state = this.state;
|
|
495
|
+
return call_state_machine_1.States.ESTABLISHED in state && state[call_state_machine_1.States.ESTABLISHED] === establishedState;
|
|
437
496
|
}
|
|
438
497
|
_bindEvents() {
|
|
439
498
|
if (this.sipCall instanceof invitation_1.Invitation) {
|
|
@@ -443,21 +502,22 @@ class Call extends events_1.EventEmitter {
|
|
|
443
502
|
logger.trace('call - on sip session canceled', { callId: message.callId, id: this.id });
|
|
444
503
|
onCancel(message);
|
|
445
504
|
const elsewhere = message.data.indexOf('cause=26') !== -1 && message.data.indexOf('completed elsewhere') !== -1;
|
|
446
|
-
this.
|
|
505
|
+
this.emit(exports.EVENT_CALL_CANCELED, elsewhere);
|
|
447
506
|
};
|
|
448
507
|
}
|
|
449
508
|
this.sipCall.stateChange.addListener((newState) => {
|
|
450
509
|
switch (newState) {
|
|
451
510
|
case session_state_1.SessionState.Terminating:
|
|
452
511
|
logger.info('call - terminating', { id: this.id });
|
|
453
|
-
this.
|
|
512
|
+
this.emit(exports.EVENT_CALL_TERMINATING);
|
|
454
513
|
break;
|
|
455
514
|
case session_state_1.SessionState.Terminated:
|
|
456
515
|
{
|
|
457
516
|
logger.info('call - terminated', { id: this.id });
|
|
458
517
|
// Should be called before `_onCallTerminated` or the callCount will not decrement...
|
|
459
518
|
this.endTime = new Date();
|
|
460
|
-
|
|
519
|
+
this.emit(exports.EVENT_CALL_ENDED);
|
|
520
|
+
return this.phone.onCallTerminated(this);
|
|
461
521
|
}
|
|
462
522
|
default:
|
|
463
523
|
break;
|
|
@@ -484,18 +544,17 @@ class Call extends events_1.EventEmitter {
|
|
|
484
544
|
kind,
|
|
485
545
|
});
|
|
486
546
|
if (kind === 'audio') {
|
|
487
|
-
return this.
|
|
547
|
+
return this.emit(exports.EVENT_AUDIO_STREAM, stream);
|
|
488
548
|
}
|
|
489
|
-
return this.
|
|
549
|
+
return this.emit(exports.EVENT_VIDEO_STREAM, stream, event.track.id, event);
|
|
490
550
|
};
|
|
491
551
|
peerConnection.onremovestream = (event) => {
|
|
492
552
|
logger.info('call - on remove stream called on the peer connection', {
|
|
493
553
|
id: event.stream.id,
|
|
494
554
|
tracks: event.stream.getTracks(),
|
|
495
555
|
});
|
|
496
|
-
this.
|
|
556
|
+
this.emit(exports.EVENT_REMOVE_STREAM, event.stream);
|
|
497
557
|
};
|
|
498
|
-
// Client events
|
|
499
558
|
}
|
|
500
559
|
_downgradeToAudio(withMessage = true) {
|
|
501
560
|
(0, utils_1.assertState)(this.callActor, call_state_machine_1.States.ESTABLISHED);
|
|
@@ -558,7 +617,7 @@ class Call extends events_1.EventEmitter {
|
|
|
558
617
|
if (screenTrack) {
|
|
559
618
|
screenTrack.onended = () => {
|
|
560
619
|
logger.info('call - on screenshare ended', { hadVideo, id: this.id, screenTrack });
|
|
561
|
-
this.
|
|
620
|
+
this.emit(exports.EVENT_SHARE_SCREEN_ENDING);
|
|
562
621
|
};
|
|
563
622
|
}
|
|
564
623
|
this.currentScreenShare = {
|
|
@@ -568,7 +627,7 @@ class Call extends events_1.EventEmitter {
|
|
|
568
627
|
desktop,
|
|
569
628
|
};
|
|
570
629
|
(0, webrtc_1.setLocalMediaStream)(this.sipCall, screenStream);
|
|
571
|
-
this.
|
|
630
|
+
this.emit(exports.EVENT_SHARE_SCREEN_STARTED, screenStream);
|
|
572
631
|
}
|
|
573
632
|
_sendAction(action) {
|
|
574
633
|
this.callActor.send({ type: action });
|
package/lib/voice/softphone.js
CHANGED
|
@@ -35,26 +35,30 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
35
35
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
36
36
|
};
|
|
37
37
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
38
|
-
exports.Softphone = exports.MESSAGE_TYPE_SIGNAL = exports.MESSAGE_TYPE_CHAT = exports.EVENT_ON_SIGNAL = exports.EVENT_ON_CHAT = exports.EVENT_ON_MESSAGE = exports.EVENT_VIDEO_INPUT_CHANGE = exports.EVENT_DISCONNECTED = exports.EVENT_UNREGISTERED = exports.EVENT_REGISTERED = exports.EVENT_HEARTBEAT_TIMEOUT = exports.EVENT_HEARTBEAT = exports.EVENT_INCOMING = void 0;
|
|
38
|
+
exports.Softphone = exports.MESSAGE_TYPE_SIGNAL = exports.MESSAGE_TYPE_CHAT = exports.EVENT_ON_SIGNAL = exports.EVENT_ON_CHAT = exports.EVENT_ON_MESSAGE = exports.EVENT_PLAY_INBOUND_CALL_SOUND = exports.EVENT_PLAY_RING_SOUND = exports.EVENT_PLAY_HANGUP_SOUND = exports.EVENT_TERMINATE_SOUND = exports.EVENT_VIDEO_INPUT_CHANGE = exports.EVENT_DISCONNECTED = exports.EVENT_UNREGISTERED = exports.EVENT_REGISTERED = exports.EVENT_HEARTBEAT_TIMEOUT = exports.EVENT_HEARTBEAT = exports.EVENT_OUTGOING = exports.EVENT_INCOMING = void 0;
|
|
39
39
|
/* eslint-disable no-underscore-dangle */
|
|
40
40
|
const events_1 = require("events");
|
|
41
41
|
const xstate_1 = require("xstate");
|
|
42
42
|
const InvalidState_1 = __importDefault(require("../domain/InvalidState"));
|
|
43
43
|
const __1 = __importDefault(require(".."));
|
|
44
44
|
const softphone_state_machine_1 = __importStar(require("../state-machine/softphone-state-machine"));
|
|
45
|
-
const call_state_machine_1 = require("../state-machine/call-state-machine");
|
|
46
45
|
const web_rtc_client_1 = __importDefault(require("../web-rtc-client"));
|
|
47
46
|
const IssueReporter_1 = __importDefault(require("../service/IssueReporter"));
|
|
48
47
|
const sip_logger_1 = __importDefault(require("../utils/sip-logger"));
|
|
49
48
|
const call_1 = __importDefault(require("./call"));
|
|
50
49
|
const utils_1 = require("../state-machine/utils");
|
|
51
50
|
exports.EVENT_INCOMING = 'incoming';
|
|
51
|
+
exports.EVENT_OUTGOING = 'outgoing';
|
|
52
52
|
exports.EVENT_HEARTBEAT = 'heartbeat';
|
|
53
53
|
exports.EVENT_HEARTBEAT_TIMEOUT = 'heartbeatTimeout';
|
|
54
54
|
exports.EVENT_REGISTERED = 'registered';
|
|
55
55
|
exports.EVENT_UNREGISTERED = 'unregistered';
|
|
56
56
|
exports.EVENT_DISCONNECTED = 'disconnected';
|
|
57
57
|
exports.EVENT_VIDEO_INPUT_CHANGE = 'videoInputChange';
|
|
58
|
+
exports.EVENT_TERMINATE_SOUND = 'terminateSound';
|
|
59
|
+
exports.EVENT_PLAY_HANGUP_SOUND = 'playHangupSound';
|
|
60
|
+
exports.EVENT_PLAY_RING_SOUND = 'playRingSound';
|
|
61
|
+
exports.EVENT_PLAY_INBOUND_CALL_SOUND = 'playInboundCallSound';
|
|
58
62
|
exports.EVENT_ON_MESSAGE = 'message';
|
|
59
63
|
exports.EVENT_ON_CHAT = 'phone/ON_CHAT';
|
|
60
64
|
exports.EVENT_ON_SIGNAL = 'phone/ON_SIGNAL';
|
|
@@ -64,6 +68,7 @@ const logger = IssueReporter_1.default.loggerFor('softphone');
|
|
|
64
68
|
class Softphone extends events_1.EventEmitter {
|
|
65
69
|
constructor() {
|
|
66
70
|
super();
|
|
71
|
+
this.ringingEnabled = true;
|
|
67
72
|
this.calls = [];
|
|
68
73
|
this.softphoneActor = (0, xstate_1.createActor)(softphone_state_machine_1.default);
|
|
69
74
|
this.softphoneActor.start();
|
|
@@ -138,19 +143,20 @@ class Softphone extends events_1.EventEmitter {
|
|
|
138
143
|
logger.warn('softphone - calling with empty number, bailing.');
|
|
139
144
|
return Promise.resolve(null);
|
|
140
145
|
}
|
|
141
|
-
if (this.
|
|
142
|
-
throw new InvalidState_1.default(`Invalid state ${softphone_state_machine_1.States.UNREGISTERED}`, softphone_state_machine_1.States.UNREGISTERED);
|
|
143
|
-
}
|
|
144
|
-
if (this.getState() !== softphone_state_machine_1.States.REGISTERING) {
|
|
146
|
+
if (this.state === softphone_state_machine_1.States.REGISTERING) {
|
|
145
147
|
logger.info('softphone - calling when registering, waiting for Softphone to be registered ...');
|
|
146
148
|
yield (0, utils_1.waitUntilState)(this.softphoneActor, softphone_state_machine_1.States.REGISTERED);
|
|
147
149
|
}
|
|
150
|
+
if (this.state !== softphone_state_machine_1.States.UNREGISTERED) {
|
|
151
|
+
throw new InvalidState_1.default(`Invalid state ${softphone_state_machine_1.States.UNREGISTERED}`, softphone_state_machine_1.States.UNREGISTERED);
|
|
152
|
+
}
|
|
148
153
|
// Hold other calls
|
|
149
154
|
this.holdAllCalls();
|
|
150
155
|
let sipCall;
|
|
151
156
|
try {
|
|
152
157
|
sipCall = this.client.call(options.params.To, options.withCamera, options.audioOnly, options.conference, options);
|
|
153
158
|
const call = new call_1.default(sipCall, this);
|
|
159
|
+
this.emit(exports.EVENT_OUTGOING, call);
|
|
154
160
|
return call;
|
|
155
161
|
}
|
|
156
162
|
catch (error) {
|
|
@@ -162,6 +168,58 @@ class Softphone extends events_1.EventEmitter {
|
|
|
162
168
|
removeCall(callToRemove) {
|
|
163
169
|
this.calls = this.calls.filter((call) => call.id !== callToRemove.id);
|
|
164
170
|
}
|
|
171
|
+
onCallTerminated(call) {
|
|
172
|
+
logger.info('softphone - on call terminated', { id: call.id });
|
|
173
|
+
const currentCall = this.getCurrentCall();
|
|
174
|
+
const isCurrentCall = (currentCall === null || currentCall === void 0 ? void 0 : currentCall.id) === call.id;
|
|
175
|
+
const isRinging = call.isRinging();
|
|
176
|
+
const isCurrentIncomingCall = isCurrentCall && isRinging;
|
|
177
|
+
const incomingCalls = this.getIncomingCalls();
|
|
178
|
+
const nbIncomingCalls = incomingCalls.length;
|
|
179
|
+
// Send an event to terminate ringing sound
|
|
180
|
+
if (isCurrentIncomingCall) {
|
|
181
|
+
setTimeout(() => {
|
|
182
|
+
// Avoid race condition when the other is calling and hanging up immediately
|
|
183
|
+
this.emit(exports.EVENT_TERMINATE_SOUND, call, 'call terminated');
|
|
184
|
+
}, 5);
|
|
185
|
+
}
|
|
186
|
+
// If the terminated call was an incoming call, we have to re-trigger if it's was the first incoming call
|
|
187
|
+
// Otherwise, we have to re-trigger an incoming call event if another call is incoming
|
|
188
|
+
const shouldRetrigger = isCurrentCall ? nbIncomingCalls : isCurrentIncomingCall;
|
|
189
|
+
// Re-trigger incoming call event for remaining incoming calls
|
|
190
|
+
logger.info('Softphone - check to re-trigger incoming call', { shouldRetrigger, nbIncomingCalls });
|
|
191
|
+
if (shouldRetrigger && nbIncomingCalls > 1) {
|
|
192
|
+
const nextCall = incomingCalls.find(c => c.id !== call.id);
|
|
193
|
+
// Avoid race condition
|
|
194
|
+
setTimeout(() => {
|
|
195
|
+
if (this.ringingEnabled) {
|
|
196
|
+
if (currentCall) {
|
|
197
|
+
this.emit(exports.EVENT_PLAY_RING_SOUND, this.audioRingDeviceId, this.audioRingVolume, nextCall);
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
this.emit(exports.EVENT_PLAY_INBOUND_CALL_SOUND, this.audioOutputDeviceId, this.audioOutputVolume, nextCall);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
this.emit(exports.EVENT_INCOMING, nextCall);
|
|
204
|
+
}, 100);
|
|
205
|
+
}
|
|
206
|
+
// @ts-ignore: does not exist
|
|
207
|
+
if (!call.sipCall.isCanceled) {
|
|
208
|
+
setTimeout(() => {
|
|
209
|
+
this.emit(exports.EVENT_PLAY_HANGUP_SOUND, this.audioOutputDeviceId, this.audioOutputVolume, call);
|
|
210
|
+
}, 10);
|
|
211
|
+
}
|
|
212
|
+
this.client.onCallEnded(call.sipCall);
|
|
213
|
+
this.removeCall(call);
|
|
214
|
+
}
|
|
215
|
+
enableRinging() {
|
|
216
|
+
logger.info('softphone - enable ringing');
|
|
217
|
+
this.ringingEnabled = true;
|
|
218
|
+
}
|
|
219
|
+
disableRinging() {
|
|
220
|
+
logger.info('softphone - disable ringing');
|
|
221
|
+
this.ringingEnabled = false;
|
|
222
|
+
}
|
|
165
223
|
holdAllCalls(exceptCall = null) {
|
|
166
224
|
this.calls.filter(call => !call.isHeld() && call.id === (exceptCall === null || exceptCall === void 0 ? void 0 : exceptCall.id)).forEach(call => call.hold());
|
|
167
225
|
}
|
|
@@ -216,25 +274,37 @@ class Softphone extends events_1.EventEmitter {
|
|
|
216
274
|
logger.info('softphone - changing ring volume', { volume });
|
|
217
275
|
this.audioRingVolume = volume;
|
|
218
276
|
}
|
|
219
|
-
getCalls() {
|
|
220
|
-
return this.calls;
|
|
221
|
-
}
|
|
222
277
|
getCurrentCall() {
|
|
223
278
|
return this.calls.find(call => call.isEstablished());
|
|
224
279
|
}
|
|
225
|
-
|
|
226
|
-
return this.
|
|
280
|
+
getIncomingCalls() {
|
|
281
|
+
return this.calls.filter(call => call.isRinging());
|
|
282
|
+
}
|
|
283
|
+
get state() {
|
|
284
|
+
return (0, utils_1.getState)(this.softphoneActor);
|
|
227
285
|
}
|
|
228
286
|
getUserAgent() {
|
|
229
287
|
var _a, _b;
|
|
230
288
|
return ((_b = (_a = this.client) === null || _a === void 0 ? void 0 : _a.config) === null || _b === void 0 ? void 0 : _b.userAgentString) || 'softphone';
|
|
231
289
|
}
|
|
290
|
+
isRegistered() {
|
|
291
|
+
return this.state === softphone_state_machine_1.States.REGISTERED;
|
|
292
|
+
}
|
|
232
293
|
_bindEvents() {
|
|
233
294
|
this.client.on(this.client.INVITE, (sipSession) => {
|
|
234
295
|
const call = new call_1.default(sipSession, this);
|
|
235
|
-
|
|
296
|
+
logger.info('softphone - on invite', { callId: call.id, number: call.number });
|
|
297
|
+
call.onCallIncoming();
|
|
236
298
|
this.calls.push(call);
|
|
237
|
-
this.
|
|
299
|
+
this.emit(exports.EVENT_INCOMING, call);
|
|
300
|
+
if (!this.getCurrentCall()) {
|
|
301
|
+
if (this.ringingEnabled) {
|
|
302
|
+
this.emit(exports.EVENT_PLAY_RING_SOUND, this.audioRingDeviceId, this.audioRingVolume, call);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
this.emit(exports.EVENT_PLAY_INBOUND_CALL_SOUND, this.audioOutputDeviceId, this.audioOutputVolume, call);
|
|
307
|
+
}
|
|
238
308
|
});
|
|
239
309
|
this.client.on(this.client.ON_REINVITE, (sipCall, request, updatedCalleeName, updatedNumber, hadRemoteVideo) => {
|
|
240
310
|
logger.info('softphone - on reinvite', {
|
|
@@ -247,6 +317,8 @@ class Softphone extends events_1.EventEmitter {
|
|
|
247
317
|
this._onCallEvent(sipCall, 'onReinvite', request, updatedCalleeName, updatedNumber, hadRemoteVideo);
|
|
248
318
|
});
|
|
249
319
|
this.client.on(this.client.ACCEPTED, (sipCall) => __awaiter(this, void 0, void 0, function* () {
|
|
320
|
+
const call = this._getCall(sipCall);
|
|
321
|
+
this.emit(exports.EVENT_TERMINATE_SOUND, call, 'call accepted');
|
|
250
322
|
this._onCallEvent(sipCall, 'onAccepted');
|
|
251
323
|
if (this.audioOutputDeviceId) {
|
|
252
324
|
yield this.client.changeAudioOutputDevice(this.audioOutputDeviceId);
|
|
@@ -276,12 +348,12 @@ class Softphone extends events_1.EventEmitter {
|
|
|
276
348
|
});
|
|
277
349
|
this.client.on(this.client.UNREGISTERED, () => {
|
|
278
350
|
logger.info('softphone - unregistered');
|
|
279
|
-
this.
|
|
351
|
+
this.emit(exports.EVENT_UNREGISTERED);
|
|
280
352
|
this._sendAction(softphone_state_machine_1.Actions.UNREGISTERED);
|
|
281
353
|
});
|
|
282
354
|
this.client.on(this.client.ON_DISCONNECTED, () => {
|
|
283
355
|
logger.info('softphone - disconnected');
|
|
284
|
-
this.
|
|
356
|
+
this.emit(exports.EVENT_DISCONNECTED);
|
|
285
357
|
this._sendAction(softphone_state_machine_1.Actions.TRANSPORT_CLOSED);
|
|
286
358
|
});
|
|
287
359
|
this.client.on(this.client.REGISTERED, () => {
|
|
@@ -292,7 +364,7 @@ class Softphone extends events_1.EventEmitter {
|
|
|
292
364
|
}
|
|
293
365
|
this.stopHeartbeat();
|
|
294
366
|
this._sendAction(softphone_state_machine_1.Actions.REGISTER_DONE);
|
|
295
|
-
this.
|
|
367
|
+
this.emit(exports.EVENT_REGISTERED);
|
|
296
368
|
// If the phone registered with a current callSession (eg: when switching network):
|
|
297
369
|
// send a reinvite to renegociate ICE with new IP
|
|
298
370
|
if (this.shouldSendReinvite) {
|
|
@@ -308,7 +380,7 @@ class Softphone extends events_1.EventEmitter {
|
|
|
308
380
|
});
|
|
309
381
|
this.client.on(this.client.DISCONNECTED, () => {
|
|
310
382
|
logger.info('softphone - disconnected');
|
|
311
|
-
this.
|
|
383
|
+
this.emit(exports.EVENT_DISCONNECTED);
|
|
312
384
|
// Do not trigger heartbeat if already running
|
|
313
385
|
if (!this.client.hasHeartbeat()) {
|
|
314
386
|
this.startHeartbeat();
|
|
@@ -317,17 +389,17 @@ class Softphone extends events_1.EventEmitter {
|
|
|
317
389
|
this.shouldSendReinvite = this.calls.length > 0;
|
|
318
390
|
});
|
|
319
391
|
this.client.on('onVideoInputChange', stream => {
|
|
320
|
-
this.
|
|
392
|
+
this.emit(exports.EVENT_VIDEO_INPUT_CHANGE, stream);
|
|
321
393
|
});
|
|
322
394
|
this.client.on(this.client.MESSAGE, (message) => {
|
|
323
395
|
this._onMessage(message);
|
|
324
|
-
this.
|
|
396
|
+
this.emit(exports.EVENT_ON_MESSAGE, message);
|
|
325
397
|
});
|
|
326
398
|
this.client.setOnHeartbeatTimeout(() => {
|
|
327
|
-
this.
|
|
399
|
+
this.emit(exports.EVENT_HEARTBEAT_TIMEOUT);
|
|
328
400
|
});
|
|
329
401
|
this.client.setOnHeartbeatCallback(() => {
|
|
330
|
-
this.
|
|
402
|
+
this.emit(exports.EVENT_HEARTBEAT);
|
|
331
403
|
});
|
|
332
404
|
}
|
|
333
405
|
_sendAction(action) {
|
|
@@ -360,11 +432,11 @@ class Softphone extends events_1.EventEmitter {
|
|
|
360
432
|
const { type, content, } = body;
|
|
361
433
|
switch (type) {
|
|
362
434
|
case exports.MESSAGE_TYPE_CHAT:
|
|
363
|
-
this.
|
|
435
|
+
this.emit(exports.EVENT_ON_CHAT, content);
|
|
364
436
|
break;
|
|
365
437
|
case exports.MESSAGE_TYPE_SIGNAL:
|
|
366
438
|
{
|
|
367
|
-
this.
|
|
439
|
+
this.emit(exports.EVENT_ON_SIGNAL, content);
|
|
368
440
|
break;
|
|
369
441
|
}
|
|
370
442
|
default:
|
package/package.json
CHANGED
|
@@ -3,8 +3,9 @@ import { Inviter, UserAgent, type URI } from 'sip.js';
|
|
|
3
3
|
|
|
4
4
|
import softphone from '../../voice/softphone';
|
|
5
5
|
import Call from '../../voice/call';
|
|
6
|
-
import { Actions, States } from '../../state-machine/call-state-machine';
|
|
6
|
+
import { Actions, EstablishedStates, States } from '../../state-machine/call-state-machine';
|
|
7
7
|
import { Actions as SoftphoneActions } from '../../state-machine/softphone-state-machine';
|
|
8
|
+
import ApiCall from '../../domain/Call';
|
|
8
9
|
|
|
9
10
|
import InvalidStateTransition from '../../domain/InvalidStateTransition';
|
|
10
11
|
|
|
@@ -33,6 +34,7 @@ jest.mock('../../web-rtc-client', () => jest.fn().mockImplementation(() => ({
|
|
|
33
34
|
answer: jest.fn(() => Promise.resolve()),
|
|
34
35
|
hangup: jest.fn(() => Promise.resolve()),
|
|
35
36
|
register: jest.fn(() => Promise.resolve()),
|
|
37
|
+
onCallEnded: jest.fn(() => Promise.resolve()),
|
|
36
38
|
setOnHeartbeatTimeout: jest.fn(() => Promise.resolve()),
|
|
37
39
|
setOnHeartbeatCallback: jest.fn(() => Promise.resolve()),
|
|
38
40
|
on: jest.fn(),
|
|
@@ -42,11 +44,44 @@ const ua = new UserAgent(uaOptions);
|
|
|
42
44
|
const sipCall = new Inviter(ua, UserAgent.makeURI('sip:1234@my.stack.io') as URI);
|
|
43
45
|
|
|
44
46
|
softphone.connect();
|
|
47
|
+
// @ts-ignore: private method
|
|
45
48
|
softphone._sendAction(SoftphoneActions.REGISTER_DONE);
|
|
46
49
|
|
|
47
50
|
const invalidStateTransition = (state: string, action: string) => new InvalidStateTransition(`Invalid state transition from ${state} with action ${action}`, state, action);
|
|
48
51
|
|
|
49
52
|
describe('Call', () => {
|
|
53
|
+
describe('parsing an API call', () => {
|
|
54
|
+
it('Should parse an API call and transform it to a Call instance', async () => {
|
|
55
|
+
const apiCall = new ApiCall({
|
|
56
|
+
id: '1234',
|
|
57
|
+
sipCallId: 'abcd',
|
|
58
|
+
isCaller: false,
|
|
59
|
+
isVideo: true,
|
|
60
|
+
recording: true,
|
|
61
|
+
callerName: '',
|
|
62
|
+
callerNumber: '',
|
|
63
|
+
calleeName: '',
|
|
64
|
+
calleeNumber: '0123423223',
|
|
65
|
+
dialedExtension: '',
|
|
66
|
+
lineId: null,
|
|
67
|
+
muted: true,
|
|
68
|
+
onHold: false,
|
|
69
|
+
startingTime: new Date(),
|
|
70
|
+
status: 'Up',
|
|
71
|
+
talkingToIds: [],
|
|
72
|
+
});
|
|
73
|
+
const call = Call.parseCall(apiCall);
|
|
74
|
+
|
|
75
|
+
expect(call.recording).toBeTruthy();
|
|
76
|
+
expect(call.id).toBe(apiCall.sipCallId);
|
|
77
|
+
expect(call.apiId).toBe(apiCall.id);
|
|
78
|
+
expect(call.number).toBe(apiCall.calleeNumber);
|
|
79
|
+
expect(call.answerTime).toStrictEqual(apiCall.startingTime);
|
|
80
|
+
expect(call.isMuted()).toBeTruthy();
|
|
81
|
+
expect(call.state).toStrictEqual({ [States.ESTABLISHED]: EstablishedStates.MUTED });
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
50
85
|
describe('accept', () => {
|
|
51
86
|
it('Should throw an error when accepting a not ringing call', async () => {
|
|
52
87
|
const call = new Call(sipCall, softphone);
|
|
@@ -54,16 +89,24 @@ describe('Call', () => {
|
|
|
54
89
|
await expect(call.accept()).rejects.toThrowError(invalidStateTransition(States.IDLE, Actions.ACCEPT));
|
|
55
90
|
});
|
|
56
91
|
|
|
57
|
-
it
|
|
92
|
+
it('Should throw an error when the softphone is not registered', async () => {
|
|
58
93
|
const call = new Call(sipCall, softphone);
|
|
94
|
+
// @ts-ignore: private method
|
|
59
95
|
call._sendAction(Actions.INCOMING_CALL);
|
|
96
|
+
// @ts-ignore: private method
|
|
60
97
|
softphone._sendAction(SoftphoneActions.TRANSPORT_CLOSED);
|
|
61
98
|
|
|
62
99
|
await expect(call.accept()).rejects.toThrowError(invalidStateTransition(States.RINGING, Actions.ACCEPT));
|
|
100
|
+
|
|
101
|
+
// Revert softphone state
|
|
102
|
+
softphone.connect();
|
|
103
|
+
// @ts-ignore: private method
|
|
104
|
+
softphone._sendAction(SoftphoneActions.REGISTER_DONE);
|
|
63
105
|
});
|
|
64
106
|
|
|
65
107
|
it('Should not throw an error when accepting a not ringing call', async () => {
|
|
66
108
|
const call = new Call(sipCall, softphone);
|
|
109
|
+
// @ts-ignore: private method
|
|
67
110
|
call._sendAction(Actions.INCOMING_CALL);
|
|
68
111
|
|
|
69
112
|
await call.accept();
|
|
@@ -80,7 +123,9 @@ describe('Call', () => {
|
|
|
80
123
|
|
|
81
124
|
it('Should not throw an error when terminating an established call', async () => {
|
|
82
125
|
const call = new Call(sipCall, softphone);
|
|
126
|
+
// @ts-ignore: private method
|
|
83
127
|
call._sendAction(Actions.MAKE_CALL);
|
|
128
|
+
// @ts-ignore: private method
|
|
84
129
|
call._sendAction(Actions.REMOTLY_ACCEPTED);
|
|
85
130
|
|
|
86
131
|
await call.hangup();
|