@wazo/sdk 1.0.0-alpha.1 → 1.0.0-alpha.3

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 (73) hide show
  1. package/dist/wazo-sdk.js +6 -6
  2. package/esm/__tests__/__voice__/call.test.js +45 -2
  3. package/esm/domain/Phone/CTIPhone.js +2 -2
  4. package/esm/domain/Phone/WebRTCPhone.js +6 -7
  5. package/esm/index.js +1 -1
  6. package/esm/service/CallApi.js +4 -4
  7. package/esm/simple/Phone.js +1 -0
  8. package/esm/state-machine/call-state-machine.js +1 -1
  9. package/esm/types/domain/Phone/CTIPhone.d.ts.map +1 -1
  10. package/esm/types/domain/Phone/WebRTCPhone.d.ts.map +1 -1
  11. package/esm/types/index.d.ts +1 -1
  12. package/esm/types/index.d.ts.map +1 -1
  13. package/esm/types/service/CallApi.d.ts +5 -5
  14. package/esm/types/service/CallApi.d.ts.map +1 -1
  15. package/esm/types/simple/Phone.d.ts.map +1 -1
  16. package/esm/types/state-machine/softphone-state-machine.d.ts +1 -1
  17. package/esm/types/state-machine/utils.d.ts +1 -1
  18. package/esm/types/state-machine/utils.d.ts.map +1 -1
  19. package/esm/types/utils/sdp.d.ts +2 -0
  20. package/esm/types/utils/sdp.d.ts.map +1 -1
  21. package/esm/types/utils/webrtc.d.ts +1 -0
  22. package/esm/types/utils/webrtc.d.ts.map +1 -1
  23. package/esm/types/voice/call.d.ts +27 -12
  24. package/esm/types/voice/call.d.ts.map +1 -1
  25. package/esm/types/voice/softphone.d.ts +22 -13
  26. package/esm/types/voice/softphone.d.ts.map +1 -1
  27. package/esm/utils/sdp.js +13 -0
  28. package/esm/utils/webrtc.js +20 -0
  29. package/esm/voice/call.js +115 -33
  30. package/esm/voice/softphone.js +105 -25
  31. package/lib/__tests__/__voice__/call.test.js +44 -1
  32. package/lib/domain/Phone/CTIPhone.js +2 -2
  33. package/lib/domain/Phone/WebRTCPhone.js +5 -6
  34. package/lib/index.js +2 -2
  35. package/lib/service/CallApi.js +2 -2
  36. package/lib/simple/Phone.js +1 -0
  37. package/lib/state-machine/call-state-machine.js +1 -1
  38. package/lib/types/domain/Phone/CTIPhone.d.ts.map +1 -1
  39. package/lib/types/domain/Phone/WebRTCPhone.d.ts.map +1 -1
  40. package/lib/types/index.d.ts +1 -1
  41. package/lib/types/index.d.ts.map +1 -1
  42. package/lib/types/service/CallApi.d.ts +5 -5
  43. package/lib/types/service/CallApi.d.ts.map +1 -1
  44. package/lib/types/simple/Phone.d.ts.map +1 -1
  45. package/lib/types/state-machine/softphone-state-machine.d.ts +1 -1
  46. package/lib/types/state-machine/utils.d.ts +1 -1
  47. package/lib/types/state-machine/utils.d.ts.map +1 -1
  48. package/lib/types/utils/sdp.d.ts +2 -0
  49. package/lib/types/utils/sdp.d.ts.map +1 -1
  50. package/lib/types/utils/webrtc.d.ts +1 -0
  51. package/lib/types/utils/webrtc.d.ts.map +1 -1
  52. package/lib/types/voice/call.d.ts +27 -12
  53. package/lib/types/voice/call.d.ts.map +1 -1
  54. package/lib/types/voice/softphone.d.ts +22 -13
  55. package/lib/types/voice/softphone.d.ts.map +1 -1
  56. package/lib/utils/sdp.js +16 -1
  57. package/lib/utils/webrtc.js +22 -1
  58. package/lib/voice/call.js +112 -31
  59. package/lib/voice/softphone.js +109 -25
  60. package/package.json +1 -1
  61. package/src/__tests__/__voice__/call.test.ts +48 -2
  62. package/src/domain/Phone/CTIPhone.ts +3 -2
  63. package/src/domain/Phone/WebRTCPhone.ts +9 -9
  64. package/src/index.ts +1 -1
  65. package/src/service/CallApi.ts +7 -7
  66. package/src/simple/Phone.ts +1 -0
  67. package/src/state-machine/call-state-machine.ts +1 -1
  68. package/src/state-machine/utils.ts +2 -2
  69. package/src/utils/sdp.ts +16 -0
  70. package/src/utils/webrtc.ts +23 -1
  71. package/src/voice/call.ts +149 -43
  72. package/src/voice/softphone.ts +130 -31
  73. package/src/web-rtc-client.ts +1 -1
