ttp-agent-sdk 2.48.18 → 2.48.20

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.
@@ -12129,7 +12129,7 @@ var TextChatSDK = /*#__PURE__*/function (_EventEmitter) {
12129
12129
 
12130
12130
  // SDK build time for debugging
12131
12131
  if (true) {
12132
- helloMessage.lastBuildTime = "2026-08-27T12:25:20.756Z";
12132
+ helloMessage.lastBuildTime = "2026-08-27T16:45:39.666Z";
12133
12133
  }
12134
12134
  try {
12135
12135
  this.ws.send(JSON.stringify(helloMessage));
@@ -21439,8 +21439,8 @@ var VoiceSDK = _v2_VoiceSDK_js__WEBPACK_IMPORTED_MODULE_0__["default"];
21439
21439
 
21440
21440
 
21441
21441
  // Version - injected at build time from package.json via webpack DefinePlugin
21442
- var VERSION = "2.48.18";
21443
- var BUILD_TIME = "2026-08-27T12:25:20.756Z";
21442
+ var VERSION = "2.48.20";
21443
+ var BUILD_TIME = "2026-08-27T16:45:39.666Z";
21444
21444
  console.log("%c TTP Agent SDK v".concat(VERSION, " (").concat(BUILD_TIME, ") "), 'background: #4f46e5; color: white; font-size: 12px; font-weight: bold; padding: 2px 6px; border-radius: 4px;');
21445
21445
 
21446
21446
  // Named exports
@@ -25616,6 +25616,7 @@ __webpack_require__.r(__webpack_exports__);
25616
25616
  /* harmony import */ var _codecs_PCMUCodec_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./codecs/PCMUCodec.js */ "./src/v2/codecs/PCMUCodec.js");
25617
25617
  /* harmony import */ var _codecs_PCMACodec_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./codecs/PCMACodec.js */ "./src/v2/codecs/PCMACodec.js");
25618
25618
  /* harmony import */ var _hopPlatform_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./hopPlatform.js */ "./src/v2/hopPlatform.js");
25619
+ /* harmony import */ var _loopbackAec_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./loopbackAec.js */ "./src/v2/loopbackAec.js");
25619
25620
  function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
25620
25621
  function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
25621
25622
  function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
@@ -25648,6 +25649,7 @@ function _setPrototypeOf(t, e) { return _setPrototypeOf = Object.setPrototypeOf
25648
25649
 
25649
25650
 
25650
25651
 
25652
+
25651
25653
  // iOS FIX: Shared AudioContext for playback that persists across AudioPlayer instances.
25652
25654
  // iOS WebKit doesn't release audio hardware synchronously when AudioContext.close() is called,
25653
25655
  // so creating a new AudioContext immediately after closing the old one can fail silently.
@@ -25749,6 +25751,18 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
25749
25751
  _this._htmlAudioPlayOk = false;
25750
25752
  _this._htmlAudioPlayWatchdog = null;
25751
25753
  _this._loggedHopResample = false;
25754
+ // Loopback AEC route (opt-in): the same MediaStream, but pushed through a
25755
+ // pair of local RTCPeerConnections first, because Chrome's echo canceller
25756
+ // only cancels peer-connection audio — a hidden <audio> is invisible to it.
25757
+ // See loopbackAec.js. Off unless asked for; the hop above is unchanged.
25758
+ _this._loopbackDest = null;
25759
+ _this._loopbackEl = null;
25760
+ _this._loopbackClose = null;
25761
+ _this._loopbackReady = false;
25762
+ _this._loopbackPending = false;
25763
+ _this._loopbackFailed = false;
25764
+ _this._loopbackPlayOk = false;
25765
+ _this._loopbackWanted = undefined; // memoized by wantsLoopbackAec()
25752
25766
  // Greeting (and other bursts) fire many playChunk()s without await.
25753
25767
  // Serialize context creation so they cannot open two AudioContexts
25754
25768
  // at different rates in the middle of a sentence.
@@ -27022,6 +27036,7 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27022
27036
  this.gainNode = null;
27023
27037
  }
27024
27038
  this._teardownHtmlAudioRoute();
27039
+ this._teardownLoopbackRoute();
27025
27040
  if (this.audioContext === _sharedPlayerContext) {
27026
27041
  _sharedPlayerContext = null;
27027
27042
  _sharedPlayerSampleRate = null;
@@ -27221,7 +27236,10 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27221
27236
  }, {
27222
27237
  key: "_htmlHopNeedsHardwareRate",
27223
27238
  value: function _htmlHopNeedsHardwareRate() {
27224
- return this.wantsHtmlAudioPlayback();
27239
+ // The loopback ends in the same live-MediaStream <audio> the hop does, so
27240
+ // it inherits the same device-rate requirement: a 24 kHz context makes
27241
+ // Chrome clock that stream wrong and play speech ~2x.
27242
+ return this.wantsHtmlAudioPlayback() || this.wantsLoopbackAec();
27225
27243
  }
27226
27244
  }, {
27227
27245
  key: "_isHardwarePlaybackRate",
@@ -27262,7 +27280,10 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27262
27280
  return {
27263
27281
  htmlAudioPlayback: this.wantsHtmlAudioPlayback(),
27264
27282
  htmlAudioRouteActive: this._htmlAudioRouteActive,
27265
- htmlAudioPlayOk: this._htmlAudioPlayOk
27283
+ htmlAudioPlayOk: this._htmlAudioPlayOk,
27284
+ loopbackAec: this.wantsLoopbackAec(),
27285
+ loopbackRouteActive: this._loopbackReady,
27286
+ loopbackPlayOk: this._loopbackPlayOk
27266
27287
  };
27267
27288
  }
27268
27289
 
@@ -27279,6 +27300,18 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27279
27300
  try {
27280
27301
  this.gainNode.disconnect();
27281
27302
  } catch (_) {/* ignore */}
27303
+
27304
+ // Loopback first when asked for. It returns null until the peer connections
27305
+ // are up (negotiation is async), and we deliberately fall through to a
27306
+ // normal audible route in the meantime rather than holding audio: the
27307
+ // establishment is re-entrant and calls back here when it completes.
27308
+ if (this.wantsLoopbackAec()) {
27309
+ var loopbackDest = this._ensureLoopbackRoute();
27310
+ if (loopbackDest) {
27311
+ this.gainNode.connect(loopbackDest);
27312
+ return;
27313
+ }
27314
+ }
27282
27315
  if (this.wantsHtmlAudioPlayback() && this._ensureHtmlAudioRoute()) {
27283
27316
  this.gainNode.connect(this._mediaStreamDest);
27284
27317
  return;
@@ -27303,16 +27336,7 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27303
27336
  }
27304
27337
  try {
27305
27338
  this._mediaStreamDest = this.audioContext.createMediaStreamDestination();
27306
- var el = document.createElement('audio');
27307
- el.setAttribute('playsinline', '');
27308
- el.setAttribute('webkit-playsinline', '');
27309
- el.autoplay = true;
27310
- el.controls = false;
27311
- el.preload = 'auto';
27312
- el.playbackRate = 1;
27313
- el.style.display = 'none';
27314
- el.srcObject = this._mediaStreamDest.stream;
27315
- document.body.appendChild(el);
27339
+ var el = this._createHiddenAudioEl(this._mediaStreamDest.stream);
27316
27340
  this._htmlAudioEl = el;
27317
27341
  this._htmlAudioRouteActive = true;
27318
27342
  this._ensureHtmlAudioPlaying();
@@ -27393,6 +27417,7 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27393
27417
  this.gainNode.disconnect();
27394
27418
  } catch (_) {/* ignore */}
27395
27419
  this._teardownHtmlAudioRoute();
27420
+ this._teardownLoopbackRoute();
27396
27421
  this.gainNode.connect(this.audioContext.destination);
27397
27422
  console.warn("\u26A0\uFE0F AudioPlayer: Fell back to AudioContext.destination (".concat(reason, ")"));
