ttp-agent-sdk 2.48.16 → 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-21T10:43:17.433Z";
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.16";
21443
- var BUILD_TIME = "2026-08-21T10:43:17.433Z";
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
@@ -25615,6 +25615,8 @@ __webpack_require__.r(__webpack_exports__);
25615
25615
  /* harmony import */ var _codecs_PCMCodec_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./codecs/PCMCodec.js */ "./src/v2/codecs/PCMCodec.js");
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
+ /* 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");
25618
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); }
25619
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; }
25620
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; }
@@ -25646,6 +25648,8 @@ function _setPrototypeOf(t, e) { return _setPrototypeOf = Object.setPrototypeOf
25646
25648
 
25647
25649
 
25648
25650
 
25651
+
25652
+
25649
25653
  // iOS FIX: Shared AudioContext for playback that persists across AudioPlayer instances.
25650
25654
  // iOS WebKit doesn't release audio hardware synchronously when AudioContext.close() is called,
25651
25655
  // so creating a new AudioContext immediately after closing the old one can fail silently.
@@ -25745,7 +25749,20 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
25745
25749
  _this._mediaStreamDest = null;
25746
25750
  _this._htmlAudioRouteActive = false;
25747
25751
  _this._htmlAudioPlayOk = false;
25752
+ _this._htmlAudioPlayWatchdog = null;
25748
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()
25749
25766
  // Greeting (and other bursts) fire many playChunk()s without await.
25750
25767
  // Serialize context creation so they cannot open two AudioContexts
25751
25768
  // at different rates in the middle of a sentence.
@@ -27019,6 +27036,7 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27019
27036
  this.gainNode = null;
27020
27037
  }
27021
27038
  this._teardownHtmlAudioRoute();
27039
+ this._teardownLoopbackRoute();
27022
27040
  if (this.audioContext === _sharedPlayerContext) {
27023
27041
  _sharedPlayerContext = null;
27024
27042
  _sharedPlayerSampleRate = null;
@@ -27164,37 +27182,40 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27164
27182
  }, {
27165
27183
  key: "wantsHtmlAudioPlayback",
27166
27184
  value: function wantsHtmlAudioPlayback() {
27167
- // Debug/QA override: ?ttp_hop=0|1 or localStorage 'ttp_hop' forces the route
27168
- // at the decision point, regardless of which config object reached us.
27169
- try {
27170
- var _URLSearchParams$get;
27171
- var flag = (_URLSearchParams$get = new URLSearchParams(window.location.search).get('ttp_hop')) !== null && _URLSearchParams$get !== void 0 ? _URLSearchParams$get : window.localStorage.getItem('ttp_hop');
27172
- if (flag === '0' || flag === '1') {
27173
- return flag === '1';
27174
- }
27175
- } catch (e) {/* non-browser env */}
27185
+ // Debug/QA override: ?ttp_hop=0|1, its sessionStorage mirror (survives
27186
+ // same-origin full page loads that drop the query string Borusan's
27187
+ // navigate_to_form), or localStorage 'ttp_hop'. Wins over config.
27188
+ var override = (0,_hopPlatform_js__WEBPACK_IMPORTED_MODULE_4__.readTtpHopOverride)(typeof window !== 'undefined' ? window : null);
27189
+ if (override !== null) {
27190
+ return override;
27191
+ }
27176
27192
  if (typeof this.config.htmlAudioPlayback === 'boolean') {
27177
27193
  return this.config.htmlAudioPlayback;
27178
27194
  }
27179
- // Default: the hop (AudioContext → MediaStreamDestination → HTMLAudioElement)
27180
- // gives Chrome's AEC a far-end reference, but Chrome plays that live stream
27181
- // with adaptive jitter-buffer speed control, which audibly wobbles on
27182
- // desktop Linux (PipeWire/PulseAudio). Off there, on elsewhere.
27183
- var hopOn = !this._isDesktopLinux();
27195
+ // Default: HTML hop (AudioContext → MediaStreamDestination → <audio>) gives
27196
+ // Safari/Chrome a far-end AEC reference on Apple mobile + macOS where Web
27197
+ // Audio destination is not in the echo-cancellation loop. On Windows and
27198
+ // Linux desktop, Chrome-wide AEC already covers speaker echo; the hop only
27199
+ // adds adaptive live-stream speed wobble — keep it off there (same as Linux).
27200
+ var hopOn = this._needsHtmlAudioHopForAec();
27184
27201
  if (!this._hopDefaultLogged) {
27185
27202
  this._hopDefaultLogged = true;
27186
- console.log("\uD83D\uDD0A AudioPlayer: htmlAudioPlayback default \u2192 ".concat(hopOn ? 'on (AEC far-end route)' : 'off (desktop Linux: hop causes playback speed wobble)'));
27203
+ var reason = hopOn ? 'on (mobile / macOS: AEC far-end route)' : 'off (desktop Windows/Linux: stable direct Web Audio playback)';
27204
+ console.log("\uD83D\uDD0A AudioPlayer: htmlAudioPlayback default \u2192 ".concat(reason));
27187
27205
  }
27188
27206
  return hopOn;
27189
27207
  }
