@wazo/sdk 0.43.6 → 1.0.0-alpha.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/dist/wazo-sdk.js +3 -3
- package/esm/__tests__/__voice__/call.test.js +72 -0
- package/esm/__tests__/__voice__/softphone.test.js +66 -0
- package/esm/__tests__/api-client.test.js +1 -20
- package/esm/api/auth.js +2 -13
- package/esm/checker/checks/webrtc.js +2 -1
- package/esm/domain/InvalidState.js +8 -0
- package/esm/domain/InvalidStateTransition.js +10 -0
- package/esm/domain/Phone/WebRTCPhone.js +33 -35
- package/esm/simple/Auth.js +12 -6
- package/esm/simple/Phone.js +2 -1
- package/esm/simple/__tests__/Auth.test.js +64 -8
- package/esm/simple/index.js +8 -2
- package/esm/simple/room/Room.js +2 -1
- package/esm/simple/room/SipRoom.js +2 -1
- package/esm/state-machine/call-state-machine.js +114 -0
- package/esm/state-machine/softphone-state-machine.js +39 -0
- package/esm/state-machine/utils.js +40 -0
- package/esm/types/__tests__/__voice__/call.test.d.ts +2 -0
- package/esm/types/__tests__/__voice__/call.test.d.ts.map +1 -0
- package/esm/types/__tests__/__voice__/softphone.test.d.ts +2 -0
- package/esm/types/__tests__/__voice__/softphone.test.d.ts.map +1 -0
- package/esm/types/api/auth.d.ts.map +1 -1
- package/esm/types/checker/checks/webrtc.d.ts.map +1 -1
- package/esm/types/domain/InvalidState.d.ts +6 -0
- package/esm/types/domain/InvalidState.d.ts.map +1 -0
- package/esm/types/domain/InvalidStateTransition.d.ts +7 -0
- package/esm/types/domain/InvalidStateTransition.d.ts.map +1 -0
- package/esm/types/domain/Phone/WebRTCPhone.d.ts +0 -1
- package/esm/types/domain/Phone/WebRTCPhone.d.ts.map +1 -1
- package/esm/types/domain/types.d.ts +4 -0
- package/esm/types/domain/types.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/simple/index.d.ts +7 -2
- package/esm/types/simple/index.d.ts.map +1 -1
- package/esm/types/simple/room/Room.d.ts.map +1 -1
- package/esm/types/simple/room/SipRoom.d.ts +1 -1
- package/esm/types/simple/room/SipRoom.d.ts.map +1 -1
- package/esm/types/state-machine/call-state-machine.d.ts +43 -0
- package/esm/types/state-machine/call-state-machine.d.ts.map +1 -0
- package/esm/types/state-machine/softphone-state-machine.d.ts +19 -0
- package/esm/types/state-machine/softphone-state-machine.d.ts.map +1 -0
- package/esm/types/state-machine/utils.d.ts +9 -0
- package/esm/types/state-machine/utils.d.ts.map +1 -0
- package/esm/types/utils/api-requester.d.ts.map +1 -1
- package/esm/types/utils/sdp.d.ts +6 -0
- package/esm/types/utils/sdp.d.ts.map +1 -1
- package/esm/types/utils/sip-logger.d.ts +4 -0
- package/esm/types/utils/sip-logger.d.ts.map +1 -0
- package/esm/types/utils/webrtc.d.ts +40 -0
- package/esm/types/utils/webrtc.d.ts.map +1 -1
- package/esm/types/voice/call.d.ts +110 -0
- package/esm/types/voice/call.d.ts.map +1 -0
- package/esm/types/voice/softphone.d.ts +77 -0
- package/esm/types/voice/softphone.d.ts.map +1 -0
- package/esm/types/web-rtc-client.d.ts +1 -36
- package/esm/types/web-rtc-client.d.ts.map +1 -1
- package/esm/utils/__tests__/api-requester.test.js +0 -17
- package/esm/utils/api-requester.js +1 -9
- package/esm/utils/sdp.js +26 -0
- package/esm/utils/sip-logger.js +32 -0
- package/esm/utils/webrtc.js +465 -0
- package/esm/voice/call.js +527 -0
- package/esm/voice/softphone.js +347 -0
- package/esm/web-rtc-client.js +86 -584
- package/lib/__tests__/__voice__/call.test.js +86 -0
- package/lib/__tests__/__voice__/softphone.test.js +80 -0
- package/lib/__tests__/api-client.test.js +1 -20
- package/lib/api/auth.js +2 -13
- package/lib/checker/checks/webrtc.js +2 -1
- package/lib/domain/InvalidState.js +9 -0
- package/lib/domain/InvalidStateTransition.js +10 -0
- package/lib/domain/Phone/WebRTCPhone.js +33 -35
- package/lib/simple/Auth.js +12 -6
- package/lib/simple/Phone.js +2 -1
- package/lib/simple/__tests__/Auth.test.js +64 -11
- package/lib/simple/index.js +6 -0
- package/lib/simple/room/Room.js +4 -3
- package/lib/simple/room/SipRoom.js +2 -1
- package/lib/state-machine/call-state-machine.js +120 -0
- package/lib/state-machine/softphone-state-machine.js +42 -0
- package/lib/state-machine/utils.js +52 -0
- package/lib/types/__tests__/__voice__/call.test.d.ts +2 -0
- package/lib/types/__tests__/__voice__/call.test.d.ts.map +1 -0
- package/lib/types/__tests__/__voice__/softphone.test.d.ts +2 -0
- package/lib/types/__tests__/__voice__/softphone.test.d.ts.map +1 -0
- package/lib/types/api/auth.d.ts.map +1 -1
- package/lib/types/checker/checks/webrtc.d.ts.map +1 -1
- package/lib/types/domain/InvalidState.d.ts +6 -0
- package/lib/types/domain/InvalidState.d.ts.map +1 -0
- package/lib/types/domain/InvalidStateTransition.d.ts +7 -0
- package/lib/types/domain/InvalidStateTransition.d.ts.map +1 -0
- package/lib/types/domain/Phone/WebRTCPhone.d.ts +0 -1
- package/lib/types/domain/Phone/WebRTCPhone.d.ts.map +1 -1
- package/lib/types/domain/types.d.ts +4 -0
- package/lib/types/domain/types.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/simple/index.d.ts +7 -2
- package/lib/types/simple/index.d.ts.map +1 -1
- package/lib/types/simple/room/Room.d.ts.map +1 -1
- package/lib/types/simple/room/SipRoom.d.ts +1 -1
- package/lib/types/simple/room/SipRoom.d.ts.map +1 -1
- package/lib/types/state-machine/call-state-machine.d.ts +43 -0
- package/lib/types/state-machine/call-state-machine.d.ts.map +1 -0
- package/lib/types/state-machine/softphone-state-machine.d.ts +19 -0
- package/lib/types/state-machine/softphone-state-machine.d.ts.map +1 -0
- package/lib/types/state-machine/utils.d.ts +9 -0
- package/lib/types/state-machine/utils.d.ts.map +1 -0
- package/lib/types/utils/api-requester.d.ts.map +1 -1
- package/lib/types/utils/sdp.d.ts +6 -0
- package/lib/types/utils/sdp.d.ts.map +1 -1
- package/lib/types/utils/sip-logger.d.ts +4 -0
- package/lib/types/utils/sip-logger.d.ts.map +1 -0
- package/lib/types/utils/webrtc.d.ts +40 -0
- package/lib/types/utils/webrtc.d.ts.map +1 -1
- package/lib/types/voice/call.d.ts +110 -0
- package/lib/types/voice/call.d.ts.map +1 -0
- package/lib/types/voice/softphone.d.ts +77 -0
- package/lib/types/voice/softphone.d.ts.map +1 -0
- package/lib/types/web-rtc-client.d.ts +1 -36
- package/lib/types/web-rtc-client.d.ts.map +1 -1
- package/lib/utils/__tests__/api-requester.test.js +0 -17
- package/lib/utils/api-requester.js +1 -9
- package/lib/utils/sdp.js +27 -1
- package/lib/utils/sip-logger.js +37 -0
- package/lib/utils/webrtc.js +521 -1
- package/lib/voice/call.js +577 -0
- package/lib/voice/softphone.js +375 -0
- package/lib/web-rtc-client.js +89 -601
- package/package.json +3 -2
- package/src/__tests__/__voice__/call.test.ts +90 -0
- package/src/__tests__/__voice__/softphone.test.ts +82 -0
- package/src/__tests__/api-client.test.ts +1 -22
- package/src/api/auth.ts +3 -25
- package/src/checker/checks/webrtc.ts +2 -1
- package/src/domain/InvalidState.ts +13 -0
- package/src/domain/InvalidStateTransition.ts +17 -0
- package/src/domain/Phone/WebRTCPhone.ts +34 -36
- package/src/domain/types.ts +6 -1
- package/src/simple/Auth.ts +13 -6
- package/src/simple/Phone.ts +2 -1
- package/src/simple/__tests__/Auth.test.ts +87 -9
- package/src/simple/index.ts +9 -2
- package/src/simple/room/Room.ts +2 -1
- package/src/simple/room/SipRoom.ts +2 -1
- package/src/state-machine/call-state-machine.ts +131 -0
- package/src/state-machine/softphone-state-machine.ts +49 -0
- package/src/state-machine/utils.ts +53 -0
- package/src/utils/__tests__/api-requester.test.ts +0 -20
- package/src/utils/api-requester.ts +1 -9
- package/src/utils/sdp.ts +36 -0
- package/src/utils/sip-logger.ts +40 -0
- package/src/utils/webrtc.ts +571 -0
- package/src/voice/call.ts +713 -0
- package/src/voice/softphone.ts +485 -0
- package/src/web-rtc-client.ts +90 -690
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
/* eslint-disable no-underscore-dangle */
|
|
2
|
+
import { EventEmitter } from 'events';
|
|
3
|
+
import { createActor } from 'xstate';
|
|
4
|
+
import { Invitation } from 'sip.js/lib/api/invitation';
|
|
5
|
+
import { SessionState } from 'sip.js/lib/api/session-state';
|
|
6
|
+
import callStateMachine, { Actions, EstablishedActions, States, EstablishedStates } from '../state-machine/call-state-machine';
|
|
7
|
+
import IssueReporter from '../service/IssueReporter';
|
|
8
|
+
import { getSipSessionId } from '../utils/sdp';
|
|
9
|
+
import { assertCan, assertState, getState } from '../state-machine/utils';
|
|
10
|
+
import { downgradeToAudio, getLocalStream, getLocalVideoStream, getPeerConnection, getRemoteStream, getRemoteVideoStream, getStreamFromConstraints, getWebRtcStats, hasALocalVideoTrack, hasLocalVideo, hasRemoteVideo, isVideoRemotelyHeld, setLocalMediaStream, toggleVideo, upgradeToVideo } from '../utils/webrtc';
|
|
11
|
+
const logger = IssueReporter.loggerFor('call');
|
|
12
|
+
// Events
|
|
13
|
+
export const EVENT_CALL_ACCEPTED = 'accepted';
|
|
14
|
+
export const EVENT_CALL_CANCELED = 'canceled';
|
|
15
|
+
export const EVENT_PROGRESS = 'progress';
|
|
16
|
+
export const EVENT_EARLY_MEDIA = 'earlyMedia';
|
|
17
|
+
export const EVENT_CALL_ERROR = 'error';
|
|
18
|
+
export const EVENT_CALL_REJECTED = 'rejected';
|
|
19
|
+
export const EVENT_CALL_TERMINATING = 'terminating';
|
|
20
|
+
export const EVENT_CALL_ENDED = 'ended';
|
|
21
|
+
export const EVENT_CALL_MUTED = 'muted';
|
|
22
|
+
export const EVENT_CALL_UN_MUTED = 'unMuted';
|
|
23
|
+
export const EVENT_CALL_HELD = 'held';
|
|
24
|
+
export const EVENT_CALL_RESUMED = 'resumed';
|
|
25
|
+
export const EVENT_REINVITE = 'reinvite';
|
|
26
|
+
export const EVENT_TRACK = 'track';
|
|
27
|
+
export const EVENT_AUDIO_STREAM = 'onAudioStream';
|
|
28
|
+
export const EVENT_VIDEO_STREAM = 'onVideoStream';
|
|
29
|
+
export const EVENT_REMOVE_STREAM = 'onRemoveStream';
|
|
30
|
+
export const EVENT_SHARE_SCREEN_ENDING = 'onScreenShareEnding';
|
|
31
|
+
export const EVENT_SHARE_SCREEN_STARTED = 'onScreenShareStarted';
|
|
32
|
+
export const EVENT_SHARE_SCREEN_ENDED = 'onScreenShareEnded';
|
|
33
|
+
export const EVENT_WEB_RTC_STATS = 'webRtcStats';
|
|
34
|
+
export const EVENT_CAMERA_RESUMED = 'onCameraResumed';
|
|
35
|
+
export const EVENT_CAMERA_DISABLED = 'onCameraDisabled';
|
|
36
|
+
export const EVENT_TERMINATE_SOUND = 'terminateSound';
|
|
37
|
+
export const MESSAGE_TYPE_CHAT = 'message/TYPE_CHAT';
|
|
38
|
+
export const MESSAGE_TYPE_SIGNAL = 'message/TYPE_SIGNAL';
|
|
39
|
+
export const MESSAGE_TRACK_UPDATED = 'onTrackUpdated';
|
|
40
|
+
class Call extends EventEmitter {
|
|
41
|
+
eventEmitter;
|
|
42
|
+
callActor;
|
|
43
|
+
// Wazo call id, used in Wazo Websocket and API
|
|
44
|
+
apiId;
|
|
45
|
+
sipCall;
|
|
46
|
+
talkingToIds;
|
|
47
|
+
creationTime;
|
|
48
|
+
answerTime;
|
|
49
|
+
endTime;
|
|
50
|
+
phone;
|
|
51
|
+
shouldSendReinvite = false;
|
|
52
|
+
currentScreenShare;
|
|
53
|
+
constructor(sipCall, phone) {
|
|
54
|
+
super();
|
|
55
|
+
this.callActor = createActor(callStateMachine);
|
|
56
|
+
this.callActor.start();
|
|
57
|
+
this.creationTime = new Date();
|
|
58
|
+
this.phone = phone;
|
|
59
|
+
this.sipCall = sipCall;
|
|
60
|
+
this._bindEvents();
|
|
61
|
+
}
|
|
62
|
+
async accept(options = {}) {
|
|
63
|
+
assertCan(this.callActor, Actions.ACCEPT);
|
|
64
|
+
logger.info('call - accept', { id: this.id, ...options });
|
|
65
|
+
this._sendAction(Actions.ACCEPT);
|
|
66
|
+
this.phone.holdAllCalls(this);
|
|
67
|
+
this.shouldSendReinvite = false;
|
|
68
|
+
this.answerTime = new Date();
|
|
69
|
+
return this.phone.client.answer(this.sipCall, options.withCamera).then(() => {
|
|
70
|
+
logger.info('call - accepted', { id: this.id, ...options });
|
|
71
|
+
return this.id;
|
|
72
|
+
}).catch(e => {
|
|
73
|
+
logger.error(e);
|
|
74
|
+
this.hangup();
|
|
75
|
+
throw e;
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
async reject() {
|
|
79
|
+
assertCan(this.callActor, Actions.REJECT);
|
|
80
|
+
logger.info('call - reject', { id: this.id });
|
|
81
|
+
this.eventEmitter.emit(EVENT_TERMINATE_SOUND, 'call rejected locally');
|
|
82
|
+
this.shouldSendReinvite = false;
|
|
83
|
+
return this.hangup();
|
|
84
|
+
}
|
|
85
|
+
async hangup() {
|
|
86
|
+
assertCan(this.callActor, Actions.HANGUP);
|
|
87
|
+
logger.info('call - hangup', { id: this.id });
|
|
88
|
+
await this.phone.client.hangup(this.sipCall);
|
|
89
|
+
// Remove this call from softphone's calls
|
|
90
|
+
this.phone.removeCall(this);
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
forceCancel() {
|
|
94
|
+
try {
|
|
95
|
+
// @ts-ignore: private
|
|
96
|
+
this.sipCall.outgoingInviteRequest.cancel();
|
|
97
|
+
}
|
|
98
|
+
catch (e) {
|
|
99
|
+
logger.info('call - force cancel, error', e);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
startConference(withCalls) {
|
|
103
|
+
// @TODO
|
|
104
|
+
// const participants = [this, ...withCalls].reduce((acc: Record<string, any>, call: Call) => {
|
|
105
|
+
// acc[call.talkingToIds[0]] = call;
|
|
106
|
+
// return acc;
|
|
107
|
+
// }, {});
|
|
108
|
+
// const adHocConference = new AdHocAPIConference({
|
|
109
|
+
// phone: this.phone,
|
|
110
|
+
// host,
|
|
111
|
+
// participants,
|
|
112
|
+
// });
|
|
113
|
+
// return adHocConference.start();
|
|
114
|
+
}
|
|
115
|
+
mute() {
|
|
116
|
+
assertCan(this.callActor, EstablishedActions.MUTE);
|
|
117
|
+
logger.info('call - mute', { id: this.id });
|
|
118
|
+
this._sendAction(EstablishedActions.MUTE);
|
|
119
|
+
this.phone.client.mute(this.sipCall);
|
|
120
|
+
this.eventEmitter.emit(EVENT_CALL_MUTED);
|
|
121
|
+
}
|
|
122
|
+
unMute() {
|
|
123
|
+
assertCan(this.callActor, EstablishedActions.UN_MUTE);
|
|
124
|
+
logger.info('call - un-mute', { id: this.id });
|
|
125
|
+
this._sendAction(EstablishedActions.UN_MUTE);
|
|
126
|
+
this.phone.client.unmute(this.sipCall);
|
|
127
|
+
this.eventEmitter.emit(EVENT_CALL_UN_MUTED);
|
|
128
|
+
}
|
|
129
|
+
async hold() {
|
|
130
|
+
assertCan(this.callActor, EstablishedActions.HOLD);
|
|
131
|
+
logger.info('call - hold', { id: this.id });
|
|
132
|
+
this._sendAction(EstablishedActions.HOLD);
|
|
133
|
+
const hasVideo = hasLocalVideo(this.sipCall);
|
|
134
|
+
// Stop screenshare if needed
|
|
135
|
+
if (this.currentScreenShare && this.currentScreenShare.sipSessionId === this.id) {
|
|
136
|
+
await this.stopScreenSharing(false);
|
|
137
|
+
}
|
|
138
|
+
// Downgrade to audio if needed
|
|
139
|
+
if (hasVideo) {
|
|
140
|
+
await downgradeToAudio(this.sipCall);
|
|
141
|
+
}
|
|
142
|
+
const promise = this.phone.client.hold(this.sipCall, this.isConference(), hasVideo);
|
|
143
|
+
this.eventEmitter.emit(EVENT_CALL_HELD);
|
|
144
|
+
return promise;
|
|
145
|
+
}
|
|
146
|
+
async resume() {
|
|
147
|
+
assertCan(this.callActor, EstablishedActions.RESUME);
|
|
148
|
+
this._sendAction(EstablishedActions.RESUME);
|
|
149
|
+
const isConference = this.isConference();
|
|
150
|
+
const hasVideo = hasLocalVideo(this.sipCall);
|
|
151
|
+
const wasScreensharing = this.currentScreenShare && this.currentScreenShare.sipSessionId === this.id;
|
|
152
|
+
const wasDesktop = this.currentScreenShare && this.currentScreenShare.desktop;
|
|
153
|
+
logger.info('call - resume', { id: this.id, isConference, hasVideo, wasScreensharing, wasDesktop });
|
|
154
|
+
const promise = this.phone.client.unhold(this.sipCall, isConference);
|
|
155
|
+
if (hasVideo) {
|
|
156
|
+
const constraints = {
|
|
157
|
+
audio: false,
|
|
158
|
+
video: true,
|
|
159
|
+
screen: wasScreensharing,
|
|
160
|
+
desktop: wasDesktop,
|
|
161
|
+
};
|
|
162
|
+
await upgradeToVideo(this.sipCall, constraints, isConference);
|
|
163
|
+
}
|
|
164
|
+
const onScreenSharing = (stream) => {
|
|
165
|
+
const hadVideo = this.currentScreenShare && this.currentScreenShare.hadVideo;
|
|
166
|
+
if (stream) {
|
|
167
|
+
this._onScreenSharing(stream, hadVideo, wasDesktop);
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
this.eventEmitter.emit(EVENT_CALL_RESUMED);
|
|
171
|
+
return promise.then(() => {
|
|
172
|
+
const stream = this.getLocalVideoStream();
|
|
173
|
+
if (wasScreensharing) {
|
|
174
|
+
onScreenSharing(this.getLocalVideoStream());
|
|
175
|
+
}
|
|
176
|
+
return stream;
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
turnCameraOn() {
|
|
180
|
+
assertState(this.callActor, States.ESTABLISHED);
|
|
181
|
+
logger.info('call - turn camera on', { id: this.id });
|
|
182
|
+
toggleVideo(this.sipCall, true);
|
|
183
|
+
this.eventEmitter.emit(EVENT_CAMERA_RESUMED);
|
|
184
|
+
}
|
|
185
|
+
turnCameraOff() {
|
|
186
|
+
assertState(this.callActor, States.ESTABLISHED);
|
|
187
|
+
logger.info('call - turn camera off', { id: this.id });
|
|
188
|
+
toggleVideo(this.sipCall, false);
|
|
189
|
+
this.eventEmitter.emit(EVENT_CAMERA_DISABLED);
|
|
190
|
+
}
|
|
191
|
+
isHeld() {
|
|
192
|
+
return getState(this.callActor) === EstablishedStates.HELD;
|
|
193
|
+
}
|
|
194
|
+
isEstablished() {
|
|
195
|
+
return getState(this.callActor) === States.ESTABLISHED;
|
|
196
|
+
}
|
|
197
|
+
isConference() {
|
|
198
|
+
return getPeerConnection(this.sipCall)?.sfu;
|
|
199
|
+
}
|
|
200
|
+
transfer(target) {
|
|
201
|
+
assertState(this.callActor, States.ESTABLISHED);
|
|
202
|
+
logger.info('call - transfer', { id: this.id, target });
|
|
203
|
+
this.phone.client.transfer(this.sipCall, target);
|
|
204
|
+
}
|
|
205
|
+
atxfer() {
|
|
206
|
+
assertState(this.callActor, States.ESTABLISHED);
|
|
207
|
+
logger.info('call - atxfer', { id: this.id });
|
|
208
|
+
return this.phone.client.atxfer(this.sipCall);
|
|
209
|
+
}
|
|
210
|
+
sendDigits(digits) {
|
|
211
|
+
assertState(this.callActor, States.ESTABLISHED);
|
|
212
|
+
logger.info('call - send digits', { id: this.id, digits });
|
|
213
|
+
this.phone.client.sendDTMF(this.sipCall, digits);
|
|
214
|
+
}
|
|
215
|
+
startNetworkMonitoring(interval = 1000) {
|
|
216
|
+
assertState(this.callActor, States.ESTABLISHED);
|
|
217
|
+
logger.info('call - start network monitoring', { id: this.id });
|
|
218
|
+
return this.phone.client.startNetworkMonitoring(this.sipCall, interval);
|
|
219
|
+
}
|
|
220
|
+
stopNetworkMonitoring() {
|
|
221
|
+
assertState(this.callActor, States.ESTABLISHED);
|
|
222
|
+
logger.info('call - stop network monitoring', { id: this.id });
|
|
223
|
+
return this.phone.client.stopNetworkMonitoring(this.sipCall);
|
|
224
|
+
}
|
|
225
|
+
async getStats() {
|
|
226
|
+
return getWebRtcStats(this.sipCall);
|
|
227
|
+
}
|
|
228
|
+
async sendReinvite(options = {}) {
|
|
229
|
+
assertState(this.callActor, States.ESTABLISHED);
|
|
230
|
+
logger.info('call - send reinvite', { id: this.id, ...options });
|
|
231
|
+
const shouldScreenShare = options.constraints?.screen;
|
|
232
|
+
const isUpgrade = shouldScreenShare || options.constraints?.video;
|
|
233
|
+
// Downgrade
|
|
234
|
+
if (options.constraints && !isUpgrade) {
|
|
235
|
+
// No reinvite needed
|
|
236
|
+
this._downgradeToAudio();
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (isUpgrade) {
|
|
240
|
+
const shouldReinvite = await this._upgradeToVideo(options);
|
|
241
|
+
if (!shouldReinvite) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return this.phone.client.reinvite(this.sipCall, options.constraints, options.conference, options.audioOnly, options.iceRestart);
|
|
246
|
+
}
|
|
247
|
+
sendMessage(body, contentType = 'text/plain') {
|
|
248
|
+
assertState(this.callActor, States.ESTABLISHED);
|
|
249
|
+
logger.info('call - send message', { id: this.id, body });
|
|
250
|
+
return this.phone.client.sendMessage(this.sipCall, body, contentType);
|
|
251
|
+
}
|
|
252
|
+
sendChat(content) {
|
|
253
|
+
assertState(this.callActor, States.ESTABLISHED);
|
|
254
|
+
logger.info('call - send chat', { id: this.id, content });
|
|
255
|
+
this.sendMessage(JSON.stringify({ type: MESSAGE_TYPE_CHAT, content }), 'application/json');
|
|
256
|
+
}
|
|
257
|
+
sendSignal(content) {
|
|
258
|
+
assertState(this.callActor, States.ESTABLISHED);
|
|
259
|
+
logger.info('call - send signal', { id: this.id, content });
|
|
260
|
+
this.sendMessage(JSON.stringify({ type: MESSAGE_TYPE_SIGNAL, content }), 'application/json');
|
|
261
|
+
}
|
|
262
|
+
async startScreenSharing(constraints) {
|
|
263
|
+
assertState(this.callActor, States.ESTABLISHED);
|
|
264
|
+
logger.info('call - start screen sharing', { constraints, id: this.id });
|
|
265
|
+
const screenShareStream = await getStreamFromConstraints(constraints);
|
|
266
|
+
if (!screenShareStream) {
|
|
267
|
+
throw new Error(`Can't create media stream for screensharing with: ${JSON.stringify(constraints)}`);
|
|
268
|
+
}
|
|
269
|
+
const screenTrack = screenShareStream.getVideoTracks()[0];
|
|
270
|
+
const pc = getPeerConnection(this.sipCall);
|
|
271
|
+
const sender = pc && pc.getSenders && pc.getSenders().find((s) => s && s.track && s.track.kind === 'video');
|
|
272
|
+
const localStream = getLocalStream(this.sipCall);
|
|
273
|
+
const videoTrack = localStream ? localStream.getTracks().find(track => track.kind === 'video') : null;
|
|
274
|
+
const hadVideo = !!videoTrack;
|
|
275
|
+
// Stop local video tracks
|
|
276
|
+
if (videoTrack && localStream) {
|
|
277
|
+
videoTrack.enabled = false;
|
|
278
|
+
videoTrack.stop();
|
|
279
|
+
localStream.removeTrack(videoTrack);
|
|
280
|
+
}
|
|
281
|
+
if (localStream) {
|
|
282
|
+
localStream.addTrack(screenTrack);
|
|
283
|
+
}
|
|
284
|
+
if (sender) {
|
|
285
|
+
sender.replaceTrack(screenTrack);
|
|
286
|
+
}
|
|
287
|
+
this._onScreenSharing(screenShareStream, hadVideo, constraints.desktop);
|
|
288
|
+
return screenShareStream;
|
|
289
|
+
}
|
|
290
|
+
async stopScreenSharing(restoreLocalStream = true) {
|
|
291
|
+
assertState(this.callActor, States.ESTABLISHED);
|
|
292
|
+
if (!this.currentScreenShare) {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
logger.info('call - stop screen sharing', { restoreLocalStream });
|
|
296
|
+
let reinvited = null;
|
|
297
|
+
try {
|
|
298
|
+
if (this.currentScreenShare.stream) {
|
|
299
|
+
this.currentScreenShare.stream.getTracks()
|
|
300
|
+
.filter((track) => track.enabled && track.kind === 'video')
|
|
301
|
+
.forEach((track) => track.stop());
|
|
302
|
+
}
|
|
303
|
+
if (restoreLocalStream) {
|
|
304
|
+
const conference = this.isConference();
|
|
305
|
+
// When stopping screenshare and we had video before that, we have to re-upgrade
|
|
306
|
+
// When upgrading directly to screenshare (eg: we don't have a videoLocalStream to replace)
|
|
307
|
+
// We have to downgrade to audio.
|
|
308
|
+
const screenshareStopped = this.currentScreenShare.hadVideo;
|
|
309
|
+
const constraints = {
|
|
310
|
+
audio: false,
|
|
311
|
+
video: screenshareStopped,
|
|
312
|
+
};
|
|
313
|
+
// We have to remove the video sender to be able to re-upgrade to video in `sendReinvite` below
|
|
314
|
+
this._downgradeToAudio(false);
|
|
315
|
+
// Wait for the track to be removed, unless the video sender won't have a null track in `_upgradeToVideo`.
|
|
316
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
317
|
+
reinvited = await this.sendReinvite({ constraints, conference });
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
catch (e) {
|
|
321
|
+
console.warn(e);
|
|
322
|
+
}
|
|
323
|
+
this.eventEmitter.emit(EVENT_SHARE_SCREEN_ENDED);
|
|
324
|
+
this.currentScreenShare = null;
|
|
325
|
+
return reinvited;
|
|
326
|
+
}
|
|
327
|
+
onScreenShareReinvite(response, desktop) {
|
|
328
|
+
const localStream = getLocalStream(this.sipCall);
|
|
329
|
+
logger.info('call - updrading directly in screensharing mode', { id: this.id, tracks: localStream?.getTracks() });
|
|
330
|
+
this._onScreenSharing(localStream, false, desktop);
|
|
331
|
+
}
|
|
332
|
+
onAccepted() {
|
|
333
|
+
this._sendAction(Actions.REMOTLY_ACCEPTED);
|
|
334
|
+
this.emit(EVENT_CALL_ACCEPTED);
|
|
335
|
+
}
|
|
336
|
+
onProgress() {
|
|
337
|
+
this.emit(EVENT_PROGRESS);
|
|
338
|
+
}
|
|
339
|
+
onEarlyMedia() {
|
|
340
|
+
this.emit(EVENT_EARLY_MEDIA);
|
|
341
|
+
}
|
|
342
|
+
onTrack(event) {
|
|
343
|
+
this.emit(EVENT_TRACK, event);
|
|
344
|
+
}
|
|
345
|
+
onError(error) {
|
|
346
|
+
this.emit(EVENT_CALL_ERROR, error);
|
|
347
|
+
}
|
|
348
|
+
onRejected() {
|
|
349
|
+
this.endTime = new Date();
|
|
350
|
+
this.emit(EVENT_CALL_REJECTED);
|
|
351
|
+
this.phone.removeCall(this);
|
|
352
|
+
}
|
|
353
|
+
onNetworkStats(stats, previousStats) {
|
|
354
|
+
this.emit(EVENT_WEB_RTC_STATS, stats, previousStats);
|
|
355
|
+
}
|
|
356
|
+
getRemoteStream() {
|
|
357
|
+
return getRemoteStream(this.sipCall);
|
|
358
|
+
}
|
|
359
|
+
getRemoteVideoStream() {
|
|
360
|
+
return getRemoteVideoStream(this.sipCall);
|
|
361
|
+
}
|
|
362
|
+
hasRemoteVideo() {
|
|
363
|
+
return hasRemoteVideo(this.sipCall);
|
|
364
|
+
}
|
|
365
|
+
hasLocalVideo() {
|
|
366
|
+
return hasLocalVideo(this.sipCall);
|
|
367
|
+
}
|
|
368
|
+
getLocalStream() {
|
|
369
|
+
return getLocalStream(this.sipCall);
|
|
370
|
+
}
|
|
371
|
+
getLocalVideoStream() {
|
|
372
|
+
return getLocalVideoStream(this.sipCall);
|
|
373
|
+
}
|
|
374
|
+
hasALocalVideoTrack() {
|
|
375
|
+
return hasALocalVideoTrack(this.sipCall);
|
|
376
|
+
}
|
|
377
|
+
isVideoRemotelyHeld() {
|
|
378
|
+
return isVideoRemotelyHeld(this.sipCall);
|
|
379
|
+
}
|
|
380
|
+
onReinvite(request, updatedCalleeName, updatedNumber, hadRemoteVideo) {
|
|
381
|
+
this.emit(EVENT_REINVITE, request, updatedCalleeName, updatedNumber, hadRemoteVideo);
|
|
382
|
+
}
|
|
383
|
+
get id() {
|
|
384
|
+
return getSipSessionId(this.sipCall);
|
|
385
|
+
}
|
|
386
|
+
get number() {
|
|
387
|
+
const identity = this.sipCall.remoteIdentity || this.sipCall.assertedIdentity;
|
|
388
|
+
// @ts-ignore: private
|
|
389
|
+
return identity ? identity.uri._normal.user : null;
|
|
390
|
+
}
|
|
391
|
+
_bindEvents() {
|
|
392
|
+
if (this.sipCall instanceof Invitation) {
|
|
393
|
+
// Monkey patch to know when canceled with the CANCEL message
|
|
394
|
+
const onCancel = this.sipCall._onCancel.bind(this.sipCall);
|
|
395
|
+
this.sipCall._onCancel = (message) => {
|
|
396
|
+
logger.trace('call - on sip session canceled', { callId: message.callId, id: this.id });
|
|
397
|
+
onCancel(message);
|
|
398
|
+
const elsewhere = message.data.indexOf('cause=26') !== -1 && message.data.indexOf('completed elsewhere') !== -1;
|
|
399
|
+
this.eventEmitter.emit(EVENT_CALL_CANCELED, elsewhere);
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
this.sipCall.stateChange.addListener((newState) => {
|
|
403
|
+
switch (newState) {
|
|
404
|
+
case SessionState.Terminating:
|
|
405
|
+
logger.info('call - terminating', { id: this.id });
|
|
406
|
+
this.eventEmitter.emit(EVENT_CALL_TERMINATING);
|
|
407
|
+
break;
|
|
408
|
+
case SessionState.Terminated:
|
|
409
|
+
{
|
|
410
|
+
logger.info('call - terminated', { id: this.id });
|
|
411
|
+
// Should be called before `_onCallTerminated` or the callCount will not decrement...
|
|
412
|
+
this.endTime = new Date();
|
|
413
|
+
return this.eventEmitter.emit(EVENT_CALL_ENDED);
|
|
414
|
+
}
|
|
415
|
+
default:
|
|
416
|
+
break;
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
if (!this.sipCall.sessionDescriptionHandler) {
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
// Video events
|
|
423
|
+
const sdh = this.sipCall.sessionDescriptionHandler;
|
|
424
|
+
const peerConnection = sdh.peerConnection;
|
|
425
|
+
if (!peerConnection) {
|
|
426
|
+
logger.warn('call - no peer connection', { id: this.id });
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
peerConnection.ontrack = (rawEvent) => {
|
|
430
|
+
const event = rawEvent;
|
|
431
|
+
const [stream] = event.streams;
|
|
432
|
+
const kind = event && event.track && event.track.kind;
|
|
433
|
+
logger.info('call - on track stream called on the peer connection', {
|
|
434
|
+
id: this.id,
|
|
435
|
+
streamId: stream ? stream.id : null,
|
|
436
|
+
tracks: stream ? stream.getTracks() : null,
|
|
437
|
+
kind,
|
|
438
|
+
});
|
|
439
|
+
if (kind === 'audio') {
|
|
440
|
+
return this.eventEmitter.emit(EVENT_AUDIO_STREAM, stream);
|
|
441
|
+
}
|
|
442
|
+
return this.eventEmitter.emit(EVENT_VIDEO_STREAM, stream, event.track.id, event);
|
|
443
|
+
};
|
|
444
|
+
peerConnection.onremovestream = (event) => {
|
|
445
|
+
logger.info('call - on remove stream called on the peer connection', {
|
|
446
|
+
id: event.stream.id,
|
|
447
|
+
tracks: event.stream.getTracks(),
|
|
448
|
+
});
|
|
449
|
+
this.eventEmitter.emit(EVENT_REMOVE_STREAM, event.stream);
|
|
450
|
+
};
|
|
451
|
+
// Client events
|
|
452
|
+
}
|
|
453
|
+
_downgradeToAudio(withMessage = true) {
|
|
454
|
+
assertState(this.callActor, States.ESTABLISHED);
|
|
455
|
+
logger.info('call - downgrade to audio', { id: this.id, withMessage });
|
|
456
|
+
downgradeToAudio(this.sipCall);
|
|
457
|
+
if (withMessage) {
|
|
458
|
+
this._sendReinviteMessage(false);
|
|
459
|
+
}
|
|
460
|
+
return true;
|
|
461
|
+
}
|
|
462
|
+
// Returns true if we need to send a re-INVITE request
|
|
463
|
+
async _upgradeToVideo(options = {}) {
|
|
464
|
+
assertState(this.callActor, States.ESTABLISHED);
|
|
465
|
+
logger.info('call - upgrade to video', { id: this.id, ...options });
|
|
466
|
+
const shouldScreenShare = options.constraints?.screen;
|
|
467
|
+
const desktop = options.constraints?.desktop;
|
|
468
|
+
const sdhOptions = this.sipCall.sessionDescriptionHandlerOptionsReInvite;
|
|
469
|
+
const wasAudioOnly = sdhOptions?.audioOnly;
|
|
470
|
+
const newStream = await upgradeToVideo(this.sipCall, options.constraints || {}, !!options.conference);
|
|
471
|
+
// If no stream is returned, it means we have to reinvite
|
|
472
|
+
if (!newStream) {
|
|
473
|
+
return true;
|
|
474
|
+
}
|
|
475
|
+
this._sendReinviteMessage(true);
|
|
476
|
+
if (shouldScreenShare) {
|
|
477
|
+
this._onScreenSharing(newStream, false, desktop);
|
|
478
|
+
}
|
|
479
|
+
// We have to reinvite to change the direction on the bundle when upgrading from an audioOnly conference
|
|
480
|
+
if (options.conference && wasAudioOnly) {
|
|
481
|
+
return true;
|
|
482
|
+
}
|
|
483
|
+
// No reinvite needed here
|
|
484
|
+
return false;
|
|
485
|
+
}
|
|
486
|
+
_sendReinviteMessage(isUpgrade) {
|
|
487
|
+
assertState(this.callActor, States.ESTABLISHED);
|
|
488
|
+
logger.info('call - sending reinvite message', { id: this.id, isUpgrade });
|
|
489
|
+
// Have to send the message after a delay due to latency to update the remote peer
|
|
490
|
+
setTimeout(() => {
|
|
491
|
+
this.sendMessage(JSON.stringify({
|
|
492
|
+
type: MESSAGE_TYPE_SIGNAL,
|
|
493
|
+
content: {
|
|
494
|
+
type: MESSAGE_TRACK_UPDATED,
|
|
495
|
+
update: isUpgrade ? 'upgrade' : 'downgrade',
|
|
496
|
+
sipCallId: this.id,
|
|
497
|
+
callId: this.apiId,
|
|
498
|
+
number: this.number,
|
|
499
|
+
},
|
|
500
|
+
}));
|
|
501
|
+
}, 2500);
|
|
502
|
+
}
|
|
503
|
+
_onScreenSharing(screenStream, hadVideo, desktop) {
|
|
504
|
+
const screenTrack = screenStream.getVideoTracks()[0];
|
|
505
|
+
const pc = this.phone.client.getPeerConnection(this.id);
|
|
506
|
+
const sender = pc && pc.getSenders && pc.getSenders().find((s) => s && s.track && s.track.kind === 'video');
|
|
507
|
+
logger.info('call - on screensharing', { hadVideo, id: this.id, screenTrack });
|
|
508
|
+
if (screenTrack) {
|
|
509
|
+
screenTrack.onended = () => {
|
|
510
|
+
logger.info('call - on screenshare ended', { hadVideo, id: this.id, screenTrack });
|
|
511
|
+
this.eventEmitter.emit(EVENT_SHARE_SCREEN_ENDING);
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
this.currentScreenShare = {
|
|
515
|
+
stream: screenStream,
|
|
516
|
+
hadVideo,
|
|
517
|
+
sender,
|
|
518
|
+
desktop,
|
|
519
|
+
};
|
|
520
|
+
setLocalMediaStream(this.sipCall, screenStream);
|
|
521
|
+
this.eventEmitter.emit(EVENT_SHARE_SCREEN_STARTED, screenStream);
|
|
522
|
+
}
|
|
523
|
+
_sendAction(action) {
|
|
524
|
+
this.callActor.send({ type: action });
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
export default Call;
|