@telegenta/webclient 3.2.2 → 3.4.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.
@@ -245,6 +245,9 @@
245
245
 
246
246
  // Enable call initialized tone
247
247
  playCallInitializedTone: true,
248
+
249
+ // Enable call ended tone
250
+ playCallEndedTone: true,
248
251
  };
249
252
 
250
253
  try {
package/dist/telegenta.js CHANGED
@@ -219,15 +219,15 @@ function createUUID() {
219
219
  d += performance.now(); //use high-precision timer if available
220
220
  }
221
221
  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
222
- let r = (d + Math.random() * 16) % 16 | 0;
222
+ let r = ((d + Math.random() * 16) % 16) | 0;
223
223
  d = Math.floor(d / 16);
224
- return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
224
+ return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16);
225
225
  });
226
226
  }
227
227
 
228
228
  // Compare properties of two objects. Comparison check if all properties in obj1 exists and has same values in obj2
229
229
  function compareObjects(obj1, obj2) {
230
- if ((!obj1) || (!obj2)) {
230
+ if (!obj1 || !obj2) {
231
231
  return false;
232
232
  }
233
233
 
@@ -241,7 +241,7 @@ function compareObjects(obj1, obj2) {
241
241
  return false;
242
242
  }
243
243
 
244
- return (obj1[k] === obj2[k]);
244
+ return obj1[k] === obj2[k];
245
245
  });
246
246
  }
247
247
 
@@ -257,13 +257,13 @@ function createDurationString(milliSeconds) {
257
257
  }
258
258
  // Hour
259
259
  if (seconds / 3600 >= 1) {
260
- s = s + Math.floor(seconds / 3600) % 24 + 'h ';
260
+ s = s + (Math.floor(seconds / 3600) % 24) + 'h ';
261
261
  }
262
262
 
263
263
  // Seconds
264
264
  if (seconds >= 0) {
265
- s = s + ("00" + Math.floor(seconds / 60) % 60).slice(-2) + ':';
266
- s = s + ("00" + (seconds % 60)).slice(-2);
265
+ s = s + ('00' + (Math.floor(seconds / 60) % 60)).slice(-2) + ':';
266
+ s = s + ('00' + (seconds % 60)).slice(-2);
267
267
  } else {
268
268
  s = '00:00';
269
269
  }
@@ -273,23 +273,23 @@ function createDurationString(milliSeconds) {
273
273
 
274
274
  // Browser checks
275
275
  function browserIsOpera() {
276
- return ((navigator.userAgent.indexOf("Opera") || navigator.userAgent.indexOf('OPR')) !== -1)
276
+ return (navigator.userAgent.indexOf('Opera') || navigator.userAgent.indexOf('OPR')) !== -1;
277
277
  }
278
278
 
279
279
  function browserIsChrome() {
280
- return (navigator.userAgent.indexOf("Chrome") !== -1)
280
+ return navigator.userAgent.indexOf('Chrome') !== -1;
281
281
  }
282
282
 
283
283
  function browserIsFirefox() {
284
- return (navigator.userAgent.indexOf("Firefox") !== -1)
284
+ return navigator.userAgent.indexOf('Firefox') !== -1;
285
285
  }
286
286
 
287
287
  function browserIsSafari() {
288
- return (navigator.userAgent.indexOf("Safari") !== -1)
288
+ return navigator.userAgent.indexOf('Safari') !== -1;
289
289
  }
290
290
 
291
291
  function browserIsEdge() {
292
- return (navigator.userAgent.indexOf("Edge") !== -1)
292
+ return navigator.userAgent.indexOf('Edge') !== -1;
293
293
  }
294
294
 
295
295
  // SIP header names are RFC 3261 tokens
@@ -329,6 +329,14 @@ function buildCustomHeaders(setHeader) {
329
329
  });
330
330
  }
331
331
 
332
+ // Rewrite Opus stereo=1 to stereo=0 on every a=fmtp line.
333
+ // Firefox always offers stereo=1 and Media Switches then opens a 2-channel leg, on which
334
+ // the recording carries the same mix on both channels.
335
+ // The result is recordings contain identical audio on both channels, affecting transcription diarization.
336
+ function withoutOpusStereo(sdp) {
337
+ return sdp.replace(/^a=fmtp:.*$/gm, (line) => line.replace(/stereo=1/g, 'stereo=0'));
338
+ }
339
+
332
340
  // Request timeout after 10 sec
333
341
  const REQUEST_TIMEOUT = 10000;
