@telegenta/webclient 3.1.2 → 3.2.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.
@@ -3,10 +3,10 @@
3
3
  <![endif]--><script src="scripts/third-party/hljs.js" defer="defer"></script><script src="scripts/third-party/hljs-line-num.js" defer="defer"></script><script src="scripts/third-party/popper.js" defer="defer"></script><script src="scripts/third-party/tippy.js" defer="defer"></script><script src="scripts/third-party/tocbot.min.js"></script><script>var baseURL="/",locationPathname="";baseURL=(locationPathname=document.location.pathname).substr(0,locationPathname.lastIndexOf("/")+1)</script><link rel="stylesheet" href="styles/clean-jsdoc-theme.min.css"><svg aria-hidden="true" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="display:none"><defs><symbol id="copy-icon" viewbox="0 0 488.3 488.3"><g><path d="M314.25,85.4h-227c-21.3,0-38.6,17.3-38.6,38.6v325.7c0,21.3,17.3,38.6,38.6,38.6h227c21.3,0,38.6-17.3,38.6-38.6V124 C352.75,102.7,335.45,85.4,314.25,85.4z M325.75,449.6c0,6.4-5.2,11.6-11.6,11.6h-227c-6.4,0-11.6-5.2-11.6-11.6V124 c0-6.4,5.2-11.6,11.6-11.6h227c6.4,0,11.6,5.2,11.6,11.6V449.6z"/><path d="M401.05,0h-227c-21.3,0-38.6,17.3-38.6,38.6c0,7.5,6,13.5,13.5,13.5s13.5-6,13.5-13.5c0-6.4,5.2-11.6,11.6-11.6h227 c6.4,0,11.6,5.2,11.6,11.6v325.7c0,6.4-5.2,11.6-11.6,11.6c-7.5,0-13.5,6-13.5,13.5s6,13.5,13.5,13.5c21.3,0,38.6-17.3,38.6-38.6 V38.6C439.65,17.3,422.35,0,401.05,0z"/></g></symbol><symbol id="search-icon" viewBox="0 0 512 512"><g><g><path d="M225.474,0C101.151,0,0,101.151,0,225.474c0,124.33,101.151,225.474,225.474,225.474 c124.33,0,225.474-101.144,225.474-225.474C450.948,101.151,349.804,0,225.474,0z M225.474,409.323 c-101.373,0-183.848-82.475-183.848-183.848S124.101,41.626,225.474,41.626s183.848,82.475,183.848,183.848 S326.847,409.323,225.474,409.323z"/></g></g><g><g><path d="M505.902,476.472L386.574,357.144c-8.131-8.131-21.299-8.131-29.43,0c-8.131,8.124-8.131,21.306,0,29.43l119.328,119.328 c4.065,4.065,9.387,6.098,14.715,6.098c5.321,0,10.649-2.033,14.715-6.098C514.033,497.778,514.033,484.596,505.902,476.472z"/></g></g></symbol><symbol id="font-size-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M11.246 15H4.754l-2 5H.6L7 4h2l6.4 16h-2.154l-2-5zm-.8-2L8 6.885 5.554 13h4.892zM21 12.535V12h2v8h-2v-.535a4 4 0 1 1 0-6.93zM19 18a2 2 0 1 0 0-4 2 2 0 0 0 0 4z"/></symbol><symbol id="add-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M11 11V5h2v6h6v2h-6v6h-2v-6H5v-2z"/></symbol><symbol id="minus-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M5 11h14v2H5z"/></symbol><symbol id="dark-theme-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M10 7a7 7 0 0 0 12 4.9v.1c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2h.1A6.979 6.979 0 0 0 10 7zm-6 5a8 8 0 0 0 15.062 3.762A9 9 0 0 1 8.238 4.938 7.999 7.999 0 0 0 4 12z"/></symbol><symbol id="light-theme-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M12 18a6 6 0 1 1 0-12 6 6 0 0 1 0 12zm0-2a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM11 1h2v3h-2V1zm0 19h2v3h-2v-3zM3.515 4.929l1.414-1.414L7.05 5.636 5.636 7.05 3.515 4.93zM16.95 18.364l1.414-1.414 2.121 2.121-1.414 1.414-2.121-2.121zm2.121-14.85l1.414 1.415-2.121 2.121-1.414-1.414 2.121-2.121zM5.636 16.95l1.414 1.414-2.121 2.121-1.414-1.414 2.121-2.121zM23 11v2h-3v-2h3zM4 11v2H1v-2h3z"/></symbol><symbol id="reset-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M18.537 19.567A9.961 9.961 0 0 1 12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10c0 2.136-.67 4.116-1.81 5.74L17 12h3a8 8 0 1 0-2.46 5.772l.997 1.795z"/></symbol><symbol id="down-icon" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M12.7803 6.21967C13.0732 6.51256 13.0732 6.98744 12.7803 7.28033L8.53033 11.5303C8.23744 11.8232 7.76256 11.8232 7.46967 11.5303L3.21967 7.28033C2.92678 6.98744 2.92678 6.51256 3.21967 6.21967C3.51256 5.92678 3.98744 5.92678 4.28033 6.21967L8 9.93934L11.7197 6.21967C12.0126 5.92678 12.4874 5.92678 12.7803 6.21967Z"></path></symbol><symbol id="codepen-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M16.5 13.202L13 15.535v3.596L19.197 15 16.5 13.202zM14.697 12L12 10.202 9.303 12 12 13.798 14.697 12zM20 10.869L18.303 12 20 13.131V10.87zM19.197 9L13 4.869v3.596l3.5 2.333L19.197 9zM7.5 10.798L11 8.465V4.869L4.803 9 7.5 10.798zM4.803 15L11 19.131v-3.596l-3.5-2.333L4.803 15zM4 13.131L5.697 12 4 10.869v2.262zM2 9a1 1 0 0 1 .445-.832l9-6a1 1 0 0 1 1.11 0l9 6A1 1 0 0 1 22 9v6a1 1 0 0 1-.445.832l-9 6a1 1 0 0 1-1.11 0l-9-6A1 1 0 0 1 2 15V9z"/></symbol><symbol id="close-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M12 10.586l4.95-4.95 1.414 1.414-4.95 4.95 4.95 4.95-1.414 1.414-4.95-4.95-4.95 4.95-1.414-1.414 4.95-4.95-4.95-4.95L7.05 5.636z"/></symbol><symbol id="menu-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M3 4h18v2H3V4zm0 7h18v2H3v-2zm0 7h18v2H3v-2z"/></symbol></defs></svg></head><body data-theme="dark"><div class="sidebar-container"><div class="sidebar" id="sidebar"><div class="sidebar-items-container"><div class="sidebar-section-title with-arrow" data-isopen="false" id="sidebar-classes"><div>Classes</div><svg><use xlink:href="#down-icon"></use></svg></div><div class="sidebar-section-children-container"><div class="sidebar-section-children"><a href="EventClient.html">EventClient</a></div><div class="sidebar-section-children"><a href="EventServiceNotConnected.html">EventServiceNotConnected</a></div><div class="sidebar-section-children"><a href="InvalidMediaDevice.html">InvalidMediaDevice</a></div><div class="sidebar-section-children"><a href="InvalidOptions.html">InvalidOptions</a></div><div class="sidebar-section-children"><a href="InvalidParameter.html">InvalidParameter</a></div><div class="sidebar-section-children"><a href="LogHandler.html">LogHandler</a></div><div class="sidebar-section-children"><a href="MediaNotPrepared.html">MediaNotPrepared</a></div><div class="sidebar-section-children"><a href="NoActiveSession.html">NoActiveSession</a></div><div class="sidebar-section-children"><a href="NotAllowed.html">NotAllowed</a></div><div class="sidebar-section-children"><a href="NotReady.html">NotReady</a></div><div class="sidebar-section-children"><a href="Phone.html">Phone</a></div><div class="sidebar-section-children"><a href="Request.html">Request</a></div><div class="sidebar-section-children"><a href="Requestor.html">Requestor</a></div><div class="sidebar-section-children"><a href="Session.html">Session</a></div><div class="sidebar-section-children"><a href="UnableToConnect.html">UnableToConnect</a></div></div><div class="sidebar-section-title with-arrow" data-isopen="false" id="sidebar-events"><div>Events</div><svg><use xlink:href="#down-icon"></use></svg></div><div class="sidebar-section-children-container"><div class="sidebar-section-children"><a href="global.html#event:callDataInfo">callDataInfo</a></div><div class="sidebar-section-children"><a href="global.html#event:callDurationUpdate">callDurationUpdate</a></div><div class="sidebar-section-children"><a href="global.html#event:callEnded">callEnded</a></div><div class="sidebar-section-children"><a href="global.html#event:callQualityReportUpdate">callQualityReportUpdate</a></div><div class="sidebar-section-children"><a href="global.html#event:connectionStateChange">connectionStateChange</a></div><div class="sidebar-section-children"><a href="global.html#event:holdStateChange">holdStateChange</a></div><div class="sidebar-section-children"><a href="global.html#event:incomingCall">incomingCall</a></div><div class="sidebar-section-children"><a href="global.html#event:muteStateChange">muteStateChange</a></div><div class="sidebar-section-children"><a href="global.html#event:recordingStateChange">recordingStateChange</a></div><div class="sidebar-section-children"><a href="global.html#event:sessionCreated">sessionCreated</a></div><div class="sidebar-section-children"><a href="global.html#event:sessionRemoved">sessionRemoved</a></div><div class="sidebar-section-children"><a href="global.html#event:sessionStateChange">sessionStateChange</a></div><div class="sidebar-section-children"><a href="global.html#event:whisperModeChange">whisperModeChange</a></div></div><div class="sidebar-section-title with-arrow" data-isopen="false" id="sidebar-global"><div>Global</div><svg><use xlink:href="#down-icon"></use></svg></div><div class="sidebar-section-children-container"><div class="sidebar-section-children"><a href="global.html#CALL_DIRECTION">CALL_DIRECTION</a></div><div class="sidebar-section-children"><a href="global.html#CALL_DISPOSITION">CALL_DISPOSITION</a></div><div class="sidebar-section-children"><a href="global.html#CONNECTION_STATE">CONNECTION_STATE</a></div><div class="sidebar-section-children"><a href="global.html#LOGLVL">LOGLVL</a></div><div class="sidebar-section-children"><a href="global.html#SESSION_STATE">SESSION_STATE</a></div></div></div></div></div><div class="navbar-container" id="VuAckcnZhf"><nav class="navbar"><div class="navbar-left-items"></div><div class="navbar-right-items"><div class="navbar-right-item"><button class="icon-button search-button" aria-label="open-search"><svg><use xlink:href="#search-icon"></use></svg></button></div><div class="navbar-right-item"><button class="icon-button theme-toggle" aria-label="toggle-theme"><svg><use class="theme-svg-use" xlink:href="#light-theme-icon"></use></svg></button></div><div class="navbar-right-item"><button class="icon-button font-size" aria-label="change-font-size"><svg><use xlink:href="#font-size-icon"></use></svg></button></div></div><nav></nav></nav></div><div class="toc-container"><div class="toc-content"><span class="bold">On this page</span><div id="eed4d2a0bfd64539bb9df78095dec881"></div></div></div><div class="body-wrapper"><div class="main-content"><div class="main-wrapper"><section id="source-page" class="source-page"><header><h1 id="title" class="has-anchor">phone-client.js</h1></header><article><pre class="prettyprint source lang-js"><code>// 'use strict';