27208
+
27209
+ /** Hop default on only where Web Audio destination lacks AEC far-end reference.
27210
+ * Policy lives in hopPlatform.js (pure, unit-tested); this only supplies the
27211
+ * environment strings. */
27190
27212
  }, {
27191
- key: "_isDesktopLinux",
27192
- value: function _isDesktopLinux() {
27213
+ key: "_needsHtmlAudioHopForAec",
27214
+ value: function _needsHtmlAudioHopForAec() {
27193
27215
  try {
27194
27216
  var ua = navigator.userAgent || '';
27195
- if (/Android|iPhone|iPad|iPod/i.test(ua)) return false;
27196
27217
  var platform = navigator.userAgentData && navigator.userAgentData.platform || navigator.platform || '';
27197
- return /linux/i.test(platform) || /Linux|X11/i.test(ua);
27218
+ return (0,_hopPlatform_js__WEBPACK_IMPORTED_MODULE_4__.needsHtmlAudioHopForAec)(ua, platform);
27198
27219
  } catch (e) {
27199
27220
  return false;
27200
27221
  }
@@ -27215,7 +27236,10 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27215
27236
  }, {
27216
27237
  key: "_htmlHopNeedsHardwareRate",
27217
27238
  value: function _htmlHopNeedsHardwareRate() {
27218
- 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();
27219
27243
  }
27220
27244
  }, {
27221
27245
  key: "_isHardwarePlaybackRate",
@@ -27256,7 +27280,10 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27256
27280
  return {
27257
27281
  htmlAudioPlayback: this.wantsHtmlAudioPlayback(),
27258
27282
  htmlAudioRouteActive: this._htmlAudioRouteActive,
27259
- htmlAudioPlayOk: this._htmlAudioPlayOk
27283
+ htmlAudioPlayOk: this._htmlAudioPlayOk,
27284
+ loopbackAec: this.wantsLoopbackAec(),
27285
+ loopbackRouteActive: this._loopbackReady,
27286
+ loopbackPlayOk: this._loopbackPlayOk
27260
27287
  };
27261
27288
  }
27262
27289
 
@@ -27273,6 +27300,18 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27273
27300
  try {
27274
27301
  this.gainNode.disconnect();
27275
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
+ }
27276
27315
  if (this.wantsHtmlAudioPlayback() && this._ensureHtmlAudioRoute()) {
27277
27316
  this.gainNode.connect(this._mediaStreamDest);
27278
27317
  return;
@@ -27289,48 +27328,84 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27289
27328
  }
27290
27329
  if (!this.audioContext || typeof this.audioContext.createMediaStreamDestination !== 'function') {
27291
27330
  console.warn('⚠️ AudioPlayer: MediaStreamDestination unavailable — using AudioContext.destination');
27331
+ this.emit('htmlAudioRoute', {
27332
+ state: 'fallback',
27333
+ reason: 'no_media_stream_destination'
27334
+ });
27292
27335
  return false;
27293
27336
  }
27294
27337
  try {
27295
27338
  this._mediaStreamDest = this.audioContext.createMediaStreamDestination();
27296
- var el = document.createElement('audio');
27297
- el.setAttribute('playsinline', '');
27298
- el.setAttribute('webkit-playsinline', '');
27299
- el.autoplay = true;
27300
- el.controls = false;
27301
- el.preload = 'auto';
27302
- el.playbackRate = 1;
27303
- el.style.display = 'none';
27304
- el.srcObject = this._mediaStreamDest.stream;
27305
- document.body.appendChild(el);
27339
+ var el = this._createHiddenAudioEl(this._mediaStreamDest.stream);
27306
27340
  this._htmlAudioEl = el;
27307
27341
  this._htmlAudioRouteActive = true;
27308
27342
  this._ensureHtmlAudioPlaying();
27343
+ this._armHtmlAudioPlayWatchdog(el);
27309
27344
  console.log('🔊 AudioPlayer: Playback last-hop is HTMLAudioElement (AEC far-end route)');
27310
27345
  return true;
27311
27346
  } catch (e) {
27312
27347
  console.warn('⚠️ AudioPlayer: HTML audio route failed, falling back to destination:', e);
27313
27348
  this._teardownHtmlAudioRoute();
27349
+ this.emit('htmlAudioRoute', {
27350
+ state: 'fallback',
27351
+ reason: 'route_init_failed'
27352
+ });
27314
27353
  return false;
27315
27354
  }
27316
27355
  }
27356
+
27357
+ /**
27358
+ * play() on a MediaStream-backed element can stay pending forever on some
27359
+ * WebKit builds — neither resolving nor rejecting. If the element is still
27360
+ * paused shortly after route creation, report it: the hop never engaged and
27361
+ * AEC has no far-end reference, but audio still reaches the user via the
27362
+ * (silent-failure) direct path only if a fallback happened — so this state
27363
+ * means "route exists, playback through it unconfirmed".
27364
+ */
27365
+ }, {
27366
+ key: "_armHtmlAudioPlayWatchdog",
27367
+ value: function _armHtmlAudioPlayWatchdog(el) {
27368
+ var _this7 = this;
27369
+ this._clearHtmlAudioPlayWatchdog();
27370
+ this._htmlAudioPlayWatchdog = setTimeout(function () {
27371
+ _this7._htmlAudioPlayWatchdog = null;
27372
+ if (_this7._htmlAudioRouteActive && _this7._htmlAudioEl === el && !_this7._htmlAudioPlayOk && el.paused) {
27373
+ console.warn('⚠️ AudioPlayer: HTMLAudioElement.play() still pending after 2.5s — hop engagement unconfirmed');
27374
+ _this7.emit('htmlAudioRoute', {
27375
+ state: 'play_pending'
27376
+ });
27377
+ }
27378
+ }, 2500);
27379
+ }
27380
+ }, {
27381
+ key: "_clearHtmlAudioPlayWatchdog",
27382
+ value: function _clearHtmlAudioPlayWatchdog() {
27383
+ if (this._htmlAudioPlayWatchdog) {
27384
+ clearTimeout(this._htmlAudioPlayWatchdog);
27385
+ this._htmlAudioPlayWatchdog = null;
27386
+ }
27387
+ }
27317
27388
  }, {
27318
27389
  key: "_ensureHtmlAudioPlaying",
27319
27390
  value: function _ensureHtmlAudioPlaying() {
27320
- var _this7 = this;
27391
+ var _this8 = this;
27321
27392
  var el = this._htmlAudioEl;
27322
27393
  if (!el) return;
27323
27394
  if (!el.paused && this._htmlAudioPlayOk) return;
27324
27395
  var playResult = el.play();
27325
27396
  if (playResult && typeof playResult.then === 'function') {
27326
27397
  playResult.then(function () {
27327
- _this7._htmlAudioPlayOk = true;
27398
+ _this8._htmlAudioPlayOk = true;
27399
+ _this8._clearHtmlAudioPlayWatchdog();
27400
+ _this8.emit('htmlAudioRoute', {
27401
+ state: 'established'
27402
+ });
27328
27403
  }).catch(function (err) {
27329
- if (_this7._htmlAudioPlayOk || !el.paused) {
27404
+ if (_this8._htmlAudioPlayOk || !el.paused) {
27330
27405
  return;
27331
27406
  }
27332
27407
  console.warn('⚠️ AudioPlayer: HTMLAudioElement.play() rejected — falling back to destination:', err);
27333
- _this7._fallbackToDestination('play_rejected');
27408
+ _this8._fallbackToDestination('play_rejected');
27334
27409
  });
27335
27410
  }
27336
27411
  }
@@ -27342,12 +27417,18 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27342
27417
  this.gainNode.disconnect();
27343
27418
  } catch (_) {/* ignore */}
27344
27419
  this._teardownHtmlAudioRoute();
27420
+ this._teardownLoopbackRoute();
27345
27421
  this.gainNode.connect(this.audioContext.destination);
27346
27422
  console.warn("\u26A0\uFE0F AudioPlayer: Fell back to AudioContext.destination (".concat(reason, ")"));
27423
+ this.emit('htmlAudioRoute', {
27424
+ state: 'fallback',
27425
+ reason: reason
27426
+ });
27347
27427
  }
27348
27428
  }, {
27349
27429
  key: "_teardownHtmlAudioRoute",
27350
27430
  value: function _teardownHtmlAudioRoute() {
27431
+ this._clearHtmlAudioPlayWatchdog();
27351
27432
  if (this._htmlAudioEl) {
27352
27433
  try {
27353
27434
  this._htmlAudioEl.pause();
@@ -27364,29 +27445,216 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27364
27445
  this._htmlAudioRouteActive = false;
27365
27446
  }
27366
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
+
27367
27635
  /**
27368
27636
  * Initialize audio context with correct sample rate
27369
27637
  */
27370
27638
  }, {
27371
27639
  key: "initializeAudioContext",
27372
27640
  value: (function () {
27373
- var _initializeAudioContext = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee7() {
27374
- var _this8 = this;
27375
- return _regenerator().w(function (_context8) {
27376
- 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) {
27377
27645
  case 0:
27378
27646
  if (!this._audioContextInitPromise) {
27379
- _context8.n = 1;
27647
+ _context9.n = 1;
27380
27648
  break;
27381
27649
  }
27382
- return _context8.a(2, this._audioContextInitPromise);
27650
+ return _context9.a(2, this._audioContextInitPromise);
27383
27651
  case 1:
27384
27652
  this._audioContextInitPromise = this._initializeAudioContextImpl().finally(function () {
27385
- _this8._audioContextInitPromise = null;
27653
+ _this1._audioContextInitPromise = null;
27386
27654
  });
27387
- return _context8.a(2, this._audioContextInitPromise);
27655
+ return _context9.a(2, this._audioContextInitPromise);
27388
27656
  }
27389
- }, _callee7, this);
27657
+ }, _callee8, this);
27390
27658
  }));
