ttp-agent-sdk 2.48.18 → 2.48.21

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-27T17:29:05.172Z";
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.21";
21443
+ var BUILD_TIME = "2026-08-27T17:29:05.172Z";
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",
@@ -27256,13 +27274,76 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27256
27274
  sampleRate: audioDataSampleRate
27257
27275
  };
27258
27276
  }
27277
+
27278
+ /**
27279
+ * Read-only playback-route telemetry for hello (`client`) and client_audio_info.
27280
+ * Does not influence route selection — mirrors flags set by _connectGainToOutput.
27281
+ */
27282
+ }, {
27283
+ key: "getPlaybackRouteReport",
27284
+ value: function getPlaybackRouteReport() {
27285
+ var htmlAudioPlayback = this.wantsHtmlAudioPlayback();
27286
+ var loopbackWanted = this.wantsLoopbackAec();
27287
+ var playbackRoute = this._reportEffectivePlaybackRoute();
27288
+ return {
27289
+ htmlAudioPlayback: htmlAudioPlayback,
27290
+ playbackRoute: playbackRoute,
27291
+ loopbackAttach: this._reportLoopbackAttach(loopbackWanted, playbackRoute),
27292
+ // Legacy booleans — kept for downstream consumers that predated tri-state.
27293
+ htmlAudioRouteActive: this._htmlAudioRouteActive,
27294
+ htmlAudioPlayOk: this._htmlAudioPlayOk,
27295
+ loopbackAec: loopbackWanted,
27296
+ loopbackRouteActive: this._loopbackReady,
27297
+ loopbackPlayOk: this._loopbackPlayOk
27298
+ };
27299
+ }
27259
27300
  }, {
27260
27301
  key: "getHtmlAudioPlaybackInfo",
27261
27302
  value: function getHtmlAudioPlaybackInfo() {
27303
+ return this.getPlaybackRouteReport();
27304
+ }
27305
+
27306
+ /** Effective last-hop route from established player state (not config intent). */
27307
+ }, {
27308
+ key: "_reportEffectivePlaybackRoute",
27309
+ value: function _reportEffectivePlaybackRoute() {
27310
+ if (this._loopbackReady && !this._loopbackFailed) {
27311
+ return 'loopback';
27312
+ }
27313
+ if (this._htmlAudioRouteActive) {
27314
+ return 'htmlHop';
27315
+ }
27316
+ return 'destination';
27317
+ }
27318
+ }, {
27319
+ key: "_reportLoopbackAttach",
27320
+ value: function _reportLoopbackAttach(loopbackWanted, playbackRoute) {
27321
+ if (!loopbackWanted) {
27322
+ return {
27323
+ outcome: 'not_attempted'
27324
+ };
27325
+ }
27326
+ if (this._loopbackFailed) {
27327
+ return {
27328
+ outcome: 'failed',
27329
+ fallbackTo: playbackRoute
27330
+ };
27331
+ }
27332
+ if (this._loopbackReady && this._loopbackPlayOk) {
27333
+ return {
27334
+ outcome: 'success'
27335
+ };
27336
+ }
27337
+ if (this._loopbackReady && !this._loopbackPlayOk) {
27338
+ // Peer path is up; element play() not yet confirmed.
27339
+ return {
27340
+ outcome: 'pending'
27341
+ };
27342
+ }
27343
+ // Negotiation in flight — audio may be on an interim hop/destination route.
27262
27344
  return {
27263
- htmlAudioPlayback: this.wantsHtmlAudioPlayback(),
27264
- htmlAudioRouteActive: this._htmlAudioRouteActive,
27265
- htmlAudioPlayOk: this._htmlAudioPlayOk
27345
+ outcome: 'pending',
27346
+ fallbackTo: playbackRoute
27266
27347
  };
27267
27348
  }
27268
27349
 
@@ -27279,6 +27360,18 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27279
27360
  try {
27280
27361
  this.gainNode.disconnect();
27281
27362
  } catch (_) {/* ignore */}
27363
+
27364
+ // Loopback first when asked for. It returns null until the peer connections
27365
+ // are up (negotiation is async), and we deliberately fall through to a
27366
+ // normal audible route in the meantime rather than holding audio: the
27367
+ // establishment is re-entrant and calls back here when it completes.
27368
+ if (this.wantsLoopbackAec()) {
27369
+ var loopbackDest = this._ensureLoopbackRoute();
27370
+ if (loopbackDest) {
27371
+ this.gainNode.connect(loopbackDest);
27372
+ return;
27373
+ }
27374
+ }
27282
27375
  if (this.wantsHtmlAudioPlayback() && this._ensureHtmlAudioRoute()) {
27283
27376
  this.gainNode.connect(this._mediaStreamDest);
27284
27377
  return;
@@ -27303,16 +27396,7 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27303
27396
  }
27304
27397
  try {
27305
27398
  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);
27399
+ var el = this._createHiddenAudioEl(this._mediaStreamDest.stream);
27316
27400
  this._htmlAudioEl = el;
27317
27401
  this._htmlAudioRouteActive = true;
27318
27402
  this._ensureHtmlAudioPlaying();
@@ -27393,6 +27477,7 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27393
27477
  this.gainNode.disconnect();
27394
27478
  } catch (_) {/* ignore */}
27395
27479
  this._teardownHtmlAudioRoute();
27480
+ this._teardownLoopbackRoute();
27396
27481
  this.gainNode.connect(this.audioContext.destination);
27397
27482
  console.warn("\u26A0\uFE0F AudioPlayer: Fell back to AudioContext.destination (".concat(reason, ")"));