334
342
 
@@ -1559,6 +1567,7 @@ const options = {
1559
1567
  autoRemoveSessionTime: 3000,
1560
1568
  playRingOnIncoming: true,
1561
1569
  playCallInitializedTone: false,
1570
+ playCallEndedTone: false,
1562
1571
  };
1563
1572
 
1564
1573
  // DTMF Frequency table
@@ -1595,6 +1604,11 @@ const ST_CALLINIT = [
1595
1604
  { d: 200, f1: 500, f2: 0, g: 0 },
1596
1605
  { d: 200, f1: 700, f2: 0, g: 5000 },
1597
1606
  ];
1607
+ const ST_CALLENDED = [
1608
+ { d: 150, f1: 600, f2: 0, g: 50 },
1609
+ { d: 150, f1: 500, f2: 0, g: 50 },
1610
+ { d: 150, f1: 400, f2: 0, g: 0 },
1611
+ ];
1598
1612
  const ST_RINGBACK = [{ d: 1000, f1: 425, f2: 0, g: 4000 }];
1599
1613
  const ST_INCOMING = [
1600
1614
  { d: 150, f1: 400, f2: 0, g: 0 },
@@ -1753,10 +1767,7 @@ class Phone extends EventEmitter {
1753
1767
  return false;
1754
1768
  }
1755
1769
  const sinkId = deviceId || '';
1756
- await Promise.all([
1757
- this._remoteAudioElement.setSinkId(sinkId),
1758
- this._toneAudioElement.setSinkId(sinkId),
1759
- ]);
1770
+ await Promise.all([this._remoteAudioElement.setSinkId(sinkId), this._toneAudioElement.setSinkId(sinkId)]);
1760
1771
  return true;
1761
1772
  }
1762
1773
 