@@ -10,7 +10,6 @@ import InvalidState from '../domain/InvalidState';
10
10
 
11
11
  import Wazo from '..';
12
12
  import softphoneStateMachine, { type SoftphoneActorRef, Actions, type StateTypes, States, ActionTypes } from '../state-machine/softphone-state-machine';
13
- import { Actions as CallActions } from '../state-machine/call-state-machine';
14
13
  import { WebRtcConfig, SipCall } from '../domain/types';
15
14
  import WazoWebRTCClient from '../web-rtc-client';
16
15
  import IssueReporter from '../service/IssueReporter';
@@ -19,12 +18,17 @@ import Call from './call';
19
18
  import { assertCan, can, getState, waitUntilState } from '../state-machine/utils';
20
19
 
21
20
  export const EVENT_INCOMING = 'incoming';
21
+ export const EVENT_OUTGOING = 'outgoing';
22
22
  export const EVENT_HEARTBEAT = 'heartbeat';
23
23
  export const EVENT_HEARTBEAT_TIMEOUT = 'heartbeatTimeout';
24
24
  export const EVENT_REGISTERED = 'registered';
25
25
  export const EVENT_UNREGISTERED = 'unregistered';
26
26
  export const EVENT_DISCONNECTED = 'disconnected';
27
27
  export const EVENT_VIDEO_INPUT_CHANGE = 'videoInputChange';
28
+ export const EVENT_TERMINATE_SOUND = 'terminateSound';
29
+ export const EVENT_PLAY_HANGUP_SOUND = 'playHangupSound';
30
+ export const EVENT_PLAY_RING_SOUND = 'playRingSound';
31
+ export const EVENT_PLAY_INBOUND_CALL_SOUND = 'playInboundCallSound';
28
32
  export const EVENT_ON_MESSAGE = 'message';
29
33
  export const EVENT_ON_CHAT = 'phone/ON_CHAT';
30
34
  export const EVENT_ON_SIGNAL = 'phone/ON_SIGNAL';
@@ -40,6 +44,7 @@ export type CallOptions = {
40
44
  // If audioOnly is set to true, all video stream will be deactivated, even remotes ones.
41
45
  audioOnly?: boolean,
42
46
  conference?: boolean,
47
+ extraHeaders?: Record<string, any>,
43
48
  };
44
49
 
45
50
  const logger = IssueReporter.loggerFor('softphone');