27398
27483
  this.emit('htmlAudioRoute', {
@@ -27420,29 +27505,216 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27420
27505
  this._htmlAudioRouteActive = false;
27421
27506
  }
27422
27507
 
27508
+ /**
27509
+ * Shared factory for the hidden playback element. Both last-hop routes need
27510
+ * exactly this: autoplay, inline on iOS, invisible, rate pinned to 1 so a
27511
+ * page-level playbackRate can't stretch TTS under us.
27512
+ */
27513
+ }, {
27514
+ key: "_createHiddenAudioEl",
27515
+ value: function _createHiddenAudioEl(stream) {
27516
+ var el = document.createElement('audio');
27517
+ el.setAttribute('playsinline', '');
27518
+ el.setAttribute('webkit-playsinline', '');
27519
+ el.autoplay = true;
27520
+ el.controls = false;
27521
+ el.preload = 'auto';
27522
+ el.playbackRate = 1;
27523
+ el.style.display = 'none';
27524
+ el.srcObject = stream;
27525
+ document.body.appendChild(el);
27526
+ return el;
27527
+ }
27528
+
27529
+ /**
27530
+ * Opt-in loopback route. Default OFF: this is an unproven replacement for the
27531
+ * hop running on live customer sites, so it ships dark and is turned on per
27532
+ * machine (?ttp_loopback=1 / localStorage) or per integration (config).
27533
+ */
27534
+ }, {
27535
+ key: "wantsLoopbackAec",
27536
+ value: function wantsLoopbackAec() {
27537
+ // Memoized per player: _htmlHopNeedsHardwareRate() consults this from
27538
+ // _honestRateSamples, i.e. once per audio chunk, and reading it live would
27539
+ // put a synchronous storage hit on the hot path for every machine where
27540
+ // the hop is off (there the || cannot short-circuit). Changing the flag
27541
+ // requires a reload anyway — that is how it is documented and tested.
27542
+ if (this._loopbackWanted === undefined) {
27543
+ var override = (0,_loopbackAec_js__WEBPACK_IMPORTED_MODULE_5__.readTtpLoopbackOverride)(typeof window !== 'undefined' ? window : null);
27544
+ this._loopbackWanted = override !== null ? override : typeof this.config.loopbackAec === 'boolean' ? this.config.loopbackAec : false;
27545
+ if (this._loopbackWanted) {
27546
+ console.log('🔊 AudioPlayer: loopback AEC requested — TTS will play via a local peer connection');
27547
+ }
27548
+ }
27549
+ return this._loopbackWanted;
27550
+ }
27551
+
27552
+ /**
27553
+ * Returns the MediaStreamDestination to play into once the loopback is up,
27554
+ * or null while it is not. Never blocks: the caller routes audio somewhere
27555
+ * audible in the meantime and we re-route from _establishLoopback.
27556
+ */
27557
+ }, {
27558
+ key: "_ensureLoopbackRoute",
27559
+ value: function _ensureLoopbackRoute() {
27560
+ var _this9 = this;
27561
+ if (this._loopbackReady && this._loopbackDest) {
27562
+ this._ensureLoopbackPlaying();
27563
+ return this._loopbackDest;
27564
+ }
27565
+ if (this._loopbackFailed || this._loopbackPending) return null;
27566
+ if (!this.audioContext || typeof this.audioContext.createMediaStreamDestination !== 'function') {
27567
+ this._failLoopback('no_media_stream_destination');
27568
+ return null;
27569
+ }
27570
+ if (!(0,_loopbackAec_js__WEBPACK_IMPORTED_MODULE_5__.isLoopbackAecSupported)(typeof window !== 'undefined' ? window : null)) {
27571
+ this._failLoopback('no_peer_connection');
27572
+ return null;
27573
+ }
27574
+ this._loopbackPending = true;
27575
+ this._establishLoopback().catch(function (e) {
27576
+ console.warn('⚠️ AudioPlayer: loopback AEC route unavailable, keeping existing playback route:', e && e.message);
27577
+ _this9._failLoopback('negotiation_failed');
27578
+ });
27579
+ return null;
27580
+ }
27581
+ }, {
27582
+ key: "_establishLoopback",
27583
+ value: function () {
27584
+ var _establishLoopback2 = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee7() {
27585
+ var gen, dest, _yield$createAecLoopb, remoteStream, close;
27586
+ return _regenerator().w(function (_context8) {
27587
+ while (1) switch (_context8.n) {
27588
+ case 0:
27589
+ // Generation token: a teardown (call end, context rebuild) while we are
27590
+ // negotiating must not resurrect a route into a dead AudioContext.
27591
+ gen = this._loopbackGen = (this._loopbackGen || 0) + 1;
27592
+ dest = this.audioContext.createMediaStreamDestination();
27593
+ _context8.n = 1;
27594
+ return (0,_loopbackAec_js__WEBPACK_IMPORTED_MODULE_5__.createAecLoopback)({
27595
+ stream: dest.stream
27596
+ });
27597
+ case 1:
27598
+ _yield$createAecLoopb = _context8.v;
27599
+ remoteStream = _yield$createAecLoopb.remoteStream;
27600
+ close = _yield$createAecLoopb.close;
27601
+ if (!(gen !== this._loopbackGen || !this.audioContext)) {
27602
+ _context8.n = 2;
27603
+ break;
27604
+ }
27605
+ try {
27606
+ close();
27607
+ } catch (_) {/* ignore */}
27608
+ return _context8.a(2);
27609
+ case 2:
27610
+ // Drop the transitional hop element if one was built while we negotiated,
27611
+ // or it keeps playing a destination nothing is connected to any more.
27612
+ this._teardownHtmlAudioRoute();
27613
+ this._loopbackDest = dest;
27614
+ this._loopbackClose = close;
27615
+ this._loopbackEl = this._createHiddenAudioEl(remoteStream);
27616
+ this._loopbackReady = true;
27617
+ this._loopbackPending = false;
27618
+ this._ensureLoopbackPlaying();
27619
+ this._connectGainToOutput();
27620
+ console.log('🔊 AudioPlayer: Playback last-hop is a local WebRTC loopback (Chrome-cancellable AEC far-end)');
27621
+ this.emit('htmlAudioRoute', {
27622
+ state: 'loopback_established'
27623
+ });
27624
+ case 3:
27625
+ return _context8.a(2);
27626
+ }
27627
+ }, _callee7, this);
27628
+ }));
27629
+ function _establishLoopback() {
27630
+ return _establishLoopback2.apply(this, arguments);
27631
+ }
27632
+ return _establishLoopback;
27633
+ }()
27634
+ }, {
27635
+ key: "_ensureLoopbackPlaying",
27636
+ value: function _ensureLoopbackPlaying() {
27637
+ var _this0 = this;
27638
+ var el = this._loopbackEl;
27639
+ if (!el) return;
27640
+ if (!el.paused && this._loopbackPlayOk) return;
27641
+ var playResult = el.play();
27642
+ if (playResult && typeof playResult.then === 'function') {
27643
+ playResult.then(function () {
27644
+ _this0._loopbackPlayOk = true;
27645
+ }).catch(function (err) {
27646
+ if (_this0._loopbackPlayOk || !el.paused) return;
27647
+ console.warn('⚠️ AudioPlayer: loopback element play() rejected — reverting to the previous route:', err);
27648
+ _this0._failLoopback('play_rejected');
27649
+ _this0._connectGainToOutput();
27650
+ });
27651
+ }
27652
+ }
27653
+
27654
+ /** One-way latch: once the loopback is off for this player it stays off,
27655
+ * rather than retrying every reconnect and stuttering the route. */
27656
+ }, {
27657
+ key: "_failLoopback",
27658
+ value: function _failLoopback(reason) {
27659
+ this._loopbackFailed = true;
27660
+ this._loopbackPending = false;
27661
+ this._teardownLoopbackRoute();
27662
+ this.emit('htmlAudioRoute', {
27663
+ state: 'loopback_fallback',
27664
+ reason: reason
27665
+ });
27666
+ }
27667
+ }, {
27668
+ key: "_teardownLoopbackRoute",
27669
+ value: function _teardownLoopbackRoute() {
27670
+ this._loopbackGen = (this._loopbackGen || 0) + 1;
27671
+ if (this._loopbackEl) {
27672
+ try {
27673
+ this._loopbackEl.pause();
27674
+ } catch (_) {/* ignore */}
27675
+ try {
27676
+ this._loopbackEl.srcObject = null;
27677
+ } catch (_) {/* ignore */}
27678
+ if (this._loopbackEl.parentNode) {
27679
+ this._loopbackEl.parentNode.removeChild(this._loopbackEl);
27680
+ }
27681
+ this._loopbackEl = null;
27682
+ }
27683
+ if (this._loopbackClose) {
27684
+ try {
27685
+ this._loopbackClose();
27686
+ } catch (_) {/* ignore */}
27687
+ this._loopbackClose = null;
27688
+ }
27689
+ this._loopbackDest = null;
27690
+ this._loopbackReady = false;
27691
+ this._loopbackPending = false;
27692
+ this._loopbackPlayOk = false;
27693
+ }
27694
+
27423
27695
  /**
27424
27696
  * Initialize audio context with correct sample rate
27425
27697
  */
27426
27698
  }, {
27427
27699
  key: "initializeAudioContext",
27428
27700
  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) {
27701
+ var _initializeAudioContext = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee8() {
27702
+ var _this1 = this;
27703
+ return _regenerator().w(function (_context9) {
27704
+ while (1) switch (_context9.n) {
27433
27705
  case 0:
27434
27706
  if (!this._audioContextInitPromise) {
27435
- _context8.n = 1;
27707
+ _context9.n = 1;
27436
27708
  break;
27437
27709
  }
27438
- return _context8.a(2, this._audioContextInitPromise);
27710
+ return _context9.a(2, this._audioContextInitPromise);
27439
27711
  case 1:
27440
27712
  this._audioContextInitPromise = this._initializeAudioContextImpl().finally(function () {
27441
- _this9._audioContextInitPromise = null;
27713
+ _this1._audioContextInitPromise = null;
27442
27714
  });
27443
- return _context8.a(2, this._audioContextInitPromise);
27715
+ return _context9.a(2, this._audioContextInitPromise);
27444
27716
  }
27445
- }, _callee7, this);
27717
+ }, _callee8, this);
27446
27718
  }));
