@wazo/sdk 1.0.0-alpha.0 → 1.0.0-alpha.2

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 (83) hide show
  1. package/dist/wazo-sdk.js +6 -6
  2. package/esm/__tests__/__voice__/call.test.js +44 -2
  3. package/esm/__tests__/api-client.test.js +20 -1
  4. package/esm/api/auth.js +13 -2
  5. package/esm/domain/Phone/WebRTCPhone.js +6 -7
  6. package/esm/index.js +2 -0
  7. package/esm/simple/Auth.js +6 -12
  8. package/esm/simple/Phone.js +1 -0
  9. package/esm/simple/__tests__/Auth.test.js +8 -64
  10. package/esm/state-machine/call-state-machine.js +1 -1
  11. package/esm/types/api/auth.d.ts.map +1 -1
  12. package/esm/types/domain/Phone/WebRTCPhone.d.ts.map +1 -1
  13. package/esm/types/index.d.ts +2 -0
  14. package/esm/types/index.d.ts.map +1 -1
  15. package/esm/types/simple/Auth.d.ts.map +1 -1
  16. package/esm/types/simple/Phone.d.ts.map +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/api-requester.d.ts.map +1 -1
  20. package/esm/types/utils/sdp.d.ts +2 -0
  21. package/esm/types/utils/sdp.d.ts.map +1 -1
  22. package/esm/types/utils/webrtc.d.ts +1 -0
  23. package/esm/types/utils/webrtc.d.ts.map +1 -1
  24. package/esm/types/voice/call.d.ts +19 -11
  25. package/esm/types/voice/call.d.ts.map +1 -1
  26. package/esm/types/voice/softphone.d.ts +18 -13
  27. package/esm/types/voice/softphone.d.ts.map +1 -1
  28. package/esm/utils/__tests__/api-requester.test.js +17 -0
  29. package/esm/utils/api-requester.js +9 -1
  30. package/esm/utils/sdp.js +13 -0
  31. package/esm/utils/webrtc.js +21 -1
  32. package/esm/voice/call.js +90 -30
  33. package/esm/voice/softphone.js +94 -23
  34. package/lib/__tests__/__voice__/call.test.js +43 -1
  35. package/lib/__tests__/api-client.test.js +20 -1
  36. package/lib/api/auth.js +13 -2
  37. package/lib/domain/Phone/WebRTCPhone.js +5 -6
  38. package/lib/index.js +6 -2
  39. package/lib/simple/Auth.js +6 -12
  40. package/lib/simple/Phone.js +1 -0
  41. package/lib/simple/__tests__/Auth.test.js +11 -64
  42. package/lib/state-machine/call-state-machine.js +1 -1
  43. package/lib/types/api/auth.d.ts.map +1 -1
  44. package/lib/types/domain/Phone/WebRTCPhone.d.ts.map +1 -1
  45. package/lib/types/index.d.ts +2 -0
  46. package/lib/types/index.d.ts.map +1 -1
  47. package/lib/types/simple/Auth.d.ts.map +1 -1
  48. package/lib/types/simple/Phone.d.ts.map +1 -1
  49. package/lib/types/state-machine/utils.d.ts +1 -1
  50. package/lib/types/state-machine/utils.d.ts.map +1 -1
  51. package/lib/types/utils/api-requester.d.ts.map +1 -1
  52. package/lib/types/utils/sdp.d.ts +2 -0
  53. package/lib/types/utils/sdp.d.ts.map +1 -1
  54. package/lib/types/utils/webrtc.d.ts +1 -0
  55. package/lib/types/utils/webrtc.d.ts.map +1 -1
  56. package/lib/types/voice/call.d.ts +19 -11
  57. package/lib/types/voice/call.d.ts.map +1 -1
  58. package/lib/types/voice/softphone.d.ts +18 -13
  59. package/lib/types/voice/softphone.d.ts.map +1 -1
  60. package/lib/utils/__tests__/api-requester.test.js +17 -0
  61. package/lib/utils/api-requester.js +9 -1
  62. package/lib/utils/sdp.js +16 -1
  63. package/lib/utils/webrtc.js +23 -2
  64. package/lib/voice/call.js +87 -28
  65. package/lib/voice/softphone.js +95 -23
  66. package/package.json +1 -1
  67. package/src/__tests__/__voice__/call.test.ts +47 -2
  68. package/src/__tests__/api-client.test.ts +22 -1
  69. package/src/api/auth.ts +25 -3
  70. package/src/domain/Phone/WebRTCPhone.ts +9 -9
  71. package/src/index.ts +2 -0
  72. package/src/simple/Auth.ts +6 -13
  73. package/src/simple/Phone.ts +1 -0
  74. package/src/simple/__tests__/Auth.test.ts +9 -87
  75. package/src/state-machine/call-state-machine.ts +1 -1
  76. package/src/state-machine/utils.ts +2 -2
  77. package/src/utils/__tests__/api-requester.test.ts +20 -0
  78. package/src/utils/api-requester.ts +9 -1
  79. package/src/utils/sdp.ts +16 -0
  80. package/src/utils/webrtc.ts +24 -2
  81. package/src/voice/call.ts +118 -40
  82. package/src/voice/softphone.ts +116 -30
  83. package/src/web-rtc-client.ts +1 -1
