@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/lib/voice/call.js CHANGED
@@ -35,7 +35,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
35
35
  return (mod && mod.__esModule) ? mod : { "default": mod };
36
36
  };
37
37
  Object.defineProperty(exports, "__esModule", { value: true });
38
- exports.MESSAGE_TRACK_UPDATED = exports.MESSAGE_TYPE_SIGNAL = exports.MESSAGE_TYPE_CHAT = exports.EVENT_TERMINATE_SOUND = exports.EVENT_CAMERA_DISABLED = exports.EVENT_CAMERA_RESUMED = exports.EVENT_WEB_RTC_STATS = exports.EVENT_SHARE_SCREEN_ENDED = exports.EVENT_SHARE_SCREEN_STARTED = exports.EVENT_SHARE_SCREEN_ENDING = exports.EVENT_REMOVE_STREAM = exports.EVENT_VIDEO_STREAM = exports.EVENT_AUDIO_STREAM = exports.EVENT_TRACK = exports.EVENT_REINVITE = exports.EVENT_CALL_RESUMED = exports.EVENT_CALL_HELD = exports.EVENT_CALL_UN_MUTED = exports.EVENT_CALL_MUTED = exports.EVENT_CALL_ENDED = exports.EVENT_CALL_TERMINATING = exports.EVENT_CALL_REJECTED = exports.EVENT_CALL_ERROR = exports.EVENT_EARLY_MEDIA = exports.EVENT_PROGRESS = exports.EVENT_CALL_CANCELED = exports.EVENT_CALL_ACCEPTED = void 0;
38
+ exports.MESSAGE_TRACK_UPDATED = exports.MESSAGE_TYPE_SIGNAL = exports.MESSAGE_TYPE_CHAT = exports.EVENT_CAMERA_DISABLED = exports.EVENT_CAMERA_RESUMED = exports.EVENT_WEB_RTC_STATS = exports.EVENT_SHARE_SCREEN_ENDED = exports.EVENT_SHARE_SCREEN_STARTED = exports.EVENT_SHARE_SCREEN_ENDING = exports.EVENT_REMOVE_STREAM = exports.EVENT_VIDEO_STREAM = exports.EVENT_AUDIO_STREAM = exports.EVENT_TRACK = exports.EVENT_REINVITE = exports.EVENT_CALL_RESUMED = exports.EVENT_CALL_HELD = exports.EVENT_CALL_UN_MUTED = exports.EVENT_CALL_MUTED = exports.EVENT_CALL_ENDED = exports.EVENT_CALL_TERMINATING = exports.EVENT_CALL_REJECTED = exports.EVENT_CALL_ERROR = exports.EVENT_EARLY_MEDIA = exports.EVENT_PROGRESS = exports.EVENT_CALL_CANCELED = exports.EVENT_CALL_ACCEPTED = void 0;
39
39
  /* eslint-disable no-underscore-dangle */
40
40
  const events_1 = require("events");
41
41
  const xstate_1 = require("xstate");
@@ -43,6 +43,7 @@ const invitation_1 = require("sip.js/lib/api/invitation");
43
43
  const session_state_1 = require("sip.js/lib/api/session-state");
44
44
  const call_state_machine_1 = __importStar(require("../state-machine/call-state-machine"));
45
45
  const IssueReporter_1 = __importDefault(require("../service/IssueReporter"));
46
+ const softphone_1 = __importStar(require("./softphone"));
46
47
  const sdp_1 = require("../utils/sdp");
47
48
  const utils_1 = require("../state-machine/utils");
48
49
  const webrtc_1 = require("../utils/webrtc");
@@ -71,19 +72,63 @@ exports.EVENT_SHARE_SCREEN_ENDED = 'onScreenShareEnded';
71
72
  exports.EVENT_WEB_RTC_STATS = 'webRtcStats';
72
73
  exports.EVENT_CAMERA_RESUMED = 'onCameraResumed';
73
74
  exports.EVENT_CAMERA_DISABLED = 'onCameraDisabled';
74
- exports.EVENT_TERMINATE_SOUND = 'terminateSound';
75
75
  exports.MESSAGE_TYPE_CHAT = 'message/TYPE_CHAT';
76
76
  exports.MESSAGE_TYPE_SIGNAL = 'message/TYPE_SIGNAL';
77
77
  exports.MESSAGE_TRACK_UPDATED = 'onTrackUpdated';