@@ -53,8 +58,6 @@ export class Softphone extends EventEmitter {
53
58
 
54
59
  audioRingVolume: number;
55
60
 
56
- eventEmitter: EventEmitter;
57
-
58
61
  calls: Call[];
59
62
 
60
63
  client: WazoWebRTCClient;
@@ -69,6 +72,8 @@ export class Softphone extends EventEmitter {
69
72
 
70
73
  shouldSendReinvite: boolean;
71
74
 
75
+ ringingEnabled = true;
76
+
72
77
  constructor() {
73
78
  super();
74
79
 
@@ -103,6 +108,10 @@ export class Softphone extends EventEmitter {
103
108
  return this.connectWithCredentials(server, this.sipLine, session.displayName(), options);
104
109
  }
105
110
 
111
+ async hangupAllCalls() {
112
+ return Promise.all(this.calls.map(call => call.hangup()));
113
+ }
114
+
106
115
  async disconnect() {
107
116
  assertCan(this.softphoneActor, Actions.UNREGISTER);
108
117
  if (!this.client) {
@@ -110,7 +119,7 @@ export class Softphone extends EventEmitter {
110
119
  }
111
120
 
112
121
  // Terminate all calls
113
- await Promise.all(this.calls.map(call => call.hangup()));
122
+ await this.hangupAllCalls();
114
123
 
115
124
  return this.client.unregister();
116
125
  }
@@ -179,16 +188,16 @@ export class Softphone extends EventEmitter {
179
188
  return Promise.resolve(null);
180
189
  }
181
190
 
182
- if (this.getState() !== States.UNREGISTERED) {
183
- throw new InvalidState(`Invalid state ${States.UNREGISTERED}`, States.UNREGISTERED);
184
- }
185
-
186
- if (this.getState() !== States.REGISTERING) {
191
+ if (this.state === States.REGISTERING) {
187
192
  logger.info('softphone - calling when registering, waiting for Softphone to be registered ...');
188
193
 
189
194
  await waitUntilState(this.softphoneActor, States.REGISTERED);
190
195
  }
191
196
 
197
+ if (this.state !== States.UNREGISTERED) {
198
+ throw new InvalidState(`Invalid state ${States.UNREGISTERED}`, States.UNREGISTERED);
199
+ }
200
+
192
201
  // Hold other calls
193
202
  this.holdAllCalls();
194
203
 
@@ -198,6 +207,8 @@ export class Softphone extends EventEmitter {
198
207
  sipCall = this.client.call(options.params.To, options.withCamera, options.audioOnly, options.conference, options) as SipCall;
199
208
  const call = new Call(sipCall, this);
200
209
 
210
+ this.emit(EVENT_OUTGOING, call);
211
+
201
212
  return call;
202
213
  } catch (error: any) {
203
214
  logger.warn('softphone - call, error', { message: error.message, stack: error.stack });
@@ -209,6 +220,69 @@ export class Softphone extends EventEmitter {
209
220
  this.calls = this.calls.filter((call: Call) => call.id !== callToRemove.id);
210
221
  }
211
222
 
223
+ onCallTerminated(call: Call) {
224
+ logger.info('softphone - on call terminated', { id: call.id });
225
+
226
+ const currentCall = this.getCurrentCall();
227
+ const isCurrentCall = currentCall?.id === call.id;
228
+ const isRinging = call.isRinging();
229
+ const isCurrentIncomingCall = isCurrentCall && isRinging;
230
+ const incomingCalls = this.getIncomingCalls();
231
+ const nbIncomingCalls = incomingCalls.length;
232
+
233
+ // Send an event to terminate ringing sound
234
+ if (isCurrentIncomingCall) {
235
+ setTimeout(() => {
236
+ // Avoid race condition when the other is calling and hanging up immediately
237
+ this.emit(EVENT_TERMINATE_SOUND, call, 'call terminated');
238
+ }, 5);
239
+ }
240
+
241
+ // If the terminated call was an incoming call, we have to re-trigger if it's was the first incoming call
242
+ // Otherwise, we have to re-trigger an incoming call event if another call is incoming
243
+ const shouldRetrigger = isCurrentCall ? nbIncomingCalls : isCurrentIncomingCall;
244
+
245
+ // Re-trigger incoming call event for remaining incoming calls
246
+ logger.info('Softphone - check to re-trigger incoming call', { shouldRetrigger, nbIncomingCalls });
247
+
248
+ if (shouldRetrigger && nbIncomingCalls > 1) {
249
+ const nextCall = incomingCalls.find(c => c.id !== call.id);
250
+ // Avoid race condition
251
+ setTimeout(() => {
252
+ if (this.ringingEnabled) {
253
+ if (currentCall) {
254
+ this.emit(EVENT_PLAY_RING_SOUND, this.audioRingDeviceId, this.audioRingVolume, nextCall);
255
+ } else {
256
+ this.emit(EVENT_PLAY_INBOUND_CALL_SOUND, this.audioOutputDeviceId, this.audioOutputVolume, nextCall);
257
+ }
258
+ }
259
+
260
+ this.emit(EVENT_INCOMING, nextCall);
261
+ }, 100);
262
+ }
263
+
264
+ // @ts-ignore: does not exist
265
+ if (!call.sipCall.isCanceled) {
266
+ setTimeout(() => {
267
+ this.emit(EVENT_PLAY_HANGUP_SOUND, this.audioOutputDeviceId, this.audioOutputVolume, call);
268
+ }, 10);
269
+ }
270
+
271
+ this.client.onCallEnded(call.sipCall);
272
+
273
+ this.removeCall(call);
274
+ }
275
+
276
+ enableRinging(): Promise<void> | void {
277
+ logger.info('softphone - enable ringing');
278
+ this.ringingEnabled = true;
279
+ }
280
+
281
+ disableRinging(): Promise<void> | void {
282
+ logger.info('softphone - disable ringing');
283
+ this.ringingEnabled = false;
284
+ }
285
+
212
286
  holdAllCalls(exceptCall: Call | null = null) {
213
287
  this.calls.filter(call => !call.isHeld() && call.id === exceptCall?.id).forEach(call => call.hold());
214
288
  }
@@ -278,29 +352,51 @@ export class Softphone extends EventEmitter {
278
352
  this.audioRingVolume = volume;
279
353
  }
280
354
 
281
- getCalls(): Call[] {
282
- return this.calls;
355
+ getCurrentCall(): Call | undefined {
356
+ return this.getActiveCalls()[0];
283
357
  }
284
358
 
285
- getCurrentCall(): Call | undefined {
286
- return this.calls.find(call => call.isEstablished());
359
+ getActiveCalls(): Call[] {
360
+ return this.calls.filter(call => call.isEstablished());
287
361
  }
288
362
 
289
- getState(): StateTypes {
290
- return this.softphoneActor.getSnapshot().value;
363
+ getIncomingCalls(): Call[] {
364
+ return this.calls.filter(call => call.isRinging());
365
+ }
366
+
367
+ get state(): StateTypes {
368
+ return getState(this.softphoneActor) as StateTypes;
369
+ }
370
+
371
+ get user() {
372
+ return this.client?.userAgent?.contact?.uri?.user;
291
373
  }
292
374
 
293
375
  getUserAgent() {
294
376
  return this.client?.config?.userAgentString || 'softphone';
295
377
  }
296
378
 
379
+ isRegistered() {
380
+ return this.state === States.REGISTERED;
381
+ }
382
+
297
383
  _bindEvents(): void {
298
384
  this.client.on(this.client.INVITE, (sipSession: SipCall) => {
299
385
  const call = new Call(sipSession, this);
300
- call._sendAction(CallActions.INCOMING_CALL);
386
+ logger.info('softphone - on invite', { callId: call.id, number: call.number });
387
+
388
+ call.onCallIncoming();
301
389
  this.calls.push(call);
302
390
 
303
- this.eventEmitter.emit(EVENT_INCOMING, call);
391
+ this.emit(EVENT_INCOMING, call);
392
+
393
+ if (!this.getCurrentCall()) {
394
+ if (this.ringingEnabled) {
395
+ this.emit(EVENT_PLAY_RING_SOUND, this.audioRingDeviceId, this.audioRingVolume, call);
396
+ }
397
+ } else {
398
+ this.emit(EVENT_PLAY_INBOUND_CALL_SOUND, this.audioOutputDeviceId, this.audioOutputVolume, call);
399
+ }
304
400
  });
305
401
 
306
402
  this.client.on(this.client.ON_REINVITE, (sipCall: SipCall, request: IncomingRequestMessage, updatedCalleeName: string, updatedNumber: string, hadRemoteVideo: boolean) => {
@@ -316,6 +412,9 @@ export class Softphone extends EventEmitter {
316
412
  });
317
413
 
318
414
  this.client.on(this.client.ACCEPTED, async (sipCall: SipCall) => {
415
+ const call = this._getCall(sipCall);
416
+ this.emit(EVENT_TERMINATE_SOUND, call, 'call accepted');
417
+
319
418
  this._onCallEvent(sipCall, 'onAccepted');
320
419
 
321
420
  if (this.audioOutputDeviceId) {
@@ -354,14 +453,14 @@ export class Softphone extends EventEmitter {
354
453
 
355
454
  this.client.on(this.client.UNREGISTERED, () => {
356
455
  logger.info('softphone - unregistered');
357
- this.eventEmitter.emit(EVENT_UNREGISTERED);
456
+ this.emit(EVENT_UNREGISTERED);
358
457
 
359
458
  this._sendAction(Actions.UNREGISTERED);
360
459
  });
361
460
 
362
461
  this.client.on(this.client.ON_DISCONNECTED, () => {
363
462
  logger.info('softphone - disconnected');
364
- this.eventEmitter.emit(EVENT_DISCONNECTED);
463
+ this.emit(EVENT_DISCONNECTED);
365
464
 
366
465
  this._sendAction(Actions.TRANSPORT_CLOSED);
367
466
  });
@@ -376,7 +475,7 @@ export class Softphone extends EventEmitter {
376
475
  this.stopHeartbeat();
377
476
  this._sendAction(Actions.REGISTER_DONE);
378
477
 
379
- this.eventEmitter.emit(EVENT_REGISTERED);
478
+ this.emit(EVENT_REGISTERED);
380
479
 
381
480
  // If the phone registered with a current callSession (eg: when switching network):
382
481
  // send a reinvite to renegociate ICE with new IP
@@ -396,7 +495,7 @@ export class Softphone extends EventEmitter {
396
495
 
397
496
  this.client.on(this.client.DISCONNECTED, () => {
398
497
  logger.info('softphone - disconnected');
399
- this.eventEmitter.emit(EVENT_DISCONNECTED);
498
+ this.emit(EVENT_DISCONNECTED);
400
499
 
401
500
  // Do not trigger heartbeat if already running
402
501
  if (!this.client.hasHeartbeat()) {
@@ -408,33 +507,33 @@ export class Softphone extends EventEmitter {
408
507
  });
409
508
 
410
509
  this.client.on('onVideoInputChange', stream => {
411
- this.eventEmitter.emit(EVENT_VIDEO_INPUT_CHANGE, stream);
510
+ this.emit(EVENT_VIDEO_INPUT_CHANGE, stream);
412
511
  });
413
512
 
414
513
  this.client.on(this.client.MESSAGE, (message: Message) => {
415
514
  this._onMessage(message);
416
515
 
417
- this.eventEmitter.emit(EVENT_ON_MESSAGE, message);
516
+ this.emit(EVENT_ON_MESSAGE, message);
418
517
  });
419
518
 
420
519
  this.client.setOnHeartbeatTimeout(() => {
421
- this.eventEmitter.emit(EVENT_HEARTBEAT_TIMEOUT);
520
+ this.emit(EVENT_HEARTBEAT_TIMEOUT);
422
521
  });
423
522
 
424
523
  this.client.setOnHeartbeatCallback(() => {
425
- this.eventEmitter.emit(EVENT_HEARTBEAT);
524
+ this.emit(EVENT_HEARTBEAT);
426
525
  });
427
526
  }
428
527
 
429
- _sendAction(action: ActionTypes) {
528
+ private _sendAction(action: ActionTypes) {
430
529
  this.softphoneActor.send({ type: action });
431
530
  }
432
531
 
433
- _getCall(sipCall: SipCall): Call | undefined {
532
+ private _getCall(sipCall: SipCall): Call | undefined {
434
533
  return this.calls.find(call => call.id === sipCall.id);
435
534
  }
436
535
 
437
- _onCallEvent(sipCall: SipCall, eventName: string, ...args: any[]) {
536
+ private _onCallEvent(sipCall: SipCall, eventName: string, ...args: any[]) {
438
537
  logger.warn('softphone - received call event', { eventName, sipId: sipCall.id });
439
538
 
440
539
  const call = this._getCall(sipCall);
@@ -447,7 +546,7 @@ export class Softphone extends EventEmitter {
447
546
  call[eventName](...args);
448
547
  }
449
548
 
450
- _onMessage(message: Message & { method?: string, body?: string }): void {
549
+ private _onMessage(message: Message & { method?: string, body?: string }): void {
451
550
  if (!message || message.method !== 'MESSAGE') {
452
551
  return;
453
552
  }
@@ -467,12 +566,12 @@ export class Softphone extends EventEmitter {
467
566
 
468
567
  switch (type) {
469
568
  case MESSAGE_TYPE_CHAT:
470
- this.eventEmitter.emit(EVENT_ON_CHAT, content);
569
+ this.emit(EVENT_ON_CHAT, content);
471
570
  break;
472
571
 
473
572
  case MESSAGE_TYPE_SIGNAL:
474
573
  {
475
- this.eventEmitter.emit(EVENT_ON_SIGNAL, content);
574
+ this.emit(EVENT_ON_SIGNAL, content);
476
575
  break;
477
576
  }
478
577
 
@@ -31,7 +31,7 @@ import IssueReporter from './service/IssueReporter';
31
31
  import Heartbeat from './utils/Heartbeat';
32
32
  import { getSipSessionId, makeURI, replaceLocalIpModifier, SIP_ID_LENGTH } from './utils/sdp';
33
33
  import type { MediaConfig, UserAgentConfigOverrides, WebRtcConfig, IncomingResponse, PeerConnection, WazoSession, WazoTransport } from './domain/types';
34
- import { cleanupStream, createUaOptions, fetchNetworkStats, getAudioConstraints, getLocalStream, getLocalTracks, getLocalVideoStream, getMediaConfiguration, getPeerConnection, getRemoteStream, getRemoteTracks, getRemoteVideoStream, getRemoteVideoStreamFromPc, getStreamFromConstraints, getVideoConstraints, hasALocalVideoTrack, hasARemoteVideoTrack, hasAVideoTrack, hasLocalVideo, hasRemoteVideo, hasVideoTrack, isAudioOnly, isVideoRemotelyHeld, isWeb, sessionWantsToDoVideo, setLocalMediaStream, toggleAudio, toggleVideo, updateRemoteStream } from './utils/webrtc';
34
+ import { cleanupStream, createUaOptions, fetchNetworkStats, getAudioConstraints, getLocalStream, getLocalTracks, getLocalVideoStream, getMediaConfiguration, getPeerConnection, getRemoteStream, getRemoteTracks, getRemoteVideoStream, getRemoteVideoStreamFromPc, getVideoConstraints, hasALocalVideoTrack, hasARemoteVideoTrack, hasAVideoTrack, hasLocalVideo, hasRemoteVideo, hasVideoTrack, isAudioOnly, isVideoRemotelyHeld, isWeb, sessionWantsToDoVideo, setLocalMediaStream, toggleAudio, toggleVideo, updateRemoteStream } from './utils/webrtc';
35
35
 
36
36
  const SEND_STATS_DELAY = 5000;
37
37
  const states = ['STATUS_NULL', 'STATUS_NEW', 'STATUS_CONNECTING', 'STATUS_CONNECTED', 'STATUS_COMPLETED'];