27398
27423
  this.emit('htmlAudioRoute', {
@@ -27420,29 +27445,216 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27420
27445
  this._htmlAudioRouteActive = false;
27421
27446
  }
27422
27447
 
27448
+ /**
27449
+ * Shared factory for the hidden playback element. Both last-hop routes need
27450
+ * exactly this: autoplay, inline on iOS, invisible, rate pinned to 1 so a
27451
+ * page-level playbackRate can't stretch TTS under us.
27452
+ */
27453
+ }, {
27454
+ key: "_createHiddenAudioEl",
27455
+ value: function _createHiddenAudioEl(stream) {
27456
+ var el = document.createElement('audio');
27457
+ el.setAttribute('playsinline', '');
27458
+ el.setAttribute('webkit-playsinline', '');
27459
+ el.autoplay = true;
27460
+ el.controls = false;
27461
+ el.preload = 'auto';
27462
+ el.playbackRate = 1;
27463
+ el.style.display = 'none';
27464
+ el.srcObject = stream;
27465
+ document.body.appendChild(el);
27466
+ return el;
27467
+ }
27468
+
27469
+ /**
27470
+ * Opt-in loopback route. Default OFF: this is an unproven replacement for the
27471
+ * hop running on live customer sites, so it ships dark and is turned on per
27472
+ * machine (?ttp_loopback=1 / localStorage) or per integration (config).
27473
+ */
27474
+ }, {
27475
+ key: "wantsLoopbackAec",
27476
+ value: function wantsLoopbackAec() {
27477
+ // Memoized per player: _htmlHopNeedsHardwareRate() consults this from
27478
+ // _honestRateSamples, i.e. once per audio chunk, and reading it live would
27479
+ // put a synchronous storage hit on the hot path for every machine where
27480
+ // the hop is off (there the || cannot short-circuit). Changing the flag
27481
+ // requires a reload anyway — that is how it is documented and tested.
27482
+ if (this._loopbackWanted === undefined) {
27483
+ var override = (0,_loopbackAec_js__WEBPACK_IMPORTED_MODULE_5__.readTtpLoopbackOverride)(typeof window !== 'undefined' ? window : null);
27484
+ this._loopbackWanted = override !== null ? override : typeof this.config.loopbackAec === 'boolean' ? this.config.loopbackAec : false;
27485
+ if (this._loopbackWanted) {
27486
+ console.log('🔊 AudioPlayer: loopback AEC requested — TTS will play via a local peer connection');
27487
+ }
27488
+ }
27489
+ return this._loopbackWanted;
27490
+ }
27491
+
27492
+ /**
27493
+ * Returns the MediaStreamDestination to play into once the loopback is up,
27494
+ * or null while it is not. Never blocks: the caller routes audio somewhere
27495
+ * audible in the meantime and we re-route from _establishLoopback.
27496
+ */
27497
+ }, {
27498
+ key: "_ensureLoopbackRoute",
27499
+ value: function _ensureLoopbackRoute() {
27500
+ var _this9 = this;
27501
+ if (this._loopbackReady && this._loopbackDest) {
27502
+ this._ensureLoopbackPlaying();
27503
+ return this._loopbackDest;
27504
+ }
27505
+ if (this._loopbackFailed || this._loopbackPending) return null;
27506
+ if (!this.audioContext || typeof this.audioContext.createMediaStreamDestination !== 'function') {
27507
+ this._failLoopback('no_media_stream_destination');
27508
+ return null;
27509
+ }
27510
+ if (!(0,_loopbackAec_js__WEBPACK_IMPORTED_MODULE_5__.isLoopbackAecSupported)(typeof window !== 'undefined' ? window : null)) {
27511
+ this._failLoopback('no_peer_connection');
27512
+ return null;
27513
+ }
27514
+ this._loopbackPending = true;
27515
+ this._establishLoopback().catch(function (e) {
27516
+ console.warn('⚠️ AudioPlayer: loopback AEC route unavailable, keeping existing playback route:', e && e.message);
27517
+ _this9._failLoopback('negotiation_failed');
27518
+ });
27519
+ return null;
27520
+ }
27521
+ }, {
27522
+ key: "_establishLoopback",
27523
+ value: function () {
27524
+ var _establishLoopback2 = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee7() {
27525
+ var gen, dest, _yield$createAecLoopb, remoteStream, close;
27526
+ return _regenerator().w(function (_context8) {
27527
+ while (1) switch (_context8.n) {
27528
+ case 0:
27529
+ // Generation token: a teardown (call end, context rebuild) while we are
27530
+ // negotiating must not resurrect a route into a dead AudioContext.
27531
+ gen = this._loopbackGen = (this._loopbackGen || 0) + 1;
27532
+ dest = this.audioContext.createMediaStreamDestination();
27533
+ _context8.n = 1;
27534
+ return (0,_loopbackAec_js__WEBPACK_IMPORTED_MODULE_5__.createAecLoopback)({
27535
+ stream: dest.stream
27536
+ });
27537
+ case 1:
27538
+ _yield$createAecLoopb = _context8.v;
27539
+ remoteStream = _yield$createAecLoopb.remoteStream;
27540
+ close = _yield$createAecLoopb.close;
27541
+ if (!(gen !== this._loopbackGen || !this.audioContext)) {
27542
+ _context8.n = 2;
27543
+ break;
27544
+ }
27545
+ try {
27546
+ close();
27547
+ } catch (_) {/* ignore */}
27548
+ return _context8.a(2);
27549
+ case 2:
27550
+ // Drop the transitional hop element if one was built while we negotiated,
27551
+ // or it keeps playing a destination nothing is connected to any more.
27552
+ this._teardownHtmlAudioRoute();
27553
+ this._loopbackDest = dest;
27554
+ this._loopbackClose = close;
27555
+ this._loopbackEl = this._createHiddenAudioEl(remoteStream);
27556
+ this._loopbackReady = true;
27557
+ this._loopbackPending = false;
27558
+ this._ensureLoopbackPlaying();
27559
+ this._connectGainToOutput();
27560
+ console.log('🔊 AudioPlayer: Playback last-hop is a local WebRTC loopback (Chrome-cancellable AEC far-end)');
27561
+ this.emit('htmlAudioRoute', {
27562
+ state: 'loopback_established'
27563
+ });
27564
+ case 3:
27565
+ return _context8.a(2);
27566
+ }
27567
+ }, _callee7, this);
27568
+ }));
27569
+ function _establishLoopback() {
27570
+ return _establishLoopback2.apply(this, arguments);
27571
+ }
27572
+ return _establishLoopback;
27573
+ }()
27574
+ }, {
27575
+ key: "_ensureLoopbackPlaying",
27576
+ value: function _ensureLoopbackPlaying() {
27577
+ var _this0 = this;
27578
+ var el = this._loopbackEl;
27579
+ if (!el) return;
27580
+ if (!el.paused && this._loopbackPlayOk) return;
27581
+ var playResult = el.play();
27582
+ if (playResult && typeof playResult.then === 'function') {
27583
+ playResult.then(function () {
27584
+ _this0._loopbackPlayOk = true;
27585
+ }).catch(function (err) {
27586
+ if (_this0._loopbackPlayOk || !el.paused) return;
27587
+ console.warn('⚠️ AudioPlayer: loopback element play() rejected — reverting to the previous route:', err);
27588
+ _this0._failLoopback('play_rejected');
27589
+ _this0._connectGainToOutput();
27590
+ });
27591
+ }
27592
+ }
27593
+
27594
+ /** One-way latch: once the loopback is off for this player it stays off,
27595
+ * rather than retrying every reconnect and stuttering the route. */
27596
+ }, {
27597
+ key: "_failLoopback",
27598
+ value: function _failLoopback(reason) {
27599
+ this._loopbackFailed = true;
27600
+ this._loopbackPending = false;
27601
+ this._teardownLoopbackRoute();
27602
+ this.emit('htmlAudioRoute', {
27603
+ state: 'loopback_fallback',
27604
+ reason: reason
27605
+ });
27606
+ }
27607
+ }, {
27608
+ key: "_teardownLoopbackRoute",
27609
+ value: function _teardownLoopbackRoute() {
27610
+ this._loopbackGen = (this._loopbackGen || 0) + 1;
27611
+ if (this._loopbackEl) {
27612
+ try {
27613
+ this._loopbackEl.pause();
27614
+ } catch (_) {/* ignore */}
27615
+ try {
27616
+ this._loopbackEl.srcObject = null;
27617
+ } catch (_) {/* ignore */}
27618
+ if (this._loopbackEl.parentNode) {
27619
+ this._loopbackEl.parentNode.removeChild(this._loopbackEl);
27620
+ }
27621
+ this._loopbackEl = null;
27622
+ }
27623
+ if (this._loopbackClose) {
27624
+ try {
27625
+ this._loopbackClose();
27626
+ } catch (_) {/* ignore */}
27627
+ this._loopbackClose = null;
27628
+ }
27629
+ this._loopbackDest = null;
27630
+ this._loopbackReady = false;
27631
+ this._loopbackPending = false;
27632
+ this._loopbackPlayOk = false;
27633
+ }
27634
+
27423
27635
  /**
27424
27636
  * Initialize audio context with correct sample rate
27425
27637
  */
27426
27638
  }, {
27427
27639
  key: "initializeAudioContext",
27428
27640
  value: (function () {
27429
- var _initializeAudioContext = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee7() {
27430
- var _this9 = this;
27431
- return _regenerator().w(function (_context8) {
27432
- while (1) switch (_context8.n) {
27641
+ var _initializeAudioContext = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee8() {
27642
+ var _this1 = this;
27643
+ return _regenerator().w(function (_context9) {
27644
+ while (1) switch (_context9.n) {
27433
27645
  case 0:
27434
27646
  if (!this._audioContextInitPromise) {
27435
- _context8.n = 1;
27647
+ _context9.n = 1;
27436
27648
  break;
27437
27649
  }
27438
- return _context8.a(2, this._audioContextInitPromise);
27650
+ return _context9.a(2, this._audioContextInitPromise);
27439
27651
  case 1:
27440
27652
  this._audioContextInitPromise = this._initializeAudioContextImpl().finally(function () {
27441
- _this9._audioContextInitPromise = null;
27653
+ _this1._audioContextInitPromise = null;
27442
27654
  });
27443
- return _context8.a(2, this._audioContextInitPromise);
27655
+ return _context9.a(2, this._audioContextInitPromise);
27444
27656
  }
27445
- }, _callee7, this);
27657
+ }, _callee8, this);
27446
27658
  }));
27447
27659
  function initializeAudioContext() {
27448
27660
  return _initializeAudioContext.apply(this, arguments);
@@ -27452,11 +27664,11 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27452
27664
  }, {
27453
27665
  key: "_initializeAudioContextImpl",
27454
27666
  value: function () {
27455
- var _initializeAudioContextImpl2 = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee8() {
27456
- var _this0 = this;
27667
+ var _initializeAudioContextImpl2 = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee9() {
27668
+ var _this10 = this;
27457
27669
  var ttsSampleRate, useHardwareRate, desiredSampleRate, contextFits, canReuseShared, setupAfterResume, ctxOpts, _t4;
27458
- return _regenerator().w(function (_context9) {
27459
- while (1) switch (_context9.p = _context9.n) {
27670
+ return _regenerator().w(function (_context0) {
27671
+ while (1) switch (_context0.p = _context0.n) {
27460
27672
  case 0:
27461
27673
  ttsSampleRate = this._ttsSampleRate();
27462
27674
  useHardwareRate = this._htmlHopNeedsHardwareRate(); // Destination path: match TTS (24 kHz). HTML last-hop: request the standard
@@ -27474,27 +27686,27 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27474
27686
  _sharedPlayerSampleRate = null;
27475
27687
  }
27476
27688
  contextFits = function contextFits(rate) {
27477
- if (useHardwareRate) return _this0._isHardwarePlaybackRate(rate);
27689
+ if (useHardwareRate) return _this10._isHardwarePlaybackRate(rate);
27478
27690
  return Math.abs(rate - desiredSampleRate) <= 100;
27479
27691
  }; // Check if current instance AudioContext exists and matches
27480
27692
  if (!this.audioContext) {
27481
- _context9.n = 3;
27693
+ _context0.n = 3;
27482
27694
  break;
27483
27695
  }
27484
27696
  if (!(this.audioContext.state === 'closed')) {
27485
- _context9.n = 1;
27697
+ _context0.n = 1;
27486
27698
  break;
27487
27699
  }
27488
27700
  this._invalidateClosedAudioContext();
27489
- _context9.n = 3;
27701
+ _context0.n = 3;
27490
27702
  break;
27491
27703
  case 1:
27492
27704
  if (!contextFits(this.audioContext.sampleRate)) {
27493
- _context9.n = 2;
27705
+ _context0.n = 2;
27494
27706
  break;
27495
27707
  }
27496
27708
  this._ensureHtmlAudioPlaying();
27497
- return _context9.a(2);
27709
+ return _context0.a(2);
27498
27710
  case 2:
27499
27711
  console.warn("\u26A0\uFE0F AudioPlayer: AudioContext sample rate (".concat(this.audioContext.sampleRate, "Hz) doesn't match playback hop, recreating..."));
27500
27712
  this.stopImmediate();
@@ -27505,44 +27717,44 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27505
27717
  // causing newly created AudioContexts to fail silently.
27506
27718
  canReuseShared = _sharedPlayerContext && _sharedPlayerContext.state !== 'closed' && contextFits(_sharedPlayerContext.sampleRate);
27507
27719
  if (!canReuseShared) {
27508
- _context9.n = 8;
27720
+ _context0.n = 8;
27509
27721
  break;
27510
27722
  }
27511
27723
  console.log("\u267B\uFE0F AudioPlayer: Reusing shared AudioContext at ".concat(_sharedPlayerContext.sampleRate, "Hz (iOS-safe)"));
27512
27724
  this.audioContext = _sharedPlayerContext;
27513
27725
  setupAfterResume = function setupAfterResume() {
27514
- _this0.setupAudioContextStateMonitoring();
27515
- if (_this0.gainNode) {
27726
+ _this10.setupAudioContextStateMonitoring();
27727
+ if (_this10.gainNode) {
27516
27728
  try {
27517
- _this0.gainNode.disconnect();
27729
+ _this10.gainNode.disconnect();
27518
27730
  } catch (e) {
27519
27731
  console.warn('⚠️ AudioPlayer: Error disconnecting old GainNode:', e);
27520
27732
  }
27521
27733
  }
27522
- _this0.gainNode = _this0.audioContext.createGain();
27523
- _this0.gainNode.gain.value = 1.0;
27524
- _this0._connectGainToOutput();
27525
- if (!_this0._audioContextPrimed) {
27526
- _this0._primeAudioContext();
27734
+ _this10.gainNode = _this10.audioContext.createGain();
27735
+ _this10.gainNode.gain.value = 1.0;
27736
+ _this10._connectGainToOutput();
27737
+ if (!_this10._audioContextPrimed) {
27738
+ _this10._primeAudioContext();
27527
27739
  }
27528
27740
  };
27529
27741
  if (!(this.audioContext.state === 'suspended')) {
27530
- _context9.n = 7;
27742
+ _context0.n = 7;
27531
27743
  break;
27532
27744
  }
27533
- _context9.p = 4;
27534
- _context9.n = 5;
27745
+ _context0.p = 4;
27746
+ _context0.n = 5;
27535
27747
  return this.audioContext.resume();
27536
27748
  case 5:
27537
- _context9.n = 7;
27749
+ _context0.n = 7;
27538
27750
  break;
27539
27751
  case 6:
27540
- _context9.p = 6;
27541
- _t4 = _context9.v;
27752
+ _context0.p = 6;
27753
+ _t4 = _context0.v;
27542
27754
  console.warn('⚠️ AudioPlayer: Error resuming shared context:', _t4);
27543
27755
  case 7:
27544
27756
  setupAfterResume();
27545
- return _context9.a(2);
27757
+ return _context0.a(2);
27546
27758
  case 8:
27547
27759
  console.log("\uD83C\uDFB5 AudioPlayer: Creating AudioContext (tts=".concat(ttsSampleRate, "Hz, htmlHop=").concat(useHardwareRate, ", requested=").concat(desiredSampleRate, "Hz)"));
27548
27760
 
@@ -27595,9 +27807,9 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27595
27807
  this._primeAudioContext();
27596
27808
  }
27597
27809
  case 9:
27598
- return _context9.a(2);
27810
+ return _context0.a(2);
27599
27811
  }
27600
- }, _callee8, this, [[4, 6]]);
27812
+ }, _callee9, this, [[4, 6]]);
27601
27813
  }));
27602
27814
  function _initializeAudioContextImpl() {
27603
27815
  return _initializeAudioContextImpl2.apply(this, arguments);
@@ -27611,17 +27823,17 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27611
27823
  }, {
27612
27824
  key: "_primeAudioContext",
27613
27825
  value: (function () {
27614
- var _primeAudioContext2 = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee9() {
27826
+ var _primeAudioContext2 = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee0() {
27615
27827
  var sampleRate, silentBuffer, source, _t5;
27616
- return _regenerator().w(function (_context0) {
27617
- while (1) switch (_context0.p = _context0.n) {
27828
+ return _regenerator().w(function (_context1) {
27829
+ while (1) switch (_context1.p = _context1.n) {
27618
27830
  case 0:
27619
- _context0.p = 0;
27831
+ _context1.p = 0;
27620
27832
  if (!(!this.audioContext || this._audioContextPrimed)) {
27621
- _context0.n = 1;
27833
+ _context1.n = 1;
27622
27834
  break;
27623
27835
  }
27624
- return _context0.a(2);
27836
+ return _context1.a(2);
27625
27837
  case 1:
27626
27838
  sampleRate = this.audioContext.sampleRate;
27627
27839
  silentBuffer = this.audioContext.createBuffer(1, sampleRate * 0.15, sampleRate);
@@ -27632,23 +27844,23 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27632
27844
  source.connect(this.gainNode || this.audioContext.destination);
27633
27845
  source.start();
27634
27846
  this._ensureHtmlAudioPlaying();
27635
- _context0.n = 2;
27847
+ _context1.n = 2;
27636
27848
  return new Promise(function (resolve) {
27637
27849
  return setTimeout(resolve, 150);
27638
27850
  });
27639
27851
  case 2:
27640
27852
  this._audioContextPrimed = true;
27641
27853
  console.log('✅ AudioPlayer: Audio hardware primed with silent buffer');
27642
- _context0.n = 4;
27854
+ _context1.n = 4;
27643
27855
  break;
27644
27856
  case 3:
27645
- _context0.p = 3;
27646
- _t5 = _context0.v;
27857
+ _context1.p = 3;
27858
+ _t5 = _context1.v;
27647
27859
  console.error('❌ AudioPlayer: Audio priming failed:', _t5);
27648
27860
  case 4:
27649
- return _context0.a(2);
27861
+ return _context1.a(2);
27650
27862
  }
27651
- }, _callee9, this, [[0, 3]]);
27863
+ }, _callee0, this, [[0, 3]]);
27652
27864
  }));
27653
27865
  function _primeAudioContext() {
27654
27866
  return _primeAudioContext2.apply(this, arguments);
@@ -27675,6 +27887,7 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27675
27887
  this.gainNode = null;
27676
27888
  }
27677
27889
  this._teardownHtmlAudioRoute();
27890
+ this._teardownLoopbackRoute();
27678
27891
 
27679
27892
  // Remove state change listener first
27680
27893
  if (this._audioContextStateChangeHandler && this.audioContext) {
@@ -27715,7 +27928,7 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27715
27928
  * Handles mic permission grants, tab switching, browser suspension, etc.
27716
27929
  */
27717
27930
  function setupAudioContextStateMonitoring() {
27718
- var _this1 = this;
27931
+ var _this11 = this;
27719
27932
  if (!this.audioContext) {
27720
27933
  return;
27721
27934
  }
@@ -27728,38 +27941,38 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27728
27941
  // Create handler that references this.audioContext dynamically
27729
27942
  this._audioContextStateChangeHandler = function () {
27730
27943
  // Null check required because audioContext may be cleaned up while handler is queued
27731
- if (!_this1.audioContext) {
27944
+ if (!_this11.audioContext) {
27732
27945
  console.warn('⚠️ AudioPlayer: State change handler fired but AudioContext is null');
27733
27946
  return;
27734
27947
  }
27735
- console.log("\uD83C\uDFB5 AudioPlayer: AudioContext state changed to: ".concat(_this1.audioContext.state));
27736
- if (_this1.audioContext.state === 'running') {
27737
- _this1._ensureHtmlAudioPlaying();
27948
+ console.log("\uD83C\uDFB5 AudioPlayer: AudioContext state changed to: ".concat(_this11.audioContext.state));
27949
+ if (_this11.audioContext.state === 'running') {
27950
+ _this11._ensureHtmlAudioPlaying();
27738
27951
  }
27739
- if (_this1.audioContext.state === 'suspended' && _this1.isPlaying) {
27952
+ if (_this11.audioContext.state === 'suspended' && _this11.isPlaying) {
27740
27953
  // AudioContext was suspended during playback (tab switch, mic permission, etc.)
27741
27954
  console.warn('⚠️ AudioPlayer: AudioContext suspended during playback');
27742
27955
  // Note: Playback will pause automatically, but we should handle queue processing
27743
27956
  // The state change will be handled when we try to process next frame
27744
- } else if (_this1.audioContext.state === 'running' && !_this1.isPlaying && (_this1.audioQueue.length > 0 || _this1.pcmChunkQueue.length > 0 || _this1.preparedBuffer.length > 0)) {
27957
+ } else if (_this11.audioContext.state === 'running' && !_this11.isPlaying && (_this11.audioQueue.length > 0 || _this11.pcmChunkQueue.length > 0 || _this11.preparedBuffer.length > 0)) {
27745
27958
  // AudioContext resumed and we have queued frames
27746
27959
  // This handles: mic permission grant, tab switching back, browser resume, etc.
27747
27960
  console.log('✅ AudioPlayer: AudioContext resumed - resuming queue processing');
27748
27961
 
27749
27962
  // Resume queue processing if we have frames
27750
- if (_this1.audioQueue.length > 0 && !_this1.isProcessingQueue) {
27963
+ if (_this11.audioQueue.length > 0 && !_this11.isProcessingQueue) {
27751
27964
  setTimeout(function () {
27752
- return _this1.processQueue();
27965
+ return _this11.processQueue();
27753
27966
  }, 50);
27754
27967
  }
27755
- if (_this1.pcmChunkQueue.length > 0 && !_this1.isProcessingPcmQueue) {
27968
+ if (_this11.pcmChunkQueue.length > 0 && !_this11.isProcessingPcmQueue) {
27756
27969
  setTimeout(function () {
27757
- return _this1.processPcmQueue();
27970
+ return _this11.processPcmQueue();
27758
27971
  }, 50);
27759
27972
  }
27760
- if (_this1.preparedBuffer.length > 0 && !_this1.isSchedulingFrames) {
27973
+ if (_this11.preparedBuffer.length > 0 && !_this11.isSchedulingFrames) {
27761
27974
  setTimeout(function () {
27762
- return _this1.scheduleFrames();
27975
+ return _this11.scheduleFrames();
27763
27976
  }, 50);
27764
27977
  }
27765
27978
  }
@@ -27777,49 +27990,49 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27777
27990
  }, {
27778
27991
  key: "processQueue",
27779
27992
  value: (function () {
27780
- var _processQueue = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee0() {
27781
- var _this10 = this;
27993
+ var _processQueue = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee1() {
27994
+ var _this12 = this;
27782
27995
  var audioBlob, wasFirstPlay, audioContext, arrayBuffer, audioBuffer, shouldEmitStart, source, _t6;
27783
- return _regenerator().w(function (_context1) {
27784
- while (1) switch (_context1.p = _context1.n) {
27996
+ return _regenerator().w(function (_context10) {
27997
+ while (1) switch (_context10.p = _context10.n) {
27785
27998
  case 0:
27786
27999
  if (!this._isStopped) {
27787
- _context1.n = 1;
28000
+ _context10.n = 1;
27788
28001
  break;
27789
28002
  }
27790
28003
  console.log('🛑 AudioPlayer: Not processing queue - playback was stopped (barge-in)');
27791
- return _context1.a(2);
28004
+ return _context10.a(2);
27792
28005
  case 1:
27793
28006
  if (!(this.isProcessingQueue || this.audioQueue.length === 0)) {
27794
- _context1.n = 2;
28007
+ _context10.n = 2;
27795
28008
  break;
27796
28009
  }
27797
- return _context1.a(2);
28010
+ return _context10.a(2);
27798
28011
  case 2:
27799
28012
  this.isProcessingQueue = true;
27800
28013
  this._ensureHtmlAudioPlaying();
27801
28014
  audioBlob = this.audioQueue.shift();
27802
28015
  if (audioBlob) {
27803
- _context1.n = 3;
28016
+ _context10.n = 3;
27804
28017
  break;
27805
28018
  }
27806
28019
  this.isProcessingQueue = false;
27807
- return _context1.a(2);
28020
+ return _context10.a(2);
27808
28021
  case 3:
27809
- _context1.p = 3;
28022
+ _context10.p = 3;
27810
28023
  wasFirstPlay = !this.isPlaying && this.currentSource === null; // Ensure context exists and is running (handles lazy init + suspended shared context)
27811
- _context1.n = 4;
28024
+ _context10.n = 4;
27812
28025
  return this.waitForAudioContextReady();
27813
28026
  case 4:
27814
28027
  audioContext = this.audioContext; // Decode audio
27815
- _context1.n = 5;
28028
+ _context10.n = 5;
27816
28029
  return audioBlob.arrayBuffer();
27817
28030
  case 5:
27818
- arrayBuffer = _context1.v;
27819
- _context1.n = 6;
28031
+ arrayBuffer = _context10.v;
28032
+ _context10.n = 6;
27820
28033
  return audioContext.decodeAudioData(arrayBuffer);
27821
28034
  case 6:
27822
- audioBuffer = _context1.v;
28035
+ audioBuffer = _context10.v;
27823
28036
  shouldEmitStart = wasFirstPlay && !this.isPlaying && this.currentSource === null; // Create source
27824
28037
  source = audioContext.createBufferSource();
27825
28038
  source.buffer = audioBuffer;
@@ -27838,22 +28051,22 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27838
28051
  // Handle end
27839
28052
 
27840
28053
  source.onended = function () {
27841
- _this10.currentSource = null;
27842
- _this10.isProcessingQueue = false;
28054
+ _this12.currentSource = null;
28055
+ _this12.isProcessingQueue = false;
27843
28056
 
27844
28057
  // Process next chunk
27845
28058
 
27846
- if (_this10.audioQueue.length > 0) {
28059
+ if (_this12.audioQueue.length > 0) {
27847
28060
  setTimeout(function () {
27848
- return _this10.processQueue();
28061
+ return _this12.processQueue();
27849
28062
  }, 50);
27850
28063
  } else {
27851
28064
  // No more chunks - stop after delay
27852
28065
 
27853
28066
  setTimeout(function () {
27854
- if (_this10.audioQueue.length === 0 && !_this10.currentSource) {
27855
- _this10.isPlaying = false;
27856
- _this10.emit('playbackStopped');
28067
+ if (_this12.audioQueue.length === 0 && !_this12.currentSource) {
28068
+ _this12.isPlaying = false;
28069
+ _this12.emit('playbackStopped');
27857
28070
  }
27858
28071
  }, 100);
27859
28072
  }
@@ -27862,11 +28075,11 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27862
28075
  // Start playback - AudioContext should now be fully ready
27863
28076
 
27864
28077
  source.start();
27865
- _context1.n = 8;
28078
+ _context10.n = 8;
27866
28079
  break;
27867
28080
  case 7:
27868
- _context1.p = 7;
27869
- _t6 = _context1.v;
28081
+ _context10.p = 7;
28082
+ _t6 = _context10.v;
27870
28083
  console.error('❌ AudioPlayer v2: Error processing queue:', _t6);
27871
28084
  this.currentSource = null;
27872
28085
  this.emit('playbackError', _t6);
@@ -27876,7 +28089,7 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27876
28089
  if (this.audioQueue.length > 0) {
27877
28090
  this.isProcessingQueue = false;
27878
28091
  setTimeout(function () {
27879
- return _this10.processQueue();
28092
+ return _this12.processQueue();
27880
28093
  }, 100);
27881
28094
  } else {
27882
28095
  this.isPlaying = false;
@@ -27884,9 +28097,9 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27884
28097
  this.emit('playbackStopped');
27885
28098
  }
27886
28099
  case 8:
27887
- return _context1.a(2);
28100
+ return _context10.a(2);
27888
28101
  }
27889
- }, _callee0, this, [[3, 7]]);
28102
+ }, _callee1, this, [[3, 7]]);
27890
28103
  }));
27891
28104
  function processQueue() {
27892
28105
  return _processQueue.apply(this, arguments);
@@ -28099,7 +28312,7 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
28099
28312
  }, {
28100
28313
  key: "markNewSentence",
28101
28314
  value: function markNewSentence(text, synced, segmentId) {
28102
- var _this11 = this;
28315
+ var _this13 = this;
28103
28316
  var wasStopped = this._isStopped;
28104
28317
  var isCurrentlyPlaying = this.isPlaying || this.scheduledSources.size > 0;
28105
28318
 
@@ -28158,34 +28371,34 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
28158
28371
  var sentenceText = text; // Capture for timeout callback
28159
28372
  this._emptySentenceTimeout = setTimeout(function () {
28160
28373
  // Check if this sentence still has no chunks after timeout
28161
- if (_this11.pendingSentenceText === sentenceText && _this11.scheduledBuffers === 0 && _this11.preparedBuffer.length === 0 && _this11.pcmChunkQueue.length === 0 && !_this11._isStopped) {
28374
+ if (_this13.pendingSentenceText === sentenceText && _this13.scheduledBuffers === 0 && _this13.preparedBuffer.length === 0 && _this13.pcmChunkQueue.length === 0 && !_this13._isStopped) {
28162
28375
  console.warn("\u26A0\uFE0F AudioPlayer: Empty sentence detected after 5s timeout - no chunks received for: \"".concat(sentenceText.substring(0, 40), "...\""));
28163
28376
  // If this empty sentence carried a segment id, report it done (nothing was heard) and
28164
28377
  // adopt it as current so the coarse stop below matches the backend's last-sent id.
28165
- if (_this11.pendingSegmentId != null) {
28166
- var emptySegId = _this11.pendingSegmentId;
28167
- _this11.currentSegmentId = emptySegId;
28168
- _this11.pendingSegmentId = null;
28169
- _this11.emit('segmentDone', {
28378
+ if (_this13.pendingSegmentId != null) {
28379
+ var emptySegId = _this13.pendingSegmentId;
28380
+ _this13.currentSegmentId = emptySegId;
28381
+ _this13.pendingSegmentId = null;
28382
+ _this13.emit('segmentDone', {
28170
28383
  segmentId: emptySegId,
28171
28384
  status: 'finished',
28172
28385
  playedMs: 0
28173
28386
  });
28174
28387
  }
28175
28388
  // Clear pending sentence to unblock next sentence
28176
- if (_this11.pendingSentenceText === sentenceText) {
28177
- _this11.pendingSentenceText = null;
28389
+ if (_this13.pendingSentenceText === sentenceText) {
28390
+ _this13.pendingSentenceText = null;
28178
28391
  }
28179
28392
  // Emit playbackStopped to allow next sentence to start
28180
28393
  // Only if we're not currently playing (to avoid interrupting real playback)
28181
- if (!_this11.isPlaying && _this11.scheduledSources.size === 0) {
28394
+ if (!_this13.isPlaying && _this13.scheduledSources.size === 0) {
28182
28395
  console.log('🛑 AudioPlayer: Emitting playbackStopped for empty sentence timeout');
28183
- _this11.emit('playbackStopped', {
28184
- segmentId: _this11.currentSegmentId
28396
+ _this13.emit('playbackStopped', {
28397
+ segmentId: _this13.currentSegmentId
28185
28398
  });
28186
28399
  }
28187
28400
  }
28188
- _this11._emptySentenceTimeout = null;
28401
+ _this13._emptySentenceTimeout = null;
28189
28402
  }, 5000); // 5 second timeout - adjust based on expected chunk arrival rate
28190
28403
  }
28191
28404
 
@@ -28195,14 +28408,14 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
28195
28408
  }, {
28196
28409
  key: "startTranscriptChecker",
28197
28410
  value: function startTranscriptChecker() {
28198
- var _this12 = this;
28411
+ var _this14 = this;
28199
28412
  if (this.isCheckingTranscripts) return;
28200
28413
  this.isCheckingTranscripts = true;
28201
28414
  console.log('📝 AudioPlayer: Transcript checker started');
28202
28415
  var _checkLoop = function checkLoop() {
28203
- if (!_this12.isCheckingTranscripts || !_this12.audioContext) return;
28204
- var currentTime = _this12.audioContext.currentTime;
28205
- var _iterator2 = _createForOfIteratorHelper(_this12.sentenceTimings),
28416
+ if (!_this14.isCheckingTranscripts || !_this14.audioContext) return;
28417
+ var currentTime = _this14.audioContext.currentTime;
28418
+ var _iterator2 = _createForOfIteratorHelper(_this14.sentenceTimings),
28206
28419
  _step2;
28207
28420
  try {
28208
28421
  for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
@@ -28216,13 +28429,13 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
28216
28429
  eventData.synced = _timing.synced;
28217
28430
  }
28218
28431
  console.log("\uD83D\uDCDD AudioPlayer: Display transcript at ".concat(currentTime.toFixed(3), "s: \"").concat(_timing.text.substring(0, 40), "...\" (synced: ").concat(_timing.synced ? _timing.synced.length : 0, ")"));
28219
- _this12.emit('transcriptDisplay', eventData);
28432
+ _this14.emit('transcriptDisplay', eventData);
28220
28433
  }
28221
28434
  // Per-segment natural finish: this segment's audio window has fully elapsed.
28222
28435
  if (!_timing.doneReported && _timing.endTime != null && currentTime >= _timing.endTime) {
28223
28436
  _timing.doneReported = true;
28224
28437
  var _playedMs = Math.round((_timing.endTime - _timing.startTime) * 1000);
28225
- _this12.emit('segmentDone', {
28438
+ _this14.emit('segmentDone', {
28226
28439
  segmentId: _timing.segmentId,
28227
28440
  status: 'finished',
28228
28441
  playedMs: _playedMs
@@ -28234,12 +28447,12 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
28234
28447
  } finally {
28235
28448
  _iterator2.f();
28236
28449
  }
28237
- if (_this12.isPlaying || _this12.scheduledBuffers > 0) {
28450
+ if (_this14.isPlaying || _this14.scheduledBuffers > 0) {
28238
28451
  requestAnimationFrame(_checkLoop);
28239
28452
  } else {
28240
28453
  // Playback drained naturally — flush any segment whose finish tick we may have missed
28241
28454
  // (the last buffer's onended can flip isPlaying=false before this loop's next tick).
28242
- var _iterator3 = _createForOfIteratorHelper(_this12.sentenceTimings),
28455
+ var _iterator3 = _createForOfIteratorHelper(_this14.sentenceTimings),
28243
28456
  _step3;
28244
28457
  try {
28245
28458
  for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
@@ -28248,7 +28461,7 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
28248
28461
  timing.doneReported = true;
28249
28462
  var end = timing.endTime != null ? timing.endTime : timing.startTime;
28250
28463
  var playedMs = Math.round((end - timing.startTime) * 1000);
28251
- _this12.emit('segmentDone', {
28464
+ _this14.emit('segmentDone', {
28252
28465
  segmentId: timing.segmentId,
28253
28466
  status: 'finished',
28254
28467
  playedMs: playedMs
@@ -28260,7 +28473,7 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
28260
28473
  } finally {
28261
28474
  _iterator3.f();
28262
28475
  }
28263
- _this12.isCheckingTranscripts = false;
28476
+ _this14.isCheckingTranscripts = false;
28264
28477
  console.log('📝 AudioPlayer: Transcript checker stopped');
28265
28478
  }
28266
28479
  };
@@ -28291,43 +28504,43 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
28291
28504
  }, {
28292
28505
  key: "resumeAudioContext",
28293
28506
  value: (function () {
28294
- var _resumeAudioContext = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee1() {
28507
+ var _resumeAudioContext = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee10() {
28295
28508
  var _t7;
28296
- return _regenerator().w(function (_context10) {
28297
- while (1) switch (_context10.p = _context10.n) {
28509
+ return _regenerator().w(function (_context11) {
28510
+ while (1) switch (_context11.p = _context11.n) {
28298
28511
  case 0:
28299
28512
  if (!(!this.audioContext || this.audioContext.state === 'closed')) {
28300
- _context10.n = 2;
28513
+ _context11.n = 2;
28301
28514
  break;
28302
28515
  }
28303
- _context10.n = 1;
28516
+ _context11.n = 1;
28304
28517
  return this.initializeAudioContext();
28305
28518
  case 1:
28306
- _context10.n = 6;
28519
+ _context11.n = 6;
28307
28520
  break;
28308
28521
  case 2:
28309
28522
  if (!(this.audioContext.state === 'suspended')) {
28310
- _context10.n = 6;
28523
+ _context11.n = 6;
28311
28524
  break;
28312
28525
  }
28313
- _context10.p = 3;
28314
- _context10.n = 4;
28526
+ _context11.p = 3;
28527
+ _context11.n = 4;
28315
28528
  return this.audioContext.resume();
28316
28529
  case 4:
28317
28530
  console.log('✅ AudioPlayer v2: AudioContext resumed after mic permission');
28318
- _context10.n = 6;
28531
+ _context11.n = 6;
28319
28532
  break;
28320
28533
  case 5:
28321
- _context10.p = 5;
28322
- _t7 = _context10.v;
28534
+ _context11.p = 5;
28535
+ _t7 = _context11.v;
28323
28536
  console.warn('⚠️ AudioPlayer v2: Failed to resume AudioContext:', _t7);
28324
28537
  case 6:
28325
- _context10.n = 7;
28538
+ _context11.n = 7;
28326
28539
  return this.waitForAudioContextReady();
28327
28540
  case 7:
28328
- return _context10.a(2);
28541
+ return _context11.a(2);
28329
28542
  }
28330
- }, _callee1, this, [[3, 5]]);
28543
+ }, _callee10, this, [[3, 5]]);
28331
28544
  }));
28332
28545
  function resumeAudioContext() {
28333
28546
  return _resumeAudioContext.apply(this, arguments);
@@ -28578,6 +28791,11 @@ var VoiceSDK_v2 = /*#__PURE__*/function (_EventEmitter) {
28578
28791
  // Pass through undefined so AudioPlayer applies the platform default
28579
28792
  return config.htmlAudioPlayback;
28580
28793
  }(),
28794
+ // Opt-in: play TTS through a pair of local RTCPeerConnections so Chrome's
28795
+ // echo canceller can actually see it as a far-end reference (it ignores
28796
+ // <audio> elements). Default off — AudioPlayer also honours
28797
+ // ?ttp_loopback=1 / localStorage 'ttp_loopback' on its own.
28798
+ loopbackAec: config.loopbackAec,
28581
28799
  // Protocol version
28582
28800
 
28583
28801
  protocolVersion: config.protocolVersion || 2,
@@ -29478,7 +29696,7 @@ var VoiceSDK_v2 = /*#__PURE__*/function (_EventEmitter) {
29478
29696
  // iOS in-app webviews lack "Safari" in the UA (real Safari always has it).
29479
29697
  var webview = isAndroid && (/\bwv\b/.test(ua) || /Version\/[\d.]+.*Chrome/.test(ua)) || isIos && !/Safari/i.test(ua) || false;
29480
29698
  var env = {
29481
- sdkVersion: true ? "2.48.18" : 0,
29699
+ sdkVersion: true ? "2.48.20" : 0,
29482
29700
  ua: ua,
29483
29701
  platform: (uaData === null || uaData === void 0 ? void 0 : uaData.platform) || navigator.platform || '',
29484
29702
  mobile: (_uaData$mobile = uaData === null || uaData === void 0 ? void 0 : uaData.mobile) !== null && _uaData$mobile !== void 0 ? _uaData$mobile : isAndroid || isIos,
@@ -29493,7 +29711,7 @@ var VoiceSDK_v2 = /*#__PURE__*/function (_EventEmitter) {
29493
29711
  } catch (e) {
29494
29712
  console.warn('⚠️ VoiceSDK v2: Failed to build client env:', e);
29495
29713
  return {
29496
- sdkVersion: true ? "2.48.18" : 0
29714
+ sdkVersion: true ? "2.48.20" : 0
29497
29715
  };
29498
29716
  }
29499
29717
  }
@@ -29641,7 +29859,7 @@ var VoiceSDK_v2 = /*#__PURE__*/function (_EventEmitter) {
29641
29859
 
29642
29860
  // Include SDK build time for debugging
29643
29861
  if (true) {
29644
- helloMessage.lastBuildTime = "2026-08-27T12:25:20.756Z";
29862
+ helloMessage.lastBuildTime = "2026-08-27T16:45:39.666Z";
29645
29863
  }
29646
29864
 
29647
29865
  // Client environment (device/browser/webview) for backend logs + Langfuse metadata
@@ -32359,6 +32577,283 @@ function readTtpHopOverride(win) {
32359
32577
 
32360
32578
  /***/ }),
32361
32579
 
32580
+ /***/ "./src/v2/loopbackAec.js":
32581
+ /*!*******************************!*\
32582
+ !*** ./src/v2/loopbackAec.js ***!
32583
+ \*******************************/
32584
+ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
32585
+
32586
+ "use strict";
32587
+ __webpack_require__.r(__webpack_exports__);
32588
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
32589
+ /* harmony export */ TTP_LOOPBACK_SESSION_KEY: () => (/* binding */ TTP_LOOPBACK_SESSION_KEY),
32590
+ /* harmony export */ createAecLoopback: () => (/* binding */ createAecLoopback),
32591
+ /* harmony export */ isLoopbackAecSupported: () => (/* binding */ isLoopbackAecSupported),
32592
+ /* harmony export */ preferHighQualityOpus: () => (/* binding */ preferHighQualityOpus),
32593
+ /* harmony export */ readTtpLoopbackOverride: () => (/* binding */ readTtpLoopbackOverride)
32594
+ /* harmony export */ });
32595
+ function _regenerator() { /*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/babel/babel/blob/main/packages/babel-helpers/LICENSE */ var e, t, r = "function" == typeof Symbol ? Symbol : {}, n = r.iterator || "@@iterator", o = r.toStringTag || "@@toStringTag"; function i(r, n, o, i) { var c = n && n.prototype instanceof Generator ? n : Generator, u = Object.create(c.prototype); return _regeneratorDefine2(u, "_invoke", function (r, n, o) { var i, c, u, f = 0, p = o || [], y = !1, G = { p: 0, n: 0, v: e, a: d, f: d.bind(e, 4), d: function d(t, r) { return i = t, c = 0, u = e, G.n = r, a; } }; function d(r, n) { for (c = r, u = n, t = 0; !y && f && !o && t < p.length; t++) { var o, i = p[t], d = G.p, l = i[2]; r > 3 ? (o = l === n) && (u = i[(c = i[4]) ? 5 : (c = 3, 3)], i[4] = i[5] = e) : i[0] <= d && ((o = r < 2 && d < i[1]) ? (c = 0, G.v = n, G.n = i[1]) : d < l && (o = r < 3 || i[0] > n || n > l) && (i[4] = r, i[5] = n, G.n = l, c = 0)); } if (o || r > 1) return a; throw y = !0, n; } return function (o, p, l) { if (f > 1) throw TypeError("Generator is already running"); for (y && 1 === p && d(p, l), c = p, u = l; (t = c < 2 ? e : u) || !y;) { i || (c ? c < 3 ? (c > 1 && (G.n = -1), d(c, u)) : G.n = u : G.v = u); try { if (f = 2, i) { if (c || (o = "next"), t = i[o]) { if (!(t = t.call(i, u))) throw TypeError("iterator result is not an object"); if (!t.done) return t; u = t.value, c < 2 && (c = 0); } else 1 === c && (t = i.return) && t.call(i), c < 2 && (u = TypeError("The iterator does not provide a '" + o + "' method"), c = 1); i = e; } else if ((t = (y = G.n < 0) ? u : r.call(n, G)) !== a) break; } catch (t) { i = e, c = 1, u = t; } finally { f = 1; } } return { value: t, done: y }; }; }(r, o, i), !0), u; } var a = {}; function Generator() {} function GeneratorFunction() {} function GeneratorFunctionPrototype() {} t = Object.getPrototypeOf; var c = [][n] ? t(t([][n]())) : (_regeneratorDefine2(t = {}, n, function () { return this; }), t), u = GeneratorFunctionPrototype.prototype = Generator.prototype = Object.create(c); function f(e) { return Object.setPrototypeOf ? Object.setPrototypeOf(e, GeneratorFunctionPrototype) : (e.__proto__ = GeneratorFunctionPrototype, _regeneratorDefine2(e, o, "GeneratorFunction")), e.prototype = Object.create(u), e; } return GeneratorFunction.prototype = GeneratorFunctionPrototype, _regeneratorDefine2(u, "constructor", GeneratorFunctionPrototype), _regeneratorDefine2(GeneratorFunctionPrototype, "constructor", GeneratorFunction), GeneratorFunction.displayName = "GeneratorFunction", _regeneratorDefine2(GeneratorFunctionPrototype, o, "GeneratorFunction"), _regeneratorDefine2(u), _regeneratorDefine2(u, o, "Generator"), _regeneratorDefine2(u, n, function () { return this; }), _regeneratorDefine2(u, "toString", function () { return "[object Generator]"; }), (_regenerator = function _regenerator() { return { w: i, m: f }; })(); }
32596
+ function _regeneratorDefine2(e, r, n, t) { var i = Object.defineProperty; try { i({}, "", {}); } catch (e) { i = 0; } _regeneratorDefine2 = function _regeneratorDefine(e, r, n, t) { function o(r, n) { _regeneratorDefine2(e, r, function (e) { return this._invoke(r, n, e); }); } r ? i ? i(e, r, { value: n, enumerable: !t, configurable: !t, writable: !t }) : e[r] = n : (o("next", 0), o("throw", 1), o("return", 2)); }, _regeneratorDefine2(e, r, n, t); }
32597
+ function asyncGeneratorStep(n, t, e, r, o, a, c) { try { var i = n[a](c), u = i.value; } catch (n) { return void e(n); } i.done ? t(u) : Promise.resolve(u).then(r, o); }
32598
+ function _asyncToGenerator(n) { return function () { var t = this, e = arguments; return new Promise(function (r, o) { var a = n.apply(t, e); function _next(n) { asyncGeneratorStep(a, r, o, _next, _throw, "next", n); } function _throw(n) { asyncGeneratorStep(a, r, o, _next, _throw, "throw", n); } _next(void 0); }); }; }
32599
+ /**
32600
+ * Local WebRTC loopback for echo cancellation.
32601
+ *
32602
+ * WHY THIS EXISTS. Chrome's echo canceller only cancels audio that arrived
32603
+ * through a WebRTC peer connection — not <audio> elements, not Web Audio.
32604
+ * (Firefox/Safari/Edge consider all browser audio; Chrome does not.) That is
32605
+ * why the HTML hop — AudioContext → MediaStreamDestination → hidden <audio> —
32606
+ * never reliably fixed speaker echo on Chrome: where it appeared to work, what
32607
+ * actually helped was Chrome-wide echo cancellation, a separate system-level
32608
+ * feature that is not always available. Two people at the same customer, same
32609
+ * OS class, both on speakers, got opposite results from the same build.
32610
+ *
32611
+ * The fix is to make TTS *actually be* peer-connection audio: wire two
32612
+ * RTCPeerConnections together inside this tab and push playback through them.
32613
+ * The remote track is then far-end reference material Chrome's AEC will
32614
+ * cancel. No server, no SFU, no TURN, no ports — both ends are local and ICE
32615
+ * never leaves the host.
32616
+ *
32617
+ * Cost is one Opus encode/decode (~20-40 ms added latency, and lossy
32618
+ * compression of TTS that was already lossy over the wire). Bitrate is munged
32619
+ * up because the "link" is a memcpy; see preferHighQualityOpus.
32620
+ *
32621
+ * Kept free of AudioPlayer state and injectable so the peer-connection dance
32622
+ * is unit-testable without a browser: see test/loopback-aec.test.mjs.
32623
+ */
32624
+
32625
+ /** sessionStorage key mirroring an explicit ?ttp_loopback= URL value. */
32626
+ var TTP_LOOPBACK_SESSION_KEY = 'ttp_loopback_pref';
32627
+
32628
+ /**
32629
+ * QA/rollout override for the loopback route. true (force on), false (force
32630
+ * off), or null (no override — apply config, which defaults off).
32631
+ *
32632
+ * Precedence and the sessionStorage mirror match readTtpHopOverride for the
32633
+ * same reason: Borusan's navigate_to_form does window.location.href = '/path',
32634
+ * a same-origin full load that drops the query string mid-call (call
32635
+ * 84b5af7d). Without the mirror the route would silently revert on the very
32636
+ * navigation the tester is exercising.
32637
+ */
32638
+ function readTtpLoopbackOverride(win) {
32639
+ var w = win || (typeof window !== 'undefined' ? window : null);
32640
+ if (!w) return null;
32641
+ var flag = null;
32642
+ try {
32643
+ flag = new URLSearchParams(w.location.search).get('ttp_loopback');
32644
+ } catch (e) {/* no location in this env */}
32645
+ if (flag === '0' || flag === '1') {
32646
+ try {
32647
+ w.sessionStorage.setItem(TTP_LOOPBACK_SESSION_KEY, flag);
32648
+ } catch (e) {/* storage blocked */}
32649
+ return flag === '1';
32650
+ }
32651
+ var mirrored = null;
32652
+ try {
32653
+ mirrored = w.sessionStorage.getItem(TTP_LOOPBACK_SESSION_KEY);
32654
+ } catch (e) {/* storage blocked */}
32655
+ if (mirrored === '0' || mirrored === '1') return mirrored === '1';
32656
+ var stored = null;
32657
+ try {
32658
+ stored = w.localStorage.getItem('ttp_loopback');
32659
+ } catch (e) {/* storage blocked */}
32660
+ if (stored === '0' || stored === '1') return stored === '1';
32661
+ return null;
32662
+ }
32663
+ function isLoopbackAecSupported(win) {
32664
+ var w = win || (typeof window !== 'undefined' ? window : null);
32665
+ if (!w) return false;
32666
+ return typeof w.RTCPeerConnection === 'function' && typeof w.MediaStream === 'function';
32667
+ }
32668
+
32669
+ /**
32670
+ * Raise Opus quality on a loopback that has no bandwidth cost, and disable the
32671
+ * two features that are wrong for synthesized speech on a local link:
32672
+ *
32673
+ * - usedtx=0: discontinuous transmission replaces "silence" with comfort
32674
+ * noise. TTS leading/trailing silence is real audio the segment ledger
32675
+ * accounts for; letting the codec invent it would desynchronize playedMs.
32676
+ * - maxaveragebitrate: the default ~32 kbps is chosen for real networks. This
32677
+ * link is a memcpy, so spend bits instead of degrading a voice the customer
32678
+ * is paying for.
32679
+ *
32680
+ * Returns the SDP unchanged if there is no Opus line to munge — never throws,
32681
+ * because a failed munge must not cost us the route.
32682
+ */
32683
+ function preferHighQualityOpus(sdp) {
32684
+ var bitrate = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 128000;
32685
+ if (typeof sdp !== 'string' || !sdp) return sdp;
32686
+ var rtpmap = sdp.match(/^a=rtpmap:(\d+)\s+opus\/48000(?:\/2)?/mi);
32687
+ if (!rtpmap) return sdp;
32688
+ var pt = rtpmap[1];
32689
+ var params = "stereo=0;useinbandfec=1;usedtx=0;maxaveragebitrate=".concat(bitrate);
32690
+ var fmtpRe = new RegExp("^a=fmtp:".concat(pt, " (.*)$"), 'mi');
32691
+ if (fmtpRe.test(sdp)) {
32692
+ return sdp.replace(fmtpRe, function (line, existing) {
32693
+ // Drop any key we are about to set, keep everything else the browser chose.
32694
+ var kept = existing.split(';').filter(function (kv) {
32695
+ return !/^\s*(stereo|useinbandfec|usedtx|maxaveragebitrate)\s*=/i.test(kv);
32696
+ }).filter(function (kv) {
32697
+ return kv.trim().length > 0;
32698
+ });
32699
+ return "a=fmtp:".concat(pt, " ").concat(kept.concat(params.split(';')).join(';'));
32700
+ });
32701
+ }
32702
+ return sdp.replace(rtpmap[0], "".concat(rtpmap[0], "\r\na=fmtp:").concat(pt, " ").concat(params));
32703
+ }
32704
+
32705
+ /**
32706
+ * Wire two peer connections together in this tab and return the remote end of
32707
+ * the given stream.
32708
+ *
32709
+ * Resolves { remoteStream, close } once the receiving side reports connected
32710
+ * AND a track has arrived — both, because either alone has been seen without
32711
+ * audible audio. Rejects on timeout so a stalled negotiation degrades to the
32712
+ * caller's existing route rather than leaving the call silent.
32713
+ *
32714
+ * Dependencies are injected (win) so this runs under node --test.
32715
+ */
32716
+ function createAecLoopback() {
32717
+ var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
32718
+ stream = _ref.stream,
32719
+ win = _ref.win,
32720
+ _ref$timeoutMs = _ref.timeoutMs,
32721
+ timeoutMs = _ref$timeoutMs === void 0 ? 4000 : _ref$timeoutMs,
32722
+ _ref$bitrate = _ref.bitrate,
32723
+ bitrate = _ref$bitrate === void 0 ? 128000 : _ref$bitrate;
32724
+ var w = win || (typeof window !== 'undefined' ? window : null);
32725
+ if (!w) return Promise.reject(new Error('loopback: no window'));
32726
+ if (!isLoopbackAecSupported(w)) return Promise.reject(new Error('loopback: RTCPeerConnection unavailable'));
32727
+ var tracks = stream && typeof stream.getAudioTracks === 'function' ? stream.getAudioTracks() : [];
32728
+ if (!tracks.length) return Promise.reject(new Error('loopback: source stream has no audio track'));
32729
+
32730
+ // No ICE servers: both ends are in this process, so host candidates are the
32731
+ // only ones that can possibly be useful. Asking for STUN would leak the
32732
+ // user's IP to a third party for a connection that never leaves the tab.
32733
+ var pcSend = new w.RTCPeerConnection({
32734
+ iceServers: []
32735
+ });
32736
+ var pcRecv = new w.RTCPeerConnection({
32737
+ iceServers: []
32738
+ });
32739
+ var settled = false;
32740
+ var timer = null;
32741
+ var remoteStream = new w.MediaStream();
32742
+ var close = function close() {
32743
+ if (timer) {
32744
+ clearTimeout(timer);
32745
+ timer = null;
32746
+ }
32747
+ try {
32748
+ pcSend.close();
32749
+ } catch (e) {/* already closed */}
32750
+ try {
32751
+ pcRecv.close();
32752
+ } catch (e) {/* already closed */}
32753
+ };
32754
+ return new Promise(function (resolve, reject) {
32755
+ var fail = function fail(err) {
32756
+ if (settled) return;
32757
+ settled = true;
32758
+ close();
32759
+ reject(err instanceof Error ? err : new Error(String(err)));
32760
+ };
32761
+ var maybeResolve = function maybeResolve() {
32762
+ if (settled) return;
32763
+ if (!remoteStream.getAudioTracks().length) return;
32764
+ var state = pcRecv.connectionState || pcRecv.iceConnectionState;
32765
+ if (state !== 'connected' && state !== 'completed') return;
32766
+ settled = true;
32767
+ if (timer) {
32768
+ clearTimeout(timer);
32769
+ timer = null;
32770
+ }
32771
+ resolve({
32772
+ remoteStream: remoteStream,
32773
+ close: close,
32774
+ pcSend: pcSend,
32775
+ pcRecv: pcRecv
32776
+ });
32777
+ };
32778
+ timer = setTimeout(function () {
32779
+ return fail(new Error('loopback: negotiation timed out'));
32780
+ }, timeoutMs);
32781
+ pcSend.onicecandidate = function (e) {
32782
+ if (e.candidate) {
32783
+ try {
32784
+ pcRecv.addIceCandidate(e.candidate);
32785
+ } catch (err) {/* late candidate */}
32786
+ }
32787
+ };
32788
+ pcRecv.onicecandidate = function (e) {
32789
+ if (e.candidate) {
32790
+ try {
32791
+ pcSend.addIceCandidate(e.candidate);
32792
+ } catch (err) {/* late candidate */}
32793
+ }
32794
+ };
32795
+ pcRecv.ontrack = function (e) {
32796
+ var track = e.track || e.streams && e.streams[0] && e.streams[0].getAudioTracks()[0];
32797
+ if (track) remoteStream.addTrack(track);
32798
+ maybeResolve();
32799
+ };
32800
+ pcRecv.onconnectionstatechange = function () {
32801
+ var s = pcRecv.connectionState;
32802
+ if (s === 'failed' || s === 'closed') return fail(new Error("loopback: receiver ".concat(s)));
32803
+ maybeResolve();
32804
+ };
32805
+ pcRecv.oniceconnectionstatechange = function () {
32806
+ var s = pcRecv.iceConnectionState;
32807
+ if (s === 'failed') return fail(new Error('loopback: ice failed'));
32808
+ maybeResolve();
32809
+ };
32810
+ _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee() {
32811
+ var offer, tunedOffer, answer, tunedAnswer;
32812
+ return _regenerator().w(function (_context) {
32813
+ while (1) switch (_context.n) {
32814
+ case 0:
32815
+ tracks.forEach(function (t) {
32816
+ return pcSend.addTrack(t, stream);
32817
+ });
32818
+ _context.n = 1;
32819
+ return pcSend.createOffer();
32820
+ case 1:
32821
+ offer = _context.v;
32822
+ tunedOffer = {
32823
+ type: offer.type,
32824
+ sdp: preferHighQualityOpus(offer.sdp, bitrate)
32825
+ };
32826
+ _context.n = 2;
32827
+ return pcSend.setLocalDescription(tunedOffer);
32828
+ case 2:
32829
+ _context.n = 3;
32830
+ return pcRecv.setRemoteDescription(tunedOffer);
32831
+ case 3:
32832
+ _context.n = 4;
32833
+ return pcRecv.createAnswer();
32834
+ case 4:
32835
+ answer = _context.v;
32836
+ tunedAnswer = {
32837
+ type: answer.type,
32838
+ sdp: preferHighQualityOpus(answer.sdp, bitrate)
32839
+ };
32840
+ _context.n = 5;
32841
+ return pcRecv.setLocalDescription(tunedAnswer);
32842
+ case 5:
32843
+ _context.n = 6;
32844
+ return pcSend.setRemoteDescription(tunedAnswer);
32845
+ case 6:
32846
+ maybeResolve();
32847
+ case 7:
32848
+ return _context.a(2);
32849
+ }
32850
+ }, _callee);
32851
+ }))().catch(fail);
32852
+ });
32853
+ }
32854
+
32855
+ /***/ }),
32856
+
32362
32857
  /***/ "./src/v2/utils/AudioFormatConverter.js":
32363
32858
  /*!**********************************************!*\
32364
32859
  !*** ./src/v2/utils/AudioFormatConverter.js ***!
@@ -35963,7 +36458,7 @@ var TTPChatWidget = /*#__PURE__*/function () {
35963
36458
  return;
35964
36459
  }
35965
36460
  this._ensureAboutStyles();
35966
- var version = true ? "2.48.18" : 0;
36461
+ var version = true ? "2.48.20" : 0;
35967
36462
  var convId = this._getLastConversationId();
35968
36463
  var t = function t(k, fb) {
35969
36464
  try {