4
4
  import EventEmitter from 'events';
5
5
  import JsSIP from 'jssip';
6
- import { LogHandler, LOGLVL } from './logger';
7
- import * as Exceptions from './exceptions';
8
- import * as Tools from './tools';
9
- import { EventClient } from './event-client';
6
+ import { LogHandler, LOGLVL } from './logger.js';
7
+ import * as Exceptions from './exceptions.js';
8
+ import * as Tools from './tools.js';
9
+ import { EventClient } from './event-client.js';
10
10
  import camelcaseKeys from 'camelcase-keys';
11
11
 
12
12
  const options = {
@@ -112,6 +112,9 @@ export class Phone extends EventEmitter {
112
112
  this._eventClient = null;
113
113
  this._options = null;
114
114
 
115
+ // getUserMedia audio constraint: true (default device) or { deviceId: { exact } }
116
+ this._audioConstraints = true;
117
+
115
118
  // List of active sessions
116
119
  this._sessionCounter = 0;
117
120
  this._activeSessions = {};
@@ -127,6 +130,7 @@ export class Phone extends EventEmitter {
127
130
  this._repeatToneActive = false;
128
131
  this._repeatToneSequence = [];
129
132
  this._repeatToneSequencePos = 0;
133
+ this._toneContext = null;
130
134
  this._toneGainProcessor = null;
131
135
  this._oscillator1 = null;
132
136
  this._oscillator2 = null;
@@ -146,29 +150,83 @@ export class Phone extends EventEmitter {
146
150
  * Prepare media and get media permission from browser
147
151
  * This must be done prior to using the phone or getting device lists.
148
152
  *
149
- * @return A promise which will be resolved when user accepts access to media devices
153
+ * On failure the previously selected input device (see {@link setInputDevice}) and the
154
+ * current media-permission state are left untouched, unless the browser reports the
155
+ * request was actually denied (NotAllowedError / SecurityError), in which case
156
+ * hasMediaPermission/hasMediaPermissionError are updated to reflect that.
150
157
  *
158
+ * @param {object} [mediaOptions]
159
+ * @param {string} [mediaOptions.deviceId] Microphone deviceId (from enumerateDevices); omit or leave
160
+ * out to select the browser default device, which also resets a device previously chosen with
161
+ * {@link setInputDevice}
162
+ * @return {Promise} Resolved when user accepts access to media devices; rejected (never thrown
163
+ * synchronously) otherwise, e.g. when the browser denies permission or has no mediaDevices API
151
164
  */
152
- prepareMedia() {
165
+ async prepareMedia(mediaOptions = {}) {
153
166
  this.log.info('Get media permission');
154
- const self = this;
155
- const constraints = { audio: true, video: false };
156
- return new Promise(function (resolve, reject) {
157
- navigator.mediaDevices.getUserMedia(constraints).then(
158
- () => {
159
- self._mediaPermission = true;
160
- self._mediaPermissionError = false;
161
- self.log.info('Media permission granted');
162
- resolve();
163
- },
164
- (err) => {
165
- self._mediaPermission = false;
166
- self._mediaPermissionError = true;
167
- self.log.error('Denied media permission: ' + err);
168
- reject(err);
169
- },
170
- );
171
- });
167
+ const deviceId = (mediaOptions || {}).deviceId || null;
168
+ const audioConstraints = deviceId ? { deviceId: { exact: deviceId } } : true;
169
+ const constraints = { audio: audioConstraints, video: false };
170
+
171
+ try {
172
+ const stream = await navigator.mediaDevices.getUserMedia(constraints);
173
+ // Only the permission is needed here; release the device again.
174
+ stream.getTracks().forEach((track) => track.stop());
175
+ this.setInputDevice(deviceId);
176
+ this._mediaPermission = true;
177
+ this._mediaPermissionError = false;
178
+ this.log.info('Media permission granted');
179
+ } catch (err) {
180
+ if (err &amp;&amp; (err.name === 'NotAllowedError' || err.name === 'SecurityError')) {
181
+ this._mediaPermission = false;
182
+ this._mediaPermissionError = true;
183
+ this.log.error('Denied media permission: ' + err);
184
+ } else {
185
+ this.log.error('Unable to get requested media device: ' + err);
186
+ }
187
+ throw err;
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Select the microphone used for subsequent calls and answers.
193
+ * @param {string|null} deviceId Microphone deviceId, or null for the browser default
194
+ */
195
+ setInputDevice(deviceId) {
196
+ this._audioConstraints = deviceId ? { deviceId: { exact: deviceId } } : true;
197
+ }
198
+
199
+ /**
200
+ * Current getUserMedia audio constraint (true = default device)
201
+ */
202
+ get audioConstraints() {
203
+ return this._audioConstraints;
204
+ }
205
+
206
+ /**
207
+ * True when the browser can route audio to a chosen output device (HTMLMediaElement.setSinkId)
208
+ */
209
+ get outputDeviceSelectionSupported() {
210
+ return typeof this._remoteAudioElement.setSinkId === 'function';
211
+ }
212
+
213
+ /**
214
+ * Route call audio and signal tones to an output device.
215
+ * @param {string|null} deviceId Output deviceId (from enumerateDevices), or null for the default
216
+ * @return {Promise&lt;boolean>} false when the browser does not support output selection
217
+ * @throws Rejects if the browser's HTMLMediaElement.setSinkId() call rejects, e.g. for an
218
+ * unknown deviceId or one the browser is not allowed to use (NotAllowedError)
219
+ */
220
+ async setOutputDevice(deviceId) {
221
+ if (!this.outputDeviceSelectionSupported) {
222
+ return false;
223
+ }
224
+ const sinkId = deviceId || '';
225
+ await Promise.all([
226
+ this._remoteAudioElement.setSinkId(sinkId),
227
+ this._toneAudioElement.setSinkId(sinkId),
228
+ ]);
229
+ return true;
172
230
  }
173
231
 
174
232
  /**
@@ -204,16 +262,16 @@ export class Phone extends EventEmitter {
204
262
  }
205
263
 
206
264
  // Apply options
207
- this._options = Object.assign(options, phoneOptions);
265
+ this._options = Object.assign({}, options, phoneOptions);
208
266
 
209
267
  this._initToneGenerator();
210
268
 
211
269
  // Unpack provisioning token if specified
212
270
  if (this._options.provisionToken) {
213
- let pt = null;
271
+ let pt;
214
272
  try {
215
273
  pt = JSON.parse(atob(this._options.provisionToken));
216
- } catch (err) {
274
+ } catch {
217
275
  throw new Exceptions.InvalidOptions('provisionToken', 'Invalid token');
218
276
  }
219
277
 
@@ -289,7 +347,7 @@ export class Phone extends EventEmitter {
289
347
  );
290
348
  }
291
349
 
292
- let wsSocket = null;
350
+ let wsSocket;
293
351
  try {
294
352
  wsSocket = new JsSIP.WebSocketInterface(this._options.wssServer);
295
353
  } catch (err) {
@@ -367,8 +425,11 @@ export class Phone extends EventEmitter {
367
425
  self._setConnectionState('UNREGISTERED');
368
426
  });
369
427
 
370
- this._UA.on('registrationFailed', function (/* data */) {
371
- self._setConnectionState('REGISTRATION_FAILED');
428
+ this._UA.on('registrationFailed', function (data) {
429
+ self._setConnectionState('REGISTRATION_FAILED', {
430
+ cause: data?.cause,
431
+ statusCode: data?.response?.status_code,
432
+ });
372
433
  });
373
434
 
374
435
  this._UA.start();
@@ -393,6 +454,11 @@ export class Phone extends EventEmitter {
393
454
  this._UA.stop();
394
455
  delete this._UA;
395
456
 
457
+ // Always mark the phone stopped from here on, even if there is no event client to
458
+ // disconnect below - otherwise isStarted stays true and a later start() would try to
459
+ // stop() this same (already torn down) instance again.
460
+ this._isStarted = false;
461
+
396
462
  // Disconnect event client if needed
397
463
  if (!this._eventClient) {
398
464
  this.log.debug('Event client not running, not stopping');
@@ -401,14 +467,26 @@ export class Phone extends EventEmitter {
401
467
  try {
402
468
  this._eventClient.disconnect();
403
469
  delete this._eventClient;
404
- } catch (e) {
470
+ } catch {
405
471
  this.log.warning('Unable to disconnect event client');
406
472
  }
407
-
408
- this._isStarted = false;
409
473
  this.log.info('Phone client successfully stopped');
410
474
  }
411
475
 
476
+ /**
477
+ * Re-send REGISTER without tearing down the websocket connection or any active calls.
478
+ * Use this to recover from a REGISTER refresh that failed for a reason other than bad
479
+ * credentials (e.g. a timeout or a 5xx from the registrar) while the transport itself stayed
480
+ * up - JsSIP does not retry those on its own. Safe to call during an active call.
481
+ * @throws NotReady - If phone is not started
482
+ */
483
+ register() {
484
+ if (!this._isStarted) {
485
+ throw new Exceptions.NotReady('Phone is not started');
486
+ }
487
+ this._UA.register();
488
+ }
489
+
412
490
  /**
413
491
  * Start a new call
414
492
  * If you have other active calls they will be set on hold
@@ -492,7 +570,7 @@ export class Phone extends EventEmitter {
492
570
  */
493
571
  getSessionByUUID(uuid) {
494
572
  for (let key in this._activeSessions) {
495
- if (this._activeSessions.hasOwnProperty(key)) {
573
+ if (Object.hasOwn(this._activeSessions, key)) {
496
574
  if (this._activeSessions[key].uuid === uuid) {
497
575
  return this._activeSessions[key];
498
576
  }
@@ -510,6 +588,27 @@ export class Phone extends EventEmitter {
510
588
  return this._options;
511
589
  }
512
590
 
591
+ /**
592
+ * Replace the call token used by subsequent calls and answers.
593
+ * Call tokens expire (30 min); renew via the API and pass the new token here.
594
+ *
595
+ * With useCallDataInfoEvent, the callDataInfo subscription keeps the token from start();
596
+ * call start() again to move it to the new token.
597
+ *
598
+ * @param {string} token New call token
599
+ * @throws NotReady - If phone is not started
600
+ * @throws InvalidOptions - If token is empty
601
+ */
602
+ setCallToken(token) {
603
+ if (!this._isStarted) {
604
+ throw new Exceptions.NotReady('Phone is not started');
605
+ }
606
+ if (!token) {
607
+ throw new Exceptions.InvalidOptions('callToken', 'Missing call token');
608
+ }
609
+ this._options.callToken = token;
610
+ }
611
+
513
612
  /**
514
613
  * Return list of current sessions
515
614
  * @returns [Session]
@@ -517,7 +616,7 @@ export class Phone extends EventEmitter {
517
616
  get sessions() {
518
617
  let s = [];
519
618
  for (let key in this._activeSessions) {
520
- if (this._activeSessions.hasOwnProperty(key)) {
619
+ if (Object.hasOwn(this._activeSessions, key)) {
521
620
  s.push(this._activeSessions[key]);
522
621
  }
523
622
  }
@@ -575,6 +674,7 @@ export class Phone extends EventEmitter {
575
674
  */
576
675
  playTone(duration, freq1, freq2 = 0, callback = null) {
577
676
  this.log.debug(`Playing tone ${freq1} / ${freq2} for ${duration} ms`);
677
+ this._resumeToneContext();
578
678
 
579
679
  this._oscillator1.frequency.value = freq1;
580
680
  this._oscillator2.frequency.value = freq2;
@@ -641,8 +741,35 @@ export class Phone extends EventEmitter {
641
741
  }
642
742
  }
643
743
 
644
- // Initialize tone generator
744
+ /**
745
+ * Resume the Web Audio context behind signal tones (ringing, ringback, busy, DTMF feedback).
746
+ * Browsers create an AudioContext "suspended" when it is not created during a user gesture
747
+ * (e.g. a phone started on page load); call this from a user-gesture handler (click, keydown)
748
+ * so tones are audible. Safe to call before {@link start} and any number of times.
749
+ * @return {Promise&lt;boolean>} false when the browser has no Web Audio API or resuming failed
750
+ */
751
+ async resumeAudio() {
752
+ try {
753
+ this._initToneGenerator();
754
+ } catch {
755
+ return false;
756
+ }
757
+ try {
758
+ await this._toneContext.resume();
759
+ return this._toneContext.state === 'running';
760
+ } catch (err) {
761
+ this.log.warning('Unable to resume audio context: ' + err);
762
+ return false;
763
+ }
764
+ }
765
+
766
+ // Initialize tone generator. One AudioContext per Phone, reused across start()/stop() -
767
+ // browsers cap the number of live contexts and a new one per start() would leak.
645
768
  _initToneGenerator() {
769
+ if (this._toneContext &amp;&amp; this._toneContext.state !== 'closed') {
770
+ return;
771
+ }
772
+
646
773
  // Create audio context
647
774
  const AudioContext = window.AudioContext || window.webkitAudioContext || false;
648
775
  if (!AudioContext) {
@@ -650,6 +777,7 @@ export class Phone extends EventEmitter {
650
777
  }
651
778
 
652
779
  const ctx = new AudioContext();
780
+ this._toneContext = ctx;
653
781
  this._toneStreamNode = ctx.createMediaStreamDestination();
654
782
  this._toneAudioElement.srcObject = this._toneStreamNode.stream;
655
783
 
@@ -669,6 +797,14 @@ export class Phone extends EventEmitter {
669
797
  this._oscillator2.start();
670
798
  }
671
799
 
800
+ // Best effort: a context created outside a user gesture is suspended until resumed.
801
+ _resumeToneContext() {
802
+ const ctx = this._toneContext;
803
+ if (ctx &amp;&amp; ctx.state === 'suspended' &amp;&amp; typeof ctx.resume === 'function') {
804
+ ctx.resume().catch((err) => this.log.debug('Audio context resume refused: ' + err));
805
+ }
806
+ }
807
+
672
808
  /**
673
809
  * Hold all active calls
674
810
  * @throws NotAllowed If there are connection calls
@@ -682,7 +818,7 @@ export class Phone extends EventEmitter {
682
818
 
683
819
  // Iterate through all active calls and hold them
684
820
  for (let key in this._activeSessions) {
685
- if (this._activeSessions.hasOwnProperty(key)) {
821
+ if (Object.hasOwn(this._activeSessions, key)) {
686
822
  let session = this._activeSessions[key];
687
823
  if (session.state === 'ACTIVE') {
688
824
  session.hold();
@@ -756,7 +892,7 @@ export class Phone extends EventEmitter {
756
892
  // Return true if there are calls currently connecting
757
893
  _checkForConnectingCalls() {
758
894
  for (let key in this._activeSessions) {
759
- if (this._activeSessions.hasOwnProperty(key)) {
895
+ if (Object.hasOwn(this._activeSessions, key)) {
760
896
  let session = this._activeSessions[key];
761
897
  if (
762
898
  session.state === 'INITIALIZING' ||
@@ -781,17 +917,23 @@ export class Phone extends EventEmitter {
781
917
  * @property {string} REGISTRATION_FAILED Registration failed
782
918
  */
783
919
  // Set connection state and emit event
784
- _setConnectionState(state) {
920
+ _setConnectionState(state, extra = {}) {
785
921
  /**
786
922
  * @event connectionStateChange
787
923
  * @type {object}
788
924
  * @param {CONNECTION_STATE} state - The current connection state
925
+ * @param {string} [cause] - For REGISTRATION_FAILED only: JsSIP's registrationFailed
926
+ * cause, e.g. 'Connection Error', 'Request Timeout' (network/transport issues, JsSIP
927
+ * will keep retrying on its own), 'Authentication Error' (401), or 'Rejected' (403 -
928
+ * account disabled or credentials rejected by the registrar)
929
+ * @param {number} [statusCode] - For REGISTRATION_FAILED only: the SIP status code of
930
+ * the failed REGISTER response, when JsSIP received one (e.g. 401, 403)
789
931
  */
790
932
 
791
933
  this.log.info('Connection state: ' + state);
792
934
  this._connectionState = state;
793
935
  try {
794
- this.emit('connectionStateChange', { state: state });
936
+ this.emit('connectionStateChange', { state: state, ...extra });
795
937
  } catch (e) {
796
938
  this.log.error('Exception in connectionStateChange event: ' + e);
797
939
  }
@@ -839,18 +981,15 @@ export class Phone extends EventEmitter {
839
981
  * @param {string} effectiveCidNameStr - Phone number name used as a-number for call
840
982
  * @param {boolean} callPrivacy - Call privacy used for call (number hidden)
841
983
  * @param {number} hangupCauseQ850 - Q.850 hangup cause code
842
- * @param {Object[]} recordings - List of recordings done for this call
843
- * @param {number} recordings[].startTime - Time recording was started
844
- * @param {number} recordings[].length - Recording length in milliseconds
845
- * @param {number} recordings[].size - Recording size (bytes)
846
- * @param {number} recordings[].urlPlayer - URL for recording with player
847
- * @param {number} recordings[].url - URL for recording without player
848
- * @param {number} recordings[].retentionDate - Date recording will be automatically deleted
849
984
  */
850
985
 
851
986
  let e;
852
987
  e = camelcaseKeys(eventData);
853
- e.recordings = camelcaseKeys(eventData.recordings);
988
+ // The call data event no longer carries recordings. Uploads are
989
+ // asynchronous, so the array was almost always empty by the time the CDR
990
+ // was processed, and the event cannot be re-fired once a recording lands
991
+ // because call_token is never persisted on the CDR. A dedicated
992
+ // "recording ready" event will replace it.
854
993
 
855
994
  // Event all duration fields are in seconds, convert to millisecs
856
995
  e.duration = e.duration * 1000;
@@ -943,7 +1082,7 @@ export class Session extends EventEmitter {
943
1082
  this.destinationNumber = number;
944
1083
 
945
1084
  let options = {
946
- mediaConstraints: { audio: true, video: false },
1085
+ mediaConstraints: { audio: this._phone.audioConstraints, video: false },
947
1086
  pcConfig: {
948
1087
  iceServers: [{ urls: ['stun:stun.l.google.com:19302'] }],
949
1088
  },
@@ -1140,7 +1279,7 @@ export class Session extends EventEmitter {
1140
1279
  self.log.info(`Answer call timeout after ${self._answerCallTimeout} seconds`);
1141
1280
  try {
1142
1281
  self.hangup();
1143
- } catch (e) {
1282
+ } catch {
1144
1283
  self.log.info('No active session, ignoring answer call timeout');
1145
1284
  }
1146
1285
  }, self._answerCallTimeout * 1000);
@@ -1297,7 +1436,7 @@ export class Session extends EventEmitter {
1297
1436
  */
1298
1437
  answer(metaInfo = null) {
1299
1438
  let options = {
1300
- mediaConstraints: { audio: true, video: false },
1439
+ mediaConstraints: { audio: this._phone.audioConstraints, video: false },
1301
1440
  pcConfig: {
1302
1441
  iceServers: [{ urls: ['stun:stun.l.google.com:19302'] }],
1303
1442
  },
@@ -1,10 +1,10 @@
1
1
  <!DOCTYPE html><html lang="en" style="font-size:16px"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Source: requestor.js</title><!--[if lt IE 9]>
2
2
  <script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
3
3
  <![endif]--><script src="scripts/third-party/hljs.js" defer="defer"></script><script src="scripts/third-party/hljs-line-num.js" defer="defer"></script><script src="scripts/third-party/popper.js" defer="defer"></script><script src="scripts/third-party/tippy.js" defer="defer"></script><script src="scripts/third-party/tocbot.min.js"></script><script>var baseURL="/",locationPathname="";baseURL=(locationPathname=document.location.pathname).substr(0,locationPathname.lastIndexOf("/")+1)</script><link rel="stylesheet" href="styles/clean-jsdoc-theme.min.css"><svg aria-hidden="true" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="display:none"><defs><symbol id="copy-icon" viewbox="0 0 488.3 488.3"><g><path d="M314.25,85.4h-227c-21.3,0-38.6,17.3-38.6,38.6v325.7c0,21.3,17.3,38.6,38.6,38.6h227c21.3,0,38.6-17.3,38.6-38.6V124 C352.75,102.7,335.45,85.4,314.25,85.4z M325.75,449.6c0,6.4-5.2,11.6-11.6,11.6h-227c-6.4,0-11.6-5.2-11.6-11.6V124 c0-6.4,5.2-11.6,11.6-11.6h227c6.4,0,11.6,5.2,11.6,11.6V449.6z"/><path d="M401.05,0h-227c-21.3,0-38.6,17.3-38.6,38.6c0,7.5,6,13.5,13.5,13.5s13.5-6,13.5-13.5c0-6.4,5.2-11.6,11.6-11.6h227 c6.4,0,11.6,5.2,11.6,11.6v325.7c0,6.4-5.2,11.6-11.6,11.6c-7.5,0-13.5,6-13.5,13.5s6,13.5,13.5,13.5c21.3,0,38.6-17.3,38.6-38.6 V38.6C439.65,17.3,422.35,0,401.05,0z"/></g></symbol><symbol id="search-icon" viewBox="0 0 512 512"><g><g><path d="M225.474,0C101.151,0,0,101.151,0,225.474c0,124.33,101.151,225.474,225.474,225.474 c124.33,0,225.474-101.144,225.474-225.474C450.948,101.151,349.804,0,225.474,0z M225.474,409.323 c-101.373,0-183.848-82.475-183.848-183.848S124.101,41.626,225.474,41.626s183.848,82.475,183.848,183.848 S326.847,409.323,225.474,409.323z"/></g></g><g><g><path d="M505.902,476.472L386.574,357.144c-8.131-8.131-21.299-8.131-29.43,0c-8.131,8.124-8.131,21.306,0,29.43l119.328,119.328 c4.065,4.065,9.387,6.098,14.715,6.098c5.321,0,10.649-2.033,14.715-6.098C514.033,497.778,514.033,484.596,505.902,476.472z"/></g></g></symbol><symbol id="font-size-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M11.246 15H4.754l-2 5H.6L7 4h2l6.4 16h-2.154l-2-5zm-.8-2L8 6.885 5.554 13h4.892zM21 12.535V12h2v8h-2v-.535a4 4 0 1 1 0-6.93zM19 18a2 2 0 1 0 0-4 2 2 0 0 0 0 4z"/></symbol><symbol id="add-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M11 11V5h2v6h6v2h-6v6h-2v-6H5v-2z"/></symbol><symbol id="minus-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M5 11h14v2H5z"/></symbol><symbol id="dark-theme-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M10 7a7 7 0 0 0 12 4.9v.1c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2h.1A6.979 6.979 0 0 0 10 7zm-6 5a8 8 0 0 0 15.062 3.762A9 9 0 0 1 8.238 4.938 7.999 7.999 0 0 0 4 12z"/></symbol><symbol id="light-theme-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M12 18a6 6 0 1 1 0-12 6 6 0 0 1 0 12zm0-2a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM11 1h2v3h-2V1zm0 19h2v3h-2v-3zM3.515 4.929l1.414-1.414L7.05 5.636 5.636 7.05 3.515 4.93zM16.95 18.364l1.414-1.414 2.121 2.121-1.414 1.414-2.121-2.121zm2.121-14.85l1.414 1.415-2.121 2.121-1.414-1.414 2.121-2.121zM5.636 16.95l1.414 1.414-2.121 2.121-1.414-1.414 2.121-2.121zM23 11v2h-3v-2h3zM4 11v2H1v-2h3z"/></symbol><symbol id="reset-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M18.537 19.567A9.961 9.961 0 0 1 12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10c0 2.136-.67 4.116-1.81 5.74L17 12h3a8 8 0 1 0-2.46 5.772l.997 1.795z"/></symbol><symbol id="down-icon" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M12.7803 6.21967C13.0732 6.51256 13.0732 6.98744 12.7803 7.28033L8.53033 11.5303C8.23744 11.8232 7.76256 11.8232 7.46967 11.5303L3.21967 7.28033C2.92678 6.98744 2.92678 6.51256 3.21967 6.21967C3.51256 5.92678 3.98744 5.92678 4.28033 6.21967L8 9.93934L11.7197 6.21967C12.0126 5.92678 12.4874 5.92678 12.7803 6.21967Z"></path></symbol><symbol id="codepen-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M16.5 13.202L13 15.535v3.596L19.197 15 16.5 13.202zM14.697 12L12 10.202 9.303 12 12 13.798 14.697 12zM20 10.869L18.303 12 20 13.131V10.87zM19.197 9L13 4.869v3.596l3.5 2.333L19.197 9zM7.5 10.798L11 8.465V4.869L4.803 9 7.5 10.798zM4.803 15L11 19.131v-3.596l-3.5-2.333L4.803 15zM4 13.131L5.697 12 4 10.869v2.262zM2 9a1 1 0 0 1 .445-.832l9-6a1 1 0 0 1 1.11 0l9 6A1 1 0 0 1 22 9v6a1 1 0 0 1-.445.832l-9 6a1 1 0 0 1-1.11 0l-9-6A1 1 0 0 1 2 15V9z"/></symbol><symbol id="close-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M12 10.586l4.95-4.95 1.414 1.414-4.95 4.95 4.95 4.95-1.414 1.414-4.95-4.95-4.95 4.95-1.414-1.414 4.95-4.95-4.95-4.95L7.05 5.636z"/></symbol><symbol id="menu-icon" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M3 4h18v2H3V4zm0 7h18v2H3v-2zm0 7h18v2H3v-2z"/></symbol></defs></svg></head><body data-theme="dark"><div class="sidebar-container"><div class="sidebar" id="sidebar"><div class="sidebar-items-container"><div class="sidebar-section-title with-arrow" data-isopen="false" id="sidebar-classes"><div>Classes</div><svg><use xlink:href="#down-icon"></use></svg></div><div class="sidebar-section-children-container"><div class="sidebar-section-children"><a href="EventClient.html">EventClient</a></div><div class="sidebar-section-children"><a href="EventServiceNotConnected.html">EventServiceNotConnected</a></div><div class="sidebar-section-children"><a href="InvalidMediaDevice.html">InvalidMediaDevice</a></div><div class="sidebar-section-children"><a href="InvalidOptions.html">InvalidOptions</a></div><div class="sidebar-section-children"><a href="InvalidParameter.html">InvalidParameter</a></div><div class="sidebar-section-children"><a href="LogHandler.html">LogHandler</a></div><div class="sidebar-section-children"><a href="MediaNotPrepared.html">MediaNotPrepared</a></div><div class="sidebar-section-children"><a href="NoActiveSession.html">NoActiveSession</a></div><div class="sidebar-section-children"><a href="NotAllowed.html">NotAllowed</a></div><div class="sidebar-section-children"><a href="NotReady.html">NotReady</a></div><div class="sidebar-section-children"><a href="Phone.html">Phone</a></div><div class="sidebar-section-children"><a href="Request.html">Request</a></div><div class="sidebar-section-children"><a href="Requestor.html">Requestor</a></div><div class="sidebar-section-children"><a href="Session.html">Session</a></div><div class="sidebar-section-children"><a href="UnableToConnect.html">UnableToConnect</a></div></div><div class="sidebar-section-title with-arrow" data-isopen="false" id="sidebar-events"><div>Events</div><svg><use xlink:href="#down-icon"></use></svg></div><div class="sidebar-section-children-container"><div class="sidebar-section-children"><a href="global.html#event:callDataInfo">callDataInfo</a></div><div class="sidebar-section-children"><a href="global.html#event:callDurationUpdate">callDurationUpdate</a></div><div class="sidebar-section-children"><a href="global.html#event:callEnded">callEnded</a></div><div class="sidebar-section-children"><a href="global.html#event:callQualityReportUpdate">callQualityReportUpdate</a></div><div class="sidebar-section-children"><a href="global.html#event:connectionStateChange">connectionStateChange</a></div><div class="sidebar-section-children"><a href="global.html#event:holdStateChange">holdStateChange</a></div><div class="sidebar-section-children"><a href="global.html#event:incomingCall">incomingCall</a></div><div class="sidebar-section-children"><a href="global.html#event:muteStateChange">muteStateChange</a></div><div class="sidebar-section-children"><a href="global.html#event:recordingStateChange">recordingStateChange</a></div><div class="sidebar-section-children"><a href="global.html#event:sessionCreated">sessionCreated</a></div><div class="sidebar-section-children"><a href="global.html#event:sessionRemoved">sessionRemoved</a></div><div class="sidebar-section-children"><a href="global.html#event:sessionStateChange">sessionStateChange</a></div><div class="sidebar-section-children"><a href="global.html#event:whisperModeChange">whisperModeChange</a></div></div><div class="sidebar-section-title with-arrow" data-isopen="false" id="sidebar-global"><div>Global</div><svg><use xlink:href="#down-icon"></use></svg></div><div class="sidebar-section-children-container"><div class="sidebar-section-children"><a href="global.html#CALL_DIRECTION">CALL_DIRECTION</a></div><div class="sidebar-section-children"><a href="global.html#CALL_DISPOSITION">CALL_DISPOSITION</a></div><div class="sidebar-section-children"><a href="global.html#CONNECTION_STATE">CONNECTION_STATE</a></div><div class="sidebar-section-children"><a href="global.html#LOGLVL">LOGLVL</a></div><div class="sidebar-section-children"><a href="global.html#SESSION_STATE">SESSION_STATE</a></div></div></div></div></div><div class="navbar-container" id="VuAckcnZhf"><nav class="navbar"><div class="navbar-left-items"></div><div class="navbar-right-items"><div class="navbar-right-item"><button class="icon-button search-button" aria-label="open-search"><svg><use xlink:href="#search-icon"></use></svg></button></div><div class="navbar-right-item"><button class="icon-button theme-toggle" aria-label="toggle-theme"><svg><use class="theme-svg-use" xlink:href="#light-theme-icon"></use></svg></button></div><div class="navbar-right-item"><button class="icon-button font-size" aria-label="change-font-size"><svg><use xlink:href="#font-size-icon"></use></svg></button></div></div><nav></nav></nav></div><div class="toc-container"><div class="toc-content"><span class="bold">On this page</span><div id="eed4d2a0bfd64539bb9df78095dec881"></div></div></div><div class="body-wrapper"><div class="main-content"><div class="main-wrapper"><section id="source-page" class="source-page"><header><h1 id="title" class="has-anchor">requestor.js</h1></header><article><pre class="prettyprint source lang-js"><code>import EventEmitter from "events";
4
- import * as Logger from "./logger";
5
- import * as Exceptions from "./exceptions";
6
- import { createUUID } from "./tools";
7
- import { NotAllowed } from "./exceptions";
4
+ import * as Logger from "./logger.js";
5
+ import * as Exceptions from "./exceptions.js";
6
+ import { createUUID } from "./tools.js";
7
+ import { NotAllowed } from "./exceptions.js";
8
8
 
9
9
  // Request timeout after 10 sec
10
10
  const REQUEST_TIMEOUT = 10000;
@@ -78,13 +78,13 @@ export class Requestor extends EventEmitter {
78
78
  let msg;
79
79
  try {
80
80
  msg = JSON.parse(message);
81
- } catch (err) {
81
+ } catch {
82
82
  this.log.warning("Unable to parse message: " + message);
83
83
  return;
84
84
  }
85
85
 
86
86
  // Validate msg fields
87
- if (!msg.hasOwnProperty("type") || !msg.hasOwnProperty("id")) {
87
+ if (!Object.hasOwn(msg, "type") || !Object.hasOwn(msg, "id")) {
88
88
  this.log.warning(`Invalid message received: ${msg} - missing type or id`);
89
89
  return;
90
90
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telegenta/webclient",
3
- "version": "3.1.2",
3
+ "version": "3.2.0",
4
4
  "type": "module",
5
5
  "description": "Webphone and event client library",
6
6
  "main": "dist/telegenta.js",
@@ -9,13 +9,18 @@
9
9
  "watch-docs": "rimraf docs && watch 'npm run build-docs'",
10
10
  "build-docs": "rimraf docs && npx jsdoc -c jsdoc.conf.json",
11
11
  "deploy-docs": "aws s3 cp docs/ s3://www.telegenta.com/docs/client-lib/$npm_package_version --recursive --profile telegenta && aws cloudfront --profile telegenta create-invalidation --distribution-id E1A9LQ3WGY7RF9 --paths /docs/*",
12
- "test": "node --test",
12
+ "test": "node --test test/*.test.js",
13
13
  "lint": "eslint .",
14
14
  "lint:fix": "eslint . --fix",
15
15
  "deploy-demos": "aws s3 cp demos/ s3://www.telegenta.com/demos/ --recursive --profile telegenta && aws cloudfront --profile telegenta create-invalidation --distribution-id E1A9LQ3WGY7RF9 --paths /demos/* ",
16
16
  "build": "rimraf dist && rollup -c",
17
17
  "dev": "rollup -c -w"
18
18
  },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/akantodevs/telegenta.git",
22
+ "directory": "client-lib"
23
+ },
19
24
  "author": "Allan Kristensen",
20
25
  "license": "MIT",
21
26
  "dependencies": {