78
78
  class Call extends events_1.EventEmitter {
79
+ static parseCall(apiCall) {
80
+ const sipCall = {
81
+ id: apiCall.sipCallId,
82
+ stateChange: {
83
+ addListener: () => { },
84
+ },
85
+ remoteIdentity: {
86
+ uri: {
87
+ _normal: {
88
+ user: apiCall.calleeNumber,
89
+ },
90
+ },
91
+ },
92
+ // Mock sdk so `hasLocalVideo` will return true.
93
+ sessionDescriptionHandler: {
94
+ localMediaStream: {
95
+ getTracks: () => (apiCall.isVideo ? [{ kind: 'video', readyState: 'live' }] : []),
96
+ },
97
+ },
98
+ };
99
+ const call = new Call(sipCall, softphone_1.default);
100
+ // call._sendAction(Actions.ES);
101
+ call.apiId = apiCall.id;
102
+ call.answerTime = new Date(+apiCall.startingTime);
103
+ // Update call state
104
+ const snapshot = call.callActor.getSnapshot();
105
+ if (apiCall.isUp()) {
106
+ snapshot.value = { [call_state_machine_1.States.ESTABLISHED]: call_state_machine_1.EstablishedStates.ONGOING };
107
+ }
108
+ if (apiCall.muted) {
109
+ snapshot.value = { [call_state_machine_1.States.ESTABLISHED]: call_state_machine_1.EstablishedStates.MUTED };
110
+ }
111
+ if (apiCall.isRinging()) {
112
+ snapshot.value = call_state_machine_1.States.RINGING;
113
+ }
114
+ if (apiCall.isRecording()) {
115
+ call.recording = true;
116
+ }
117
+ // @ts-ignore
118
+ call.callActor.update(snapshot);
119
+ return call;
120
+ }
79
121
  constructor(sipCall, phone) {
80
122
  super();
81
123
  this.shouldSendReinvite = false;
124
+ this.recording = false;
82
125
  this.callActor = (0, xstate_1.createActor)(call_state_machine_1.default);
83
126
  this.callActor.start();
84
127
  this.creationTime = new Date();
85
128
  this.phone = phone;
86
129
  this.sipCall = sipCall;
130
+ this.number = (0, sdp_1.getCallNumber)(sipCall);
131
+ this.displayName = (0, sdp_1.getCallDisplayName)(sipCall);
87
132
  this._bindEvents();
88
133
  }
89
134
  accept() {
@@ -108,7 +153,7 @@ class Call extends events_1.EventEmitter {
108
153
  return __awaiter(this, void 0, void 0, function* () {
109
154
  (0, utils_1.assertCan)(this.callActor, call_state_machine_1.Actions.REJECT);
110
155
  logger.info('call - reject', { id: this.id });
111
- this.eventEmitter.emit(exports.EVENT_TERMINATE_SOUND, 'call rejected locally');
156
+ this.phone.emit(softphone_1.EVENT_TERMINATE_SOUND, 'call rejected locally');
112
157
  this.shouldSendReinvite = false;
113
158
  return this.hangup();
114
159
  });
@@ -119,7 +164,7 @@ class Call extends events_1.EventEmitter {
119
164
  logger.info('call - hangup', { id: this.id });
120
165
  yield this.phone.client.hangup(this.sipCall);
121
166
  // Remove this call from softphone's calls
122
- this.phone.removeCall(this);
167
+ this.phone.onCallTerminated(this);
123
168
  return true;
124
169
  });
125
170
  }
@@ -132,7 +177,7 @@ class Call extends events_1.EventEmitter {
132
177
  logger.info('call - force cancel, error', e);
133
178
  }
134
179
  }