package/src/voice/call.ts CHANGED
@@ -8,13 +8,15 @@ import { SessionDescriptionHandler } from 'sip.js/lib/platform/web';
8
8
  import { SessionDescriptionHandlerOptions } from 'sip.js/lib/api';
9
9
  import type { IncomingRequestMessage } from 'sip.js/lib/core/messages/incoming-request-message';
10
10
 
11
- import callStateMachine, { type ActionTypes, Actions, type EstablishedActionTypes, EstablishedActions, States, EstablishedStates, type CallActorRef } from '../state-machine/call-state-machine';
11
+ import callStateMachine, { type ActionTypes, Actions, type EstablishedActionTypes, EstablishedActions, States, type StateTypes, EstablishedStates, type EstablishedStateTypes, type CallActorRef } from '../state-machine/call-state-machine';
12
12
  import { SipCall, PeerConnection } from '../domain/types';
13
+ import ApiCall from '../domain/Call';
14
+
13
15
  import IssueReporter from '../service/IssueReporter';
14
- import { Softphone } from './softphone';
15
- import { getSipSessionId } from '../utils/sdp';
16
+ import softphone, { EVENT_TERMINATE_SOUND, Softphone } from './softphone';
17
+ import { getCallDisplayName, getCallNumber, getSipSessionId } from '../utils/sdp';
16
18
  import { assertCan, assertState, getState } from '../state-machine/utils';
17
- import { downgradeToAudio, getLocalStream, getLocalVideoStream, getPeerConnection, getRemoteStream, getRemoteVideoStream, getStreamFromConstraints, getWebRtcStats, hasALocalVideoTrack, hasLocalVideo, hasRemoteVideo, isVideoRemotelyHeld, setLocalMediaStream, toggleVideo, upgradeToVideo } from '../utils/webrtc';
19
+ import { downgradeToAudio, getLocalStream, getLocalVideoStream, getPeerConnection, getRemoteStream, getRemoteVideoStream, getStreamFromConstraints, getWebRtcStats, hasALocalVideoTrack, hasLocalVideo, hasRemoteVideo, isVideoMuted, isVideoRemotelyHeld, setLocalMediaStream, toggleVideo, upgradeToVideo } from '../utils/webrtc';
18
20
 
19
21
  const logger = IssueReporter.loggerFor('call');
20
22
 
@@ -53,15 +55,12 @@ export const EVENT_SHARE_SCREEN_ENDED = 'onScreenShareEnded';
53
55
  export const EVENT_WEB_RTC_STATS = 'webRtcStats';
54
56
  export const EVENT_CAMERA_RESUMED = 'onCameraResumed';
55
57
  export const EVENT_CAMERA_DISABLED = 'onCameraDisabled';
56
- export const EVENT_TERMINATE_SOUND = 'terminateSound';
57
58
 
58
59
  export const MESSAGE_TYPE_CHAT = 'message/TYPE_CHAT';
59
60
  export const MESSAGE_TYPE_SIGNAL = 'message/TYPE_SIGNAL';
60
61
  export const MESSAGE_TRACK_UPDATED = 'onTrackUpdated';
61
62
 
