@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.
Files changed (158) hide show
  1. package/dist/wazo-sdk.js +3 -3
  2. package/esm/__tests__/__voice__/call.test.js +72 -0
  3. package/esm/__tests__/__voice__/softphone.test.js +66 -0
  4. package/esm/__tests__/api-client.test.js +1 -20
  5. package/esm/api/auth.js +2 -13
  6. package/esm/checker/checks/webrtc.js +2 -1
  7. package/esm/domain/InvalidState.js +8 -0
  8. package/esm/domain/InvalidStateTransition.js +10 -0
  9. package/esm/domain/Phone/WebRTCPhone.js +33 -35
  10. package/esm/simple/Auth.js +12 -6
  11. package/esm/simple/Phone.js +2 -1
  12. package/esm/simple/__tests__/Auth.test.js +64 -8
  13. package/esm/simple/index.js +8 -2
  14. package/esm/simple/room/Room.js +2 -1
  15. package/esm/simple/room/SipRoom.js +2 -1
  16. package/esm/state-machine/call-state-machine.js +114 -0
  17. package/esm/state-machine/softphone-state-machine.js +39 -0
  18. package/esm/state-machine/utils.js +40 -0
  19. package/esm/types/__tests__/__voice__/call.test.d.ts +2 -0
  20. package/esm/types/__tests__/__voice__/call.test.d.ts.map +1 -0
  21. package/esm/types/__tests__/__voice__/softphone.test.d.ts +2 -0
  22. package/esm/types/__tests__/__voice__/softphone.test.d.ts.map +1 -0
  23. package/esm/types/api/auth.d.ts.map +1 -1
  24. package/esm/types/checker/checks/webrtc.d.ts.map +1 -1
  25. package/esm/types/domain/InvalidState.d.ts +6 -0
  26. package/esm/types/domain/InvalidState.d.ts.map +1 -0
  27. package/esm/types/domain/InvalidStateTransition.d.ts +7 -0
  28. package/esm/types/domain/InvalidStateTransition.d.ts.map +1 -0
  29. package/esm/types/domain/Phone/WebRTCPhone.d.ts +0 -1
  30. package/esm/types/domain/Phone/WebRTCPhone.d.ts.map +1 -1
  31. package/esm/types/domain/types.d.ts +4 -0
  32. package/esm/types/domain/types.d.ts.map +1 -1
  33. package/esm/types/simple/Auth.d.ts.map +1 -1
  34. package/esm/types/simple/Phone.d.ts.map +1 -1
  35. package/esm/types/simple/index.d.ts +7 -2
  36. package/esm/types/simple/index.d.ts.map +1 -1
  37. package/esm/types/simple/room/Room.d.ts.map +1 -1
  38. package/esm/types/simple/room/SipRoom.d.ts +1 -1
  39. package/esm/types/simple/room/SipRoom.d.ts.map +1 -1
  40. package/esm/types/state-machine/call-state-machine.d.ts +43 -0
  41. package/esm/types/state-machine/call-state-machine.d.ts.map +1 -0
  42. package/esm/types/state-machine/softphone-state-machine.d.ts +19 -0
  43. package/esm/types/state-machine/softphone-state-machine.d.ts.map +1 -0
  44. package/esm/types/state-machine/utils.d.ts +9 -0
  45. package/esm/types/state-machine/utils.d.ts.map +1 -0
  46. package/esm/types/utils/api-requester.d.ts.map +1 -1
  47. package/esm/types/utils/sdp.d.ts +6 -0
  48. package/esm/types/utils/sdp.d.ts.map +1 -1
  49. package/esm/types/utils/sip-logger.d.ts +4 -0
  50. package/esm/types/utils/sip-logger.d.ts.map +1 -0
  51. package/esm/types/utils/webrtc.d.ts +40 -0
  52. package/esm/types/utils/webrtc.d.ts.map +1 -1
  53. package/esm/types/voice/call.d.ts +110 -0
  54. package/esm/types/voice/call.d.ts.map +1 -0
  55. package/esm/types/voice/softphone.d.ts +77 -0
  56. package/esm/types/voice/softphone.d.ts.map +1 -0
  57. package/esm/types/web-rtc-client.d.ts +1 -36
  58. package/esm/types/web-rtc-client.d.ts.map +1 -1
  59. package/esm/utils/__tests__/api-requester.test.js +0 -17
  60. package/esm/utils/api-requester.js +1 -9
  61. package/esm/utils/sdp.js +26 -0
  62. package/esm/utils/sip-logger.js +32 -0
  63. package/esm/utils/webrtc.js +465 -0
  64. package/esm/voice/call.js +527 -0
  65. package/esm/voice/softphone.js +347 -0
  66. package/esm/web-rtc-client.js +86 -584
  67. package/lib/__tests__/__voice__/call.test.js +86 -0
  68. package/lib/__tests__/__voice__/softphone.test.js +80 -0
  69. package/lib/__tests__/api-client.test.js +1 -20
  70. package/lib/api/auth.js +2 -13
  71. package/lib/checker/checks/webrtc.js +2 -1
  72. package/lib/domain/InvalidState.js +9 -0
  73. package/lib/domain/InvalidStateTransition.js +10 -0
  74. package/lib/domain/Phone/WebRTCPhone.js +33 -35
  75. package/lib/simple/Auth.js +12 -6
  76. package/lib/simple/Phone.js +2 -1
  77. package/lib/simple/__tests__/Auth.test.js +64 -11
  78. package/lib/simple/index.js +6 -0
  79. package/lib/simple/room/Room.js +4 -3
  80. package/lib/simple/room/SipRoom.js +2 -1
  81. package/lib/state-machine/call-state-machine.js +120 -0
  82. package/lib/state-machine/softphone-state-machine.js +42 -0
  83. package/lib/state-machine/utils.js +52 -0
  84. package/lib/types/__tests__/__voice__/call.test.d.ts +2 -0
  85. package/lib/types/__tests__/__voice__/call.test.d.ts.map +1 -0
  86. package/lib/types/__tests__/__voice__/softphone.test.d.ts +2 -0
  87. package/lib/types/__tests__/__voice__/softphone.test.d.ts.map +1 -0
  88. package/lib/types/api/auth.d.ts.map +1 -1
  89. package/lib/types/checker/checks/webrtc.d.ts.map +1 -1
  90. package/lib/types/domain/InvalidState.d.ts +6 -0
  91. package/lib/types/domain/InvalidState.d.ts.map +1 -0
  92. package/lib/types/domain/InvalidStateTransition.d.ts +7 -0
  93. package/lib/types/domain/InvalidStateTransition.d.ts.map +1 -0
  94. package/lib/types/domain/Phone/WebRTCPhone.d.ts +0 -1
  95. package/lib/types/domain/Phone/WebRTCPhone.d.ts.map +1 -1
  96. package/lib/types/domain/types.d.ts +4 -0
  97. package/lib/types/domain/types.d.ts.map +1 -1
  98. package/lib/types/simple/Auth.d.ts.map +1 -1
  99. package/lib/types/simple/Phone.d.ts.map +1 -1
  100. package/lib/types/simple/index.d.ts +7 -2
  101. package/lib/types/simple/index.d.ts.map +1 -1
  102. package/lib/types/simple/room/Room.d.ts.map +1 -1
  103. package/lib/types/simple/room/SipRoom.d.ts +1 -1
  104. package/lib/types/simple/room/SipRoom.d.ts.map +1 -1
  105. package/lib/types/state-machine/call-state-machine.d.ts +43 -0
  106. package/lib/types/state-machine/call-state-machine.d.ts.map +1 -0
  107. package/lib/types/state-machine/softphone-state-machine.d.ts +19 -0
  108. package/lib/types/state-machine/softphone-state-machine.d.ts.map +1 -0
  109. package/lib/types/state-machine/utils.d.ts +9 -0
  110. package/lib/types/state-machine/utils.d.ts.map +1 -0
  111. package/lib/types/utils/api-requester.d.ts.map +1 -1
  112. package/lib/types/utils/sdp.d.ts +6 -0
  113. package/lib/types/utils/sdp.d.ts.map +1 -1
  114. package/lib/types/utils/sip-logger.d.ts +4 -0
  115. package/lib/types/utils/sip-logger.d.ts.map +1 -0
  116. package/lib/types/utils/webrtc.d.ts +40 -0
  117. package/lib/types/utils/webrtc.d.ts.map +1 -1
  118. package/lib/types/voice/call.d.ts +110 -0
  119. package/lib/types/voice/call.d.ts.map +1 -0
  120. package/lib/types/voice/softphone.d.ts +77 -0
  121. package/lib/types/voice/softphone.d.ts.map +1 -0
  122. package/lib/types/web-rtc-client.d.ts +1 -36
  123. package/lib/types/web-rtc-client.d.ts.map +1 -1
  124. package/lib/utils/__tests__/api-requester.test.js +0 -17
  125. package/lib/utils/api-requester.js +1 -9
  126. package/lib/utils/sdp.js +27 -1
  127. package/lib/utils/sip-logger.js +37 -0
  128. package/lib/utils/webrtc.js +521 -1
  129. package/lib/voice/call.js +577 -0
  130. package/lib/voice/softphone.js +375 -0
  131. package/lib/web-rtc-client.js +89 -601
  132. package/package.json +3 -2
  133. package/src/__tests__/__voice__/call.test.ts +90 -0
  134. package/src/__tests__/__voice__/softphone.test.ts +82 -0
  135. package/src/__tests__/api-client.test.ts +1 -22
  136. package/src/api/auth.ts +3 -25
  137. package/src/checker/checks/webrtc.ts +2 -1
  138. package/src/domain/InvalidState.ts +13 -0
  139. package/src/domain/InvalidStateTransition.ts +17 -0
  140. package/src/domain/Phone/WebRTCPhone.ts +34 -36
  141. package/src/domain/types.ts +6 -1
  142. package/src/simple/Auth.ts +13 -6
  143. package/src/simple/Phone.ts +2 -1
  144. package/src/simple/__tests__/Auth.test.ts +87 -9
  145. package/src/simple/index.ts +9 -2
  146. package/src/simple/room/Room.ts +2 -1
  147. package/src/simple/room/SipRoom.ts +2 -1
  148. package/src/state-machine/call-state-machine.ts +131 -0
  149. package/src/state-machine/softphone-state-machine.ts +49 -0
  150. package/src/state-machine/utils.ts +53 -0
  151. package/src/utils/__tests__/api-requester.test.ts +0 -20
  152. package/src/utils/api-requester.ts +1 -9
  153. package/src/utils/sdp.ts +36 -0
  154. package/src/utils/sip-logger.ts +40 -0
  155. package/src/utils/webrtc.ts +571 -0
  156. package/src/voice/call.ts +713 -0
  157. package/src/voice/softphone.ts +485 -0
  158. package/src/web-rtc-client.ts +90 -690
@@ -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
+ };