135
- startConference(withCalls) {
180
+ startConference( /* withCalls: Call[] */) {
136
181
  // @TODO
137
182
  // const participants = [this, ...withCalls].reduce((acc: Record<string, any>, call: Call) => {
138
183
  // acc[call.talkingToIds[0]] = call;
@@ -150,14 +195,14 @@ class Call extends events_1.EventEmitter {
150
195
  logger.info('call - mute', { id: this.id });
151
196
  this._sendAction(call_state_machine_1.EstablishedActions.MUTE);
152
197
  this.phone.client.mute(this.sipCall);
153
- this.eventEmitter.emit(exports.EVENT_CALL_MUTED);
198
+ this.emit(exports.EVENT_CALL_MUTED);
154
199
  }
155
200
  unMute() {
156
201
  (0, utils_1.assertCan)(this.callActor, call_state_machine_1.EstablishedActions.UN_MUTE);
157
202
  logger.info('call - un-mute', { id: this.id });
158
203
  this._sendAction(call_state_machine_1.EstablishedActions.UN_MUTE);
159
204
  this.phone.client.unmute(this.sipCall);
160
- this.eventEmitter.emit(exports.EVENT_CALL_UN_MUTED);
205
+ this.emit(exports.EVENT_CALL_UN_MUTED);
161
206
  }
162
207
  hold() {
163
208
  return __awaiter(this, void 0, void 0, function* () {
@@ -174,7 +219,7 @@ class Call extends events_1.EventEmitter {
174
219
  yield (0, webrtc_1.downgradeToAudio)(this.sipCall);
175
220
  }
176
221
  const promise = this.phone.client.hold(this.sipCall, this.isConference(), hasVideo);
177
- this.eventEmitter.emit(exports.EVENT_CALL_HELD);
222
+ this.emit(exports.EVENT_CALL_HELD);
178
223
  return promise;
179
224
  });
180
225
  }
@@ -203,7 +248,7 @@ class Call extends events_1.EventEmitter {
203
248
  this._onScreenSharing(stream, hadVideo, wasDesktop);
204
249
  }
205
250
  };
206
- this.eventEmitter.emit(exports.EVENT_CALL_RESUMED);
251
+ this.emit(exports.EVENT_CALL_RESUMED);
207
252
  return promise.then(() => {
208
253
  const stream = this.getLocalVideoStream();
209
254
  if (wasScreensharing) {
@@ -217,19 +262,25 @@ class Call extends events_1.EventEmitter {
217
262
  (0, utils_1.assertState)(this.callActor, call_state_machine_1.States.ESTABLISHED);
218
263
  logger.info('call - turn camera on', { id: this.id });
219
264
  (0, webrtc_1.toggleVideo)(this.sipCall, true);
220
- this.eventEmitter.emit(exports.EVENT_CAMERA_RESUMED);
265
+ this.emit(exports.EVENT_CAMERA_RESUMED);
221
266
  }
222
267
  turnCameraOff() {
223
268
  (0, utils_1.assertState)(this.callActor, call_state_machine_1.States.ESTABLISHED);
224
269
  logger.info('call - turn camera off', { id: this.id });
225
270
  (0, webrtc_1.toggleVideo)(this.sipCall, false);
226
- this.eventEmitter.emit(exports.EVENT_CAMERA_DISABLED);
271
+ this.emit(exports.EVENT_CAMERA_DISABLED);
272
+ }
273
+ isMuted() {
274
+ return this._hasEstablishedState(call_state_machine_1.EstablishedStates.MUTED);
275
+ }
276
+ isCameraMuted() {
277
+ return (0, webrtc_1.isVideoMuted)(this.sipCall);
227
278
  }
228
279
  isHeld() {
229
- return (0, utils_1.getState)(this.callActor) === call_state_machine_1.EstablishedStates.HELD;
280
+ return this._hasEstablishedState(call_state_machine_1.EstablishedStates.HELD);
230
281
  }
231
282
  isEstablished() {
232
- return (0, utils_1.getState)(this.callActor) === call_state_machine_1.States.ESTABLISHED;
283
+ return call_state_machine_1.States.ESTABLISHED in this.state;
233
284
  }
234
285
  isConference() {
235
286
  var _a;
@@ -366,7 +417,7 @@ class Call extends events_1.EventEmitter {
366
417
  catch (e) {
367
418
  console.warn(e);
368
419
  }
369
- this.eventEmitter.emit(exports.EVENT_SHARE_SCREEN_ENDED);
420
+ this.emit(exports.EVENT_SHARE_SCREEN_ENDED);
370
421
  this.currentScreenShare = null;
371
422
  return reinvited;
372
423
  });
@@ -392,10 +443,13 @@ class Call extends events_1.EventEmitter {
392
443
  onError(error) {
393
444
  this.emit(exports.EVENT_CALL_ERROR, error);
394
445
  }
446
+ onCallIncoming() {
447
+ this._sendAction(call_state_machine_1.Actions.INCOMING_CALL);
448
+ }
395
449
  onRejected() {
396
450
  this.endTime = new Date();
397
451
  this.emit(exports.EVENT_CALL_REJECTED);
398
- this.phone.removeCall(this);
452
+ this.phone.onCallTerminated(this);
399
453
  }
400
454
  onNetworkStats(stats, previousStats) {
401
455
  this.emit(exports.EVENT_WEB_RTC_STATS, stats, previousStats);
@@ -424,16 +478,21 @@ class Call extends events_1.EventEmitter {
424
478
  isVideoRemotelyHeld() {
425
479
  return (0, webrtc_1.isVideoRemotelyHeld)(this.sipCall);
426
480
  }
481
+ isRinging() {
482
+ return this.state === call_state_machine_1.States.RINGING;
483
+ }
427
484
  onReinvite(request, updatedCalleeName, updatedNumber, hadRemoteVideo) {
428
485
  this.emit(exports.EVENT_REINVITE, request, updatedCalleeName, updatedNumber, hadRemoteVideo);
429
486
  }
430
487
  get id() {
431
488
  return (0, sdp_1.getSipSessionId)(this.sipCall);
432
489
  }
433
- get number() {
434
- const identity = this.sipCall.remoteIdentity || this.sipCall.assertedIdentity;
435
- // @ts-ignore: private
436
- return identity ? identity.uri._normal.user : null;
490
+ get state() {
491
+ return (0, utils_1.getState)(this.callActor);
492
+ }
493
+ _hasEstablishedState(establishedState) {
494
+ const state = this.state;
495
+ return call_state_machine_1.States.ESTABLISHED in state && state[call_state_machine_1.States.ESTABLISHED] === establishedState;
437
496
  }
438
497
  _bindEvents() {
439
498
  if (this.sipCall instanceof invitation_1.Invitation) {
@@ -443,21 +502,22 @@ class Call extends events_1.EventEmitter {
443
502
  logger.trace('call - on sip session canceled', { callId: message.callId, id: this.id });
444
503
  onCancel(message);
445
504
  const elsewhere = message.data.indexOf('cause=26') !== -1 && message.data.indexOf('completed elsewhere') !== -1;
446
- this.eventEmitter.emit(exports.EVENT_CALL_CANCELED, elsewhere);
505
+ this.emit(exports.EVENT_CALL_CANCELED, elsewhere);
447
506
  };
448
507
  }
449
508
  this.sipCall.stateChange.addListener((newState) => {
450
509
  switch (newState) {
451
510
  case session_state_1.SessionState.Terminating:
452
511
  logger.info('call - terminating', { id: this.id });
453
- this.eventEmitter.emit(exports.EVENT_CALL_TERMINATING);
512
+ this.emit(exports.EVENT_CALL_TERMINATING);
454
513
  break;
455
514
  case session_state_1.SessionState.Terminated:
456
515
  {
457
516
  logger.info('call - terminated', { id: this.id });
458
517
  // Should be called before `_onCallTerminated` or the callCount will not decrement...
459
518
  this.endTime = new Date();
460
- return this.eventEmitter.emit(exports.EVENT_CALL_ENDED);
519
+ this.emit(exports.EVENT_CALL_ENDED);
520
+ return this.phone.onCallTerminated(this);
461
521
  }
462
522
  default:
463
523
  break;
@@ -484,18 +544,17 @@ class Call extends events_1.EventEmitter {
484
544
  kind,
485
545
  });
486
546
  if (kind === 'audio') {
487
- return this.eventEmitter.emit(exports.EVENT_AUDIO_STREAM, stream);
547
+ return this.emit(exports.EVENT_AUDIO_STREAM, stream);
488
548
  }
489
- return this.eventEmitter.emit(exports.EVENT_VIDEO_STREAM, stream, event.track.id, event);
549
+ return this.emit(exports.EVENT_VIDEO_STREAM, stream, event.track.id, event);
490
550
  };
491
551
  peerConnection.onremovestream = (event) => {
492
552
  logger.info('call - on remove stream called on the peer connection', {
493
553
  id: event.stream.id,
494
554
  tracks: event.stream.getTracks(),
495
555
  });
496
- this.eventEmitter.emit(exports.EVENT_REMOVE_STREAM, event.stream);
556
+ this.emit(exports.EVENT_REMOVE_STREAM, event.stream);
497
557
  };
498
- // Client events
499
558
  }
500
559
  _downgradeToAudio(withMessage = true) {
501
560
  (0, utils_1.assertState)(this.callActor, call_state_machine_1.States.ESTABLISHED);
@@ -558,7 +617,7 @@ class Call extends events_1.EventEmitter {
558
617
  if (screenTrack) {
559
618
  screenTrack.onended = () => {
560
619
  logger.info('call - on screenshare ended', { hadVideo, id: this.id, screenTrack });
561
- this.eventEmitter.emit(exports.EVENT_SHARE_SCREEN_ENDING);
620
+ this.emit(exports.EVENT_SHARE_SCREEN_ENDING);
562
621
  };
563
622
  }
564
623
  this.currentScreenShare = {
@@ -568,7 +627,7 @@ class Call extends events_1.EventEmitter {
568
627
  desktop,
569
628
  };
570
629
  (0, webrtc_1.setLocalMediaStream)(this.sipCall, screenStream);
571
- this.eventEmitter.emit(exports.EVENT_SHARE_SCREEN_STARTED, screenStream);
630
+ this.emit(exports.EVENT_SHARE_SCREEN_STARTED, screenStream);
572
631
  }
573
632
  _sendAction(action) {
574
633
  this.callActor.send({ type: action });
@@ -35,26 +35,30 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
35
35
  return (mod && mod.__esModule) ? mod : { "default": mod };
36
36
  };
37
37
  Object.defineProperty(exports, "__esModule", { value: true });
38
- exports.Softphone = exports.MESSAGE_TYPE_SIGNAL = exports.MESSAGE_TYPE_CHAT = exports.EVENT_ON_SIGNAL = exports.EVENT_ON_CHAT = exports.EVENT_ON_MESSAGE = exports.EVENT_VIDEO_INPUT_CHANGE = exports.EVENT_DISCONNECTED = exports.EVENT_UNREGISTERED = exports.EVENT_REGISTERED = exports.EVENT_HEARTBEAT_TIMEOUT = exports.EVENT_HEARTBEAT = exports.EVENT_INCOMING = void 0;
38
+ exports.Softphone = exports.MESSAGE_TYPE_SIGNAL = exports.MESSAGE_TYPE_CHAT = exports.EVENT_ON_SIGNAL = exports.EVENT_ON_CHAT = exports.EVENT_ON_MESSAGE = exports.EVENT_PLAY_INBOUND_CALL_SOUND = exports.EVENT_PLAY_RING_SOUND = exports.EVENT_PLAY_HANGUP_SOUND = exports.EVENT_TERMINATE_SOUND = exports.EVENT_VIDEO_INPUT_CHANGE = exports.EVENT_DISCONNECTED = exports.EVENT_UNREGISTERED = exports.EVENT_REGISTERED = exports.EVENT_HEARTBEAT_TIMEOUT = exports.EVENT_HEARTBEAT = exports.EVENT_OUTGOING = exports.EVENT_INCOMING = void 0;
39
39
  /* eslint-disable no-underscore-dangle */
40
40
  const events_1 = require("events");
41
41
  const xstate_1 = require("xstate");
42
42
  const InvalidState_1 = __importDefault(require("../domain/InvalidState"));
43
43
  const __1 = __importDefault(require(".."));
44
44
  const softphone_state_machine_1 = __importStar(require("../state-machine/softphone-state-machine"));
45
- const call_state_machine_1 = require("../state-machine/call-state-machine");
46
45
  const web_rtc_client_1 = __importDefault(require("../web-rtc-client"));
47
46
  const IssueReporter_1 = __importDefault(require("../service/IssueReporter"));
48
47
  const sip_logger_1 = __importDefault(require("../utils/sip-logger"));
49
48
  const call_1 = __importDefault(require("./call"));
50
49
  const utils_1 = require("../state-machine/utils");
51
50
  exports.EVENT_INCOMING = 'incoming';
51
+ exports.EVENT_OUTGOING = 'outgoing';
52
52
  exports.EVENT_HEARTBEAT = 'heartbeat';
53
53
  exports.EVENT_HEARTBEAT_TIMEOUT = 'heartbeatTimeout';
54
54
  exports.EVENT_REGISTERED = 'registered';
55
55
  exports.EVENT_UNREGISTERED = 'unregistered';
56
56
  exports.EVENT_DISCONNECTED = 'disconnected';
57
57
  exports.EVENT_VIDEO_INPUT_CHANGE = 'videoInputChange';
58
+ exports.EVENT_TERMINATE_SOUND = 'terminateSound';
59
+ exports.EVENT_PLAY_HANGUP_SOUND = 'playHangupSound';
60
+ exports.EVENT_PLAY_RING_SOUND = 'playRingSound';
61
+ exports.EVENT_PLAY_INBOUND_CALL_SOUND = 'playInboundCallSound';
58
62
  exports.EVENT_ON_MESSAGE = 'message';
59
63
  exports.EVENT_ON_CHAT = 'phone/ON_CHAT';
60
64
  exports.EVENT_ON_SIGNAL = 'phone/ON_SIGNAL';
@@ -64,6 +68,7 @@ const logger = IssueReporter_1.default.loggerFor('softphone');
64
68
  class Softphone extends events_1.EventEmitter {
65
69
  constructor() {
66
70
  super();
71
+ this.ringingEnabled = true;
67
72
  this.calls = [];
68
73
  this.softphoneActor = (0, xstate_1.createActor)(softphone_state_machine_1.default);
69
74
  this.softphoneActor.start();
@@ -138,19 +143,20 @@ class Softphone extends events_1.EventEmitter {
138
143
  logger.warn('softphone - calling with empty number, bailing.');
139
144
  return Promise.resolve(null);
140
145
  }
141
- if (this.getState() !== softphone_state_machine_1.States.UNREGISTERED) {
142
- throw new InvalidState_1.default(`Invalid state ${softphone_state_machine_1.States.UNREGISTERED}`, softphone_state_machine_1.States.UNREGISTERED);
143
- }
144
- if (this.getState() !== softphone_state_machine_1.States.REGISTERING) {
146
+ if (this.state === softphone_state_machine_1.States.REGISTERING) {
145
147
  logger.info('softphone - calling when registering, waiting for Softphone to be registered ...');
146
148
  yield (0, utils_1.waitUntilState)(this.softphoneActor, softphone_state_machine_1.States.REGISTERED);
147
149
  }
150
+ if (this.state !== softphone_state_machine_1.States.UNREGISTERED) {
151
+ throw new InvalidState_1.default(`Invalid state ${softphone_state_machine_1.States.UNREGISTERED}`, softphone_state_machine_1.States.UNREGISTERED);
152
+ }
148
153
  // Hold other calls
149
154
  this.holdAllCalls();
150
155
  let sipCall;
151
156
  try {
152
157
  sipCall = this.client.call(options.params.To, options.withCamera, options.audioOnly, options.conference, options);
153
158
  const call = new call_1.default(sipCall, this);
159
+ this.emit(exports.EVENT_OUTGOING, call);
154
160
  return call;
155
161
  }
156
162
  catch (error) {
@@ -162,6 +168,58 @@ class Softphone extends events_1.EventEmitter {
162
168
  removeCall(callToRemove) {
163
169
  this.calls = this.calls.filter((call) => call.id !== callToRemove.id);
164
170
  }
171
+ onCallTerminated(call) {
172
+ logger.info('softphone - on call terminated', { id: call.id });
173
+ const currentCall = this.getCurrentCall();
174
+ const isCurrentCall = (currentCall === null || currentCall === void 0 ? void 0 : currentCall.id) === call.id;
175
+ const isRinging = call.isRinging();
176
+ const isCurrentIncomingCall = isCurrentCall && isRinging;
177
+ const incomingCalls = this.getIncomingCalls();
178
+ const nbIncomingCalls = incomingCalls.length;
179
+ // Send an event to terminate ringing sound
180
+ if (isCurrentIncomingCall) {
181
+ setTimeout(() => {
182
+ // Avoid race condition when the other is calling and hanging up immediately
183
+ this.emit(exports.EVENT_TERMINATE_SOUND, call, 'call terminated');
184
+ }, 5);
185
+ }
186
+ // If the terminated call was an incoming call, we have to re-trigger if it's was the first incoming call
187
+ // Otherwise, we have to re-trigger an incoming call event if another call is incoming
188
+ const shouldRetrigger = isCurrentCall ? nbIncomingCalls : isCurrentIncomingCall;
189
+ // Re-trigger incoming call event for remaining incoming calls
190
+ logger.info('Softphone - check to re-trigger incoming call', { shouldRetrigger, nbIncomingCalls });
191
+ if (shouldRetrigger && nbIncomingCalls > 1) {
192
+ const nextCall = incomingCalls.find(c => c.id !== call.id);
193
+ // Avoid race condition
194
+ setTimeout(() => {
195
+ if (this.ringingEnabled) {
196
+ if (currentCall) {
197
+ this.emit(exports.EVENT_PLAY_RING_SOUND, this.audioRingDeviceId, this.audioRingVolume, nextCall);
198
+ }
199
+ else {
200
+ this.emit(exports.EVENT_PLAY_INBOUND_CALL_SOUND, this.audioOutputDeviceId, this.audioOutputVolume, nextCall);
201
+ }
202
+ }
203
+ this.emit(exports.EVENT_INCOMING, nextCall);
204
+ }, 100);
205
+ }
206
+ // @ts-ignore: does not exist
207
+ if (!call.sipCall.isCanceled) {
208
+ setTimeout(() => {
209
+ this.emit(exports.EVENT_PLAY_HANGUP_SOUND, this.audioOutputDeviceId, this.audioOutputVolume, call);
210
+ }, 10);
211
+ }
212
+ this.client.onCallEnded(call.sipCall);
213
+ this.removeCall(call);
214
+ }
215
+ enableRinging() {
216
+ logger.info('softphone - enable ringing');
217
+ this.ringingEnabled = true;
218
+ }
219
+ disableRinging() {
220
+ logger.info('softphone - disable ringing');
221
+ this.ringingEnabled = false;
222
+ }
165
223
  holdAllCalls(exceptCall = null) {
166
224
  this.calls.filter(call => !call.isHeld() && call.id === (exceptCall === null || exceptCall === void 0 ? void 0 : exceptCall.id)).forEach(call => call.hold());
167
225
  }
@@ -216,25 +274,37 @@ class Softphone extends events_1.EventEmitter {
216
274
  logger.info('softphone - changing ring volume', { volume });
217
275
  this.audioRingVolume = volume;
218
276
  }
219
- getCalls() {
220
- return this.calls;
221
- }
222
277
  getCurrentCall() {
223
278
  return this.calls.find(call => call.isEstablished());
224
279
  }
225
- getState() {
226
- return this.softphoneActor.getSnapshot().value;
280
+ getIncomingCalls() {
281
+ return this.calls.filter(call => call.isRinging());
282
+ }
283
+ get state() {
284
+ return (0, utils_1.getState)(this.softphoneActor);
227
285
  }
228
286
  getUserAgent() {
229
287
  var _a, _b;
230
288
  return ((_b = (_a = this.client) === null || _a === void 0 ? void 0 : _a.config) === null || _b === void 0 ? void 0 : _b.userAgentString) || 'softphone';
231
289
  }
290
+ isRegistered() {
291
+ return this.state === softphone_state_machine_1.States.REGISTERED;
292
+ }
232
293
  _bindEvents() {
233
294
  this.client.on(this.client.INVITE, (sipSession) => {
234
295
  const call = new call_1.default(sipSession, this);
235
- call._sendAction(call_state_machine_1.Actions.INCOMING_CALL);
296
+ logger.info('softphone - on invite', { callId: call.id, number: call.number });
297
+ call.onCallIncoming();
236
298
  this.calls.push(call);
237
- this.eventEmitter.emit(exports.EVENT_INCOMING, call);
299
+ this.emit(exports.EVENT_INCOMING, call);
300
+ if (!this.getCurrentCall()) {
301
+ if (this.ringingEnabled) {
302
+ this.emit(exports.EVENT_PLAY_RING_SOUND, this.audioRingDeviceId, this.audioRingVolume, call);
303
+ }
304
+ }
305
+ else {
306
+ this.emit(exports.EVENT_PLAY_INBOUND_CALL_SOUND, this.audioOutputDeviceId, this.audioOutputVolume, call);
307
+ }
238
308
  });
239
309
  this.client.on(this.client.ON_REINVITE, (sipCall, request, updatedCalleeName, updatedNumber, hadRemoteVideo) => {
240
310
  logger.info('softphone - on reinvite', {
@@ -247,6 +317,8 @@ class Softphone extends events_1.EventEmitter {
247
317
  this._onCallEvent(sipCall, 'onReinvite', request, updatedCalleeName, updatedNumber, hadRemoteVideo);
248
318
  });
249
319
  this.client.on(this.client.ACCEPTED, (sipCall) => __awaiter(this, void 0, void 0, function* () {
320
+ const call = this._getCall(sipCall);
321
+ this.emit(exports.EVENT_TERMINATE_SOUND, call, 'call accepted');
250
322
  this._onCallEvent(sipCall, 'onAccepted');
251
323
  if (this.audioOutputDeviceId) {
252
324
  yield this.client.changeAudioOutputDevice(this.audioOutputDeviceId);
@@ -276,12 +348,12 @@ class Softphone extends events_1.EventEmitter {
276
348
  });
277
349
  this.client.on(this.client.UNREGISTERED, () => {
278
350
  logger.info('softphone - unregistered');
279
- this.eventEmitter.emit(exports.EVENT_UNREGISTERED);
351
+ this.emit(exports.EVENT_UNREGISTERED);
280
352
  this._sendAction(softphone_state_machine_1.Actions.UNREGISTERED);
281
353
  });
282
354
  this.client.on(this.client.ON_DISCONNECTED, () => {
283
355
  logger.info('softphone - disconnected');
284
- this.eventEmitter.emit(exports.EVENT_DISCONNECTED);
356
+ this.emit(exports.EVENT_DISCONNECTED);
285
357
  this._sendAction(softphone_state_machine_1.Actions.TRANSPORT_CLOSED);
286
358
  });
287
359
  this.client.on(this.client.REGISTERED, () => {
@@ -292,7 +364,7 @@ class Softphone extends events_1.EventEmitter {
292
364
  }
293
365
  this.stopHeartbeat();
294
366
  this._sendAction(softphone_state_machine_1.Actions.REGISTER_DONE);
295
- this.eventEmitter.emit(exports.EVENT_REGISTERED);
367
+ this.emit(exports.EVENT_REGISTERED);
296
368
  // If the phone registered with a current callSession (eg: when switching network):
297
369
  // send a reinvite to renegociate ICE with new IP
298
370
  if (this.shouldSendReinvite) {
@@ -308,7 +380,7 @@ class Softphone extends events_1.EventEmitter {
308
380
  });
309
381
  this.client.on(this.client.DISCONNECTED, () => {
310
382
  logger.info('softphone - disconnected');
311
- this.eventEmitter.emit(exports.EVENT_DISCONNECTED);
383
+ this.emit(exports.EVENT_DISCONNECTED);
312
384
  // Do not trigger heartbeat if already running
313
385
  if (!this.client.hasHeartbeat()) {
314
386
  this.startHeartbeat();
@@ -317,17 +389,17 @@ class Softphone extends events_1.EventEmitter {
317
389
  this.shouldSendReinvite = this.calls.length > 0;
318
390
  });
319
391
  this.client.on('onVideoInputChange', stream => {
320
- this.eventEmitter.emit(exports.EVENT_VIDEO_INPUT_CHANGE, stream);
392
+ this.emit(exports.EVENT_VIDEO_INPUT_CHANGE, stream);
321
393
  });
322
394
  this.client.on(this.client.MESSAGE, (message) => {
323
395
  this._onMessage(message);
324
- this.eventEmitter.emit(exports.EVENT_ON_MESSAGE, message);
396
+ this.emit(exports.EVENT_ON_MESSAGE, message);
325
397
  });
326
398
  this.client.setOnHeartbeatTimeout(() => {
327
- this.eventEmitter.emit(exports.EVENT_HEARTBEAT_TIMEOUT);
399
+ this.emit(exports.EVENT_HEARTBEAT_TIMEOUT);
328
400
  });
329
401
  this.client.setOnHeartbeatCallback(() => {
330
- this.eventEmitter.emit(exports.EVENT_HEARTBEAT);
402
+ this.emit(exports.EVENT_HEARTBEAT);
331
403
  });
332
404
  }
333
405
  _sendAction(action) {
@@ -360,11 +432,11 @@ class Softphone extends events_1.EventEmitter {
360
432
  const { type, content, } = body;
361
433
  switch (type) {
362
434
  case exports.MESSAGE_TYPE_CHAT:
363
- this.eventEmitter.emit(exports.EVENT_ON_CHAT, content);
435
+ this.emit(exports.EVENT_ON_CHAT, content);
364
436
  break;
365
437
  case exports.MESSAGE_TYPE_SIGNAL:
366
438
  {
367
- this.eventEmitter.emit(exports.EVENT_ON_SIGNAL, content);
439
+ this.emit(exports.EVENT_ON_SIGNAL, content);
368
440
  break;
369
441
  }
370
442
  default:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wazo/sdk",
3
- "version": "1.0.0-alpha.0",
3
+ "version": "1.0.0-alpha.2",
4
4
  "description": "Wazo's JavaScript Software Development Kit.",
5
5
  "main": "index.js",
6
6
  "types": "lib/types/index.d.ts",
@@ -3,8 +3,9 @@ import { Inviter, UserAgent, type URI } from 'sip.js';
3
3
 
4
4
  import softphone from '../../voice/softphone';
5
5
  import Call from '../../voice/call';
6
- import { Actions, States } from '../../state-machine/call-state-machine';
6
+ import { Actions, EstablishedStates, States } from '../../state-machine/call-state-machine';
7
7
  import { Actions as SoftphoneActions } from '../../state-machine/softphone-state-machine';
8
+ import ApiCall from '../../domain/Call';
8
9
 
9
10
  import InvalidStateTransition from '../../domain/InvalidStateTransition';
10
11
 
@@ -33,6 +34,7 @@ jest.mock('../../web-rtc-client', () => jest.fn().mockImplementation(() => ({
33
34
  answer: jest.fn(() => Promise.resolve()),
34
35
  hangup: jest.fn(() => Promise.resolve()),
35
36
  register: jest.fn(() => Promise.resolve()),
37
+ onCallEnded: jest.fn(() => Promise.resolve()),
36
38
  setOnHeartbeatTimeout: jest.fn(() => Promise.resolve()),
37
39
  setOnHeartbeatCallback: jest.fn(() => Promise.resolve()),
38
40
  on: jest.fn(),
@@ -42,11 +44,44 @@ const ua = new UserAgent(uaOptions);
42
44
  const sipCall = new Inviter(ua, UserAgent.makeURI('sip:1234@my.stack.io') as URI);
43
45
 
44
46
  softphone.connect();
47
+ // @ts-ignore: private method
45
48
  softphone._sendAction(SoftphoneActions.REGISTER_DONE);
46
49
 
47
50
  const invalidStateTransition = (state: string, action: string) => new InvalidStateTransition(`Invalid state transition from ${state} with action ${action}`, state, action);
48
51
 
49
52
  describe('Call', () => {
53
+ describe('parsing an API call', () => {
54
+ it('Should parse an API call and transform it to a Call instance', async () => {
55
+ const apiCall = new ApiCall({
56
+ id: '1234',
57
+ sipCallId: 'abcd',
58
+ isCaller: false,
59
+ isVideo: true,
60
+ recording: true,
61
+ callerName: '',
62
+ callerNumber: '',
63
+ calleeName: '',
64
+ calleeNumber: '0123423223',
65
+ dialedExtension: '',
66
+ lineId: null,
67
+ muted: true,
68
+ onHold: false,
69
+ startingTime: new Date(),
70
+ status: 'Up',
71
+ talkingToIds: [],
72
+ });
73
+ const call = Call.parseCall(apiCall);
74
+
75
+ expect(call.recording).toBeTruthy();
76
+ expect(call.id).toBe(apiCall.sipCallId);
77
+ expect(call.apiId).toBe(apiCall.id);
78
+ expect(call.number).toBe(apiCall.calleeNumber);
79
+ expect(call.answerTime).toStrictEqual(apiCall.startingTime);
80
+ expect(call.isMuted()).toBeTruthy();
81
+ expect(call.state).toStrictEqual({ [States.ESTABLISHED]: EstablishedStates.MUTED });
82
+ });
83
+ });
84
+
50
85
  describe('accept', () => {
51
86
  it('Should throw an error when accepting a not ringing call', async () => {
52
87
  const call = new Call(sipCall, softphone);
@@ -54,16 +89,24 @@ describe('Call', () => {
54
89
  await expect(call.accept()).rejects.toThrowError(invalidStateTransition(States.IDLE, Actions.ACCEPT));
55
90
  });
56
91
 
57
- it.only('Should throw an error when the softphone is not registered', async () => {
92
+ it('Should throw an error when the softphone is not registered', async () => {
58
93
  const call = new Call(sipCall, softphone);
94
+ // @ts-ignore: private method
59
95
  call._sendAction(Actions.INCOMING_CALL);
96
+ // @ts-ignore: private method
60
97
  softphone._sendAction(SoftphoneActions.TRANSPORT_CLOSED);
61
98
 
62
99
  await expect(call.accept()).rejects.toThrowError(invalidStateTransition(States.RINGING, Actions.ACCEPT));
100
+
101
+ // Revert softphone state
102
+ softphone.connect();
103
+ // @ts-ignore: private method
104
+ softphone._sendAction(SoftphoneActions.REGISTER_DONE);
63
105
  });
64
106
 
65
107
  it('Should not throw an error when accepting a not ringing call', async () => {
66
108
  const call = new Call(sipCall, softphone);
109
+ // @ts-ignore: private method
67
110
  call._sendAction(Actions.INCOMING_CALL);
68
111
 
69
112
  await call.accept();
@@ -80,7 +123,9 @@ describe('Call', () => {
80
123
 
81
124
  it('Should not throw an error when terminating an established call', async () => {
82
125
  const call = new Call(sipCall, softphone);
126
+ // @ts-ignore: private method
83
127
  call._sendAction(Actions.MAKE_CALL);
128
+ // @ts-ignore: private method
84
129
  call._sendAction(Actions.REMOTLY_ACCEPTED);
85
130
 
86
131
  await call.hangup();