62
63
  class Call extends EventEmitter {
63
- eventEmitter: EventEmitter;
64
-
65
64
  callActor: CallActorRef;
66
65
 
67
66
  // Wazo call id, used in Wazo Websocket and API
@@ -81,8 +80,65 @@ class Call extends EventEmitter {
81
80
 
82
81
  shouldSendReinvite = false;
83
82
 
83
+ recording = false;
84
+
85
+ displayName: string | null;
86
+
87
+ number: string | null;
88
+
84
89
  currentScreenShare: Record<string, any> | null;
85
90
 
91
+ static parseCall(apiCall: ApiCall): Call {
92
+ const sipCall = {
93
+ id: apiCall.sipCallId,
94
+ stateChange: {
95
+ addListener: () => {},
96
+ },
97
+ remoteIdentity: {
98
+ uri: {
99
+ _normal: {
100
+ user: apiCall.calleeNumber,
101
+ },
102
+ },
103
+ },
104
+ // Mock sdk so `hasLocalVideo` will return true.
105
+ sessionDescriptionHandler: {
106
+ localMediaStream: {
107
+ getTracks: () => (apiCall.isVideo ? [{ kind: 'video', readyState: 'live' }] : []),
108
+ },
109
+ },
110
+ } as any;
111
+
112
+ const call = new Call(sipCall, softphone);
113
+ // call._sendAction(Actions.ES);
114
+ call.apiId = apiCall.id;
115
+ call.answerTime = new Date(+apiCall.startingTime);
116
+
117
+ // Update call state
118
+ const snapshot = call.callActor.getSnapshot();
119
+
120
+ if (apiCall.isUp()) {
121
+ snapshot.value = { [States.ESTABLISHED]: EstablishedStates.ONGOING };
122
+ }
123
+
124
+ if (apiCall.muted) {
125
+ snapshot.value = { [States.ESTABLISHED]: EstablishedStates.MUTED };
126
+ }
127
+
128
+ if (apiCall.isRinging()) {
129
+ snapshot.value = States.RINGING;
130
+ }
131
+
132
+ if (apiCall.isRecording()) {
133
+ call.recording = true;
134
+ }
135
+
136
+ // @ts-ignore
137
+ call.callActor.update(snapshot);
138
+
139
+ return call;
140
+ }
141
+
86
142
  constructor(sipCall: SipCall, phone: Softphone) {
87
143
  super();
88
144
 
@@ -94,6 +150,9 @@ class Call extends EventEmitter {
94
150
  this.phone = phone;
95
151
  this.sipCall = sipCall;
96
152
 
153
+ this.number = getCallNumber(sipCall);
154
+ this.displayName = getCallDisplayName(sipCall);
155
+
97
156
  this._bindEvents();
98
157
  }
99
158
 
@@ -123,7 +182,7 @@ class Call extends EventEmitter {
123
182
  assertCan(this.callActor, Actions.REJECT);
124
183
  logger.info('call - reject', { id: this.id });
125
184
 
126
- this.eventEmitter.emit(EVENT_TERMINATE_SOUND, 'call rejected locally');
185
+ this.phone.emit(EVENT_TERMINATE_SOUND, 'call rejected locally');
127
186
 
128
187
  this.shouldSendReinvite = false;
129
188
 
@@ -137,7 +196,7 @@ class Call extends EventEmitter {
137
196
  await this.phone.client.hangup(this.sipCall);
138
197
 
139
198
  // Remove this call from softphone's calls
140
- this.phone.removeCall(this);
199
+ this.phone.onCallTerminated(this);
141
200
 
142
201
  return true;
143
202
  }
@@ -151,7 +210,7 @@ class Call extends EventEmitter {
151
210
  }
152
211
  }
153
212
 
154
- startConference(withCalls: Call[]) {
213
+ startConference(/* withCalls: Call[] */) {
155
214
  // @TODO
156
215
  // const participants = [this, ...withCalls].reduce((acc: Record<string, any>, call: Call) => {
157
216
  // acc[call.talkingToIds[0]] = call;
@@ -174,7 +233,7 @@ class Call extends EventEmitter {
174
233
 
175
234
  this.phone.client.mute(this.sipCall);
176
235
 
177
- this.eventEmitter.emit(EVENT_CALL_MUTED);
236
+ this.emit(EVENT_CALL_MUTED);
178
237
  }
179
238
 
180
239
  unMute() {
@@ -185,7 +244,7 @@ class Call extends EventEmitter {
185
244
 
186
245
  this.phone.client.unmute(this.sipCall);
187
246
 
188
- this.eventEmitter.emit(EVENT_CALL_UN_MUTED);
247
+ this.emit(EVENT_CALL_UN_MUTED);
189
248
  }
190
249
 
191
250
  async hold() {
@@ -208,7 +267,7 @@ class Call extends EventEmitter {
208
267
 
209
268
  const promise = this.phone.client.hold(this.sipCall, this.isConference(), hasVideo);
210
269
 
211
- this.eventEmitter.emit(EVENT_CALL_HELD);
270
+ this.emit(EVENT_CALL_HELD);
212
271
 
213
272
  return promise;
214
273
  }
@@ -244,7 +303,7 @@ class Call extends EventEmitter {
244
303
  }
245
304
  };
246
305
 
247
- this.eventEmitter.emit(EVENT_CALL_RESUMED);
306
+ this.emit(EVENT_CALL_RESUMED);
248
307
 
249
308
  return promise.then(() => {
250
309
  const stream = this.getLocalVideoStream();
@@ -261,7 +320,7 @@ class Call extends EventEmitter {
261
320
  logger.info('call - turn camera on', { id: this.id });
262
321
  toggleVideo(this.sipCall, true);
263
322
 
264
- this.eventEmitter.emit(EVENT_CAMERA_RESUMED);
323
+ this.emit(EVENT_CAMERA_RESUMED);
265
324
  }
266
325
 
267
326
  turnCameraOff(): void {
@@ -269,15 +328,23 @@ class Call extends EventEmitter {
269
328
  logger.info('call - turn camera off', { id: this.id });
270
329
  toggleVideo(this.sipCall, false);
271
330
 
272
- this.eventEmitter.emit(EVENT_CAMERA_DISABLED);
331
+ this.emit(EVENT_CAMERA_DISABLED);
332
+ }
333
+
334
+ isMuted() {
335
+ return this._hasEstablishedState(EstablishedStates.MUTED);
336
+ }
337
+
338
+ isCameraMuted() {
339
+ return isVideoMuted(this.sipCall);
273
340
  }
274
341
 
275
342
  isHeld() {
276
- return getState(this.callActor) === EstablishedStates.HELD;
343
+ return this._hasEstablishedState(EstablishedStates.HELD);
277
344
  }
278
345
 
279
346
  isEstablished() {
280
- return getState(this.callActor) === States.ESTABLISHED;
347
+ return States.ESTABLISHED in (this.state as Record<StateTypes, EstablishedStateTypes>);
281
348
  }
282
349
 
283
350
  isConference(): boolean {
@@ -446,7 +513,7 @@ class Call extends EventEmitter {
446
513
  console.warn(e);
447
514
  }
448
515
 
449
- this.eventEmitter.emit(EVENT_SHARE_SCREEN_ENDED);
516
+ this.emit(EVENT_SHARE_SCREEN_ENDED);
450
517
  this.currentScreenShare = null;
451
518
 
452
519
  return reinvited;
@@ -480,12 +547,16 @@ class Call extends EventEmitter {
480
547
  this.emit(EVENT_CALL_ERROR, error);
481
548
  }
482
549
 
550
+ onCallIncoming() {
551
+ this._sendAction(Actions.INCOMING_CALL);
552
+ }
553
+
483
554
  onRejected() {
484
555
  this.endTime = new Date();
485
556
 
486
557
  this.emit(EVENT_CALL_REJECTED);
487
558
 
488
- this.phone.removeCall(this);
559
+ this.phone.onCallTerminated(this);
489
560
  }
490
561
 
491
562
  onNetworkStats(stats: Record<string, any>, previousStats: Record<string, any>) {
@@ -524,6 +595,10 @@ class Call extends EventEmitter {
524
595
  return isVideoRemotelyHeld(this.sipCall);
525
596
  }
526
597
 
598
+ isRinging() {
599
+ return this.state === States.RINGING;
600
+ }
601
+
527
602
  onReinvite(request: IncomingRequestMessage, updatedCalleeName: string, updatedNumber: string, hadRemoteVideo: boolean) {
528
603
  this.emit(EVENT_REINVITE, request, updatedCalleeName, updatedNumber, hadRemoteVideo);
529
604
  }
@@ -532,13 +607,16 @@ class Call extends EventEmitter {
532
607
  return getSipSessionId(this.sipCall);
533
608
  }
534
609
 
535
- get number() {
536
- const identity = this.sipCall.remoteIdentity || this.sipCall.assertedIdentity;
537
- // @ts-ignore: private
538
- return identity ? identity.uri._normal.user : null;
610
+ get state(): StateTypes | Record<StateTypes, EstablishedStateTypes> {
611
+ return getState(this.callActor) as StateTypes | Record<StateTypes, EstablishedStateTypes>;
539
612
  }
540
613
 
541
- _bindEvents() {
614
+ private _hasEstablishedState(establishedState: EstablishedStateTypes) {
615
+ const state = this.state as Record<StateTypes, EstablishedStateTypes>;
616
+ return States.ESTABLISHED in state && state[States.ESTABLISHED] === establishedState;
617
+ }
618
+
619
+ private _bindEvents() {
542
620
  if (this.sipCall instanceof Invitation) {
543
621
  // Monkey patch to know when canceled with the CANCEL message
544
622
  const onCancel = (this.sipCall as Invitation)._onCancel.bind(this.sipCall);
@@ -548,7 +626,7 @@ class Call extends EventEmitter {
548
626
  onCancel(message);
549
627
  const elsewhere = message.data.indexOf('cause=26') !== -1 && message.data.indexOf('completed elsewhere') !== -1;
550
628
 
551
- this.eventEmitter.emit(EVENT_CALL_CANCELED, elsewhere);
629
+ this.emit(EVENT_CALL_CANCELED, elsewhere);
552
630
  };
553
631
  }
554
632
 
@@ -556,7 +634,7 @@ class Call extends EventEmitter {
556
634
  switch (newState) {
557
635
  case SessionState.Terminating:
558
636
  logger.info('call - terminating', { id: this.id });
559
- this.eventEmitter.emit(EVENT_CALL_TERMINATING);
637
+ this.emit(EVENT_CALL_TERMINATING);
560
638
  break;
561
639
 
562
640
  case SessionState.Terminated:
@@ -566,7 +644,9 @@ class Call extends EventEmitter {
566
644
  // Should be called before `_onCallTerminated` or the callCount will not decrement...
567
645
  this.endTime = new Date();
568
646
 
569
- return this.eventEmitter.emit(EVENT_CALL_ENDED);
647
+ this.emit(EVENT_CALL_ENDED);
648
+
649
+ return this.phone.onCallTerminated(this);
570
650
  }
571
651
 
572
652
  default:
@@ -599,10 +679,10 @@ class Call extends EventEmitter {
599
679
  });
600
680
 
601
681
  if (kind === 'audio') {
602
- return this.eventEmitter.emit(EVENT_AUDIO_STREAM, stream);
682
+ return this.emit(EVENT_AUDIO_STREAM, stream);
603
683
  }
604
684
 
605
- return this.eventEmitter.emit(EVENT_VIDEO_STREAM, stream, event.track.id, event);
685
+ return this.emit(EVENT_VIDEO_STREAM, stream, event.track.id, event);
606
686
  };
607
687
 
608
688
  peerConnection.onremovestream = (event: any) => {
@@ -610,13 +690,11 @@ class Call extends EventEmitter {
610
690
  id: event.stream.id,
611
691
  tracks: event.stream.getTracks(),
612
692
  });
613
- this.eventEmitter.emit(EVENT_REMOVE_STREAM, event.stream);
693
+ this.emit(EVENT_REMOVE_STREAM, event.stream);
614
694
  };
615
-
616
- // Client events
617
695
  }
618
696
 
619
- _downgradeToAudio(withMessage = true) {
697
+ private _downgradeToAudio(withMessage = true) {
620
698
  assertState(this.callActor, States.ESTABLISHED);
621
699
  logger.info('call - downgrade to audio', { id: this.id, withMessage });
622
700
 
@@ -630,7 +708,7 @@ class Call extends EventEmitter {
630
708
  }
631
709
 
632
710
  // Returns true if we need to send a re-INVITE request
633
- async _upgradeToVideo(options: ReinviteOptions = {}): Promise<boolean> {
711
+ private async _upgradeToVideo(options: ReinviteOptions = {}): Promise<boolean> {
634
712
  assertState(this.callActor, States.ESTABLISHED);
635
713
  logger.info('call - upgrade to video', { id: this.id, ...options });
636
714
 
@@ -660,7 +738,7 @@ class Call extends EventEmitter {
660
738
  return false;
661
739
  }
662
740
 
663
- _sendReinviteMessage(isUpgrade: boolean) {
741
+ private _sendReinviteMessage(isUpgrade: boolean) {
664
742
  assertState(this.callActor, States.ESTABLISHED);
665
743
  logger.info('call - sending reinvite message', { id: this.id, isUpgrade });
666
744
 
@@ -679,7 +757,7 @@ class Call extends EventEmitter {
679
757
  }, 2500);
680
758
  }
681
759
 
682
- _onScreenSharing(screenStream: MediaStream, hadVideo: boolean, desktop?: boolean | null | undefined) {
760
+ private _onScreenSharing(screenStream: MediaStream, hadVideo: boolean, desktop?: boolean | null | undefined) {
683
761
  const screenTrack = screenStream.getVideoTracks()[0];
684
762
  const pc = this.phone.client.getPeerConnection(this.id);
685
763
  const sender = pc && pc.getSenders && pc.getSenders().find((s) => s && s.track && s.track.kind === 'video');
@@ -690,7 +768,7 @@ class Call extends EventEmitter {
690
768
  screenTrack.onended = () => {
691
769
  logger.info('call - on screenshare ended', { hadVideo, id: this.id, screenTrack });
692
770
 
693
- this.eventEmitter.emit(EVENT_SHARE_SCREEN_ENDING);
771
+ this.emit(EVENT_SHARE_SCREEN_ENDING);
694
772
  };
695
773
  }
696
774
 
@@ -702,10 +780,10 @@ class Call extends EventEmitter {
702
780
  };
703
781
 
704
782
  setLocalMediaStream(this.sipCall, screenStream);
705
- this.eventEmitter.emit(EVENT_SHARE_SCREEN_STARTED, screenStream);
783
+ this.emit(EVENT_SHARE_SCREEN_STARTED, screenStream);
706
784
  }
707
785
 
708
- _sendAction(action: ActionTypes | EstablishedActionTypes) {
786
+ private _sendAction(action: ActionTypes | EstablishedActionTypes) {
709
787
  this.callActor.send({ type: action });
710
788
  }
711
789
  }
@@ -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';
@@ -53,8 +57,6 @@ export class Softphone extends EventEmitter {
53
57
 
54
58
  audioRingVolume: number;
55
59
 
56
- eventEmitter: EventEmitter;
57
-
58
60
  calls: Call[];
59
61
 
60
62
  client: WazoWebRTCClient;
@@ -69,6 +71,8 @@ export class Softphone extends EventEmitter {
69
71
 
70
72
  shouldSendReinvite: boolean;
71
73
 
74
+ ringingEnabled = true;
75
+
72
76
  constructor() {
73
77
  super();
74
78
 
@@ -179,16 +183,16 @@ export class Softphone extends EventEmitter {
179
183
  return Promise.resolve(null);
180
184
  }
181
185
 
182
- if (this.getState() !== States.UNREGISTERED) {
183
- throw new InvalidState(`Invalid state ${States.UNREGISTERED}`, States.UNREGISTERED);
184
- }
185
-
186
- if (this.getState() !== States.REGISTERING) {
186
+ if (this.state === States.REGISTERING) {
187
187
  logger.info('softphone - calling when registering, waiting for Softphone to be registered ...');
188
188
 
189
189
  await waitUntilState(this.softphoneActor, States.REGISTERED);
190
190
  }
191
191
 
192
+ if (this.state !== States.UNREGISTERED) {
193
+ throw new InvalidState(`Invalid state ${States.UNREGISTERED}`, States.UNREGISTERED);
194
+ }
195
+
192
196
  // Hold other calls
193
197
  this.holdAllCalls();
194
198
 
@@ -198,6 +202,8 @@ export class Softphone extends EventEmitter {
198
202
  sipCall = this.client.call(options.params.To, options.withCamera, options.audioOnly, options.conference, options) as SipCall;
199
203
  const call = new Call(sipCall, this);
200
204
 
205
+ this.emit(EVENT_OUTGOING, call);
206
+
201
207
  return call;
202
208
  } catch (error: any) {
203
209
  logger.warn('softphone - call, error', { message: error.message, stack: error.stack });
@@ -209,6 +215,69 @@ export class Softphone extends EventEmitter {
209
215
  this.calls = this.calls.filter((call: Call) => call.id !== callToRemove.id);
210
216
  }
211
217
 
218
+ onCallTerminated(call: Call) {
219
+ logger.info('softphone - on call terminated', { id: call.id });
220
+
221
+ const currentCall = this.getCurrentCall();
222
+ const isCurrentCall = currentCall?.id === call.id;
223
+ const isRinging = call.isRinging();
224
+ const isCurrentIncomingCall = isCurrentCall && isRinging;
225
+ const incomingCalls = this.getIncomingCalls();
226
+ const nbIncomingCalls = incomingCalls.length;
227
+
228
+ // Send an event to terminate ringing sound
229
+ if (isCurrentIncomingCall) {
230
+ setTimeout(() => {
231
+ // Avoid race condition when the other is calling and hanging up immediately
232
+ this.emit(EVENT_TERMINATE_SOUND, call, 'call terminated');
233
+ }, 5);
234
+ }
235
+
236
+ // If the terminated call was an incoming call, we have to re-trigger if it's was the first incoming call
237
+ // Otherwise, we have to re-trigger an incoming call event if another call is incoming
238
+ const shouldRetrigger = isCurrentCall ? nbIncomingCalls : isCurrentIncomingCall;
239
+
240
+ // Re-trigger incoming call event for remaining incoming calls
241
+ logger.info('Softphone - check to re-trigger incoming call', { shouldRetrigger, nbIncomingCalls });
242
+
243
+ if (shouldRetrigger && nbIncomingCalls > 1) {
244
+ const nextCall = incomingCalls.find(c => c.id !== call.id);
245
+ // Avoid race condition
246
+ setTimeout(() => {
247
+ if (this.ringingEnabled) {
248
+ if (currentCall) {
249
+ this.emit(EVENT_PLAY_RING_SOUND, this.audioRingDeviceId, this.audioRingVolume, nextCall);
250
+ } else {
251
+ this.emit(EVENT_PLAY_INBOUND_CALL_SOUND, this.audioOutputDeviceId, this.audioOutputVolume, nextCall);
252
+ }
253
+ }
254
+
255
+ this.emit(EVENT_INCOMING, nextCall);
256
+ }, 100);
257
+ }
258
+
259
+ // @ts-ignore: does not exist
260
+ if (!call.sipCall.isCanceled) {
261
+ setTimeout(() => {
262
+ this.emit(EVENT_PLAY_HANGUP_SOUND, this.audioOutputDeviceId, this.audioOutputVolume, call);
263
+ }, 10);
264
+ }
265
+
266
+ this.client.onCallEnded(call.sipCall);
267
+
268
+ this.removeCall(call);
269
+ }
270
+
271
+ enableRinging(): Promise<void> | void {
272
+ logger.info('softphone - enable ringing');
273
+ this.ringingEnabled = true;
274
+ }
275
+
276
+ disableRinging(): Promise<void> | void {
277
+ logger.info('softphone - disable ringing');
278
+ this.ringingEnabled = false;
279
+ }
280
+
212
281
  holdAllCalls(exceptCall: Call | null = null) {
213
282
  this.calls.filter(call => !call.isHeld() && call.id === exceptCall?.id).forEach(call => call.hold());
214
283
  }
@@ -278,29 +347,43 @@ export class Softphone extends EventEmitter {
278
347
  this.audioRingVolume = volume;
279
348
  }
280
349
 
281
- getCalls(): Call[] {
282
- return this.calls;
283
- }
284
-
285
350
  getCurrentCall(): Call | undefined {
286
351
  return this.calls.find(call => call.isEstablished());
287
352
  }
288
353
 
289
- getState(): StateTypes {
290
- return this.softphoneActor.getSnapshot().value;
354
+ getIncomingCalls(): Call[] {
355
+ return this.calls.filter(call => call.isRinging());
356
+ }
357
+
358
+ get state(): StateTypes {
359
+ return getState(this.softphoneActor) as StateTypes;
291
360
  }
292
361
 
293
362
  getUserAgent() {
294
363
  return this.client?.config?.userAgentString || 'softphone';
295
364
  }
296
365
 
366
+ isRegistered() {
367
+ return this.state === States.REGISTERED;
368
+ }
369
+
297
370
  _bindEvents(): void {
298
371
  this.client.on(this.client.INVITE, (sipSession: SipCall) => {
299
372
  const call = new Call(sipSession, this);
300
- call._sendAction(CallActions.INCOMING_CALL);
373
+ logger.info('softphone - on invite', { callId: call.id, number: call.number });
374
+
375
+ call.onCallIncoming();
301
376
  this.calls.push(call);
302
377
 
303
- this.eventEmitter.emit(EVENT_INCOMING, call);
378
+ this.emit(EVENT_INCOMING, call);
379
+
380
+ if (!this.getCurrentCall()) {
381
+ if (this.ringingEnabled) {
382
+ this.emit(EVENT_PLAY_RING_SOUND, this.audioRingDeviceId, this.audioRingVolume, call);
383
+ }
384
+ } else {
385
+ this.emit(EVENT_PLAY_INBOUND_CALL_SOUND, this.audioOutputDeviceId, this.audioOutputVolume, call);
386
+ }
304
387
  });
305
388
 
306
389
  this.client.on(this.client.ON_REINVITE, (sipCall: SipCall, request: IncomingRequestMessage, updatedCalleeName: string, updatedNumber: string, hadRemoteVideo: boolean) => {
@@ -316,6 +399,9 @@ export class Softphone extends EventEmitter {
316
399
  });
317
400
 
318
401
  this.client.on(this.client.ACCEPTED, async (sipCall: SipCall) => {
402
+ const call = this._getCall(sipCall);
403
+ this.emit(EVENT_TERMINATE_SOUND, call, 'call accepted');
404
+
319
405
  this._onCallEvent(sipCall, 'onAccepted');
320
406
 
321
407
  if (this.audioOutputDeviceId) {
@@ -354,14 +440,14 @@ export class Softphone extends EventEmitter {
354
440
 
355
441
  this.client.on(this.client.UNREGISTERED, () => {
356
442
  logger.info('softphone - unregistered');
357
- this.eventEmitter.emit(EVENT_UNREGISTERED);
443
+ this.emit(EVENT_UNREGISTERED);
358
444
 
359
445
  this._sendAction(Actions.UNREGISTERED);
360
446
  });
361
447
 
362
448
  this.client.on(this.client.ON_DISCONNECTED, () => {
363
449
  logger.info('softphone - disconnected');
364
- this.eventEmitter.emit(EVENT_DISCONNECTED);
450
+ this.emit(EVENT_DISCONNECTED);
365
451
 
366
452
  this._sendAction(Actions.TRANSPORT_CLOSED);
367
453
  });
@@ -376,7 +462,7 @@ export class Softphone extends EventEmitter {
376
462
  this.stopHeartbeat();
377
463
  this._sendAction(Actions.REGISTER_DONE);
378
464
 
379
- this.eventEmitter.emit(EVENT_REGISTERED);
465
+ this.emit(EVENT_REGISTERED);
380
466
 
381
467
  // If the phone registered with a current callSession (eg: when switching network):
382
468
  // send a reinvite to renegociate ICE with new IP
@@ -396,7 +482,7 @@ export class Softphone extends EventEmitter {
396
482
 
397
483
  this.client.on(this.client.DISCONNECTED, () => {
398
484
  logger.info('softphone - disconnected');
399
- this.eventEmitter.emit(EVENT_DISCONNECTED);
485
+ this.emit(EVENT_DISCONNECTED);
400
486
 
401
487
  // Do not trigger heartbeat if already running
402
488
  if (!this.client.hasHeartbeat()) {
@@ -408,33 +494,33 @@ export class Softphone extends EventEmitter {
408
494
  });
409
495
 
410
496
  this.client.on('onVideoInputChange', stream => {
411
- this.eventEmitter.emit(EVENT_VIDEO_INPUT_CHANGE, stream);
497
+ this.emit(EVENT_VIDEO_INPUT_CHANGE, stream);
412
498
  });
413
499
 
414
500
  this.client.on(this.client.MESSAGE, (message: Message) => {
415
501
  this._onMessage(message);
416
502
 
417
- this.eventEmitter.emit(EVENT_ON_MESSAGE, message);
503
+ this.emit(EVENT_ON_MESSAGE, message);
418
504
  });
419
505
 
420
506
  this.client.setOnHeartbeatTimeout(() => {
421
- this.eventEmitter.emit(EVENT_HEARTBEAT_TIMEOUT);
507
+ this.emit(EVENT_HEARTBEAT_TIMEOUT);
422
508
  });
423
509
 
424
510
  this.client.setOnHeartbeatCallback(() => {
425
- this.eventEmitter.emit(EVENT_HEARTBEAT);
511
+ this.emit(EVENT_HEARTBEAT);
426
512
  });
427
513
  }
428
514
 
429
- _sendAction(action: ActionTypes) {
515
+ private _sendAction(action: ActionTypes) {
430
516
  this.softphoneActor.send({ type: action });
431
517
  }
432
518
 
433
- _getCall(sipCall: SipCall): Call | undefined {
519
+ private _getCall(sipCall: SipCall): Call | undefined {
434
520
  return this.calls.find(call => call.id === sipCall.id);
435
521
  }
436
522
 
437
- _onCallEvent(sipCall: SipCall, eventName: string, ...args: any[]) {
523
+ private _onCallEvent(sipCall: SipCall, eventName: string, ...args: any[]) {
438
524
  logger.warn('softphone - received call event', { eventName, sipId: sipCall.id });
439
525
 
440
526
  const call = this._getCall(sipCall);
@@ -447,7 +533,7 @@ export class Softphone extends EventEmitter {
447
533
  call[eventName](...args);
448
534
  }
449
535
 
450
- _onMessage(message: Message & { method?: string, body?: string }): void {
536
+ private _onMessage(message: Message & { method?: string, body?: string }): void {
451
537
  if (!message || message.method !== 'MESSAGE') {
452
538
  return;
453
539
  }
@@ -467,12 +553,12 @@ export class Softphone extends EventEmitter {
467
553
 
468
554
  switch (type) {
469
555
  case MESSAGE_TYPE_CHAT:
470
- this.eventEmitter.emit(EVENT_ON_CHAT, content);
556
+ this.emit(EVENT_ON_CHAT, content);
471
557
  break;
472
558
 
473
559
  case MESSAGE_TYPE_SIGNAL:
474
560
  {
475
- this.eventEmitter.emit(EVENT_ON_SIGNAL, content);
561
+ this.emit(EVENT_ON_SIGNAL, content);
476
562
  break;
477
563
  }
478
564
 
@@ -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'];