@wazo/sdk 0.43.6 → 1.0.0-alpha.1

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