@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/src/voice/call.ts
CHANGED
|
@@ -8,13 +8,15 @@ import { SessionDescriptionHandler } from 'sip.js/lib/platform/web';
|
|
|
8
8
|
import { SessionDescriptionHandlerOptions } from 'sip.js/lib/api';
|
|
9
9
|
import type { IncomingRequestMessage } from 'sip.js/lib/core/messages/incoming-request-message';
|
|
10
10
|
|
|
11
|
-
import callStateMachine, { type ActionTypes, Actions, type EstablishedActionTypes, EstablishedActions, States, EstablishedStates, type CallActorRef } from '../state-machine/call-state-machine';
|
|
11
|
+
import callStateMachine, { type ActionTypes, Actions, type EstablishedActionTypes, EstablishedActions, States, type StateTypes, EstablishedStates, type EstablishedStateTypes, type CallActorRef } from '../state-machine/call-state-machine';
|
|
12
12
|
import { SipCall, PeerConnection } from '../domain/types';
|
|
13
|
+
import ApiCall from '../domain/Call';
|
|
14
|
+
|
|
13
15
|
import IssueReporter from '../service/IssueReporter';
|
|
14
|
-
import { Softphone } from './softphone';
|
|
15
|
-
import { getSipSessionId } from '../utils/sdp';
|
|
16
|
+
import softphone, { EVENT_TERMINATE_SOUND, Softphone } from './softphone';
|
|
17
|
+
import { getCallDisplayName, getCallNumber, getSipSessionId } from '../utils/sdp';
|
|
16
18
|
import { assertCan, assertState, getState } from '../state-machine/utils';
|
|
17
|
-
import { downgradeToAudio, getLocalStream, getLocalVideoStream, getPeerConnection, getRemoteStream, getRemoteVideoStream, getStreamFromConstraints, getWebRtcStats, hasALocalVideoTrack, hasLocalVideo, hasRemoteVideo, isVideoRemotelyHeld, setLocalMediaStream, toggleVideo, upgradeToVideo } from '../utils/webrtc';
|
|
19
|
+
import { downgradeToAudio, getLocalStream, getLocalVideoStream, getPeerConnection, getRemoteStream, getRemoteVideoStream, getStreamFromConstraints, getWebRtcStats, hasALocalVideoTrack, hasLocalVideo, hasRemoteVideo, isVideoMuted, isVideoRemotelyHeld, setLocalMediaStream, toggleVideo, upgradeToVideo } from '../utils/webrtc';
|
|
18
20
|
|
|
19
21
|
const logger = IssueReporter.loggerFor('call');
|
|
20
22
|
|
|
@@ -53,15 +55,12 @@ export const EVENT_SHARE_SCREEN_ENDED = 'onScreenShareEnded';
|
|
|
53
55
|
export const EVENT_WEB_RTC_STATS = 'webRtcStats';
|
|
54
56
|
export const EVENT_CAMERA_RESUMED = 'onCameraResumed';
|
|
55
57
|
export const EVENT_CAMERA_DISABLED = 'onCameraDisabled';
|
|
56
|
-
export const EVENT_TERMINATE_SOUND = 'terminateSound';
|
|
57
58
|
|
|
58
59
|
export const MESSAGE_TYPE_CHAT = 'message/TYPE_CHAT';
|
|
59
60
|
export const MESSAGE_TYPE_SIGNAL = 'message/TYPE_SIGNAL';
|
|
60
61
|
export const MESSAGE_TRACK_UPDATED = 'onTrackUpdated';
|
|
61
62
|
|
|
62
63
|
class Call extends EventEmitter {
|
|
63
|
-
eventEmitter: EventEmitter;
|
|
64
|
-
|
|
65
64
|
callActor: CallActorRef;
|
|
66
65
|
|
|
67
66
|
// Wazo call id, used in Wazo Websocket and API
|
|
@@ -81,8 +80,65 @@ class Call extends EventEmitter {
|
|
|
81
80
|
|
|
82
81
|
shouldSendReinvite = false;
|
|
83
82
|
|
|
83
|
+
recording = false;
|
|
84
|
+
|
|
85
|
+
displayName: string | null;
|
|
86
|
+
|
|
87
|
+
number: string | null;
|
|
88
|
+
|
|
84
89
|
currentScreenShare: Record<string, any> | null;
|
|
85
90
|
|
|
91
|
+
static parseCall(apiCall: ApiCall): Call {
|
|
92
|
+
const sipCall = {
|
|
93
|
+
id: apiCall.sipCallId,
|
|
94
|
+
stateChange: {
|
|
95
|
+
addListener: () => {},
|
|
96
|
+
},
|
|
97
|
+
remoteIdentity: {
|
|
98
|
+
uri: {
|
|
99
|
+
_normal: {
|
|
100
|
+
user: apiCall.calleeNumber,
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
// Mock sdk so `hasLocalVideo` will return true.
|
|
105
|
+
sessionDescriptionHandler: {
|
|
106
|
+
localMediaStream: {
|
|
107
|
+
getTracks: () => (apiCall.isVideo ? [{ kind: 'video', readyState: 'live' }] : []),
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
} as any;
|
|
111
|
+
|
|
112
|
+
const call = new Call(sipCall, softphone);
|
|
113
|
+
// call._sendAction(Actions.ES);
|
|
114
|
+
call.apiId = apiCall.id;
|
|
115
|
+
call.answerTime = new Date(+apiCall.startingTime);
|
|
116
|
+
|
|
117
|
+
// Update call state
|
|
118
|
+
const snapshot = call.callActor.getSnapshot();
|
|
119
|
+
|
|
120
|
+
if (apiCall.isUp()) {
|
|
121
|
+
snapshot.value = { [States.ESTABLISHED]: EstablishedStates.ONGOING };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (apiCall.muted) {
|
|
125
|
+
snapshot.value = { [States.ESTABLISHED]: EstablishedStates.MUTED };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (apiCall.isRinging()) {
|
|
129
|
+
snapshot.value = States.RINGING;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (apiCall.isRecording()) {
|
|
133
|
+
call.recording = true;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// @ts-ignore
|
|
137
|
+
call.callActor.update(snapshot);
|
|
138
|
+
|
|
139
|
+
return call;
|
|
140
|
+
}
|
|
141
|
+
|
|
86
142
|
constructor(sipCall: SipCall, phone: Softphone) {
|
|
87
143
|
super();
|
|
88
144
|
|
|
@@ -94,6 +150,9 @@ class Call extends EventEmitter {
|
|
|
94
150
|
this.phone = phone;
|
|
95
151
|
this.sipCall = sipCall;
|
|
96
152
|
|
|
153
|
+
this.number = getCallNumber(sipCall);
|
|
154
|
+
this.displayName = getCallDisplayName(sipCall);
|
|
155
|
+
|
|
97
156
|
this._bindEvents();
|
|
98
157
|
}
|
|
99
158
|
|
|
@@ -123,7 +182,7 @@ class Call extends EventEmitter {
|
|
|
123
182
|
assertCan(this.callActor, Actions.REJECT);
|
|
124
183
|
logger.info('call - reject', { id: this.id });
|
|
125
184
|
|
|
126
|
-
this.
|
|
185
|
+
this.phone.emit(EVENT_TERMINATE_SOUND, 'call rejected locally');
|
|
127
186
|
|
|
128
187
|
this.shouldSendReinvite = false;
|
|
129
188
|
|
|
@@ -137,7 +196,7 @@ class Call extends EventEmitter {
|
|
|
137
196
|
await this.phone.client.hangup(this.sipCall);
|
|
138
197
|
|
|
139
198
|
// Remove this call from softphone's calls
|
|
140
|
-
this.phone.
|
|
199
|
+
this.phone.onCallTerminated(this);
|
|
141
200
|
|
|
142
201
|
return true;
|
|
143
202
|
}
|
|
@@ -151,7 +210,7 @@ class Call extends EventEmitter {
|
|
|
151
210
|
}
|
|
152
211
|
}
|
|
153
212
|
|
|
154
|
-
startConference(withCalls: Call[]) {
|
|
213
|
+
startConference(/* withCalls: Call[] */) {
|
|
155
214
|
// @TODO
|
|
156
215
|
// const participants = [this, ...withCalls].reduce((acc: Record<string, any>, call: Call) => {
|
|
157
216
|
// acc[call.talkingToIds[0]] = call;
|
|
@@ -174,7 +233,7 @@ class Call extends EventEmitter {
|
|
|
174
233
|
|
|
175
234
|
this.phone.client.mute(this.sipCall);
|
|
176
235
|
|
|
177
|
-
this.
|
|
236
|
+
this.emit(EVENT_CALL_MUTED);
|
|
178
237
|
}
|
|
179
238
|
|
|
180
239
|
unMute() {
|
|
@@ -185,7 +244,7 @@ class Call extends EventEmitter {
|
|
|
185
244
|
|
|
186
245
|
this.phone.client.unmute(this.sipCall);
|
|
187
246
|
|
|
188
|
-
this.
|
|
247
|
+
this.emit(EVENT_CALL_UN_MUTED);
|
|
189
248
|
}
|
|
190
249
|
|
|
191
250
|
async hold() {
|
|
@@ -208,7 +267,7 @@ class Call extends EventEmitter {
|
|
|
208
267
|
|
|
209
268
|
const promise = this.phone.client.hold(this.sipCall, this.isConference(), hasVideo);
|
|
210
269
|
|
|
211
|
-
this.
|
|
270
|
+
this.emit(EVENT_CALL_HELD);
|
|
212
271
|
|
|
213
272
|
return promise;
|
|
214
273
|
}
|
|
@@ -244,7 +303,7 @@ class Call extends EventEmitter {
|
|
|
244
303
|
}
|
|
245
304
|
};
|
|
246
305
|
|
|
247
|
-
this.
|
|
306
|
+
this.emit(EVENT_CALL_RESUMED);
|
|
248
307
|
|
|
249
308
|
return promise.then(() => {
|
|
250
309
|
const stream = this.getLocalVideoStream();
|
|
@@ -261,7 +320,7 @@ class Call extends EventEmitter {
|
|
|
261
320
|
logger.info('call - turn camera on', { id: this.id });
|
|
262
321
|
toggleVideo(this.sipCall, true);
|
|
263
322
|
|
|
264
|
-
this.
|
|
323
|
+
this.emit(EVENT_CAMERA_RESUMED);
|
|
265
324
|
}
|
|
266
325
|
|
|
267
326
|
turnCameraOff(): void {
|
|
@@ -269,15 +328,23 @@ class Call extends EventEmitter {
|
|
|
269
328
|
logger.info('call - turn camera off', { id: this.id });
|
|
270
329
|
toggleVideo(this.sipCall, false);
|
|
271
330
|
|
|
272
|
-
this.
|
|
331
|
+
this.emit(EVENT_CAMERA_DISABLED);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
isMuted() {
|
|
335
|
+
return this._hasEstablishedState(EstablishedStates.MUTED);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
isCameraMuted() {
|
|
339
|
+
return isVideoMuted(this.sipCall);
|
|
273
340
|
}
|
|
274
341
|
|
|
275
342
|
isHeld() {
|
|
276
|
-
return
|
|
343
|
+
return this._hasEstablishedState(EstablishedStates.HELD);
|
|
277
344
|
}
|
|
278
345
|
|
|
279
346
|
isEstablished() {
|
|
280
|
-
return
|
|
347
|
+
return States.ESTABLISHED in (this.state as Record<StateTypes, EstablishedStateTypes>);
|
|
281
348
|
}
|
|
282
349
|
|
|
283
350
|
isConference(): boolean {
|
|
@@ -446,7 +513,7 @@ class Call extends EventEmitter {
|
|
|
446
513
|
console.warn(e);
|
|
447
514
|
}
|
|
448
515
|
|
|
449
|
-
this.
|
|
516
|
+
this.emit(EVENT_SHARE_SCREEN_ENDED);
|
|
450
517
|
this.currentScreenShare = null;
|
|
451
518
|
|
|
452
519
|
return reinvited;
|
|
@@ -480,12 +547,16 @@ class Call extends EventEmitter {
|
|
|
480
547
|
this.emit(EVENT_CALL_ERROR, error);
|
|
481
548
|
}
|
|
482
549
|
|
|
550
|
+
onCallIncoming() {
|
|
551
|
+
this._sendAction(Actions.INCOMING_CALL);
|
|
552
|
+
}
|
|
553
|
+
|
|
483
554
|
onRejected() {
|
|
484
555
|
this.endTime = new Date();
|
|
485
556
|
|
|
486
557
|
this.emit(EVENT_CALL_REJECTED);
|
|
487
558
|
|
|
488
|
-
this.phone.
|
|
559
|
+
this.phone.onCallTerminated(this);
|
|
489
560
|
}
|
|
490
561
|
|
|
491
562
|
onNetworkStats(stats: Record<string, any>, previousStats: Record<string, any>) {
|
|
@@ -524,6 +595,10 @@ class Call extends EventEmitter {
|
|
|
524
595
|
return isVideoRemotelyHeld(this.sipCall);
|
|
525
596
|
}
|
|
526
597
|
|
|
598
|
+
isRinging() {
|
|
599
|
+
return this.state === States.RINGING;
|
|
600
|
+
}
|
|
601
|
+
|
|
527
602
|
onReinvite(request: IncomingRequestMessage, updatedCalleeName: string, updatedNumber: string, hadRemoteVideo: boolean) {
|
|
528
603
|
this.emit(EVENT_REINVITE, request, updatedCalleeName, updatedNumber, hadRemoteVideo);
|
|
529
604
|
}
|
|
@@ -532,13 +607,16 @@ class Call extends EventEmitter {
|
|
|
532
607
|
return getSipSessionId(this.sipCall);
|
|
533
608
|
}
|
|
534
609
|
|
|
535
|
-
get
|
|
536
|
-
|
|
537
|
-
// @ts-ignore: private
|
|
538
|
-
return identity ? identity.uri._normal.user : null;
|
|
610
|
+
get state(): StateTypes | Record<StateTypes, EstablishedStateTypes> {
|
|
611
|
+
return getState(this.callActor) as StateTypes | Record<StateTypes, EstablishedStateTypes>;
|
|
539
612
|
}
|
|
540
613
|
|
|
541
|
-
|
|
614
|
+
private _hasEstablishedState(establishedState: EstablishedStateTypes) {
|
|
615
|
+
const state = this.state as Record<StateTypes, EstablishedStateTypes>;
|
|
616
|
+
return States.ESTABLISHED in state && state[States.ESTABLISHED] === establishedState;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
private _bindEvents() {
|
|
542
620
|
if (this.sipCall instanceof Invitation) {
|
|
543
621
|
// Monkey patch to know when canceled with the CANCEL message
|
|
544
622
|
const onCancel = (this.sipCall as Invitation)._onCancel.bind(this.sipCall);
|
|
@@ -548,7 +626,7 @@ class Call extends EventEmitter {
|
|
|
548
626
|
onCancel(message);
|
|
549
627
|
const elsewhere = message.data.indexOf('cause=26') !== -1 && message.data.indexOf('completed elsewhere') !== -1;
|
|
550
628
|
|
|
551
|
-
this.
|
|
629
|
+
this.emit(EVENT_CALL_CANCELED, elsewhere);
|
|
552
630
|
};
|
|
553
631
|
}
|
|
554
632
|
|
|
@@ -556,7 +634,7 @@ class Call extends EventEmitter {
|
|
|
556
634
|
switch (newState) {
|
|
557
635
|
case SessionState.Terminating:
|
|
558
636
|
logger.info('call - terminating', { id: this.id });
|
|
559
|
-
this.
|
|
637
|
+
this.emit(EVENT_CALL_TERMINATING);
|
|
560
638
|
break;
|
|
561
639
|
|
|
562
640
|
case SessionState.Terminated:
|
|
@@ -566,7 +644,9 @@ class Call extends EventEmitter {
|
|
|
566
644
|
// Should be called before `_onCallTerminated` or the callCount will not decrement...
|
|
567
645
|
this.endTime = new Date();
|
|
568
646
|
|
|
569
|
-
|
|
647
|
+
this.emit(EVENT_CALL_ENDED);
|
|
648
|
+
|
|
649
|
+
return this.phone.onCallTerminated(this);
|
|
570
650
|
}
|
|
571
651
|
|
|
572
652
|
default:
|
|
@@ -599,10 +679,10 @@ class Call extends EventEmitter {
|
|
|
599
679
|
});
|
|
600
680
|
|
|
601
681
|
if (kind === 'audio') {
|
|
602
|
-
return this.
|
|
682
|
+
return this.emit(EVENT_AUDIO_STREAM, stream);
|
|
603
683
|
}
|
|
604
684
|
|
|
605
|
-
return this.
|
|
685
|
+
return this.emit(EVENT_VIDEO_STREAM, stream, event.track.id, event);
|
|
606
686
|
};
|
|
607
687
|
|
|
608
688
|
peerConnection.onremovestream = (event: any) => {
|
|
@@ -610,13 +690,11 @@ class Call extends EventEmitter {
|
|
|
610
690
|
id: event.stream.id,
|
|
611
691
|
tracks: event.stream.getTracks(),
|
|
612
692
|
});
|
|
613
|
-
this.
|
|
693
|
+
this.emit(EVENT_REMOVE_STREAM, event.stream);
|
|
614
694
|
};
|
|
615
|
-
|
|
616
|
-
// Client events
|
|
617
695
|
}
|
|
618
696
|
|
|
619
|
-
_downgradeToAudio(withMessage = true) {
|
|
697
|
+
private _downgradeToAudio(withMessage = true) {
|
|
620
698
|
assertState(this.callActor, States.ESTABLISHED);
|
|
621
699
|
logger.info('call - downgrade to audio', { id: this.id, withMessage });
|
|
622
700
|
|
|
@@ -630,7 +708,7 @@ class Call extends EventEmitter {
|
|
|
630
708
|
}
|
|
631
709
|
|
|
632
710
|
// Returns true if we need to send a re-INVITE request
|
|
633
|
-
async _upgradeToVideo(options: ReinviteOptions = {}): Promise<boolean> {
|
|
711
|
+
private async _upgradeToVideo(options: ReinviteOptions = {}): Promise<boolean> {
|
|
634
712
|
assertState(this.callActor, States.ESTABLISHED);
|
|
635
713
|
logger.info('call - upgrade to video', { id: this.id, ...options });
|
|
636
714
|
|
|
@@ -660,7 +738,7 @@ class Call extends EventEmitter {
|
|
|
660
738
|
return false;
|
|
661
739
|
}
|
|
662
740
|
|
|
663
|
-
_sendReinviteMessage(isUpgrade: boolean) {
|
|
741
|
+
private _sendReinviteMessage(isUpgrade: boolean) {
|
|
664
742
|
assertState(this.callActor, States.ESTABLISHED);
|
|
665
743
|
logger.info('call - sending reinvite message', { id: this.id, isUpgrade });
|
|
666
744
|
|
|
@@ -679,7 +757,7 @@ class Call extends EventEmitter {
|
|
|
679
757
|
}, 2500);
|
|
680
758
|
}
|
|
681
759
|
|
|
682
|
-
_onScreenSharing(screenStream: MediaStream, hadVideo: boolean, desktop?: boolean | null | undefined) {
|
|
760
|
+
private _onScreenSharing(screenStream: MediaStream, hadVideo: boolean, desktop?: boolean | null | undefined) {
|
|
683
761
|
const screenTrack = screenStream.getVideoTracks()[0];
|
|
684
762
|
const pc = this.phone.client.getPeerConnection(this.id);
|
|
685
763
|
const sender = pc && pc.getSenders && pc.getSenders().find((s) => s && s.track && s.track.kind === 'video');
|
|
@@ -690,7 +768,7 @@ class Call extends EventEmitter {
|
|
|
690
768
|
screenTrack.onended = () => {
|
|
691
769
|
logger.info('call - on screenshare ended', { hadVideo, id: this.id, screenTrack });
|
|
692
770
|
|
|
693
|
-
this.
|
|
771
|
+
this.emit(EVENT_SHARE_SCREEN_ENDING);
|
|
694
772
|
};
|
|
695
773
|
}
|
|
696
774
|
|
|
@@ -702,10 +780,10 @@ class Call extends EventEmitter {
|
|
|
702
780
|
};
|
|
703
781
|
|
|
704
782
|
setLocalMediaStream(this.sipCall, screenStream);
|
|
705
|
-
this.
|
|
783
|
+
this.emit(EVENT_SHARE_SCREEN_STARTED, screenStream);
|
|
706
784
|
}
|
|
707
785
|
|
|
708
|
-
_sendAction(action: ActionTypes | EstablishedActionTypes) {
|
|
786
|
+
private _sendAction(action: ActionTypes | EstablishedActionTypes) {
|
|
709
787
|
this.callActor.send({ type: action });
|
|
710
788
|
}
|
|
711
789
|
}
|
package/src/voice/softphone.ts
CHANGED
|
@@ -10,7 +10,6 @@ import InvalidState from '../domain/InvalidState';
|
|
|
10
10
|
|
|
11
11
|
import Wazo from '..';
|
|
12
12
|
import softphoneStateMachine, { type SoftphoneActorRef, Actions, type StateTypes, States, ActionTypes } from '../state-machine/softphone-state-machine';
|
|
13
|
-
import { Actions as CallActions } from '../state-machine/call-state-machine';
|
|
14
13
|
import { WebRtcConfig, SipCall } from '../domain/types';
|
|
15
14
|
import WazoWebRTCClient from '../web-rtc-client';
|
|
16
15
|
import IssueReporter from '../service/IssueReporter';
|
|
@@ -19,12 +18,17 @@ import Call from './call';
|
|
|
19
18
|
import { assertCan, can, getState, waitUntilState } from '../state-machine/utils';
|
|
20
19
|
|
|
21
20
|
export const EVENT_INCOMING = 'incoming';
|
|
21
|
+
export const EVENT_OUTGOING = 'outgoing';
|
|
22
22
|
export const EVENT_HEARTBEAT = 'heartbeat';
|
|
23
23
|
export const EVENT_HEARTBEAT_TIMEOUT = 'heartbeatTimeout';
|
|
24
24
|
export const EVENT_REGISTERED = 'registered';
|
|
25
25
|
export const EVENT_UNREGISTERED = 'unregistered';
|
|
26
26
|
export const EVENT_DISCONNECTED = 'disconnected';
|
|
27
27
|
export const EVENT_VIDEO_INPUT_CHANGE = 'videoInputChange';
|
|
28
|
+
export const EVENT_TERMINATE_SOUND = 'terminateSound';
|
|
29
|
+
export const EVENT_PLAY_HANGUP_SOUND = 'playHangupSound';
|
|
30
|
+
export const EVENT_PLAY_RING_SOUND = 'playRingSound';
|
|
31
|
+
export const EVENT_PLAY_INBOUND_CALL_SOUND = 'playInboundCallSound';
|
|
28
32
|
export const EVENT_ON_MESSAGE = 'message';
|
|
29
33
|
export const EVENT_ON_CHAT = 'phone/ON_CHAT';
|
|
30
34
|
export const EVENT_ON_SIGNAL = 'phone/ON_SIGNAL';
|
|
@@ -53,8 +57,6 @@ export class Softphone extends EventEmitter {
|
|
|
53
57
|
|
|
54
58
|
audioRingVolume: number;
|
|
55
59
|
|
|
56
|
-
eventEmitter: EventEmitter;
|
|
57
|
-
|
|
58
60
|
calls: Call[];
|
|
59
61
|
|
|
60
62
|
client: WazoWebRTCClient;
|
|
@@ -69,6 +71,8 @@ export class Softphone extends EventEmitter {
|
|
|
69
71
|
|
|
70
72
|
shouldSendReinvite: boolean;
|
|
71
73
|
|
|
74
|
+
ringingEnabled = true;
|
|
75
|
+
|
|
72
76
|
constructor() {
|
|
73
77
|
super();
|
|
74
78
|
|
|
@@ -179,16 +183,16 @@ export class Softphone extends EventEmitter {
|
|
|
179
183
|
return Promise.resolve(null);
|
|
180
184
|
}
|
|
181
185
|
|
|
182
|
-
if (this.
|
|
183
|
-
throw new InvalidState(`Invalid state ${States.UNREGISTERED}`, States.UNREGISTERED);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
if (this.getState() !== States.REGISTERING) {
|
|
186
|
+
if (this.state === States.REGISTERING) {
|
|
187
187
|
logger.info('softphone - calling when registering, waiting for Softphone to be registered ...');
|
|
188
188
|
|
|
189
189
|
await waitUntilState(this.softphoneActor, States.REGISTERED);
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
+
if (this.state !== States.UNREGISTERED) {
|
|
193
|
+
throw new InvalidState(`Invalid state ${States.UNREGISTERED}`, States.UNREGISTERED);
|
|
194
|
+
}
|
|
195
|
+
|
|
192
196
|
// Hold other calls
|
|
193
197
|
this.holdAllCalls();
|
|
194
198
|
|
|
@@ -198,6 +202,8 @@ export class Softphone extends EventEmitter {
|
|
|
198
202
|
sipCall = this.client.call(options.params.To, options.withCamera, options.audioOnly, options.conference, options) as SipCall;
|
|
199
203
|
const call = new Call(sipCall, this);
|
|
200
204
|
|
|
205
|
+
this.emit(EVENT_OUTGOING, call);
|
|
206
|
+
|
|
201
207
|
return call;
|
|
202
208
|
} catch (error: any) {
|
|
203
209
|
logger.warn('softphone - call, error', { message: error.message, stack: error.stack });
|
|
@@ -209,6 +215,69 @@ export class Softphone extends EventEmitter {
|
|
|
209
215
|
this.calls = this.calls.filter((call: Call) => call.id !== callToRemove.id);
|
|
210
216
|
}
|
|
211
217
|
|
|
218
|
+
onCallTerminated(call: Call) {
|
|
219
|
+
logger.info('softphone - on call terminated', { id: call.id });
|
|
220
|
+
|
|
221
|
+
const currentCall = this.getCurrentCall();
|
|
222
|
+
const isCurrentCall = currentCall?.id === call.id;
|
|
223
|
+
const isRinging = call.isRinging();
|
|
224
|
+
const isCurrentIncomingCall = isCurrentCall && isRinging;
|
|
225
|
+
const incomingCalls = this.getIncomingCalls();
|
|
226
|
+
const nbIncomingCalls = incomingCalls.length;
|
|
227
|
+
|
|
228
|
+
// Send an event to terminate ringing sound
|
|
229
|
+
if (isCurrentIncomingCall) {
|
|
230
|
+
setTimeout(() => {
|
|
231
|
+
// Avoid race condition when the other is calling and hanging up immediately
|
|
232
|
+
this.emit(EVENT_TERMINATE_SOUND, call, 'call terminated');
|
|
233
|
+
}, 5);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// If the terminated call was an incoming call, we have to re-trigger if it's was the first incoming call
|
|
237
|
+
// Otherwise, we have to re-trigger an incoming call event if another call is incoming
|
|
238
|
+
const shouldRetrigger = isCurrentCall ? nbIncomingCalls : isCurrentIncomingCall;
|
|
239
|
+
|
|
240
|
+
// Re-trigger incoming call event for remaining incoming calls
|
|
241
|
+
logger.info('Softphone - check to re-trigger incoming call', { shouldRetrigger, nbIncomingCalls });
|
|
242
|
+
|
|
243
|
+
if (shouldRetrigger && nbIncomingCalls > 1) {
|
|
244
|
+
const nextCall = incomingCalls.find(c => c.id !== call.id);
|
|
245
|
+
// Avoid race condition
|
|
246
|
+
setTimeout(() => {
|
|
247
|
+
if (this.ringingEnabled) {
|
|
248
|
+
if (currentCall) {
|
|
249
|
+
this.emit(EVENT_PLAY_RING_SOUND, this.audioRingDeviceId, this.audioRingVolume, nextCall);
|
|
250
|
+
} else {
|
|
251
|
+
this.emit(EVENT_PLAY_INBOUND_CALL_SOUND, this.audioOutputDeviceId, this.audioOutputVolume, nextCall);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
this.emit(EVENT_INCOMING, nextCall);
|
|
256
|
+
}, 100);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// @ts-ignore: does not exist
|
|
260
|
+
if (!call.sipCall.isCanceled) {
|
|
261
|
+
setTimeout(() => {
|
|
262
|
+
this.emit(EVENT_PLAY_HANGUP_SOUND, this.audioOutputDeviceId, this.audioOutputVolume, call);
|
|
263
|
+
}, 10);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
this.client.onCallEnded(call.sipCall);
|
|
267
|
+
|
|
268
|
+
this.removeCall(call);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
enableRinging(): Promise<void> | void {
|
|
272
|
+
logger.info('softphone - enable ringing');
|
|
273
|
+
this.ringingEnabled = true;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
disableRinging(): Promise<void> | void {
|
|
277
|
+
logger.info('softphone - disable ringing');
|
|
278
|
+
this.ringingEnabled = false;
|
|
279
|
+
}
|
|
280
|
+
|
|
212
281
|
holdAllCalls(exceptCall: Call | null = null) {
|
|
213
282
|
this.calls.filter(call => !call.isHeld() && call.id === exceptCall?.id).forEach(call => call.hold());
|
|
214
283
|
}
|
|
@@ -278,29 +347,43 @@ export class Softphone extends EventEmitter {
|
|
|
278
347
|
this.audioRingVolume = volume;
|
|
279
348
|
}
|
|
280
349
|
|
|
281
|
-
getCalls(): Call[] {
|
|
282
|
-
return this.calls;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
350
|
getCurrentCall(): Call | undefined {
|
|
286
351
|
return this.calls.find(call => call.isEstablished());
|
|
287
352
|
}
|
|
288
353
|
|
|
289
|
-
|
|
290
|
-
return this.
|
|
354
|
+
getIncomingCalls(): Call[] {
|
|
355
|
+
return this.calls.filter(call => call.isRinging());
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
get state(): StateTypes {
|
|
359
|
+
return getState(this.softphoneActor) as StateTypes;
|
|
291
360
|
}
|
|
292
361
|
|
|
293
362
|
getUserAgent() {
|
|
294
363
|
return this.client?.config?.userAgentString || 'softphone';
|
|
295
364
|
}
|
|
296
365
|
|
|
366
|
+
isRegistered() {
|
|
367
|
+
return this.state === States.REGISTERED;
|
|
368
|
+
}
|
|
369
|
+
|
|
297
370
|
_bindEvents(): void {
|
|
298
371
|
this.client.on(this.client.INVITE, (sipSession: SipCall) => {
|
|
299
372
|
const call = new Call(sipSession, this);
|
|
300
|
-
|
|
373
|
+
logger.info('softphone - on invite', { callId: call.id, number: call.number });
|
|
374
|
+
|
|
375
|
+
call.onCallIncoming();
|
|
301
376
|
this.calls.push(call);
|
|
302
377
|
|
|
303
|
-
this.
|
|
378
|
+
this.emit(EVENT_INCOMING, call);
|
|
379
|
+
|
|
380
|
+
if (!this.getCurrentCall()) {
|
|
381
|
+
if (this.ringingEnabled) {
|
|
382
|
+
this.emit(EVENT_PLAY_RING_SOUND, this.audioRingDeviceId, this.audioRingVolume, call);
|
|
383
|
+
}
|
|
384
|
+
} else {
|
|
385
|
+
this.emit(EVENT_PLAY_INBOUND_CALL_SOUND, this.audioOutputDeviceId, this.audioOutputVolume, call);
|
|
386
|
+
}
|
|
304
387
|
});
|
|
305
388
|
|
|
306
389
|
this.client.on(this.client.ON_REINVITE, (sipCall: SipCall, request: IncomingRequestMessage, updatedCalleeName: string, updatedNumber: string, hadRemoteVideo: boolean) => {
|
|
@@ -316,6 +399,9 @@ export class Softphone extends EventEmitter {
|
|
|
316
399
|
});
|
|
317
400
|
|
|
318
401
|
this.client.on(this.client.ACCEPTED, async (sipCall: SipCall) => {
|
|
402
|
+
const call = this._getCall(sipCall);
|
|
403
|
+
this.emit(EVENT_TERMINATE_SOUND, call, 'call accepted');
|
|
404
|
+
|
|
319
405
|
this._onCallEvent(sipCall, 'onAccepted');
|
|
320
406
|
|
|
321
407
|
if (this.audioOutputDeviceId) {
|
|
@@ -354,14 +440,14 @@ export class Softphone extends EventEmitter {
|
|
|
354
440
|
|
|
355
441
|
this.client.on(this.client.UNREGISTERED, () => {
|
|
356
442
|
logger.info('softphone - unregistered');
|
|
357
|
-
this.
|
|
443
|
+
this.emit(EVENT_UNREGISTERED);
|
|
358
444
|
|
|
359
445
|
this._sendAction(Actions.UNREGISTERED);
|
|
360
446
|
});
|
|
361
447
|
|
|
362
448
|
this.client.on(this.client.ON_DISCONNECTED, () => {
|
|
363
449
|
logger.info('softphone - disconnected');
|
|
364
|
-
this.
|
|
450
|
+
this.emit(EVENT_DISCONNECTED);
|
|
365
451
|
|
|
366
452
|
this._sendAction(Actions.TRANSPORT_CLOSED);
|
|
367
453
|
});
|
|
@@ -376,7 +462,7 @@ export class Softphone extends EventEmitter {
|
|
|
376
462
|
this.stopHeartbeat();
|
|
377
463
|
this._sendAction(Actions.REGISTER_DONE);
|
|
378
464
|
|
|
379
|
-
this.
|
|
465
|
+
this.emit(EVENT_REGISTERED);
|
|
380
466
|
|
|
381
467
|
// If the phone registered with a current callSession (eg: when switching network):
|
|
382
468
|
// send a reinvite to renegociate ICE with new IP
|
|
@@ -396,7 +482,7 @@ export class Softphone extends EventEmitter {
|
|
|
396
482
|
|
|
397
483
|
this.client.on(this.client.DISCONNECTED, () => {
|
|
398
484
|
logger.info('softphone - disconnected');
|
|
399
|
-
this.
|
|
485
|
+
this.emit(EVENT_DISCONNECTED);
|
|
400
486
|
|
|
401
487
|
// Do not trigger heartbeat if already running
|
|
402
488
|
if (!this.client.hasHeartbeat()) {
|
|
@@ -408,33 +494,33 @@ export class Softphone extends EventEmitter {
|
|
|
408
494
|
});
|
|
409
495
|
|
|
410
496
|
this.client.on('onVideoInputChange', stream => {
|
|
411
|
-
this.
|
|
497
|
+
this.emit(EVENT_VIDEO_INPUT_CHANGE, stream);
|
|
412
498
|
});
|
|
413
499
|
|
|
414
500
|
this.client.on(this.client.MESSAGE, (message: Message) => {
|
|
415
501
|
this._onMessage(message);
|
|
416
502
|
|
|
417
|
-
this.
|
|
503
|
+
this.emit(EVENT_ON_MESSAGE, message);
|
|
418
504
|
});
|
|
419
505
|
|
|
420
506
|
this.client.setOnHeartbeatTimeout(() => {
|
|
421
|
-
this.
|
|
507
|
+
this.emit(EVENT_HEARTBEAT_TIMEOUT);
|
|
422
508
|
});
|
|
423
509
|
|
|
424
510
|
this.client.setOnHeartbeatCallback(() => {
|
|
425
|
-
this.
|
|
511
|
+
this.emit(EVENT_HEARTBEAT);
|
|
426
512
|
});
|
|
427
513
|
}
|
|
428
514
|
|
|
429
|
-
_sendAction(action: ActionTypes) {
|
|
515
|
+
private _sendAction(action: ActionTypes) {
|
|
430
516
|
this.softphoneActor.send({ type: action });
|
|
431
517
|
}
|
|
432
518
|
|
|
433
|
-
_getCall(sipCall: SipCall): Call | undefined {
|
|
519
|
+
private _getCall(sipCall: SipCall): Call | undefined {
|
|
434
520
|
return this.calls.find(call => call.id === sipCall.id);
|
|
435
521
|
}
|
|
436
522
|
|
|
437
|
-
_onCallEvent(sipCall: SipCall, eventName: string, ...args: any[]) {
|
|
523
|
+
private _onCallEvent(sipCall: SipCall, eventName: string, ...args: any[]) {
|
|
438
524
|
logger.warn('softphone - received call event', { eventName, sipId: sipCall.id });
|
|
439
525
|
|
|
440
526
|
const call = this._getCall(sipCall);
|
|
@@ -447,7 +533,7 @@ export class Softphone extends EventEmitter {
|
|
|
447
533
|
call[eventName](...args);
|
|
448
534
|
}
|
|
449
535
|
|
|
450
|
-
_onMessage(message: Message & { method?: string, body?: string }): void {
|
|
536
|
+
private _onMessage(message: Message & { method?: string, body?: string }): void {
|
|
451
537
|
if (!message || message.method !== 'MESSAGE') {
|
|
452
538
|
return;
|
|
453
539
|
}
|
|
@@ -467,12 +553,12 @@ export class Softphone extends EventEmitter {
|
|
|
467
553
|
|
|
468
554
|
switch (type) {
|
|
469
555
|
case MESSAGE_TYPE_CHAT:
|
|
470
|
-
this.
|
|
556
|
+
this.emit(EVENT_ON_CHAT, content);
|
|
471
557
|
break;
|
|
472
558
|
|
|
473
559
|
case MESSAGE_TYPE_SIGNAL:
|
|
474
560
|
{
|
|
475
|
-
this.
|
|
561
|
+
this.emit(EVENT_ON_SIGNAL, content);
|
|
476
562
|
break;
|
|
477
563
|
}
|
|
478
564
|
|
package/src/web-rtc-client.ts
CHANGED
|
@@ -31,7 +31,7 @@ import IssueReporter from './service/IssueReporter';
|
|
|
31
31
|
import Heartbeat from './utils/Heartbeat';
|
|
32
32
|
import { getSipSessionId, makeURI, replaceLocalIpModifier, SIP_ID_LENGTH } from './utils/sdp';
|
|
33
33
|
import type { MediaConfig, UserAgentConfigOverrides, WebRtcConfig, IncomingResponse, PeerConnection, WazoSession, WazoTransport } from './domain/types';
|
|
34
|
-
import { cleanupStream, createUaOptions, fetchNetworkStats, getAudioConstraints, getLocalStream, getLocalTracks, getLocalVideoStream, getMediaConfiguration, getPeerConnection, getRemoteStream, getRemoteTracks, getRemoteVideoStream, getRemoteVideoStreamFromPc,
|
|
34
|
+
import { cleanupStream, createUaOptions, fetchNetworkStats, getAudioConstraints, getLocalStream, getLocalTracks, getLocalVideoStream, getMediaConfiguration, getPeerConnection, getRemoteStream, getRemoteTracks, getRemoteVideoStream, getRemoteVideoStreamFromPc, getVideoConstraints, hasALocalVideoTrack, hasARemoteVideoTrack, hasAVideoTrack, hasLocalVideo, hasRemoteVideo, hasVideoTrack, isAudioOnly, isVideoRemotelyHeld, isWeb, sessionWantsToDoVideo, setLocalMediaStream, toggleAudio, toggleVideo, updateRemoteStream } from './utils/webrtc';
|
|
35
35
|
|
|
36
36
|
const SEND_STATS_DELAY = 5000;
|
|
37
37
|
const states = ['STATUS_NULL', 'STATUS_NEW', 'STATUS_CONNECTING', 'STATUS_CONNECTED', 'STATUS_COMPLETED'];
|