27447
27719
  function initializeAudioContext() {
27448
27720
  return _initializeAudioContext.apply(this, arguments);
@@ -27452,11 +27724,11 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27452
27724
  }, {
27453
27725
  key: "_initializeAudioContextImpl",
27454
27726
  value: function () {
27455
- var _initializeAudioContextImpl2 = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee8() {
27456
- var _this0 = this;
27727
+ var _initializeAudioContextImpl2 = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee9() {
27728
+ var _this10 = this;
27457
27729
  var ttsSampleRate, useHardwareRate, desiredSampleRate, contextFits, canReuseShared, setupAfterResume, ctxOpts, _t4;
27458
- return _regenerator().w(function (_context9) {
27459
- while (1) switch (_context9.p = _context9.n) {
27730
+ return _regenerator().w(function (_context0) {
27731
+ while (1) switch (_context0.p = _context0.n) {
27460
27732
  case 0:
27461
27733
  ttsSampleRate = this._ttsSampleRate();
27462
27734
  useHardwareRate = this._htmlHopNeedsHardwareRate(); // Destination path: match TTS (24 kHz). HTML last-hop: request the standard
@@ -27474,27 +27746,27 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27474
27746
  _sharedPlayerSampleRate = null;
27475
27747
  }
27476
27748
  contextFits = function contextFits(rate) {
27477
- if (useHardwareRate) return _this0._isHardwarePlaybackRate(rate);
27749
+ if (useHardwareRate) return _this10._isHardwarePlaybackRate(rate);
27478
27750
  return Math.abs(rate - desiredSampleRate) <= 100;
27479
27751
  }; // Check if current instance AudioContext exists and matches
27480
27752
  if (!this.audioContext) {
27481
- _context9.n = 3;
27753
+ _context0.n = 3;
27482
27754
  break;
27483
27755
  }
27484
27756
  if (!(this.audioContext.state === 'closed')) {
27485
- _context9.n = 1;
27757
+ _context0.n = 1;
27486
27758
  break;
27487
27759
  }
27488
27760
  this._invalidateClosedAudioContext();
27489
- _context9.n = 3;
27761
+ _context0.n = 3;
27490
27762
  break;
27491
27763
  case 1:
27492
27764
  if (!contextFits(this.audioContext.sampleRate)) {
27493
- _context9.n = 2;
27765
+ _context0.n = 2;
27494
27766
  break;
27495
27767
  }
27496
27768
  this._ensureHtmlAudioPlaying();
27497
- return _context9.a(2);
27769
+ return _context0.a(2);
27498
27770
  case 2:
27499
27771
  console.warn("\u26A0\uFE0F AudioPlayer: AudioContext sample rate (".concat(this.audioContext.sampleRate, "Hz) doesn't match playback hop, recreating..."));
27500
27772
  this.stopImmediate();
@@ -27505,44 +27777,44 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27505
27777
  // causing newly created AudioContexts to fail silently.
27506
27778
  canReuseShared = _sharedPlayerContext && _sharedPlayerContext.state !== 'closed' && contextFits(_sharedPlayerContext.sampleRate);
27507
27779
  if (!canReuseShared) {
27508
- _context9.n = 8;
27780
+ _context0.n = 8;
27509
27781
  break;
27510
27782
  }
27511
27783
  console.log("\u267B\uFE0F AudioPlayer: Reusing shared AudioContext at ".concat(_sharedPlayerContext.sampleRate, "Hz (iOS-safe)"));
27512
27784
  this.audioContext = _sharedPlayerContext;
27513
27785
  setupAfterResume = function setupAfterResume() {
27514
- _this0.setupAudioContextStateMonitoring();
27515
- if (_this0.gainNode) {
27786
+ _this10.setupAudioContextStateMonitoring();
27787
+ if (_this10.gainNode) {
27516
27788
  try {
27517
- _this0.gainNode.disconnect();
27789
+ _this10.gainNode.disconnect();
27518
27790
  } catch (e) {
27519
27791
  console.warn('⚠️ AudioPlayer: Error disconnecting old GainNode:', e);
27520
27792
  }
27521
27793
  }
27522
- _this0.gainNode = _this0.audioContext.createGain();
27523
- _this0.gainNode.gain.value = 1.0;
27524
- _this0._connectGainToOutput();
27525
- if (!_this0._audioContextPrimed) {
27526
- _this0._primeAudioContext();
27794
+ _this10.gainNode = _this10.audioContext.createGain();
27795
+ _this10.gainNode.gain.value = 1.0;
27796
+ _this10._connectGainToOutput();
27797
+ if (!_this10._audioContextPrimed) {
27798
+ _this10._primeAudioContext();
27527
27799
  }
27528
27800
  };
27529
27801
  if (!(this.audioContext.state === 'suspended')) {
27530
- _context9.n = 7;
27802
+ _context0.n = 7;
27531
27803
  break;
27532
27804
  }
27533
- _context9.p = 4;
27534
- _context9.n = 5;
27805
+ _context0.p = 4;
27806
+ _context0.n = 5;
27535
27807
  return this.audioContext.resume();
27536
27808
  case 5:
27537
- _context9.n = 7;
27809
+ _context0.n = 7;
27538
27810
  break;
27539
27811
  case 6:
27540
- _context9.p = 6;
27541
- _t4 = _context9.v;
27812
+ _context0.p = 6;
27813
+ _t4 = _context0.v;
27542
27814
  console.warn('⚠️ AudioPlayer: Error resuming shared context:', _t4);
27543
27815
  case 7:
27544
27816
  setupAfterResume();
27545
- return _context9.a(2);
27817
+ return _context0.a(2);
27546
27818
  case 8:
27547
27819
  console.log("\uD83C\uDFB5 AudioPlayer: Creating AudioContext (tts=".concat(ttsSampleRate, "Hz, htmlHop=").concat(useHardwareRate, ", requested=").concat(desiredSampleRate, "Hz)"));
27548
27820
 
@@ -27595,9 +27867,9 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27595
27867
  this._primeAudioContext();
27596
27868
  }
27597
27869
  case 9:
27598
- return _context9.a(2);
27870
+ return _context0.a(2);
27599
27871
  }
27600
- }, _callee8, this, [[4, 6]]);
27872
+ }, _callee9, this, [[4, 6]]);
27601
27873
  }));
27602
27874
  function _initializeAudioContextImpl() {
27603
27875
  return _initializeAudioContextImpl2.apply(this, arguments);
@@ -27611,17 +27883,17 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27611
27883
  }, {
27612
27884
  key: "_primeAudioContext",
27613
27885
  value: (function () {
27614
- var _primeAudioContext2 = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee9() {
27886
+ var _primeAudioContext2 = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee0() {
27615
27887
  var sampleRate, silentBuffer, source, _t5;
27616
- return _regenerator().w(function (_context0) {
27617
- while (1) switch (_context0.p = _context0.n) {
27888
+ return _regenerator().w(function (_context1) {
27889
+ while (1) switch (_context1.p = _context1.n) {
27618
27890
  case 0:
27619
- _context0.p = 0;
27891
+ _context1.p = 0;
27620
27892
  if (!(!this.audioContext || this._audioContextPrimed)) {
27621
- _context0.n = 1;
27893
+ _context1.n = 1;
27622
27894
  break;
27623
27895
  }
27624
- return _context0.a(2);
27896
+ return _context1.a(2);
27625
27897
  case 1:
27626
27898
  sampleRate = this.audioContext.sampleRate;
27627
27899
  silentBuffer = this.audioContext.createBuffer(1, sampleRate * 0.15, sampleRate);
@@ -27632,23 +27904,23 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27632
27904
  source.connect(this.gainNode || this.audioContext.destination);
27633
27905
  source.start();
27634
27906
  this._ensureHtmlAudioPlaying();
27635
- _context0.n = 2;
27907
+ _context1.n = 2;
27636
27908
  return new Promise(function (resolve) {
27637
27909
  return setTimeout(resolve, 150);
27638
27910
  });
27639
27911
  case 2:
27640
27912
  this._audioContextPrimed = true;
27641
27913
  console.log('✅ AudioPlayer: Audio hardware primed with silent buffer');
27642
- _context0.n = 4;
27914
+ _context1.n = 4;
27643
27915
  break;
27644
27916
  case 3:
27645
- _context0.p = 3;
27646
- _t5 = _context0.v;
27917
+ _context1.p = 3;
27918
+ _t5 = _context1.v;
27647
27919
  console.error('❌ AudioPlayer: Audio priming failed:', _t5);
27648
27920
  case 4:
27649
- return _context0.a(2);
27921
+ return _context1.a(2);
27650
27922
  }
27651
- }, _callee9, this, [[0, 3]]);
27923
+ }, _callee0, this, [[0, 3]]);
27652
27924
  }));
27653
27925
  function _primeAudioContext() {
27654
27926
  return _primeAudioContext2.apply(this, arguments);
@@ -27675,6 +27947,7 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27675
27947
  this.gainNode = null;
27676
27948
  }
27677
27949
  this._teardownHtmlAudioRoute();
27950
+ this._teardownLoopbackRoute();
27678
27951
 
27679
27952
  // Remove state change listener first
27680
27953
  if (this._audioContextStateChangeHandler && this.audioContext) {
@@ -27715,7 +27988,7 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27715
27988
  * Handles mic permission grants, tab switching, browser suspension, etc.
27716
27989
  */
27717
27990
  function setupAudioContextStateMonitoring() {
27718
- var _this1 = this;
27991
+ var _this11 = this;
27719
27992
  if (!this.audioContext) {
27720
27993
  return;
27721
27994
  }
@@ -27728,38 +28001,38 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27728
28001
  // Create handler that references this.audioContext dynamically
27729
28002
  this._audioContextStateChangeHandler = function () {
27730
28003
  // Null check required because audioContext may be cleaned up while handler is queued
27731
- if (!_this1.audioContext) {
28004
+ if (!_this11.audioContext) {
27732
28005
  console.warn('⚠️ AudioPlayer: State change handler fired but AudioContext is null');
27733
28006
  return;
27734
28007
  }
27735
- console.log("\uD83C\uDFB5 AudioPlayer: AudioContext state changed to: ".concat(_this1.audioContext.state));
27736
- if (_this1.audioContext.state === 'running') {
27737
- _this1._ensureHtmlAudioPlaying();
28008
+ console.log("\uD83C\uDFB5 AudioPlayer: AudioContext state changed to: ".concat(_this11.audioContext.state));
28009
+ if (_this11.audioContext.state === 'running') {
28010
+ _this11._ensureHtmlAudioPlaying();
27738
28011
  }
27739
- if (_this1.audioContext.state === 'suspended' && _this1.isPlaying) {
28012
+ if (_this11.audioContext.state === 'suspended' && _this11.isPlaying) {
27740
28013
  // AudioContext was suspended during playback (tab switch, mic permission, etc.)
27741
28014
  console.warn('⚠️ AudioPlayer: AudioContext suspended during playback');
27742
28015
  // Note: Playback will pause automatically, but we should handle queue processing
27743
28016
  // 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)) {
28017
+ } else if (_this11.audioContext.state === 'running' && !_this11.isPlaying && (_this11.audioQueue.length > 0 || _this11.pcmChunkQueue.length > 0 || _this11.preparedBuffer.length > 0)) {
27745
28018
  // AudioContext resumed and we have queued frames
27746
28019
  // This handles: mic permission grant, tab switching back, browser resume, etc.
27747
28020
  console.log('✅ AudioPlayer: AudioContext resumed - resuming queue processing');
27748
28021
 
27749
28022
  // Resume queue processing if we have frames
27750
- if (_this1.audioQueue.length > 0 && !_this1.isProcessingQueue) {
28023
+ if (_this11.audioQueue.length > 0 && !_this11.isProcessingQueue) {
27751
28024
  setTimeout(function () {
27752
- return _this1.processQueue();
28025
+ return _this11.processQueue();
27753
28026
  }, 50);
27754
28027
  }
27755
- if (_this1.pcmChunkQueue.length > 0 && !_this1.isProcessingPcmQueue) {
28028
+ if (_this11.pcmChunkQueue.length > 0 && !_this11.isProcessingPcmQueue) {
27756
28029
  setTimeout(function () {
27757
- return _this1.processPcmQueue();
28030
+ return _this11.processPcmQueue();
27758
28031
  }, 50);
27759
28032
  }
27760
- if (_this1.preparedBuffer.length > 0 && !_this1.isSchedulingFrames) {
28033
+ if (_this11.preparedBuffer.length > 0 && !_this11.isSchedulingFrames) {
27761
28034
  setTimeout(function () {
27762
- return _this1.scheduleFrames();
28035
+ return _this11.scheduleFrames();
27763
28036
  }, 50);
27764
28037
  }
27765
28038
  }
@@ -27777,49 +28050,49 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27777
28050
  }, {
27778
28051
  key: "processQueue",
27779
28052
  value: (function () {
27780
- var _processQueue = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee0() {
27781
- var _this10 = this;
28053
+ var _processQueue = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee1() {
28054
+ var _this12 = this;
27782
28055
  var audioBlob, wasFirstPlay, audioContext, arrayBuffer, audioBuffer, shouldEmitStart, source, _t6;
27783
- return _regenerator().w(function (_context1) {
27784
- while (1) switch (_context1.p = _context1.n) {
28056
+ return _regenerator().w(function (_context10) {
28057
+ while (1) switch (_context10.p = _context10.n) {
27785
28058
  case 0:
27786
28059
  if (!this._isStopped) {
27787
- _context1.n = 1;
28060
+ _context10.n = 1;
27788
28061
  break;
27789
28062
  }
27790
28063
  console.log('🛑 AudioPlayer: Not processing queue - playback was stopped (barge-in)');
27791
- return _context1.a(2);
28064
+ return _context10.a(2);
27792
28065
  case 1:
27793
28066
  if (!(this.isProcessingQueue || this.audioQueue.length === 0)) {
27794
- _context1.n = 2;
28067
+ _context10.n = 2;
27795
28068
  break;
27796
28069
  }
27797
- return _context1.a(2);
28070
+ return _context10.a(2);
27798
28071
  case 2:
27799
28072
  this.isProcessingQueue = true;
27800
28073
  this._ensureHtmlAudioPlaying();
27801
28074
  audioBlob = this.audioQueue.shift();
27802
28075
  if (audioBlob) {
27803
- _context1.n = 3;
28076
+ _context10.n = 3;
27804
28077
  break;
27805
28078
  }
27806
28079
  this.isProcessingQueue = false;
27807
- return _context1.a(2);
28080
+ return _context10.a(2);
27808
28081
  case 3:
27809
- _context1.p = 3;
28082
+ _context10.p = 3;
27810
28083
  wasFirstPlay = !this.isPlaying && this.currentSource === null; // Ensure context exists and is running (handles lazy init + suspended shared context)
27811
- _context1.n = 4;
28084
+ _context10.n = 4;
27812
28085
  return this.waitForAudioContextReady();
27813
28086
  case 4:
27814
28087
  audioContext = this.audioContext; // Decode audio
27815
- _context1.n = 5;
28088
+ _context10.n = 5;
27816
28089
  return audioBlob.arrayBuffer();
27817
28090
  case 5:
27818
- arrayBuffer = _context1.v;
27819
- _context1.n = 6;
28091
+ arrayBuffer = _context10.v;
28092
+ _context10.n = 6;
27820
28093
  return audioContext.decodeAudioData(arrayBuffer);
27821
28094
  case 6:
27822
- audioBuffer = _context1.v;
28095
+ audioBuffer = _context10.v;
27823
28096
  shouldEmitStart = wasFirstPlay && !this.isPlaying && this.currentSource === null; // Create source
27824
28097
  source = audioContext.createBufferSource();
27825
28098
  source.buffer = audioBuffer;
@@ -27838,22 +28111,22 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27838
28111
  // Handle end
27839
28112
 
27840
28113
  source.onended = function () {
27841
- _this10.currentSource = null;
27842
- _this10.isProcessingQueue = false;
28114
+ _this12.currentSource = null;
28115
+ _this12.isProcessingQueue = false;
27843
28116
 
27844
28117
  // Process next chunk
27845
28118
 
27846
- if (_this10.audioQueue.length > 0) {
28119
+ if (_this12.audioQueue.length > 0) {
27847
28120
  setTimeout(function () {
27848
- return _this10.processQueue();
28121
+ return _this12.processQueue();
27849
28122
  }, 50);
27850
28123
  } else {
27851
28124
  // No more chunks - stop after delay
27852
28125
 
27853
28126
  setTimeout(function () {
27854
- if (_this10.audioQueue.length === 0 && !_this10.currentSource) {
27855
- _this10.isPlaying = false;
27856
- _this10.emit('playbackStopped');
28127
+ if (_this12.audioQueue.length === 0 && !_this12.currentSource) {
28128
+ _this12.isPlaying = false;
28129
+ _this12.emit('playbackStopped');
27857
28130
  }
27858
28131
  }, 100);
27859
28132
  }
@@ -27862,11 +28135,11 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27862
28135
  // Start playback - AudioContext should now be fully ready
27863
28136
 
27864
28137
  source.start();
27865
- _context1.n = 8;
28138
+ _context10.n = 8;
27866
28139
  break;
27867
28140
  case 7:
27868
- _context1.p = 7;
27869
- _t6 = _context1.v;
28141
+ _context10.p = 7;
28142
+ _t6 = _context10.v;
27870
28143
  console.error('❌ AudioPlayer v2: Error processing queue:', _t6);
27871
28144
  this.currentSource = null;
27872
28145
  this.emit('playbackError', _t6);
@@ -27876,7 +28149,7 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27876
28149
  if (this.audioQueue.length > 0) {
27877
28150
  this.isProcessingQueue = false;
27878
28151
  setTimeout(function () {
27879
- return _this10.processQueue();
28152
+ return _this12.processQueue();
27880
28153
  }, 100);
27881
28154
  } else {
27882
28155
  this.isPlaying = false;
@@ -27884,9 +28157,9 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27884
28157
  this.emit('playbackStopped');
27885
28158
  }
27886
28159
  case 8:
27887
- return _context1.a(2);
28160
+ return _context10.a(2);
27888
28161
  }
27889
- }, _callee0, this, [[3, 7]]);
28162
+ }, _callee1, this, [[3, 7]]);
27890
28163
  }));
27891
28164
  function processQueue() {
27892
28165
  return _processQueue.apply(this, arguments);
@@ -28099,7 +28372,7 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
28099
28372
  }, {
28100
28373
  key: "markNewSentence",
28101
28374
  value: function markNewSentence(text, synced, segmentId) {
28102
- var _this11 = this;
28375
+ var _this13 = this;
28103
28376
  var wasStopped = this._isStopped;
28104
28377
  var isCurrentlyPlaying = this.isPlaying || this.scheduledSources.size > 0;
28105
28378
 
@@ -28158,34 +28431,34 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
28158
28431
  var sentenceText = text; // Capture for timeout callback
28159
28432
  this._emptySentenceTimeout = setTimeout(function () {
28160
28433
  // 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) {
28434
+ if (_this13.pendingSentenceText === sentenceText && _this13.scheduledBuffers === 0 && _this13.preparedBuffer.length === 0 && _this13.pcmChunkQueue.length === 0 && !_this13._isStopped) {
28162
28435
  console.warn("\u26A0\uFE0F AudioPlayer: Empty sentence detected after 5s timeout - no chunks received for: \"".concat(sentenceText.substring(0, 40), "...\""));
28163
28436
  // If this empty sentence carried a segment id, report it done (nothing was heard) and
28164
28437
  // 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', {
28438
+ if (_this13.pendingSegmentId != null) {
28439
+ var emptySegId = _this13.pendingSegmentId;
28440
+ _this13.currentSegmentId = emptySegId;
28441
+ _this13.pendingSegmentId = null;
28442
+ _this13.emit('segmentDone', {
28170
28443
  segmentId: emptySegId,
28171
28444
  status: 'finished',
28172
28445
  playedMs: 0
28173
28446
  });
28174
28447
  }
28175
28448
  // Clear pending sentence to unblock next sentence
28176
- if (_this11.pendingSentenceText === sentenceText) {
28177
- _this11.pendingSentenceText = null;
28449
+ if (_this13.pendingSentenceText === sentenceText) {
28450
+ _this13.pendingSentenceText = null;
28178
28451
  }
28179
28452
  // Emit playbackStopped to allow next sentence to start
28180
28453
  // Only if we're not currently playing (to avoid interrupting real playback)
28181
- if (!_this11.isPlaying && _this11.scheduledSources.size === 0) {
28454
+ if (!_this13.isPlaying && _this13.scheduledSources.size === 0) {
28182
28455
  console.log('🛑 AudioPlayer: Emitting playbackStopped for empty sentence timeout');
28183
- _this11.emit('playbackStopped', {
28184
- segmentId: _this11.currentSegmentId
28456
+ _this13.emit('playbackStopped', {
28457
+ segmentId: _this13.currentSegmentId
28185
28458
  });
28186
28459
  }
28187
28460
  }
28188
- _this11._emptySentenceTimeout = null;
28461
+ _this13._emptySentenceTimeout = null;
28189
28462
  }, 5000); // 5 second timeout - adjust based on expected chunk arrival rate
28190
28463
  }
28191
28464
 
@@ -28195,14 +28468,14 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
28195
28468
  }, {
28196
28469
  key: "startTranscriptChecker",
28197
28470
  value: function startTranscriptChecker() {
28198
- var _this12 = this;
28471
+ var _this14 = this;
28199
28472
  if (this.isCheckingTranscripts) return;
28200
28473
  this.isCheckingTranscripts = true;
28201
28474
  console.log('📝 AudioPlayer: Transcript checker started');
28202
28475
  var _checkLoop = function checkLoop() {
28203
- if (!_this12.isCheckingTranscripts || !_this12.audioContext) return;
28204
- var currentTime = _this12.audioContext.currentTime;
28205
- var _iterator2 = _createForOfIteratorHelper(_this12.sentenceTimings),
28476
+ if (!_this14.isCheckingTranscripts || !_this14.audioContext) return;
28477
+ var currentTime = _this14.audioContext.currentTime;
28478
+ var _iterator2 = _createForOfIteratorHelper(_this14.sentenceTimings),
28206
28479
  _step2;
28207
28480
  try {
28208
28481
  for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
@@ -28216,13 +28489,13 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
28216
28489
  eventData.synced = _timing.synced;
28217
28490
  }
28218
28491
  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);
28492
+ _this14.emit('transcriptDisplay', eventData);
28220
28493
  }
28221
28494
  // Per-segment natural finish: this segment's audio window has fully elapsed.
28222
28495
  if (!_timing.doneReported && _timing.endTime != null && currentTime >= _timing.endTime) {
28223
28496
  _timing.doneReported = true;
28224
28497
  var _playedMs = Math.round((_timing.endTime - _timing.startTime) * 1000);
28225
- _this12.emit('segmentDone', {
28498
+ _this14.emit('segmentDone', {
28226
28499
  segmentId: _timing.segmentId,
28227
28500
  status: 'finished',
28228
28501
  playedMs: _playedMs
@@ -28234,12 +28507,12 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
28234
28507
  } finally {
28235
28508
  _iterator2.f();
28236
28509
  }
28237
- if (_this12.isPlaying || _this12.scheduledBuffers > 0) {
28510
+ if (_this14.isPlaying || _this14.scheduledBuffers > 0) {
28238
28511
  requestAnimationFrame(_checkLoop);
28239
28512
  } else {
28240
28513
  // Playback drained naturally — flush any segment whose finish tick we may have missed
28241
28514
  // (the last buffer's onended can flip isPlaying=false before this loop's next tick).
28242
- var _iterator3 = _createForOfIteratorHelper(_this12.sentenceTimings),
28515
+ var _iterator3 = _createForOfIteratorHelper(_this14.sentenceTimings),
28243
28516
  _step3;
28244
28517
  try {
28245
28518
  for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
@@ -28248,7 +28521,7 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
28248
28521
  timing.doneReported = true;
28249
28522
  var end = timing.endTime != null ? timing.endTime : timing.startTime;
28250
28523
  var playedMs = Math.round((end - timing.startTime) * 1000);
28251
- _this12.emit('segmentDone', {
28524
+ _this14.emit('segmentDone', {
28252
28525
  segmentId: timing.segmentId,
28253
28526
  status: 'finished',
28254
28527
  playedMs: playedMs
@@ -28260,7 +28533,7 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
28260
28533
  } finally {
28261
28534
  _iterator3.f();
28262
28535
  }
28263
- _this12.isCheckingTranscripts = false;
28536
+ _this14.isCheckingTranscripts = false;
28264
28537
  console.log('📝 AudioPlayer: Transcript checker stopped');
28265
28538
  }
28266
28539
  };
@@ -28291,43 +28564,43 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
28291
28564
  }, {
28292
28565
  key: "resumeAudioContext",
28293
28566
  value: (function () {
28294
- var _resumeAudioContext = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee1() {
28567
+ var _resumeAudioContext = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee10() {
28295
28568
  var _t7;
28296
- return _regenerator().w(function (_context10) {
28297
- while (1) switch (_context10.p = _context10.n) {
28569
+ return _regenerator().w(function (_context11) {
28570
+ while (1) switch (_context11.p = _context11.n) {
28298
28571
  case 0:
28299
28572
  if (!(!this.audioContext || this.audioContext.state === 'closed')) {
28300
- _context10.n = 2;
28573
+ _context11.n = 2;
28301
28574
  break;
28302
28575
  }
28303
- _context10.n = 1;
28576
+ _context11.n = 1;
28304
28577
  return this.initializeAudioContext();
28305
28578
  case 1:
28306
- _context10.n = 6;
28579
+ _context11.n = 6;
28307
28580
  break;
28308
28581
  case 2:
28309
28582
  if (!(this.audioContext.state === 'suspended')) {
28310
- _context10.n = 6;
28583
+ _context11.n = 6;
28311
28584
  break;
28312
28585
  }
28313
- _context10.p = 3;
28314
- _context10.n = 4;
28586
+ _context11.p = 3;
28587
+ _context11.n = 4;
28315
28588
  return this.audioContext.resume();
28316
28589
  case 4:
28317
28590
  console.log('✅ AudioPlayer v2: AudioContext resumed after mic permission');
28318
- _context10.n = 6;
28591
+ _context11.n = 6;
28319
28592
  break;
28320
28593
  case 5:
28321
- _context10.p = 5;
28322
- _t7 = _context10.v;
28594
+ _context11.p = 5;
28595
+ _t7 = _context11.v;
28323
28596
  console.warn('⚠️ AudioPlayer v2: Failed to resume AudioContext:', _t7);
28324
28597
  case 6:
28325
- _context10.n = 7;
28598
+ _context11.n = 7;
28326
28599
  return this.waitForAudioContextReady();
28327
28600
  case 7:
28328
- return _context10.a(2);
28601
+ return _context11.a(2);
28329
28602
  }
28330
- }, _callee1, this, [[3, 5]]);
28603
+ }, _callee10, this, [[3, 5]]);
28331
28604
  }));
28332
28605
  function resumeAudioContext() {
28333
28606
  return _resumeAudioContext.apply(this, arguments);
@@ -28578,6 +28851,11 @@ var VoiceSDK_v2 = /*#__PURE__*/function (_EventEmitter) {
28578
28851
  // Pass through undefined so AudioPlayer applies the platform default
28579
28852
  return config.htmlAudioPlayback;
28580
28853
  }(),
28854
+ // Opt-in: play TTS through a pair of local RTCPeerConnections so Chrome's
28855
+ // echo canceller can actually see it as a far-end reference (it ignores
28856
+ // <audio> elements). Default off — AudioPlayer also honours
28857
+ // ?ttp_loopback=1 / localStorage 'ttp_loopback' on its own.
28858
+ loopbackAec: config.loopbackAec,
28581
28859
  // Protocol version
28582
28860
 
28583
28861
  protocolVersion: config.protocolVersion || 2,
@@ -28650,6 +28928,9 @@ var VoiceSDK_v2 = /*#__PURE__*/function (_EventEmitter) {
28650
28928
  _this.pendingInjections = new Map(); // Maps correlationId to { resolve, reject, timeout }
28651
28929
  _this.injectionCounter = 0; // Counter for generating unique correlation IDs
28652
28930
 
28931
+ // One settled playback-route report per call (first segment audible).
28932
+ _this._playbackRouteSettledSent = false;
28933
+
28653
28934
  // Barge-in detection is handled by the backend, not the frontend
28654
28935
  // The backend analyzes the audio stream and sends 'barge_in' message when user interrupts
28655
28936
  // Frontend only responds to backend barge-in messages
@@ -29062,6 +29343,18 @@ var VoiceSDK_v2 = /*#__PURE__*/function (_EventEmitter) {
29062
29343
  }
29063
29344
  _this3.emit('playbackStarted');
29064
29345
 
29346
+ // Unconditional settled playback-route report on first audible segment —
29347
+ // even when the route matches hello-time telemetry and no htmlAudioRoute
29348
+ // event fired afterward (e.g. loopback succeeded quietly).
29349
+ if (!_this3._playbackRouteSettledSent) {
29350
+ var _info$segmentId;
29351
+ _this3._playbackRouteSettledSent = true;
29352
+ _this3._sendClientAudioInfo(null, {
29353
+ settled: true,
29354
+ segmentId: (_info$segmentId = info === null || info === void 0 ? void 0 : info.segmentId) !== null && _info$segmentId !== void 0 ? _info$segmentId : null
29355
+ });
29356
+ }
29357
+
29065
29358
  // Transcripts are displayed when they arrive (between sentences)
29066
29359
  // No need to handle transcript display here - it's handled in handleTranscript()
29067
29360
 
@@ -29351,6 +29644,7 @@ var VoiceSDK_v2 = /*#__PURE__*/function (_EventEmitter) {
29351
29644
  _this5.isConnected = false;
29352
29645
  _this5.helloAckReceived = false;
29353
29646
  _this5.disclaimersPending = false;
29647
+ _this5._playbackRouteSettledSent = false;
29354
29648
 
29355
29649
  // Discard any audio still held by the first-playback gate
29356
29650
  _this5._dropPlaybackGate();
@@ -29468,7 +29762,7 @@ var VoiceSDK_v2 = /*#__PURE__*/function (_EventEmitter) {
29468
29762
  key: "_buildClientEnv",
29469
29763
  value: function _buildClientEnv() {
29470
29764
  try {
29471
- var _uaData$mobile, _window$screen, _window$screen2;
29765
+ var _uaData$mobile, _window$screen, _window$screen2, _this$audioPlayer, _this$audioPlayer$get;
29472
29766
  var ua = navigator.userAgent || '';
29473
29767
  var uaData = navigator.userAgentData;
29474
29768
  var conn = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
@@ -29477,8 +29771,8 @@ var VoiceSDK_v2 = /*#__PURE__*/function (_EventEmitter) {
29477
29771
  // Webview heuristic: Android WebView marks itself with "; wv)" or "Version/x.x Chrome";
29478
29772
  // iOS in-app webviews lack "Safari" in the UA (real Safari always has it).
29479
29773
  var webview = isAndroid && (/\bwv\b/.test(ua) || /Version\/[\d.]+.*Chrome/.test(ua)) || isIos && !/Safari/i.test(ua) || false;
29480
- var env = {
29481
- sdkVersion: true ? "2.48.18" : 0,
29774
+ var env = _objectSpread({
29775
+ sdkVersion: true ? "2.48.21" : 0,
29482
29776
  ua: ua,
29483
29777
  platform: (uaData === null || uaData === void 0 ? void 0 : uaData.platform) || navigator.platform || '',
29484
29778
  mobile: (_uaData$mobile = uaData === null || uaData === void 0 ? void 0 : uaData.mobile) !== null && _uaData$mobile !== void 0 ? _uaData$mobile : isAndroid || isIos,
@@ -29488,34 +29782,43 @@ var VoiceSDK_v2 = /*#__PURE__*/function (_EventEmitter) {
29488
29782
  cores: navigator.hardwareConcurrency || null,
29489
29783
  memoryGb: navigator.deviceMemory || null,
29490
29784
  connection: (conn === null || conn === void 0 ? void 0 : conn.effectiveType) || null
29491
- };
29785
+ }, ((_this$audioPlayer = this.audioPlayer) === null || _this$audioPlayer === void 0 || (_this$audioPlayer$get = _this$audioPlayer.getPlaybackRouteReport) === null || _this$audioPlayer$get === void 0 ? void 0 : _this$audioPlayer$get.call(_this$audioPlayer)) || {
29786
+ htmlAudioPlayback: null,
29787
+ playbackRoute: 'destination',
29788
+ loopbackAttach: {
29789
+ outcome: 'not_attempted'
29790
+ }
29791
+ });
29492
29792
  return env;
29493
29793
  } catch (e) {
29494
29794
  console.warn('⚠️ VoiceSDK v2: Failed to build client env:', e);
29495
29795
  return {
29496
- sdkVersion: true ? "2.48.18" : 0
29796
+ sdkVersion: true ? "2.48.21" : 0
29497
29797
  };
29498
29798
  }
29499
29799
  }
29500
29800
 
29501
29801
  /**
29502
- * One-shot telemetry sent right after recording starts (t:'client_audio_info').
29503
- * Reports the capture track's ACTUAL settings from getSettings() — echoCancellation /
29504
- * noiseSuppression / autoGainControl as the browser really applied them (not what we
29505
- * requested), which is the ground truth for diagnosing speakerphone echo barge-in —
29506
- * plus mic label, AudioContext rates, and whether the minimal-constraints fallback fired.
29802
+ * Telemetry sent as t:'client_audio_info'. Interim snapshots (capture start,
29803
+ * htmlAudioRoute transitions) use routeReportKind:'interim'. The first audible
29804
+ * segment emits routeReportKind:'settled' exactly once per connection.
29507
29805
  */
29508
29806
  }, {
29509
29807
  key: "_sendClientAudioInfo",
29510
29808
  value: function _sendClientAudioInfo(routeUpdate) {
29809
+ var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
29511
29810
  try {
29512
- var _this$audioRecorder, _this$audioRecorder$g, _s$echoCancellation, _s$noiseSuppression, _s$autoGainControl, _s$voiceIsolation, _s$sampleRate, _s$channelCount, _this$audioRecorder2, _this$audioPlayer, _this$audioRecorder3, _this$audioPlayer2, _this$audioPlayer2$ge;
29811
+ var _this$audioRecorder, _this$audioRecorder$g, _s$echoCancellation, _s$noiseSuppression, _s$autoGainControl, _s$voiceIsolation, _s$sampleRate, _s$channelCount, _this$audioRecorder2, _this$audioPlayer2, _this$audioRecorder3, _this$audioPlayer3, _this$audioPlayer3$ge;
29812
+ var settled = options.settled === true;
29513
29813
  var track = (_this$audioRecorder = this.audioRecorder) === null || _this$audioRecorder === void 0 || (_this$audioRecorder = _this$audioRecorder.mediaStream) === null || _this$audioRecorder === void 0 || (_this$audioRecorder$g = _this$audioRecorder.getAudioTracks) === null || _this$audioRecorder$g === void 0 ? void 0 : _this$audioRecorder$g.call(_this$audioRecorder)[0];
29514
29814
  // Route updates must go out even before the mic track exists — the hop
29515
- // state is the whole point of the follow-up.
29516
- if (!track && !routeUpdate) return;
29815
+ // state is the whole point of the follow-up. Settled reports go out even
29816
+ // if capture never started (monitor / playback-only paths).
29817
+ if (!track && !routeUpdate && !settled) return;
29517
29818
  var s = track && track.getSettings ? track.getSettings() : {};
29518
29819
  var audio = _objectSpread({
29820
+ sdkVersion: true ? "2.48.21" : 0,
29821
+ routeReportKind: settled ? 'settled' : 'interim',
29519
29822
  aec: (_s$echoCancellation = s.echoCancellation) !== null && _s$echoCancellation !== void 0 ? _s$echoCancellation : null,
29520
29823
  ns: (_s$noiseSuppression = s.noiseSuppression) !== null && _s$noiseSuppression !== void 0 ? _s$noiseSuppression : null,
29521
29824
  agc: (_s$autoGainControl = s.autoGainControl) !== null && _s$autoGainControl !== void 0 ? _s$autoGainControl : null,
@@ -29524,18 +29827,21 @@ var VoiceSDK_v2 = /*#__PURE__*/function (_EventEmitter) {
29524
29827
  channels: (_s$channelCount = s.channelCount) !== null && _s$channelCount !== void 0 ? _s$channelCount : null,
29525
29828
  mic: track ? track.label || null : null,
29526
29829
  captureCtxRate: ((_this$audioRecorder2 = this.audioRecorder) === null || _this$audioRecorder2 === void 0 || (_this$audioRecorder2 = _this$audioRecorder2.audioContext) === null || _this$audioRecorder2 === void 0 ? void 0 : _this$audioRecorder2.sampleRate) || null,
29527
- playbackCtxRate: ((_this$audioPlayer = this.audioPlayer) === null || _this$audioPlayer === void 0 || (_this$audioPlayer = _this$audioPlayer.audioContext) === null || _this$audioPlayer === void 0 ? void 0 : _this$audioPlayer.sampleRate) || null,
29830
+ playbackCtxRate: ((_this$audioPlayer2 = this.audioPlayer) === null || _this$audioPlayer2 === void 0 || (_this$audioPlayer2 = _this$audioPlayer2.audioContext) === null || _this$audioPlayer2 === void 0 ? void 0 : _this$audioPlayer2.sampleRate) || null,
29528
29831
  constraintsFallback: ((_this$audioRecorder3 = this.audioRecorder) === null || _this$audioRecorder3 === void 0 || (_this$audioRecorder3 = _this$audioRecorder3.mediaStream) === null || _this$audioRecorder3 === void 0 ? void 0 : _this$audioRecorder3._ttpConstraintsFallback) === true
29529
- }, ((_this$audioPlayer2 = this.audioPlayer) === null || _this$audioPlayer2 === void 0 || (_this$audioPlayer2$ge = _this$audioPlayer2.getHtmlAudioPlaybackInfo) === null || _this$audioPlayer2$ge === void 0 ? void 0 : _this$audioPlayer2$ge.call(_this$audioPlayer2)) || {});
29832
+ }, ((_this$audioPlayer3 = this.audioPlayer) === null || _this$audioPlayer3 === void 0 || (_this$audioPlayer3$ge = _this$audioPlayer3.getPlaybackRouteReport) === null || _this$audioPlayer3$ge === void 0 ? void 0 : _this$audioPlayer3$ge.call(_this$audioPlayer3)) || {});
29833
+ if (settled && options.segmentId != null) {
29834
+ audio.settledAtSegmentId = options.segmentId;
29835
+ }
29530
29836
  if (routeUpdate && routeUpdate.state) {
29531
- audio.routeUpdate = routeUpdate.state; // 'established' | 'fallback' | 'play_pending'
29837
+ audio.routeUpdate = routeUpdate.state; // 'established' | 'fallback' | 'play_pending' | ...
29532
29838
  if (routeUpdate.reason) audio.routeFallbackReason = routeUpdate.reason;
29533
29839
  }
29534
29840
  this.sendMessage({
29535
29841
  t: 'client_audio_info',
29536
29842
  audio: audio
29537
29843
  });
29538
- console.log('🎛️ VoiceSDK v2: Sent client_audio_info:', audio);
29844
+ console.log("\uD83C\uDF9B\uFE0F VoiceSDK v2: Sent client_audio_info (".concat(audio.routeReportKind, "):"), audio);
29539
29845
  } catch (e) {
29540
29846
  console.warn('⚠️ VoiceSDK v2: Failed to send client_audio_info:', e);
29541
29847
  }
@@ -29641,7 +29947,7 @@ var VoiceSDK_v2 = /*#__PURE__*/function (_EventEmitter) {
29641
29947
 
29642
29948
  // Include SDK build time for debugging
29643
29949
  if (true) {
29644
- helloMessage.lastBuildTime = "2026-08-27T12:25:20.756Z";
29950
+ helloMessage.lastBuildTime = "2026-08-27T17:29:05.172Z";
29645
29951
  }
29646
29952
 
29647
29953
  // Client environment (device/browser/webview) for backend logs + Langfuse metadata
@@ -32359,6 +32665,283 @@ function readTtpHopOverride(win) {
32359
32665
 
32360
32666
  /***/ }),
32361
32667
 
32668
+ /***/ "./src/v2/loopbackAec.js":
32669
+ /*!*******************************!*\
32670
+ !*** ./src/v2/loopbackAec.js ***!
32671
+ \*******************************/
32672
+ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
32673
+
32674
+ "use strict";
32675
+ __webpack_require__.r(__webpack_exports__);
32676
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
32677
+ /* harmony export */ TTP_LOOPBACK_SESSION_KEY: () => (/* binding */ TTP_LOOPBACK_SESSION_KEY),
32678
+ /* harmony export */ createAecLoopback: () => (/* binding */ createAecLoopback),
32679
+ /* harmony export */ isLoopbackAecSupported: () => (/* binding */ isLoopbackAecSupported),
32680
+ /* harmony export */ preferHighQualityOpus: () => (/* binding */ preferHighQualityOpus),
32681
+ /* harmony export */ readTtpLoopbackOverride: () => (/* binding */ readTtpLoopbackOverride)
32682
+ /* harmony export */ });
32683
+ 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 }; })(); }
32684
+ 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); }
32685
+ 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); }
32686
+ 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); }); }; }
32687
+ /**
32688
+ * Local WebRTC loopback for echo cancellation.
32689
+ *
32690
+ * WHY THIS EXISTS. Chrome's echo canceller only cancels audio that arrived
32691
+ * through a WebRTC peer connection — not <audio> elements, not Web Audio.
32692
+ * (Firefox/Safari/Edge consider all browser audio; Chrome does not.) That is
32693
+ * why the HTML hop — AudioContext → MediaStreamDestination → hidden <audio> —
32694
+ * never reliably fixed speaker echo on Chrome: where it appeared to work, what
32695
+ * actually helped was Chrome-wide echo cancellation, a separate system-level
32696
+ * feature that is not always available. Two people at the same customer, same
32697
+ * OS class, both on speakers, got opposite results from the same build.
32698
+ *
32699
+ * The fix is to make TTS *actually be* peer-connection audio: wire two
32700
+ * RTCPeerConnections together inside this tab and push playback through them.
32701
+ * The remote track is then far-end reference material Chrome's AEC will
32702
+ * cancel. No server, no SFU, no TURN, no ports — both ends are local and ICE
32703
+ * never leaves the host.
32704
+ *
32705
+ * Cost is one Opus encode/decode (~20-40 ms added latency, and lossy
32706
+ * compression of TTS that was already lossy over the wire). Bitrate is munged
32707
+ * up because the "link" is a memcpy; see preferHighQualityOpus.
32708
+ *
32709
+ * Kept free of AudioPlayer state and injectable so the peer-connection dance
32710
+ * is unit-testable without a browser: see test/loopback-aec.test.mjs.
32711
+ */
32712
+
32713
+ /** sessionStorage key mirroring an explicit ?ttp_loopback= URL value. */
32714
+ var TTP_LOOPBACK_SESSION_KEY = 'ttp_loopback_pref';
32715
+
32716
+ /**
32717
+ * QA/rollout override for the loopback route. true (force on), false (force
32718
+ * off), or null (no override — apply config, which defaults off).
32719
+ *
32720
+ * Precedence and the sessionStorage mirror match readTtpHopOverride for the
32721
+ * same reason: Borusan's navigate_to_form does window.location.href = '/path',
32722
+ * a same-origin full load that drops the query string mid-call (call
32723
+ * 84b5af7d). Without the mirror the route would silently revert on the very
32724
+ * navigation the tester is exercising.
32725
+ */
32726
+ function readTtpLoopbackOverride(win) {
32727
+ var w = win || (typeof window !== 'undefined' ? window : null);
32728
+ if (!w) return null;
32729
+ var flag = null;
32730
+ try {
32731
+ flag = new URLSearchParams(w.location.search).get('ttp_loopback');
32732
+ } catch (e) {/* no location in this env */}
32733
+ if (flag === '0' || flag === '1') {
32734
+ try {
32735
+ w.sessionStorage.setItem(TTP_LOOPBACK_SESSION_KEY, flag);
32736
+ } catch (e) {/* storage blocked */}
32737
+ return flag === '1';
32738
+ }
32739
+ var mirrored = null;
32740
+ try {
32741
+ mirrored = w.sessionStorage.getItem(TTP_LOOPBACK_SESSION_KEY);
32742
+ } catch (e) {/* storage blocked */}
32743
+ if (mirrored === '0' || mirrored === '1') return mirrored === '1';
32744
+ var stored = null;
32745
+ try {
32746
+ stored = w.localStorage.getItem('ttp_loopback');
32747
+ } catch (e) {/* storage blocked */}
32748
+ if (stored === '0' || stored === '1') return stored === '1';
32749
+ return null;
32750
+ }
32751
+ function isLoopbackAecSupported(win) {
32752
+ var w = win || (typeof window !== 'undefined' ? window : null);
32753
+ if (!w) return false;
32754
+ return typeof w.RTCPeerConnection === 'function' && typeof w.MediaStream === 'function';
32755
+ }
32756
+
32757
+ /**
32758
+ * Raise Opus quality on a loopback that has no bandwidth cost, and disable the
32759
+ * two features that are wrong for synthesized speech on a local link:
32760
+ *
32761
+ * - usedtx=0: discontinuous transmission replaces "silence" with comfort
32762
+ * noise. TTS leading/trailing silence is real audio the segment ledger
32763
+ * accounts for; letting the codec invent it would desynchronize playedMs.
32764
+ * - maxaveragebitrate: the default ~32 kbps is chosen for real networks. This
32765
+ * link is a memcpy, so spend bits instead of degrading a voice the customer
32766
+ * is paying for.
32767
+ *
32768
+ * Returns the SDP unchanged if there is no Opus line to munge — never throws,
32769
+ * because a failed munge must not cost us the route.
32770
+ */
32771
+ function preferHighQualityOpus(sdp) {
32772
+ var bitrate = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 128000;
32773
+ if (typeof sdp !== 'string' || !sdp) return sdp;
32774
+ var rtpmap = sdp.match(/^a=rtpmap:(\d+)\s+opus\/48000(?:\/2)?/mi);
32775
+ if (!rtpmap) return sdp;
32776
+ var pt = rtpmap[1];
32777
+ var params = "stereo=0;useinbandfec=1;usedtx=0;maxaveragebitrate=".concat(bitrate);
32778
+ var fmtpRe = new RegExp("^a=fmtp:".concat(pt, " (.*)$"), 'mi');
32779
+ if (fmtpRe.test(sdp)) {
32780
+ return sdp.replace(fmtpRe, function (line, existing) {
32781
+ // Drop any key we are about to set, keep everything else the browser chose.
32782
+ var kept = existing.split(';').filter(function (kv) {
32783
+ return !/^\s*(stereo|useinbandfec|usedtx|maxaveragebitrate)\s*=/i.test(kv);
32784
+ }).filter(function (kv) {
32785
+ return kv.trim().length > 0;
32786
+ });
32787
+ return "a=fmtp:".concat(pt, " ").concat(kept.concat(params.split(';')).join(';'));
32788
+ });
32789
+ }
32790
+ return sdp.replace(rtpmap[0], "".concat(rtpmap[0], "\r\na=fmtp:").concat(pt, " ").concat(params));
32791
+ }
32792
+
32793
+ /**
32794
+ * Wire two peer connections together in this tab and return the remote end of
32795
+ * the given stream.
32796
+ *
32797
+ * Resolves { remoteStream, close } once the receiving side reports connected
32798
+ * AND a track has arrived — both, because either alone has been seen without
32799
+ * audible audio. Rejects on timeout so a stalled negotiation degrades to the
32800
+ * caller's existing route rather than leaving the call silent.
32801
+ *
32802
+ * Dependencies are injected (win) so this runs under node --test.
32803
+ */
32804
+ function createAecLoopback() {
32805
+ var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
32806
+ stream = _ref.stream,
32807
+ win = _ref.win,
32808
+ _ref$timeoutMs = _ref.timeoutMs,
32809
+ timeoutMs = _ref$timeoutMs === void 0 ? 4000 : _ref$timeoutMs,
32810
+ _ref$bitrate = _ref.bitrate,
32811
+ bitrate = _ref$bitrate === void 0 ? 128000 : _ref$bitrate;
32812
+ var w = win || (typeof window !== 'undefined' ? window : null);
32813
+ if (!w) return Promise.reject(new Error('loopback: no window'));
32814
+ if (!isLoopbackAecSupported(w)) return Promise.reject(new Error('loopback: RTCPeerConnection unavailable'));
32815
+ var tracks = stream && typeof stream.getAudioTracks === 'function' ? stream.getAudioTracks() : [];
32816
+ if (!tracks.length) return Promise.reject(new Error('loopback: source stream has no audio track'));
32817
+
32818
+ // No ICE servers: both ends are in this process, so host candidates are the
32819
+ // only ones that can possibly be useful. Asking for STUN would leak the
32820
+ // user's IP to a third party for a connection that never leaves the tab.
32821
+ var pcSend = new w.RTCPeerConnection({
32822
+ iceServers: []
32823
+ });
32824
+ var pcRecv = new w.RTCPeerConnection({
32825
+ iceServers: []
32826
+ });
32827
+ var settled = false;
32828
+ var timer = null;
32829
+ var remoteStream = new w.MediaStream();
32830
+ var close = function close() {
32831
+ if (timer) {
32832
+ clearTimeout(timer);
32833
+ timer = null;
32834
+ }
32835
+ try {
32836
+ pcSend.close();
32837
+ } catch (e) {/* already closed */}
32838
+ try {
32839
+ pcRecv.close();
32840
+ } catch (e) {/* already closed */}
32841
+ };
32842
+ return new Promise(function (resolve, reject) {
32843
+ var fail = function fail(err) {
32844
+ if (settled) return;
32845
+ settled = true;
32846
+ close();
32847
+ reject(err instanceof Error ? err : new Error(String(err)));
32848
+ };
32849
+ var maybeResolve = function maybeResolve() {
32850
+ if (settled) return;
32851
+ if (!remoteStream.getAudioTracks().length) return;
32852
+ var state = pcRecv.connectionState || pcRecv.iceConnectionState;
32853
+ if (state !== 'connected' && state !== 'completed') return;
32854
+ settled = true;
32855
+ if (timer) {
32856
+ clearTimeout(timer);
32857
+ timer = null;
32858
+ }
32859
+ resolve({
32860
+ remoteStream: remoteStream,
32861
+ close: close,
32862
+ pcSend: pcSend,
32863
+ pcRecv: pcRecv
32864
+ });
32865
+ };
32866
+ timer = setTimeout(function () {
32867
+ return fail(new Error('loopback: negotiation timed out'));
32868
+ }, timeoutMs);
32869
+ pcSend.onicecandidate = function (e) {
32870
+ if (e.candidate) {
32871
+ try {
32872
+ pcRecv.addIceCandidate(e.candidate);
32873
+ } catch (err) {/* late candidate */}
32874
+ }
32875
+ };
32876
+ pcRecv.onicecandidate = function (e) {
32877
+ if (e.candidate) {
32878
+ try {
32879
+ pcSend.addIceCandidate(e.candidate);
32880
+ } catch (err) {/* late candidate */}
32881
+ }
32882
+ };
32883
+ pcRecv.ontrack = function (e) {
32884
+ var track = e.track || e.streams && e.streams[0] && e.streams[0].getAudioTracks()[0];
32885
+ if (track) remoteStream.addTrack(track);
32886
+ maybeResolve();
32887
+ };
32888
+ pcRecv.onconnectionstatechange = function () {
32889
+ var s = pcRecv.connectionState;
32890
+ if (s === 'failed' || s === 'closed') return fail(new Error("loopback: receiver ".concat(s)));
32891
+ maybeResolve();
32892
+ };
32893
+ pcRecv.oniceconnectionstatechange = function () {
32894
+ var s = pcRecv.iceConnectionState;
32895
+ if (s === 'failed') return fail(new Error('loopback: ice failed'));
32896
+ maybeResolve();
32897
+ };
32898
+ _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee() {
32899
+ var offer, tunedOffer, answer, tunedAnswer;
32900
+ return _regenerator().w(function (_context) {
32901
+ while (1) switch (_context.n) {
32902
+ case 0:
32903
+ tracks.forEach(function (t) {
32904
+ return pcSend.addTrack(t, stream);
32905
+ });
32906
+ _context.n = 1;
32907
+ return pcSend.createOffer();
32908
+ case 1:
32909
+ offer = _context.v;
32910
+ tunedOffer = {
32911
+ type: offer.type,
32912
+ sdp: preferHighQualityOpus(offer.sdp, bitrate)
32913
+ };
32914
+ _context.n = 2;
32915
+ return pcSend.setLocalDescription(tunedOffer);
32916
+ case 2:
32917
+ _context.n = 3;
32918
+ return pcRecv.setRemoteDescription(tunedOffer);
32919
+ case 3:
32920
+ _context.n = 4;
32921
+ return pcRecv.createAnswer();
32922
+ case 4:
32923
+ answer = _context.v;
32924
+ tunedAnswer = {
32925
+ type: answer.type,
32926
+ sdp: preferHighQualityOpus(answer.sdp, bitrate)
32927
+ };
32928
+ _context.n = 5;
32929
+ return pcRecv.setLocalDescription(tunedAnswer);
32930
+ case 5:
32931
+ _context.n = 6;
32932
+ return pcSend.setRemoteDescription(tunedAnswer);
32933
+ case 6:
32934
+ maybeResolve();
32935
+ case 7:
32936
+ return _context.a(2);
32937
+ }
32938
+ }, _callee);
32939
+ }))().catch(fail);
32940
+ });
32941
+ }
32942
+
32943
+ /***/ }),
32944
+
32362
32945
  /***/ "./src/v2/utils/AudioFormatConverter.js":
32363
32946
  /*!**********************************************!*\
32364
32947
  !*** ./src/v2/utils/AudioFormatConverter.js ***!
@@ -35963,7 +36546,7 @@ var TTPChatWidget = /*#__PURE__*/function () {
35963
36546
  return;
35964
36547
  }
35965
36548
  this._ensureAboutStyles();
35966
- var version = true ? "2.48.18" : 0;
36549
+ var version = true ? "2.48.21" : 0;
35967
36550
  var convId = this._getLastConversationId();
35968
36551
  var t = function t(k, fb) {
35969
36552
  try {