27391
27659
  function initializeAudioContext() {
27392
27660
  return _initializeAudioContext.apply(this, arguments);
@@ -27396,11 +27664,11 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27396
27664
  }, {
27397
27665
  key: "_initializeAudioContextImpl",
27398
27666
  value: function () {
27399
- var _initializeAudioContextImpl2 = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee8() {
27400
- var _this9 = this;
27667
+ var _initializeAudioContextImpl2 = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee9() {
27668
+ var _this10 = this;
27401
27669
  var ttsSampleRate, useHardwareRate, desiredSampleRate, contextFits, canReuseShared, setupAfterResume, ctxOpts, _t4;
27402
- return _regenerator().w(function (_context9) {
27403
- while (1) switch (_context9.p = _context9.n) {
27670
+ return _regenerator().w(function (_context0) {
27671
+ while (1) switch (_context0.p = _context0.n) {
27404
27672
  case 0:
27405
27673
  ttsSampleRate = this._ttsSampleRate();
27406
27674
  useHardwareRate = this._htmlHopNeedsHardwareRate(); // Destination path: match TTS (24 kHz). HTML last-hop: request the standard
@@ -27418,27 +27686,27 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27418
27686
  _sharedPlayerSampleRate = null;
27419
27687
  }
27420
27688
  contextFits = function contextFits(rate) {
27421
- if (useHardwareRate) return _this9._isHardwarePlaybackRate(rate);
27689
+ if (useHardwareRate) return _this10._isHardwarePlaybackRate(rate);
27422
27690
  return Math.abs(rate - desiredSampleRate) <= 100;
27423
27691
  }; // Check if current instance AudioContext exists and matches
27424
27692
  if (!this.audioContext) {
27425
- _context9.n = 3;
27693
+ _context0.n = 3;
27426
27694
  break;
27427
27695
  }
27428
27696
  if (!(this.audioContext.state === 'closed')) {
27429
- _context9.n = 1;
27697
+ _context0.n = 1;
27430
27698
  break;
27431
27699
  }
27432
27700
  this._invalidateClosedAudioContext();
27433
- _context9.n = 3;
27701
+ _context0.n = 3;
27434
27702
  break;
27435
27703
  case 1:
27436
27704
  if (!contextFits(this.audioContext.sampleRate)) {
27437
- _context9.n = 2;
27705
+ _context0.n = 2;
27438
27706
  break;
27439
27707
  }
27440
27708
  this._ensureHtmlAudioPlaying();
27441
- return _context9.a(2);
27709
+ return _context0.a(2);
27442
27710
  case 2:
27443
27711
  console.warn("\u26A0\uFE0F AudioPlayer: AudioContext sample rate (".concat(this.audioContext.sampleRate, "Hz) doesn't match playback hop, recreating..."));
27444
27712
  this.stopImmediate();
@@ -27449,44 +27717,44 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27449
27717
  // causing newly created AudioContexts to fail silently.
27450
27718
  canReuseShared = _sharedPlayerContext && _sharedPlayerContext.state !== 'closed' && contextFits(_sharedPlayerContext.sampleRate);
27451
27719
  if (!canReuseShared) {
27452
- _context9.n = 8;
27720
+ _context0.n = 8;
27453
27721
  break;
27454
27722
  }
27455
27723
  console.log("\u267B\uFE0F AudioPlayer: Reusing shared AudioContext at ".concat(_sharedPlayerContext.sampleRate, "Hz (iOS-safe)"));
27456
27724
  this.audioContext = _sharedPlayerContext;
27457
27725
  setupAfterResume = function setupAfterResume() {
27458
- _this9.setupAudioContextStateMonitoring();
27459
- if (_this9.gainNode) {
27726
+ _this10.setupAudioContextStateMonitoring();
27727
+ if (_this10.gainNode) {
27460
27728
  try {
27461
- _this9.gainNode.disconnect();
27729
+ _this10.gainNode.disconnect();
27462
27730
  } catch (e) {
27463
27731
  console.warn('⚠️ AudioPlayer: Error disconnecting old GainNode:', e);
27464
27732
  }
27465
27733
  }
27466
- _this9.gainNode = _this9.audioContext.createGain();
27467
- _this9.gainNode.gain.value = 1.0;
27468
- _this9._connectGainToOutput();
27469
- if (!_this9._audioContextPrimed) {
27470
- _this9._primeAudioContext();
27734
+ _this10.gainNode = _this10.audioContext.createGain();
27735
+ _this10.gainNode.gain.value = 1.0;
27736
+ _this10._connectGainToOutput();
27737
+ if (!_this10._audioContextPrimed) {
27738
+ _this10._primeAudioContext();
27471
27739
  }
27472
27740
  };
27473
27741
  if (!(this.audioContext.state === 'suspended')) {
27474
- _context9.n = 7;
27742
+ _context0.n = 7;
27475
27743
  break;
27476
27744
  }
27477
- _context9.p = 4;
27478
- _context9.n = 5;
27745
+ _context0.p = 4;
27746
+ _context0.n = 5;
27479
27747
  return this.audioContext.resume();
27480
27748
  case 5:
27481
- _context9.n = 7;
27749
+ _context0.n = 7;
27482
27750
  break;
27483
27751
  case 6:
27484
- _context9.p = 6;
27485
- _t4 = _context9.v;
27752
+ _context0.p = 6;
27753
+ _t4 = _context0.v;
27486
27754
  console.warn('⚠️ AudioPlayer: Error resuming shared context:', _t4);
27487
27755
  case 7:
27488
27756
  setupAfterResume();
27489
- return _context9.a(2);
27757
+ return _context0.a(2);
27490
27758
  case 8:
27491
27759
  console.log("\uD83C\uDFB5 AudioPlayer: Creating AudioContext (tts=".concat(ttsSampleRate, "Hz, htmlHop=").concat(useHardwareRate, ", requested=").concat(desiredSampleRate, "Hz)"));
27492
27760
 
@@ -27539,9 +27807,9 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27539
27807
  this._primeAudioContext();
27540
27808
  }
27541
27809
  case 9:
27542
- return _context9.a(2);
27810
+ return _context0.a(2);
27543
27811
  }
27544
- }, _callee8, this, [[4, 6]]);
27812
+ }, _callee9, this, [[4, 6]]);
27545
27813
  }));
27546
27814
  function _initializeAudioContextImpl() {
27547
27815
  return _initializeAudioContextImpl2.apply(this, arguments);
@@ -27555,17 +27823,17 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27555
27823
  }, {
27556
27824
  key: "_primeAudioContext",
27557
27825
  value: (function () {
27558
- var _primeAudioContext2 = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee9() {
27826
+ var _primeAudioContext2 = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee0() {
27559
27827
  var sampleRate, silentBuffer, source, _t5;
27560
- return _regenerator().w(function (_context0) {
27561
- while (1) switch (_context0.p = _context0.n) {
27828
+ return _regenerator().w(function (_context1) {
27829
+ while (1) switch (_context1.p = _context1.n) {
27562
27830
  case 0:
27563
- _context0.p = 0;
27831
+ _context1.p = 0;
27564
27832
  if (!(!this.audioContext || this._audioContextPrimed)) {
27565
- _context0.n = 1;
27833
+ _context1.n = 1;
27566
27834
  break;
27567
27835
  }
27568
- return _context0.a(2);
27836
+ return _context1.a(2);
27569
27837
  case 1:
27570
27838
  sampleRate = this.audioContext.sampleRate;
27571
27839
  silentBuffer = this.audioContext.createBuffer(1, sampleRate * 0.15, sampleRate);
@@ -27576,23 +27844,23 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27576
27844
  source.connect(this.gainNode || this.audioContext.destination);
27577
27845
  source.start();
27578
27846
  this._ensureHtmlAudioPlaying();
27579
- _context0.n = 2;
27847
+ _context1.n = 2;
27580
27848
  return new Promise(function (resolve) {
27581
27849
  return setTimeout(resolve, 150);
27582
27850
  });
27583
27851
  case 2:
27584
27852
  this._audioContextPrimed = true;
27585
27853
  console.log('✅ AudioPlayer: Audio hardware primed with silent buffer');
27586
- _context0.n = 4;
27854
+ _context1.n = 4;
27587
27855
  break;
27588
27856
  case 3:
27589
- _context0.p = 3;
27590
- _t5 = _context0.v;
27857
+ _context1.p = 3;
27858
+ _t5 = _context1.v;
27591
27859
  console.error('❌ AudioPlayer: Audio priming failed:', _t5);
27592
27860
  case 4:
27593
- return _context0.a(2);
27861
+ return _context1.a(2);
27594
27862
  }
27595
- }, _callee9, this, [[0, 3]]);
27863
+ }, _callee0, this, [[0, 3]]);
27596
27864
  }));
27597
27865
  function _primeAudioContext() {
27598
27866
  return _primeAudioContext2.apply(this, arguments);
@@ -27619,6 +27887,7 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27619
27887
  this.gainNode = null;
27620
27888
  }
27621
27889
  this._teardownHtmlAudioRoute();
27890
+ this._teardownLoopbackRoute();
27622
27891
 
27623
27892
  // Remove state change listener first
27624
27893
  if (this._audioContextStateChangeHandler && this.audioContext) {
@@ -27659,7 +27928,7 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27659
27928
  * Handles mic permission grants, tab switching, browser suspension, etc.
27660
27929
  */
27661
27930
  function setupAudioContextStateMonitoring() {
27662
- var _this0 = this;
27931
+ var _this11 = this;
27663
27932
  if (!this.audioContext) {
27664
27933
  return;
27665
27934
  }
@@ -27672,38 +27941,38 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27672
27941
  // Create handler that references this.audioContext dynamically
27673
27942
  this._audioContextStateChangeHandler = function () {
27674
27943
  // Null check required because audioContext may be cleaned up while handler is queued
27675
- if (!_this0.audioContext) {
27944
+ if (!_this11.audioContext) {
27676
27945
  console.warn('⚠️ AudioPlayer: State change handler fired but AudioContext is null');
27677
27946
  return;
27678
27947
  }
27679
- console.log("\uD83C\uDFB5 AudioPlayer: AudioContext state changed to: ".concat(_this0.audioContext.state));
27680
- if (_this0.audioContext.state === 'running') {
27681
- _this0._ensureHtmlAudioPlaying();
27948
+ console.log("\uD83C\uDFB5 AudioPlayer: AudioContext state changed to: ".concat(_this11.audioContext.state));
27949
+ if (_this11.audioContext.state === 'running') {
27950
+ _this11._ensureHtmlAudioPlaying();
27682
27951
  }
27683
- if (_this0.audioContext.state === 'suspended' && _this0.isPlaying) {
27952
+ if (_this11.audioContext.state === 'suspended' && _this11.isPlaying) {
27684
27953
  // AudioContext was suspended during playback (tab switch, mic permission, etc.)
27685
27954
  console.warn('⚠️ AudioPlayer: AudioContext suspended during playback');
27686
27955
  // Note: Playback will pause automatically, but we should handle queue processing
27687
27956
  // The state change will be handled when we try to process next frame
27688
- } else if (_this0.audioContext.state === 'running' && !_this0.isPlaying && (_this0.audioQueue.length > 0 || _this0.pcmChunkQueue.length > 0 || _this0.preparedBuffer.length > 0)) {
27957
+ } else if (_this11.audioContext.state === 'running' && !_this11.isPlaying && (_this11.audioQueue.length > 0 || _this11.pcmChunkQueue.length > 0 || _this11.preparedBuffer.length > 0)) {
27689
27958
  // AudioContext resumed and we have queued frames
27690
27959
  // This handles: mic permission grant, tab switching back, browser resume, etc.
27691
27960
  console.log('✅ AudioPlayer: AudioContext resumed - resuming queue processing');
27692
27961
 
27693
27962
  // Resume queue processing if we have frames
27694
- if (_this0.audioQueue.length > 0 && !_this0.isProcessingQueue) {
27963
+ if (_this11.audioQueue.length > 0 && !_this11.isProcessingQueue) {
27695
27964
  setTimeout(function () {
27696
- return _this0.processQueue();
27965
+ return _this11.processQueue();
27697
27966
  }, 50);
27698
27967
  }
27699
- if (_this0.pcmChunkQueue.length > 0 && !_this0.isProcessingPcmQueue) {
27968
+ if (_this11.pcmChunkQueue.length > 0 && !_this11.isProcessingPcmQueue) {
27700
27969
  setTimeout(function () {
27701
- return _this0.processPcmQueue();
27970
+ return _this11.processPcmQueue();
27702
27971
  }, 50);
27703
27972
  }
27704
- if (_this0.preparedBuffer.length > 0 && !_this0.isSchedulingFrames) {
27973
+ if (_this11.preparedBuffer.length > 0 && !_this11.isSchedulingFrames) {
27705
27974
  setTimeout(function () {
27706
- return _this0.scheduleFrames();
27975
+ return _this11.scheduleFrames();
27707
27976
  }, 50);
27708
27977
  }
27709
27978
  }
@@ -27721,49 +27990,49 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27721
27990
  }, {
27722
27991
  key: "processQueue",
27723
27992
  value: (function () {
27724
- var _processQueue = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee0() {
27725
- var _this1 = this;
27993
+ var _processQueue = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee1() {
27994
+ var _this12 = this;
27726
27995
  var audioBlob, wasFirstPlay, audioContext, arrayBuffer, audioBuffer, shouldEmitStart, source, _t6;
27727
- return _regenerator().w(function (_context1) {
27728
- while (1) switch (_context1.p = _context1.n) {
27996
+ return _regenerator().w(function (_context10) {
27997
+ while (1) switch (_context10.p = _context10.n) {
27729
27998
  case 0:
27730
27999
  if (!this._isStopped) {
27731
- _context1.n = 1;
28000
+ _context10.n = 1;
27732
28001
  break;
27733
28002
  }
27734
28003
  console.log('🛑 AudioPlayer: Not processing queue - playback was stopped (barge-in)');
27735
- return _context1.a(2);
28004
+ return _context10.a(2);
27736
28005
  case 1:
27737
28006
  if (!(this.isProcessingQueue || this.audioQueue.length === 0)) {
27738
- _context1.n = 2;
28007
+ _context10.n = 2;
27739
28008
  break;
27740
28009
  }
27741
- return _context1.a(2);
28010
+ return _context10.a(2);
27742
28011
  case 2:
27743
28012
  this.isProcessingQueue = true;
27744
28013
  this._ensureHtmlAudioPlaying();
27745
28014
  audioBlob = this.audioQueue.shift();
27746
28015
  if (audioBlob) {
27747
- _context1.n = 3;
28016
+ _context10.n = 3;
27748
28017
  break;
27749
28018
  }
27750
28019
  this.isProcessingQueue = false;
27751
- return _context1.a(2);
28020
+ return _context10.a(2);
27752
28021
  case 3:
27753
- _context1.p = 3;
28022
+ _context10.p = 3;
27754
28023
  wasFirstPlay = !this.isPlaying && this.currentSource === null; // Ensure context exists and is running (handles lazy init + suspended shared context)
27755
- _context1.n = 4;
28024
+ _context10.n = 4;
27756
28025
  return this.waitForAudioContextReady();
27757
28026
  case 4:
27758
28027
  audioContext = this.audioContext; // Decode audio
27759
- _context1.n = 5;
28028
+ _context10.n = 5;
27760
28029
  return audioBlob.arrayBuffer();
27761
28030
  case 5:
27762
- arrayBuffer = _context1.v;
27763
- _context1.n = 6;
28031
+ arrayBuffer = _context10.v;
28032
+ _context10.n = 6;
27764
28033
  return audioContext.decodeAudioData(arrayBuffer);
27765
28034
  case 6:
27766
- audioBuffer = _context1.v;
28035
+ audioBuffer = _context10.v;
27767
28036
  shouldEmitStart = wasFirstPlay && !this.isPlaying && this.currentSource === null; // Create source
27768
28037
  source = audioContext.createBufferSource();
27769
28038
  source.buffer = audioBuffer;
@@ -27782,22 +28051,22 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27782
28051
  // Handle end
27783
28052
 
27784
28053
  source.onended = function () {
27785
- _this1.currentSource = null;
27786
- _this1.isProcessingQueue = false;
28054
+ _this12.currentSource = null;
28055
+ _this12.isProcessingQueue = false;
27787
28056
 
27788
28057
  // Process next chunk
27789
28058
 
27790
- if (_this1.audioQueue.length > 0) {
28059
+ if (_this12.audioQueue.length > 0) {
27791
28060
  setTimeout(function () {
27792
- return _this1.processQueue();
28061
+ return _this12.processQueue();
27793
28062
  }, 50);
27794
28063
  } else {
27795
28064
  // No more chunks - stop after delay
27796
28065
 
27797
28066
  setTimeout(function () {
27798
- if (_this1.audioQueue.length === 0 && !_this1.currentSource) {
27799
- _this1.isPlaying = false;
27800
- _this1.emit('playbackStopped');
28067
+ if (_this12.audioQueue.length === 0 && !_this12.currentSource) {
28068
+ _this12.isPlaying = false;
28069
+ _this12.emit('playbackStopped');
27801
28070
  }
27802
28071
  }, 100);
27803
28072
  }
@@ -27806,11 +28075,11 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27806
28075
  // Start playback - AudioContext should now be fully ready
27807
28076
 
27808
28077
  source.start();
27809
- _context1.n = 8;
28078
+ _context10.n = 8;
27810
28079
  break;
27811
28080
  case 7:
27812
- _context1.p = 7;
27813
- _t6 = _context1.v;
28081
+ _context10.p = 7;
28082
+ _t6 = _context10.v;
27814
28083
  console.error('❌ AudioPlayer v2: Error processing queue:', _t6);
27815
28084
  this.currentSource = null;
27816
28085
  this.emit('playbackError', _t6);
@@ -27820,7 +28089,7 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27820
28089
  if (this.audioQueue.length > 0) {
27821
28090
  this.isProcessingQueue = false;
27822
28091
  setTimeout(function () {
27823
- return _this1.processQueue();
28092
+ return _this12.processQueue();
27824
28093
  }, 100);
27825
28094
  } else {
27826
28095
  this.isPlaying = false;
@@ -27828,9 +28097,9 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
27828
28097
  this.emit('playbackStopped');
27829
28098
  }
27830
28099
  case 8:
27831
- return _context1.a(2);
28100
+ return _context10.a(2);
27832
28101
  }
27833
- }, _callee0, this, [[3, 7]]);
28102
+ }, _callee1, this, [[3, 7]]);
27834
28103
  }));
27835
28104
  function processQueue() {
27836
28105
  return _processQueue.apply(this, arguments);
@@ -28043,7 +28312,7 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
28043
28312
  }, {
28044
28313
  key: "markNewSentence",
28045
28314
  value: function markNewSentence(text, synced, segmentId) {
28046
- var _this10 = this;
28315
+ var _this13 = this;
28047
28316
  var wasStopped = this._isStopped;
28048
28317
  var isCurrentlyPlaying = this.isPlaying || this.scheduledSources.size > 0;
28049
28318
 
@@ -28102,34 +28371,34 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
28102
28371
  var sentenceText = text; // Capture for timeout callback
28103
28372
  this._emptySentenceTimeout = setTimeout(function () {
28104
28373
  // Check if this sentence still has no chunks after timeout
28105
- if (_this10.pendingSentenceText === sentenceText && _this10.scheduledBuffers === 0 && _this10.preparedBuffer.length === 0 && _this10.pcmChunkQueue.length === 0 && !_this10._isStopped) {
28374
+ if (_this13.pendingSentenceText === sentenceText && _this13.scheduledBuffers === 0 && _this13.preparedBuffer.length === 0 && _this13.pcmChunkQueue.length === 0 && !_this13._isStopped) {
28106
28375
  console.warn("\u26A0\uFE0F AudioPlayer: Empty sentence detected after 5s timeout - no chunks received for: \"".concat(sentenceText.substring(0, 40), "...\""));
28107
28376
  // If this empty sentence carried a segment id, report it done (nothing was heard) and
28108
28377
  // adopt it as current so the coarse stop below matches the backend's last-sent id.
28109
- if (_this10.pendingSegmentId != null) {
28110
- var emptySegId = _this10.pendingSegmentId;
28111
- _this10.currentSegmentId = emptySegId;
28112
- _this10.pendingSegmentId = null;
28113
- _this10.emit('segmentDone', {
28378
+ if (_this13.pendingSegmentId != null) {
28379
+ var emptySegId = _this13.pendingSegmentId;
28380
+ _this13.currentSegmentId = emptySegId;
28381
+ _this13.pendingSegmentId = null;
28382
+ _this13.emit('segmentDone', {
28114
28383
  segmentId: emptySegId,
28115
28384
  status: 'finished',
28116
28385
  playedMs: 0
28117
28386
  });
28118
28387
  }
28119
28388
  // Clear pending sentence to unblock next sentence
28120
- if (_this10.pendingSentenceText === sentenceText) {
28121
- _this10.pendingSentenceText = null;
28389
+ if (_this13.pendingSentenceText === sentenceText) {
28390
+ _this13.pendingSentenceText = null;
28122
28391
  }
28123
28392
  // Emit playbackStopped to allow next sentence to start
28124
28393
  // Only if we're not currently playing (to avoid interrupting real playback)
28125
- if (!_this10.isPlaying && _this10.scheduledSources.size === 0) {
28394
+ if (!_this13.isPlaying && _this13.scheduledSources.size === 0) {
28126
28395
  console.log('🛑 AudioPlayer: Emitting playbackStopped for empty sentence timeout');
28127
- _this10.emit('playbackStopped', {
28128
- segmentId: _this10.currentSegmentId
28396
+ _this13.emit('playbackStopped', {
28397
+ segmentId: _this13.currentSegmentId
28129
28398
  });
28130
28399
  }
28131
28400
  }
28132
- _this10._emptySentenceTimeout = null;
28401
+ _this13._emptySentenceTimeout = null;
28133
28402
  }, 5000); // 5 second timeout - adjust based on expected chunk arrival rate
28134
28403
  }
28135
28404
 
@@ -28139,14 +28408,14 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
28139
28408
  }, {
28140
28409
  key: "startTranscriptChecker",
28141
28410
  value: function startTranscriptChecker() {
28142
- var _this11 = this;
28411
+ var _this14 = this;
28143
28412
  if (this.isCheckingTranscripts) return;
28144
28413
  this.isCheckingTranscripts = true;
28145
28414
  console.log('📝 AudioPlayer: Transcript checker started');
28146
28415
  var _checkLoop = function checkLoop() {
28147
- if (!_this11.isCheckingTranscripts || !_this11.audioContext) return;
28148
- var currentTime = _this11.audioContext.currentTime;
28149
- var _iterator2 = _createForOfIteratorHelper(_this11.sentenceTimings),
28416
+ if (!_this14.isCheckingTranscripts || !_this14.audioContext) return;
28417
+ var currentTime = _this14.audioContext.currentTime;
28418
+ var _iterator2 = _createForOfIteratorHelper(_this14.sentenceTimings),
28150
28419
  _step2;
28151
28420
  try {
28152
28421
  for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
@@ -28160,13 +28429,13 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
28160
28429
  eventData.synced = _timing.synced;
28161
28430
  }
28162
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, ")"));
28163
- _this11.emit('transcriptDisplay', eventData);
28432
+ _this14.emit('transcriptDisplay', eventData);
28164
28433
  }
28165
28434
  // Per-segment natural finish: this segment's audio window has fully elapsed.
28166
28435
  if (!_timing.doneReported && _timing.endTime != null && currentTime >= _timing.endTime) {
28167
28436
  _timing.doneReported = true;
28168
28437
  var _playedMs = Math.round((_timing.endTime - _timing.startTime) * 1000);
28169
- _this11.emit('segmentDone', {
28438
+ _this14.emit('segmentDone', {
28170
28439
  segmentId: _timing.segmentId,
28171
28440
  status: 'finished',
28172
28441
  playedMs: _playedMs
@@ -28178,12 +28447,12 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
28178
28447
  } finally {
28179
28448
  _iterator2.f();
28180
28449
  }
28181
- if (_this11.isPlaying || _this11.scheduledBuffers > 0) {
28450
+ if (_this14.isPlaying || _this14.scheduledBuffers > 0) {
28182
28451
  requestAnimationFrame(_checkLoop);
28183
28452
  } else {
28184
28453
  // Playback drained naturally — flush any segment whose finish tick we may have missed
28185
28454
  // (the last buffer's onended can flip isPlaying=false before this loop's next tick).
28186
- var _iterator3 = _createForOfIteratorHelper(_this11.sentenceTimings),
28455
+ var _iterator3 = _createForOfIteratorHelper(_this14.sentenceTimings),
28187
28456
  _step3;
28188
28457
  try {
28189
28458
  for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
@@ -28192,7 +28461,7 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
28192
28461
  timing.doneReported = true;
28193
28462
  var end = timing.endTime != null ? timing.endTime : timing.startTime;
28194
28463
  var playedMs = Math.round((end - timing.startTime) * 1000);
28195
- _this11.emit('segmentDone', {
28464
+ _this14.emit('segmentDone', {
28196
28465
  segmentId: timing.segmentId,
28197
28466
  status: 'finished',
28198
28467
  playedMs: playedMs
@@ -28204,7 +28473,7 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
28204
28473
  } finally {
28205
28474
  _iterator3.f();
28206
28475
  }
28207
- _this11.isCheckingTranscripts = false;
28476
+ _this14.isCheckingTranscripts = false;
28208
28477
  console.log('📝 AudioPlayer: Transcript checker stopped');
28209
28478
  }
28210
28479
  };
@@ -28235,43 +28504,43 @@ var AudioPlayer = /*#__PURE__*/function (_EventEmitter) {
28235
28504
  }, {
28236
28505
  key: "resumeAudioContext",
28237
28506
  value: (function () {
28238
- var _resumeAudioContext = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee1() {
28507
+ var _resumeAudioContext = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee10() {
28239
28508
  var _t7;
28240
- return _regenerator().w(function (_context10) {
28241
- while (1) switch (_context10.p = _context10.n) {
28509
+ return _regenerator().w(function (_context11) {
28510
+ while (1) switch (_context11.p = _context11.n) {
28242
28511
  case 0:
28243
28512
  if (!(!this.audioContext || this.audioContext.state === 'closed')) {
28244
- _context10.n = 2;
28513
+ _context11.n = 2;
28245
28514
  break;
28246
28515
  }
28247
- _context10.n = 1;
28516
+ _context11.n = 1;
28248
28517
  return this.initializeAudioContext();
28249
28518
  case 1:
28250
- _context10.n = 6;
28519
+ _context11.n = 6;
28251
28520
  break;
28252
28521
  case 2:
28253
28522
  if (!(this.audioContext.state === 'suspended')) {
28254
- _context10.n = 6;
28523
+ _context11.n = 6;
28255
28524
  break;
28256
28525
  }
28257
- _context10.p = 3;
28258
- _context10.n = 4;
28526
+ _context11.p = 3;
28527
+ _context11.n = 4;
28259
28528
  return this.audioContext.resume();
28260
28529
  case 4:
28261
28530
  console.log('✅ AudioPlayer v2: AudioContext resumed after mic permission');
28262
- _context10.n = 6;
28531
+ _context11.n = 6;
28263
28532
  break;
28264
28533
  case 5:
28265
- _context10.p = 5;
28266
- _t7 = _context10.v;
28534
+ _context11.p = 5;
28535
+ _t7 = _context11.v;
28267
28536
  console.warn('⚠️ AudioPlayer v2: Failed to resume AudioContext:', _t7);
28268
28537
  case 6:
28269
- _context10.n = 7;
28538
+ _context11.n = 7;
28270
28539
  return this.waitForAudioContextReady();
28271
28540
  case 7:
28272
- return _context10.a(2);
28541
+ return _context11.a(2);
28273
28542
  }
28274
- }, _callee1, this, [[3, 5]]);
28543
+ }, _callee10, this, [[3, 5]]);
28275
28544
  }));
28276
28545
  function resumeAudioContext() {
28277
28546
  return _resumeAudioContext.apply(this, arguments);
@@ -28368,8 +28637,9 @@ __webpack_require__.r(__webpack_exports__);
28368
28637
  /* harmony import */ var _core_ClientToolsRegistry_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ../core/ClientToolsRegistry.js */ "./src/core/ClientToolsRegistry.js");
28369
28638
  /* harmony import */ var _core_ClientScriptManager_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ../core/ClientScriptManager.js */ "./src/core/ClientScriptManager.js");
28370
28639
  /* harmony import */ var _core_helloFlavor_js__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ../core/helloFlavor.js */ "./src/core/helloFlavor.js");
28371
- /* harmony import */ var _utils_screenshot_js__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ../utils/screenshot.js */ "./src/utils/screenshot.js");
28372
- /* harmony import */ var _utils_visual_tools_js__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! ../utils/visual-tools.js */ "./src/utils/visual-tools.js");
28640
+ /* harmony import */ var _hopPlatform_js__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ./hopPlatform.js */ "./src/v2/hopPlatform.js");
28641
+ /* harmony import */ var _utils_screenshot_js__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! ../utils/screenshot.js */ "./src/utils/screenshot.js");
28642
+ /* harmony import */ var _utils_visual_tools_js__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(/*! ../utils/visual-tools.js */ "./src/utils/visual-tools.js");
28373
28643
  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); }
28374
28644
  function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t.return || t.return(); } finally { if (u) throw o; } } }; }
28375
28645
  function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
@@ -28405,6 +28675,7 @@ function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e
28405
28675
 
28406
28676
 
28407
28677
 
28678
+
28408
28679
  /**
28409
28680
 
28410
28681
  * VoiceSDK v2 - Multi-codec speech-to-speech SDK
@@ -28502,27 +28773,29 @@ var VoiceSDK_v2 = /*#__PURE__*/function (_EventEmitter) {
28502
28773
  // Audio constraints for getUserMedia (optional)
28503
28774
  // If not provided, defaults will be used: echoCancellation: true, noiseSuppression: true, autoGainControl: true
28504
28775
  audioConstraints: config.audioConstraints || null,
28505
- // Last-hop TTS playback via hidden <audio> (AEC far-end). Default on.
28506
- // Set false to fall back to AudioContext.destination (old path).
28507
- // QA/debug override wins over config: ?ttp_hop=0 / localStorage ttp_hop=0
28508
- // forces AudioContext.destination; =1 forces the HTML hop.
28776
+ // Last-hop TTS playback via hidden <audio> (AEC far-end). Platform default
28777
+ // (hop on mobile/macOS only) applies when unset; set explicitly to force.
28778
+ // QA/debug override wins over config: ?ttp_hop=0|1 mirrored to
28779
+ // sessionStorage 'ttp_hop_pref' so it survives same-origin full page loads
28780
+ // (Borusan navigate_to_form) — then localStorage 'ttp_hop'.
28509
28781
  htmlAudioPlayback: function () {
28510
28782
  try {
28511
- var _URLSearchParams$get;
28512
- var flag = (_URLSearchParams$get = new URLSearchParams(window.location.search).get('ttp_hop')) !== null && _URLSearchParams$get !== void 0 ? _URLSearchParams$get : window.localStorage.getItem('ttp_hop');
28513
- if (flag === '0' || flag === '1') {
28514
- console.log("\uD83D\uDD27 VoiceSDK v2: ttp_hop override=".concat(flag, " \u2192 htmlAudioPlayback=").concat(flag === '1'));
28515
- return flag === '1';
28516
- }
28517
- if (flag !== null && flag !== undefined) {
28518
- console.log("\uD83D\uDD27 VoiceSDK v2: ttp_hop ignored (value '".concat(flag, "', expected '0' or '1')"));
28783
+ var override = (0,_hopPlatform_js__WEBPACK_IMPORTED_MODULE_7__.readTtpHopOverride)(typeof window !== 'undefined' ? window : null);
28784
+ if (override !== null) {
28785
+ console.log("\uD83D\uDD27 VoiceSDK v2: ttp_hop override \u2192 htmlAudioPlayback=".concat(override));
28786
+ return override;
28519
28787
  }
28520
28788
  } catch (e) {
28521
- console.log('🔧 VoiceSDK v2: ttp_hop check threw (storage blocked?):', e && e.message);
28789
+ console.log('🔧 VoiceSDK v2: ttp_hop check threw:', e && e.message);
28522
28790
  }
28523
28791
  // Pass through undefined so AudioPlayer applies the platform default
28524
28792
  return config.htmlAudioPlayback;
28525
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,
28526
28799
  // Protocol version
28527
28800
 
28528
28801
  protocolVersion: config.protocolVersion || 2,
@@ -28665,7 +28938,7 @@ var VoiceSDK_v2 = /*#__PURE__*/function (_EventEmitter) {
28665
28938
  key: "_registerBuiltInTools",
28666
28939
  value: function _registerBuiltInTools() {
28667
28940
  try {
28668
- (0,_utils_visual_tools_js__WEBPACK_IMPORTED_MODULE_8__.registerVisualTools)(this.clientToolsRegistry);
28941
+ (0,_utils_visual_tools_js__WEBPACK_IMPORTED_MODULE_9__.registerVisualTools)(this.clientToolsRegistry);
28669
28942
  } catch (error) {
28670
28943
  console.error('❌ VoiceSDK: Error registering built-in tools:', error);
28671
28944
  console.error(' Error details:', error.message, error.stack);
@@ -28991,6 +29264,12 @@ var VoiceSDK_v2 = /*#__PURE__*/function (_EventEmitter) {
28991
29264
  key: "setupAudioPlayerEvents",
28992
29265
  value: function setupAudioPlayerEvents() {
28993
29266
  var _this3 = this;
29267
+ // The initial client_audio_info snapshot (at recordingStarted) predates
29268
+ // HTML-hop establishment, so its htmlAudioRouteActive/htmlAudioPlayOk are
29269
+ // always false. Re-report once the route outcome is known.
29270
+ this.audioPlayer.on('htmlAudioRoute', function (info) {
29271
+ _this3._sendClientAudioInfo(info);
29272
+ });
28994
29273
  this.audioPlayer.on('playbackStarted', function (info) {
28995
29274
  _this3.isPlaying = true;
28996
29275
 
@@ -29417,7 +29696,7 @@ var VoiceSDK_v2 = /*#__PURE__*/function (_EventEmitter) {
29417
29696
  // iOS in-app webviews lack "Safari" in the UA (real Safari always has it).
29418
29697
  var webview = isAndroid && (/\bwv\b/.test(ua) || /Version\/[\d.]+.*Chrome/.test(ua)) || isIos && !/Safari/i.test(ua) || false;
29419
29698
  var env = {
29420
- sdkVersion: true ? "2.48.16" : 0,
29699
+ sdkVersion: true ? "2.48.20" : 0,
29421
29700
  ua: ua,
29422
29701
  platform: (uaData === null || uaData === void 0 ? void 0 : uaData.platform) || navigator.platform || '',
29423
29702
  mobile: (_uaData$mobile = uaData === null || uaData === void 0 ? void 0 : uaData.mobile) !== null && _uaData$mobile !== void 0 ? _uaData$mobile : isAndroid || isIos,
@@ -29432,7 +29711,7 @@ var VoiceSDK_v2 = /*#__PURE__*/function (_EventEmitter) {
29432
29711
  } catch (e) {
29433
29712
  console.warn('⚠️ VoiceSDK v2: Failed to build client env:', e);
29434
29713
  return {
29435
- sdkVersion: true ? "2.48.16" : 0
29714
+ sdkVersion: true ? "2.48.20" : 0
29436
29715
  };
29437
29716
  }
29438
29717
  }
@@ -29446,12 +29725,14 @@ var VoiceSDK_v2 = /*#__PURE__*/function (_EventEmitter) {
29446
29725
  */
29447
29726
  }, {
29448
29727
  key: "_sendClientAudioInfo",
29449
- value: function _sendClientAudioInfo() {
29728
+ value: function _sendClientAudioInfo(routeUpdate) {
29450
29729
  try {
29451
29730
  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;
29452
29731
  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];
29453
- if (!track) return;
29454
- var s = track.getSettings ? track.getSettings() : {};
29732
+ // Route updates must go out even before the mic track exists — the hop
29733
+ // state is the whole point of the follow-up.
29734
+ if (!track && !routeUpdate) return;
29735
+ var s = track && track.getSettings ? track.getSettings() : {};
29455
29736
  var audio = _objectSpread({
29456
29737
  aec: (_s$echoCancellation = s.echoCancellation) !== null && _s$echoCancellation !== void 0 ? _s$echoCancellation : null,
29457
29738
  ns: (_s$noiseSuppression = s.noiseSuppression) !== null && _s$noiseSuppression !== void 0 ? _s$noiseSuppression : null,
@@ -29459,11 +29740,15 @@ var VoiceSDK_v2 = /*#__PURE__*/function (_EventEmitter) {
29459
29740
  voiceIsolation: (_s$voiceIsolation = s.voiceIsolation) !== null && _s$voiceIsolation !== void 0 ? _s$voiceIsolation : null,
29460
29741
  trackRate: (_s$sampleRate = s.sampleRate) !== null && _s$sampleRate !== void 0 ? _s$sampleRate : null,
29461
29742
  channels: (_s$channelCount = s.channelCount) !== null && _s$channelCount !== void 0 ? _s$channelCount : null,
29462
- mic: track.label || null,
29743
+ mic: track ? track.label || null : null,
29463
29744
  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,
29464
29745
  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,
29465
29746
  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
29466
29747
  }, ((_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)) || {});
29748
+ if (routeUpdate && routeUpdate.state) {
29749
+ audio.routeUpdate = routeUpdate.state; // 'established' | 'fallback' | 'play_pending'
29750
+ if (routeUpdate.reason) audio.routeFallbackReason = routeUpdate.reason;
29751
+ }
29467
29752
  this.sendMessage({
29468
29753
  t: 'client_audio_info',
29469
29754
  audio: audio
@@ -29574,7 +29859,7 @@ var VoiceSDK_v2 = /*#__PURE__*/function (_EventEmitter) {
29574
29859
 
29575
29860
  // Include SDK build time for debugging
29576
29861
  if (true) {
29577
- helloMessage.lastBuildTime = "2026-08-21T10:43:17.433Z";
29862
+ helloMessage.lastBuildTime = "2026-08-27T16:45:39.666Z";
29578
29863
  }
29579
29864
 
29580
29865
  // Client environment (device/browser/webview) for backend logs + Langfuse metadata
@@ -29818,7 +30103,7 @@ var VoiceSDK_v2 = /*#__PURE__*/function (_EventEmitter) {
29818
30103
  case 'get_page_context':
29819
30104
  // Handle get_page_context request from backend
29820
30105
  console.log('📖 [VISUAL ASSISTANT] Backend requested page context');
29821
- (0,_utils_visual_tools_js__WEBPACK_IMPORTED_MODULE_8__.extractPageContext)().then(function (pageContext) {
30106
+ (0,_utils_visual_tools_js__WEBPACK_IMPORTED_MODULE_9__.extractPageContext)().then(function (pageContext) {
29822
30107
  // Use existing DOM scanner
29823
30108
  _this6.sendMessage({
29824
30109
  t: 'page_context',
@@ -29839,7 +30124,7 @@ var VoiceSDK_v2 = /*#__PURE__*/function (_EventEmitter) {
29839
30124
  console.log('📸 [VISUAL ASSISTANT] Backend requested screenshot');
29840
30125
  try {
29841
30126
  // Use existing screenshot capture function
29842
- (0,_utils_screenshot_js__WEBPACK_IMPORTED_MODULE_7__.captureScreenshot)().then(function (screenshot) {
30127
+ (0,_utils_screenshot_js__WEBPACK_IMPORTED_MODULE_8__.captureScreenshot)().then(function (screenshot) {
29843
30128
  _this6.sendMessage({
29844
30129
  t: 'screenshot',
29845
30130
  screenshot: {
@@ -32199,6 +32484,376 @@ var PCMUCodec = /*#__PURE__*/function () {
32199
32484
 
32200
32485
  /***/ }),
32201
32486
 
32487
+ /***/ "./src/v2/hopPlatform.js":
32488
+ /*!*******************************!*\
32489
+ !*** ./src/v2/hopPlatform.js ***!
32490
+ \*******************************/
32491
+ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
32492
+
32493
+ "use strict";
32494
+ __webpack_require__.r(__webpack_exports__);
32495
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
32496
+ /* harmony export */ TTP_HOP_SESSION_KEY: () => (/* binding */ TTP_HOP_SESSION_KEY),
32497
+ /* harmony export */ isMacDesktopUa: () => (/* binding */ isMacDesktopUa),
32498
+ /* harmony export */ isMobileUa: () => (/* binding */ isMobileUa),
32499
+ /* harmony export */ needsHtmlAudioHopForAec: () => (/* binding */ needsHtmlAudioHopForAec),
32500
+ /* harmony export */ readTtpHopOverride: () => (/* binding */ readTtpHopOverride)
32501
+ /* harmony export */ });
32502
+ /**
32503
+ * HTML-audio-hop platform policy + the ttp_hop QA override.
32504
+ *
32505
+ * The "hop" is AudioContext → MediaStreamDestination → hidden <audio>, used so
32506
+ * the browser's echo canceller sees agent TTS as a far-end reference. Chrome
32507
+ * renders that live stream with adaptive jitter-buffer speed control, which
32508
+ * audibly wobbles on Windows/Linux desktop — so the hop defaults ON only where
32509
+ * Web Audio → destination genuinely lacks an AEC reference (mobile + macOS).
32510
+ *
32511
+ * Kept DOM-free (all inputs passed in) so the policy is unit-testable without
32512
+ * a browser: see test/hop-platform.test.mjs.
32513
+ */
32514
+
32515
+ /** sessionStorage key mirroring an explicit ?ttp_hop= URL value (see readTtpHopOverride). */
32516
+ var TTP_HOP_SESSION_KEY = 'ttp_hop_pref';
32517
+ function isMobileUa(ua) {
32518
+ return /Android|iPhone|iPad|iPod/i.test(ua || '');
32519
+ }
32520
+ function isMacDesktopUa(ua, platform) {
32521
+ if (/iPhone|iPad|iPod/i.test(ua || '')) return false;
32522
+ return /mac/i.test(platform || '') || /Mac OS X/i.test(ua || '');
32523
+ }
32524
+
32525
+ /**
32526
+ * Hop default: on only where Web Audio destination lacks an AEC far-end
32527
+ * reference (mobile + macOS). Windows/Linux/ChromeOS desktop: off — Chrome-wide
32528
+ * AEC already covers speaker echo there, and the hop only adds speed wobble.
32529
+ * Unknown/empty environment: off (fail toward the stable direct path).
32530
+ */
32531
+ function needsHtmlAudioHopForAec(ua, platform) {
32532
+ return isMobileUa(ua) || isMacDesktopUa(ua, platform);
32533
+ }
32534
+
32535
+ /**
32536
+ * QA/debug override for the hop route. Returns true (force hop on), false
32537
+ * (force off), or null (no override — apply config/platform default).
32538
+ *
32539
+ * Precedence: URL ?ttp_hop=0|1 → sessionStorage mirror → localStorage 'ttp_hop'.
32540
+ *
32541
+ * An explicit URL value is mirrored into sessionStorage (TTP_HOP_SESSION_KEY)
32542
+ * so it survives same-origin full page loads that drop the query string —
32543
+ * Borusan's navigate_to_form does window.location.href = '/path' (call
32544
+ * 84b5af7d), which used to silently re-apply the platform default mid-call.
32545
+ * The mirror dies with the tab; localStorage remains the sticky manual knob
32546
+ * and ranks below the mirror because the URL value is the more recent intent.
32547
+ *
32548
+ * Every storage touch is individually guarded: browsers can block one storage
32549
+ * area but not another, and a blocked sessionStorage must not cost us the
32550
+ * localStorage fallback.
32551
+ */
32552
+ function readTtpHopOverride(win) {
32553
+ var w = win || (typeof window !== 'undefined' ? window : null);
32554
+ if (!w) return null;
32555
+ var flag = null;
32556
+ try {
32557
+ flag = new URLSearchParams(w.location.search).get('ttp_hop');
32558
+ } catch (e) {/* no location in this env */}
32559
+ if (flag === '0' || flag === '1') {
32560
+ try {
32561
+ w.sessionStorage.setItem(TTP_HOP_SESSION_KEY, flag);
32562
+ } catch (e) {/* storage blocked */}
32563
+ return flag === '1';
32564
+ }
32565
+ var mirrored = null;
32566
+ try {
32567
+ mirrored = w.sessionStorage.getItem(TTP_HOP_SESSION_KEY);
32568
+ } catch (e) {/* storage blocked */}
32569
+ if (mirrored === '0' || mirrored === '1') return mirrored === '1';
32570
+ var stored = null;
32571
+ try {
32572
+ stored = w.localStorage.getItem('ttp_hop');
32573
+ } catch (e) {/* storage blocked */}
32574
+ if (stored === '0' || stored === '1') return stored === '1';
32575
+ return null;
32576
+ }
32577
+
32578
+ /***/ }),
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
+
32202
32857
  /***/ "./src/v2/utils/AudioFormatConverter.js":
32203
32858
  /*!**********************************************!*\
32204
32859
  !*** ./src/v2/utils/AudioFormatConverter.js ***!
@@ -35803,7 +36458,7 @@ var TTPChatWidget = /*#__PURE__*/function () {
35803
36458
  return;
35804
36459
  }
35805
36460
  this._ensureAboutStyles();
35806
- var version = true ? "2.48.16" : 0;
36461
+ var version = true ? "2.48.20" : 0;
35807
36462
  var convId = this._getLastConversationId();
35808
36463
  var t = function t(k, fb) {
35809
36464
  try {