@@ -1778,6 +1789,7 @@ class Phone extends EventEmitter {
1778
1789
  * @param {number} [phoneOptions.autoRemoveSessionTime=3000] Automatically remove session after interval specified
1779
1790
  * @param {number} [phoneOptions.playRingOnIncoming=true] Play ringing tones on incoming calls
1780
1791
  * @param {number} [phoneOptions.playCallInitializedTone=false] Play tone when call has been initiated (before progress/ringback tones)
1792
+ * @param {boolean} [phoneOptions.playCallEndedTone=false] Play tone once when an answered call ends without the agent hanging up (remote hangup, server hangup, connection loss, timeouts)
1781
1793
  *
1782
1794
  * @throws MediaNotPrepared - If {@link prepareMedia} was not called and resolved prior to calling this method
1783
1795
  * @throws InvalidOptions - If one or more options are invalid
@@ -2213,14 +2225,18 @@ class Phone extends EventEmitter {
2213
2225
 
2214
2226
  // Set timer to stop tone again
2215
2227
  const self = this;
2216
- this._toneTimer = setTimeout(function () {
2217
- self.stopTone();
2228
+ const timer = setTimeout(function () {
2229
+ // A later playTone() has taken over the oscillators; don't silence it or cancel its timer
2230
+ if (self._toneTimer === timer) {
2231
+ self.stopTone();
2232
+ }
2218
2233
 
2219
2234
  // Handle callback now
2220
2235
  if (callback) {
2221
2236
  callback();
2222
2237
  }
2223
2238
  }, duration);
2239
+ this._toneTimer = timer;
2224
2240
  }
2225
2241
 
2226
2242
  /**
@@ -2263,6 +2279,18 @@ class Phone extends EventEmitter {
2263
2279
  }
2264
2280
  }
2265
2281
 
2282
+ // Play a tone sequence once. A repeat tone starting meanwhile (e.g. a new call's ringing) cancels the rest.
2283
+ _playToneSequence(sequence, pos = 0) {
2284
+ const toneDef = sequence[pos];
2285
+ if (!toneDef || this._repeatToneActive) {
2286
+ return;
2287
+ }
2288
+ const self = this;
2289
+ this.playTone(toneDef.d, toneDef.f1, toneDef.f2, function () {
2290
+ setTimeout(self._playToneSequence.bind(self, sequence, pos + 1), toneDef.g);
2291
+ });
2292
+ }
2293
+
2266
2294
  // End repeating tones
2267
2295
  _endRepeatTone() {
2268
2296
  if (this._repeatToneActive) {
@@ -2592,6 +2620,7 @@ class Session extends EventEmitter {
2592
2620
  this._isListenCall = false;
2593
2621
  this._whisperMode = null;
2594
2622
  this._manualDisposition = null;
2623
+ this._transferInitiated = false;
2595
2624
 
2596
2625
  // Quality report
2597
2626
  this._rtcpStatsTimer = null;
@@ -2752,6 +2781,13 @@ class Session extends EventEmitter {
2752
2781
  // Hookup events
2753
2782
  const self = this;
2754
2783
 
2784
+ // make sure to remove stereo=1 from the SDP
2785
+ this._webRTCSession.on('sdp', function (e) {
2786
+ if (e.originator === 'local') {
2787
+ e.sdp = withoutOpusStereo(e.sdp);
2788
+ }
2789
+ });
2790
+
2755
2791
  // ICE CANDIDATES
2756
2792
  this._webRTCSession.on('icecandidate', function (event) {
2757
2793
  const c = event.candidate;
@@ -2851,10 +2887,17 @@ class Session extends EventEmitter {
2851
2887
  self._setSessionState('ACTIVE');
2852
2888
  });
2853
2889
 
2854
- this._webRTCSession.on('ended', function (/* data */) {
2890
+ this._webRTCSession.on('ended', function (data) {
2855
2891
  // End any ringsbacks if needed
2856
2892
  self._phone._endRepeatTone();
2857
2893
 
2894
+ // jssip fires 'ended' once a call is answered. Its own errors (connection loss, timeouts, ICE failure)
2895
+ // also end as 'local' but with their own cause, so only a local BYE is the agent hanging up.
2896
+ const agentHangup = data.originator === 'local' && data.cause === JsSIP.C.causes.BYE;
2897
+ if (self._phone.options.playCallEndedTone && !agentHangup && !self._transferInitiated) {
2898
+ self._phone._playToneSequence(ST_CALLENDED);
2899
+ }
2900
+
2858
2901
  self._callEndTime = Date.now();
2859
2902
 
2860
2903
  // Clear answer call timeout timer if needed
@@ -3296,6 +3339,10 @@ class Session extends EventEmitter {
3296
3339
  transfer(dstSession) {
3297
3340
  this.log.info(`Transferring ${this.uuid} => ${dstSession.uuid}`);
3298
3341
 
3342
+ // The server hangs up both legs once the transfer completes - that's the agent's doing, no call-ended tone
3343
+ this._transferInitiated = true;
3344
+ dstSession._transferInitiated = true;
3345
+
3299
3346
  // Create ReferSubscriber event class
3300
3347
  const referSubscriber = this._webRTCSession.refer(this._webRTCSession._request.ruri, {
3301
3348
  replaces: dstSession._webRTCSession,
@@ -3310,10 +3357,15 @@ class Session extends EventEmitter {
3310
3357
 
3311
3358
  referSubscriber.on('requestFailed', function (cause) {
3312
3359
  self.log.error(`Transfer rejected: ${cause}`);
3360
+ // Legs stay up, so a later hangup by the other party should get the cue
3361
+ self._transferInitiated = false;
3362
+ dstSession._transferInitiated = false;
3313
3363
  });
3314
3364
 
3315
3365
  referSubscriber.on('failed', function (cause) {
3316
3366
  self.log.error(`Transfer failed: ${cause}`);
3367
+ self._transferInitiated = false;
3368
+ dstSession._transferInitiated = false;
3317
3369
  });
3318
3370
  }
3319
3371
 
@@ -3881,4 +3933,4 @@ class Session extends EventEmitter {
3881
3933
  }
3882
3934
  }
3883
3935
 
3884
- export { EventClient, EventServiceNotConnected, InvalidMediaDevice, InvalidOptions, InvalidParameter, LOGLVL, LogHandler, MediaNotPrepared, NoActiveSession, NotAllowed, NotReady, Phone, Request, Requestor, Session, UnableToConnect, browserIsChrome, browserIsEdge, browserIsFirefox, browserIsOpera, browserIsSafari, buildCustomHeaders, compareObjects, createDurationString, createUUID };
3936
+ export { EventClient, EventServiceNotConnected, InvalidMediaDevice, InvalidOptions, InvalidParameter, LOGLVL, LogHandler, MediaNotPrepared, NoActiveSession, NotAllowed, NotReady, Phone, Request, Requestor, Session, UnableToConnect, browserIsChrome, browserIsEdge, browserIsFirefox, browserIsOpera, browserIsSafari, buildCustomHeaders, compareObjects, createDurationString, createUUID, withoutOpusStereo };