@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
package/esm/utils/webrtc.js
CHANGED
|
@@ -1,3 +1,25 @@
|
|
|
1
|
+
import { defaultPeerConnectionConfiguration } from 'sip.js/lib/platform/web/session-description-handler/peer-connection-configuration-default';
|
|
2
|
+
import { stripVideo } from 'sip.js/lib/platform/web';
|
|
3
|
+
import WazoSessionDescriptionHandler, { wazoMediaStreamFactory } from '../lib/WazoSessionDescriptionHandler';
|
|
4
|
+
import { getVideoDirection, hasAnActiveVideo, makeURI, replaceLocalIpModifier } from './sdp';
|
|
5
|
+
import IssueReporter from '../service/IssueReporter';
|
|
6
|
+
import { lastIndexOf } from './array';
|
|
7
|
+
const logger = IssueReporter.loggerFor('webrtc');
|
|
8
|
+
// setting a 24hr timeout and letting the backend define the actual value
|
|
9
|
+
const NO_ANSWER_TIMEOUT = 60 * 60 * 24; // in seconds
|
|
10
|
+
const DEFAULT_ICE_TIMEOUT = 3000;
|
|
11
|
+
const privateIpRegex = /^(?:10|127|172\.(?:1[6-9]|2[0-9]|3[01])|192\.168)\..*/;
|
|
12
|
+
export const isWeb = () => typeof window === 'object' && typeof document === 'object';
|
|
13
|
+
export const isFirefox = () => isWeb() && navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
|
|
14
|
+
export const isAPrivateIp = (ip) => privateIpRegex.exec(ip) == null;
|
|
15
|
+
export const getIceServers = (ip) => {
|
|
16
|
+
if (isAPrivateIp(ip)) {
|
|
17
|
+
return [{
|
|
18
|
+
urls: ['stun:stun.l.google.com:19302', 'stun:stun4.l.google.com:19302'],
|
|
19
|
+
}];
|
|
20
|
+
}
|
|
21
|
+
return [];
|
|
22
|
+
};
|
|
1
23
|
export const parseCandidate = (line) => {
|
|
2
24
|
let parts;
|
|
3
25
|
// Parse both variants.
|
|
@@ -35,3 +57,446 @@ export const parseCandidate = (line) => {
|
|
|
35
57
|
}
|
|
36
58
|
return candidate;
|
|
37
59
|
};
|
|
60
|
+
export const getPeerConnection = (sipCall) => {
|
|
61
|
+
if (!sipCall) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
return sipCall.sessionDescriptionHandler ? sipCall.sessionDescriptionHandler.peerConnection : null;
|
|
65
|
+
};
|
|
66
|
+
export const getLocalStream = (sipCall) => {
|
|
67
|
+
return sipCall?.sessionDescriptionHandler?.localMediaStream || null;
|
|
68
|
+
};
|
|
69
|
+
export const getLocalTracks = (sipCall) => {
|
|
70
|
+
const localStream = getLocalStream(sipCall);
|
|
71
|
+
if (!localStream) {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
return localStream.getTracks();
|
|
75
|
+
};
|
|
76
|
+
export const getRemoteStream = (sipCall) => {
|
|
77
|
+
return sipCall?.sessionDescriptionHandler?.remoteMediaStream || null;
|
|
78
|
+
};
|
|
79
|
+
export const isVideoTrack = (track) => track.kind === 'video' && track.readyState === 'live';
|
|
80
|
+
export const hasLocalVideo = (sipCall) => getLocalTracks(sipCall).some(isVideoTrack);
|
|
81
|
+
export const hasALocalVideoTrack = (sipCall) => getLocalTracks(sipCall).some(track => track.kind === 'video');
|
|
82
|
+
export const getLocalVideoStream = (sipCall) => (hasLocalVideo(sipCall) ? getLocalStream(sipCall) : null);
|
|
83
|
+
export const getRemoteTracks = (sipCall) => {
|
|
84
|
+
const remoteStream = getRemoteStream(sipCall);
|
|
85
|
+
if (!remoteStream) {
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
return remoteStream.getTracks();
|
|
89
|
+
};
|
|
90
|
+
export const isVideoRemotelyHeld = (sipCall) => {
|
|
91
|
+
const pc = getPeerConnection(sipCall);
|
|
92
|
+
const sdp = pc && pc.remoteDescription ? pc.remoteDescription.sdp : null;
|
|
93
|
+
if (!sdp) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
const videoDirection = getVideoDirection(sdp);
|
|
97
|
+
return videoDirection === 'sendonly';
|
|
98
|
+
};
|
|
99
|
+
export const hasRemoteVideo = (sipCall) => getRemoteTracks(sipCall).some(isVideoTrack);
|
|
100
|
+
export const hasARemoteVideoTrack = (sipCall) => getRemoteTracks(sipCall).some(track => track.kind === 'video');
|
|
101
|
+
export const getRemoteVideoStream = (sipCall) => {
|
|
102
|
+
if (isVideoRemotelyHeld(sipCall)) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
return hasRemoteVideo(sipCall) ? getRemoteStream(sipCall) : null;
|
|
106
|
+
};
|
|
107
|
+
export const getRemoteVideoStreamFromPc = (sipCall) => {
|
|
108
|
+
const pc = getPeerConnection(sipCall);
|
|
109
|
+
if (!pc) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
return pc.getRemoteStreams().find((stream) => !!stream.getVideoTracks().length);
|
|
113
|
+
};
|
|
114
|
+
export const hasVideoTrack = (sipCall) => hasLocalVideo(sipCall) || hasRemoteVideo(sipCall);
|
|
115
|
+
export const hasAVideoTrack = (sipCall) => hasALocalVideoTrack(sipCall) || hasARemoteVideoTrack(sipCall);
|
|
116
|
+
// eslint-disable-next-line no-unused-vars
|
|
117
|
+
export const sessionWantsToDoVideo = (sipCall) => {
|
|
118
|
+
if (!sipCall) {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
const { body, } = sipCall.request || sipCall;
|
|
122
|
+
// Sometimes with InviteClientContext the body is in the body attribute ...
|
|
123
|
+
const sdp = typeof body === 'object' && body ? body.body : body;
|
|
124
|
+
return hasAnActiveVideo(sdp);
|
|
125
|
+
};
|
|
126
|
+
export const getWebRtcStats = async (sipCall) => {
|
|
127
|
+
const pc = getPeerConnection(sipCall);
|
|
128
|
+
if (!pc) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
return pc.getStats(null);
|
|
132
|
+
};
|
|
133
|
+
export const toggleAudio = (sipCall, muteAudio) => {
|
|
134
|
+
const sdh = sipCall.sessionDescriptionHandler;
|
|
135
|
+
const pc = (sdh?.peerConnection || null);
|
|
136
|
+
if (!pc) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (pc.getSenders) {
|
|
140
|
+
pc.getSenders().forEach((sender) => {
|
|
141
|
+
if (sender && sender.track && sender.track.kind === 'audio') {
|
|
142
|
+
// eslint-disable-next-line
|
|
143
|
+
sender.track.enabled = !muteAudio;
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
pc.getLocalStreams().forEach((stream) => {
|
|
149
|
+
stream.getAudioTracks().forEach(track => {
|
|
150
|
+
// eslint-disable-next-line
|
|
151
|
+
track.enabled = !muteAudio;
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
export const toggleVideo = (sipCall, muteCamera) => {
|
|
157
|
+
const sdh = sipCall.sessionDescriptionHandler;
|
|
158
|
+
const pc = sdh?.peerConnection;
|
|
159
|
+
if (pc?.getSenders) {
|
|
160
|
+
pc.getSenders().forEach((sender) => {
|
|
161
|
+
if (sender && sender.track && sender.track.kind === 'video') {
|
|
162
|
+
// eslint-disable-next-line
|
|
163
|
+
sender.track.enabled = !muteCamera;
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
pc.getLocalStreams().forEach((stream) => {
|
|
169
|
+
stream.getVideoTracks().forEach(track => {
|
|
170
|
+
// eslint-disable-next-line
|
|
171
|
+
track.enabled = !muteCamera;
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
export const cleanupStream = (stream) => {
|
|
177
|
+
stream.getTracks().filter(track => track.enabled).forEach(track => track.stop());
|
|
178
|
+
};
|
|
179
|
+
export const setLocalMediaStream = (sipCall, newStream) => {
|
|
180
|
+
logger.info('setting local media stream', {
|
|
181
|
+
id: sipCall.id,
|
|
182
|
+
tracks: newStream ? newStream.getTracks() : null,
|
|
183
|
+
});
|
|
184
|
+
if (sipCall.sessionDescriptionHandler) {
|
|
185
|
+
// eslint-disable-next-line no-underscore-dangle
|
|
186
|
+
// @ts-ignore: protected
|
|
187
|
+
sipSession.sessionDescriptionHandler._localMediaStream = newStream;
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
export const updateRemoteStream = (sipCall, allTracks = true) => {
|
|
191
|
+
const remoteStream = getRemoteStream(sipCall);
|
|
192
|
+
const pc = getPeerConnection(sipCall);
|
|
193
|
+
logger.info('Updating remote stream', {
|
|
194
|
+
id: sipCall.id,
|
|
195
|
+
tracks: remoteStream ? remoteStream.getTracks() : null,
|
|
196
|
+
receiverTracks: pc && pc.getReceivers ? pc.getReceivers().map((receiver) => receiver.track) : null,
|
|
197
|
+
});
|
|
198
|
+
if (!pc || !remoteStream) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
remoteStream.getTracks().forEach(track => {
|
|
202
|
+
if (allTracks || track.kind === 'video') {
|
|
203
|
+
remoteStream.removeTrack(track);
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
if (pc.getReceivers) {
|
|
207
|
+
pc.getReceivers().forEach((receiver) => {
|
|
208
|
+
if (allTracks || receiver.track.kind === 'video') {
|
|
209
|
+
remoteStream.addTrack(receiver.track);
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
export const isAudioOnly = (sipCall) => {
|
|
215
|
+
return Boolean(sipCall.sessionDescriptionHandlerModifiersReInvite.find(modifier => modifier === stripVideo));
|
|
216
|
+
};
|
|
217
|
+
// @ts-ignore: media constraints
|
|
218
|
+
export const getAudioConstraints = (audio) => (audio?.deviceId?.exact ? audio : true);
|
|
219
|
+
export const getVideoConstraints = (video = false) => {
|
|
220
|
+
if (!video) {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
// @ts-ignore: media constraints
|
|
224
|
+
return this.video?.deviceId?.exact ? this.video : true;
|
|
225
|
+
};
|
|
226
|
+
// eslint-disable-next-line no-unused-vars
|
|
227
|
+
const getRtcOptions = () => {
|
|
228
|
+
return {
|
|
229
|
+
mandatory: {
|
|
230
|
+
OfferToReceiveAudio: true,
|
|
231
|
+
OfferToReceiveVideo: false,
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
};
|
|
235
|
+
export const getMediaConfiguration = (enableVideo, conference = false, constraints = null) => {
|
|
236
|
+
const screenSharing = constraints && 'screen' in constraints ? constraints.screen : false;
|
|
237
|
+
const isDesktop = constraints && 'desktop' in constraints ? constraints.desktop : false;
|
|
238
|
+
const withAudio = constraints && 'audio' in constraints ? constraints.audio : true;
|
|
239
|
+
const mandatoryVideo = constraints && typeof constraints.video === 'object' ? constraints.video.mandatory : {};
|
|
240
|
+
logger.info('Retrieving media a configuration', { enableVideo, screenSharing, isDesktop, withAudio, constraints });
|
|
241
|
+
return {
|
|
242
|
+
constraints: {
|
|
243
|
+
// Exact constraint are not supported with `getDisplayMedia` and we must have a video=false in desktop screenshare
|
|
244
|
+
audio: screenSharing ? !isDesktop : constraints?.audio,
|
|
245
|
+
video: screenSharing ? isDesktop ? {
|
|
246
|
+
mandatory: {
|
|
247
|
+
chromeMediaSource: 'desktop',
|
|
248
|
+
...(mandatoryVideo || {}),
|
|
249
|
+
},
|
|
250
|
+
} : {
|
|
251
|
+
cursor: 'always',
|
|
252
|
+
} : getVideoConstraints(enableVideo),
|
|
253
|
+
screen: screenSharing,
|
|
254
|
+
desktop: isDesktop,
|
|
255
|
+
conference,
|
|
256
|
+
},
|
|
257
|
+
enableVideo,
|
|
258
|
+
conference,
|
|
259
|
+
offerOptions: {
|
|
260
|
+
OfferToReceiveAudio: true,
|
|
261
|
+
OfferToReceiveVideo: enableVideo,
|
|
262
|
+
mandatory: {
|
|
263
|
+
OfferToReceiveAudio: true,
|
|
264
|
+
OfferToReceiveVideo: enableVideo,
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
};
|
|
269
|
+
export const getStreamFromConstraints = async (constraints, conference = false) => {
|
|
270
|
+
const video = constraints && constraints.video;
|
|
271
|
+
const { constraints: newConstraints } = getMediaConfiguration(video, conference, constraints);
|
|
272
|
+
let newStream = null;
|
|
273
|
+
try {
|
|
274
|
+
newStream = await wazoMediaStreamFactory(newConstraints);
|
|
275
|
+
}
|
|
276
|
+
catch (e) { // Nothing to do when the user cancel the screensharing prompt
|
|
277
|
+
}
|
|
278
|
+
if (!newStream) {
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
newStream.local = true;
|
|
282
|
+
return newStream;
|
|
283
|
+
};
|
|
284
|
+
export const createUaOptions = (config, uaOptionsOverrides = {}, audio = false, video = false) => {
|
|
285
|
+
let { host } = config;
|
|
286
|
+
let port = config.port || 443;
|
|
287
|
+
if (config.websocketSip) {
|
|
288
|
+
const webSocketSip = config.websocketSip.split(':');
|
|
289
|
+
[host] = webSocketSip;
|
|
290
|
+
port = Number(webSocketSip[1]);
|
|
291
|
+
}
|
|
292
|
+
const { userUuid } = config;
|
|
293
|
+
const uaOptions = {
|
|
294
|
+
noAnswerTimeout: NO_ANSWER_TIMEOUT,
|
|
295
|
+
authorizationUsername: config.authorizationUser,
|
|
296
|
+
authorizationPassword: config.password,
|
|
297
|
+
displayName: config.displayName,
|
|
298
|
+
hackIpInContact: true,
|
|
299
|
+
contactParams: {
|
|
300
|
+
transport: 'wss',
|
|
301
|
+
},
|
|
302
|
+
logBuiltinEnabled: config.log ? config.log.builtinEnabled : null,
|
|
303
|
+
logLevel: config.log ? config.log.logLevel : null,
|
|
304
|
+
logConnector: config.log ? config.log.connector : null,
|
|
305
|
+
uri: makeURI(config.authorizationUser || '', config.host),
|
|
306
|
+
userAgentString: config.userAgentString || 'wazo-sdk',
|
|
307
|
+
reconnectionAttempts: 10000,
|
|
308
|
+
reconnectionDelay: 5,
|
|
309
|
+
// @ts-ignore
|
|
310
|
+
sessionDescriptionHandlerFactory: (sipCall, options = {}) => {
|
|
311
|
+
const uaLogger = sipCall.userAgent.getLogger('sip.WazoSessionDescriptionHandler');
|
|
312
|
+
const iceGatheringTimeout = 'peerConnectionOptions' in options ? options.peerConnectionOptions.iceGatheringTimeout || DEFAULT_ICE_TIMEOUT : DEFAULT_ICE_TIMEOUT;
|
|
313
|
+
const sdhOptions = { ...options,
|
|
314
|
+
iceGatheringTimeout,
|
|
315
|
+
peerConnectionConfiguration: { ...defaultPeerConnectionConfiguration(),
|
|
316
|
+
...(options.peerConnectionConfiguration || {}),
|
|
317
|
+
},
|
|
318
|
+
};
|
|
319
|
+
return new WazoSessionDescriptionHandler(uaLogger, wazoMediaStreamFactory, sdhOptions, isWeb(), sipCall);
|
|
320
|
+
},
|
|
321
|
+
transportOptions: {
|
|
322
|
+
traceSip: uaOptionsOverrides?.traceSip || false,
|
|
323
|
+
wsServers: `wss://${host}:${port}/api/asterisk/ws${userUuid ? `?userUuid=${userUuid}` : ''}`,
|
|
324
|
+
},
|
|
325
|
+
sessionDescriptionHandlerFactoryOptions: {
|
|
326
|
+
modifiers: [replaceLocalIpModifier],
|
|
327
|
+
alwaysAcquireMediaFirst: isFirefox(),
|
|
328
|
+
constraints: {
|
|
329
|
+
audio: getAudioConstraints(audio),
|
|
330
|
+
video: getVideoConstraints(video),
|
|
331
|
+
},
|
|
332
|
+
peerConnectionOptions: {
|
|
333
|
+
iceCheckingTimeout: config.iceCheckingTimeout || 1000,
|
|
334
|
+
iceGatheringTimeout: config.iceCheckingTimeout || 1000,
|
|
335
|
+
rtcConfiguration: {
|
|
336
|
+
rtcpMuxPolicy: 'require',
|
|
337
|
+
iceServers: getIceServers(config.host),
|
|
338
|
+
...getRtcOptions(),
|
|
339
|
+
...(uaOptionsOverrides?.peerConnectionOptions || {}),
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
// Configuration used in SDH to create the PeerConnection
|
|
343
|
+
peerConnectionConfiguration: {
|
|
344
|
+
rtcpMuxPolicy: 'require',
|
|
345
|
+
iceServers: getIceServers(config.host),
|
|
346
|
+
...(uaOptionsOverrides?.peerConnectionOptions || {}),
|
|
347
|
+
},
|
|
348
|
+
},
|
|
349
|
+
};
|
|
350
|
+
delete uaOptionsOverrides?.traceSip;
|
|
351
|
+
return {
|
|
352
|
+
...uaOptions,
|
|
353
|
+
...uaOptionsOverrides,
|
|
354
|
+
};
|
|
355
|
+
};
|
|
356
|
+
export const downgradeToAudio = (sipCall) => {
|
|
357
|
+
// Release local video stream when downgrading to audio
|
|
358
|
+
const sessionDescriptionHandler = sipCall.sessionDescriptionHandler;
|
|
359
|
+
const localStream = sessionDescriptionHandler?.localMediaStream;
|
|
360
|
+
const pc = sessionDescriptionHandler?.peerConnection;
|
|
361
|
+
const videoTracks = localStream.getVideoTracks();
|
|
362
|
+
// Remove video senders
|
|
363
|
+
if (pc?.getSenders) {
|
|
364
|
+
pc.getSenders().filter((sender) => sender.track && sender.track.kind === 'video').forEach((videoSender) => {
|
|
365
|
+
videoSender.replaceTrack(null);
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
videoTracks.forEach((videoTrack) => {
|
|
369
|
+
videoTrack.enabled = false;
|
|
370
|
+
videoTrack.stop();
|
|
371
|
+
localStream.removeTrack(videoTrack);
|
|
372
|
+
});
|
|
373
|
+
};
|
|
374
|
+
// Returns true if a re-INVITE is required
|
|
375
|
+
export const upgradeToVideo = async (sipCall, constraints, isConference) => {
|
|
376
|
+
const pc = getPeerConnection(sipCall);
|
|
377
|
+
// Check if a video sender already exists
|
|
378
|
+
let videoSender;
|
|
379
|
+
if (isConference) {
|
|
380
|
+
// We search for the last transceiver without `video-` in the mid (video- means remote transceiver)
|
|
381
|
+
const transceivers = pc?.getTransceivers() || [];
|
|
382
|
+
const transceiverIdx = lastIndexOf(transceivers, transceiver => transceiver.sender.track === null && transceiver.mid && transceiver.mid.indexOf('video') === -1);
|
|
383
|
+
videoSender = transceiverIdx !== -1 ? transceivers[transceiverIdx].sender : null;
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
videoSender = pc && pc.getSenders && pc.getSenders().find((sender) => sender.track === null);
|
|
387
|
+
}
|
|
388
|
+
if (!videoSender) {
|
|
389
|
+
// When no video sender found, it means that we're in the first video upgrade in 1:1
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
// Reuse bidirectional video stream
|
|
393
|
+
const newStream = await getStreamFromConstraints(constraints);
|
|
394
|
+
if (!newStream) {
|
|
395
|
+
console.warn(`Can't create media stream with: ${JSON.stringify(constraints || {})}`);
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
// Add previous local audio track
|
|
399
|
+
if (constraints && !constraints.audio) {
|
|
400
|
+
const localVideoStream = sipCall.sessionDescriptionHandler?.localMediaStream;
|
|
401
|
+
const localAudioTrack = localVideoStream.getTracks().find(track => track.kind === 'audio');
|
|
402
|
+
if (localAudioTrack) {
|
|
403
|
+
newStream.addTrack(localAudioTrack);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
const videoTrack = newStream.getVideoTracks()[0];
|
|
407
|
+
if (videoTrack) {
|
|
408
|
+
videoSender.replaceTrack(videoTrack);
|
|
409
|
+
}
|
|
410
|
+
setLocalMediaStream(sipCall, newStream);
|
|
411
|
+
return newStream;
|
|
412
|
+
};
|
|
413
|
+
export const fetchNetworkStats = async (sipCall, lastNetworkStats) => {
|
|
414
|
+
const stats = sipCall ? await getWebRtcStats(sipCall) : null;
|
|
415
|
+
if (!stats) {
|
|
416
|
+
return Promise.resolve(null);
|
|
417
|
+
}
|
|
418
|
+
const networkStats = {
|
|
419
|
+
audioBytesSent: 0,
|
|
420
|
+
videoBytesSent: 0,
|
|
421
|
+
videoBytesReceived: 0,
|
|
422
|
+
audioBytesReceived: 0,
|
|
423
|
+
};
|
|
424
|
+
const lastAudioSent = lastNetworkStats ? lastNetworkStats.audioBytesSent : 0;
|
|
425
|
+
const lastAudioContentSent = lastNetworkStats ? lastNetworkStats.audioContentSent : 0;
|
|
426
|
+
const lastVideoSent = lastNetworkStats ? lastNetworkStats.videoBytesSent : 0;
|
|
427
|
+
const lastAudioReceived = lastNetworkStats ? lastNetworkStats.audioBytesReceived : 0;
|
|
428
|
+
const lastAudioContentReceived = lastNetworkStats ? lastNetworkStats.audioContentReceived : 0;
|
|
429
|
+
const lastVideoReceived = lastNetworkStats ? lastNetworkStats.videoBytesReceived : 0;
|
|
430
|
+
const lastTransportSent = lastNetworkStats ? lastNetworkStats.transportSent : 0;
|
|
431
|
+
const lastTransportReceived = lastNetworkStats ? lastNetworkStats.transportReceived : 0;
|
|
432
|
+
let audioBytesSent = 0; // content + header
|
|
433
|
+
let audioBytesReceived = 0; // content + header
|
|
434
|
+
let audioContentSent = 0; // useful to detect blank call
|
|
435
|
+
let audioContentReceived = 0;
|
|
436
|
+
let videoBytesSent = 0;
|
|
437
|
+
let videoBytesReceived = 0;
|
|
438
|
+
let transportSent = 0;
|
|
439
|
+
let transportReceived = 0;
|
|
440
|
+
let packetsLost = 0;
|
|
441
|
+
stats.forEach(report => {
|
|
442
|
+
if (report.type === 'outbound-rtp' && report.kind === 'audio') {
|
|
443
|
+
audioBytesSent += Number(report.bytesSent) + Number(report.headerBytesSent);
|
|
444
|
+
audioContentSent += Number(report.bytesSent);
|
|
445
|
+
}
|
|
446
|
+
if (report.type === 'remote-outbound-rtp' && report.kind === 'audio') {
|
|
447
|
+
audioBytesSent += Number(report.bytesSent);
|
|
448
|
+
}
|
|
449
|
+
if (report.type === 'outbound-rtp' && report.kind === 'video') {
|
|
450
|
+
videoBytesSent += Number(report.bytesSent) + Number(report.headerBytesSent);
|
|
451
|
+
}
|
|
452
|
+
if (report.type === 'inbound-rtp' && report.kind === 'audio') {
|
|
453
|
+
packetsLost += Number(report.packetsLost);
|
|
454
|
+
networkStats.packetsReceived = Number(report.packetsReceived);
|
|
455
|
+
audioBytesReceived += Number(report.bytesReceived) + Number(report.headerBytesReceived);
|
|
456
|
+
audioContentReceived += Number(report.bytesReceived);
|
|
457
|
+
}
|
|
458
|
+
if (report.type === 'inbound-rtp' && report.kind === 'video') {
|
|
459
|
+
videoBytesReceived += Number(report.bytesReceived) + Number(report.headerBytesReceived);
|
|
460
|
+
}
|
|
461
|
+
if (report.type === 'outbound-rtp' && report.kind === 'video') {
|
|
462
|
+
if ('framesPerSecond' in report) {
|
|
463
|
+
networkStats.framesPerSecond = Number(report.framesPerSecond);
|
|
464
|
+
}
|
|
465
|
+
if ('framerateMean' in report) {
|
|
466
|
+
// framerateMean is only available in FF
|
|
467
|
+
networkStats.framesPerSecond = Math.round(report.framerateMean);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
if (report.type === 'remote-inbound-rtp' && report.kind === 'audio') {
|
|
471
|
+
packetsLost += Number(report.packetsLost);
|
|
472
|
+
networkStats.roundTripTime = Number(report.roundTripTime);
|
|
473
|
+
networkStats.jitter = Number(report.jitter);
|
|
474
|
+
}
|
|
475
|
+
if (report.type === 'remote-inbound-rtp' && report.kind === 'video') {
|
|
476
|
+
packetsLost += Number(report.packetsLost);
|
|
477
|
+
}
|
|
478
|
+
if (report.type === 'transport') {
|
|
479
|
+
transportSent += Number(report.bytesSent);
|
|
480
|
+
transportReceived += Number(report.bytesReceived);
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
networkStats.packetsLost = packetsLost;
|
|
484
|
+
networkStats.totalAudioBytesSent = audioBytesSent;
|
|
485
|
+
networkStats.audioBytesSent = audioBytesSent - lastAudioSent;
|
|
486
|
+
networkStats.totalAudioBytesReceived = audioBytesReceived - lastAudioReceived;
|
|
487
|
+
networkStats.audioBytesReceived = audioBytesReceived;
|
|
488
|
+
networkStats.totalAudioContentSent = audioContentSent;
|
|
489
|
+
networkStats.audioContentSent = audioContentSent - lastAudioContentSent;
|
|
490
|
+
networkStats.totalAudioBytesReceived = audioContentReceived - lastAudioContentReceived;
|
|
491
|
+
networkStats.audioContentReceived = audioContentReceived;
|
|
492
|
+
networkStats.totalVideoBytesSent = videoBytesSent;
|
|
493
|
+
networkStats.videoBytesSent = videoBytesSent - lastVideoSent;
|
|
494
|
+
networkStats.totalVideoBytesReceived = videoBytesReceived;
|
|
495
|
+
networkStats.videoBytesReceived = videoBytesReceived - lastVideoReceived;
|
|
496
|
+
networkStats.totalTransportSent = transportSent - lastTransportSent;
|
|
497
|
+
networkStats.transportSent = transportSent;
|
|
498
|
+
networkStats.totalTransportReceived = transportReceived;
|
|
499
|
+
networkStats.transportReceived = transportReceived - lastTransportReceived;
|
|
500
|
+
networkStats.bandwidth = networkStats.audioBytesSent + networkStats.audioBytesReceived + networkStats.videoBytesSent + networkStats.videoBytesReceived + networkStats.transportReceived + networkStats.transportSent;
|
|
501
|
+
return networkStats;
|
|
502
|
+
};
|