@signalwire/js 4.0.0-dev-20260421201955 → 4.0.0-dev-20260422003445

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.
package/dist/index.cjs CHANGED
@@ -131,7 +131,7 @@ const asyncRetry = async ({ asyncCallable, maxRetries: retries = DEFAULT_MAX_RET
131
131
 
132
132
  //#endregion
133
133
  //#region src/controllers/HTTPRequestController.ts
134
- const logger$28 = require_operators.getLogger();
134
+ const logger$30 = require_operators.getLogger();
135
135
  const GET_PARAMS = {
136
136
  method: "GET",
137
137
  headers: { Accept: "application/json" }
@@ -195,7 +195,7 @@ var HTTPRequestController = class HTTPRequestController extends Destroyable {
195
195
  this._responses$.next(response);
196
196
  return response;
197
197
  } catch (error) {
198
- logger$28.error("[HTTPRequestController] Request error:", error);
198
+ logger$30.error("[HTTPRequestController] Request error:", error);
199
199
  this._status$.next("error");
200
200
  const err = error instanceof Error ? error : new Error("HTTP request failed", { cause: error });
201
201
  this._errors$.next(err);
@@ -222,7 +222,7 @@ var HTTPRequestController = class HTTPRequestController extends Destroyable {
222
222
  const url = this.buildURL(request.url);
223
223
  const headers = this.buildHeaders(request.headers);
224
224
  const timeout$4 = request.timeout ?? this.requestTimeout;
225
- logger$28.debug("[HTTPRequestController] Executing request:", {
225
+ logger$30.debug("[HTTPRequestController] Executing request:", {
226
226
  method: request.method,
227
227
  url,
228
228
  headers: Object.keys(headers).reduce((acc, key) => {
@@ -242,7 +242,7 @@ var HTTPRequestController = class HTTPRequestController extends Destroyable {
242
242
  });
243
243
  clearTimeout(timeoutId);
244
244
  const httpResponse = await this.convertResponse(response);
245
- logger$28.debug("[HTTPRequestController] Response received:", {
245
+ logger$30.debug("[HTTPRequestController] Response received:", {
246
246
  status: response.status,
247
247
  statusText: response.statusText,
248
248
  headers: [...response.headers.entries()],
@@ -252,7 +252,7 @@ var HTTPRequestController = class HTTPRequestController extends Destroyable {
252
252
  } catch (error) {
253
253
  clearTimeout(timeoutId);
254
254
  if (error instanceof Error && error.name === "AbortError") throw new require_operators.RequestTimeoutError(`Request timeout after ${timeout$4}ms`, { cause: error });
255
- logger$28.error("[HTTPRequestController] Request failed:", error);
255
+ logger$30.error("[HTTPRequestController] Request failed:", error);
256
256
  throw error;
257
257
  }
258
258
  }
@@ -266,8 +266,8 @@ var HTTPRequestController = class HTTPRequestController extends Destroyable {
266
266
  const credential = this.getCredential();
267
267
  if (credential.token) {
268
268
  headers.Authorization = `Bearer ${credential.token}`;
269
- logger$28.debug("[HTTPRequestController] Using Bearer token auth, token length:", credential.token.length);
270
- } else logger$28.warn("[HTTPRequestController] No credentials available for authentication");
269
+ logger$30.debug("[HTTPRequestController] Using Bearer token auth, token length:", credential.token.length);
270
+ } else logger$30.warn("[HTTPRequestController] No credentials available for authentication");
271
271
  return headers;
272
272
  }
273
273
  /**
@@ -485,6 +485,12 @@ const ICE_GATHERING_COMPLETE_TIMEOUT_MS = 1e4;
485
485
  const PEER_CONNECTION_RECOVERY_WAIT_MS = 5e3;
486
486
  /** Polling interval in ms while waiting for RTCPeerConnection.connectionState to transition. */
487
487
  const PEER_CONNECTION_RECOVERY_POLL_MS = 100;
488
+ /** Polling interval for LocalAudioPipeline.level$ (ms). ~30fps is smooth for meters. */
489
+ const AUDIO_LEVEL_POLL_INTERVAL_MS = 33;
490
+ /** RMS level threshold (0..1) above which the local participant is considered speaking. */
491
+ const VAD_THRESHOLD = .03;
492
+ /** Hold window in ms below the threshold before speaking$ flips back to false. */
493
+ const VAD_HOLD_MS = 250;
488
494
  /** Whether to persist device selections to storage by default. */
489
495
  const DEFAULT_PERSIST_DEVICE_SELECTION = true;
490
496
  /** Whether to auto-apply device changes to active calls by default. */
@@ -533,7 +539,7 @@ function fromMsToSec(milliseconds) {
533
539
 
534
540
  //#endregion
535
541
  //#region src/containers/PreferencesContainer.ts
536
- const logger$27 = require_operators.getLogger();
542
+ const logger$29 = require_operators.getLogger();
537
543
  var PreferencesContainer = class PreferencesContainer {
538
544
  static get instance() {
539
545
  this._instance ??= new PreferencesContainer();
@@ -1195,7 +1201,7 @@ var ClientPreferences = class {
1195
1201
  if (!this._storage) return;
1196
1202
  const data = collectStoredPreferences();
1197
1203
  this._storage.setItem(PREFERENCES_STORAGE_KEY, data, "local").catch((error) => {
1198
- logger$27.error(`[ClientPreferences] Failed to save preferences: ${String(error)}`);
1204
+ logger$29.error(`[ClientPreferences] Failed to save preferences: ${String(error)}`);
1199
1205
  });
1200
1206
  }
1201
1207
  /** Loads preferences from storage and applies them to the container. */
@@ -1204,7 +1210,7 @@ var ClientPreferences = class {
1204
1210
  this._storage.getItem(PREFERENCES_STORAGE_KEY, "local").then((stored) => {
1205
1211
  if (stored) applyStoredPreferences(stored);
1206
1212
  }).catch((error) => {
1207
- logger$27.error(`[ClientPreferences] Failed to load preferences: ${String(error)}`);
1213
+ logger$29.error(`[ClientPreferences] Failed to load preferences: ${String(error)}`);
1208
1214
  });
1209
1215
  }
1210
1216
  };
@@ -1225,7 +1231,7 @@ function toError(value) {
1225
1231
 
1226
1232
  //#endregion
1227
1233
  //#region src/controllers/NavigatorDeviceController.ts
1228
- const logger$26 = require_operators.getLogger();
1234
+ const logger$28 = require_operators.getLogger();
1229
1235
  /** Maps a device kind to its storage key. */
1230
1236
  const DEVICE_STORAGE_KEYS = {
1231
1237
  audioinput: DEVICE_STORAGE_KEY_AUDIO_INPUT,
@@ -1247,7 +1253,7 @@ var NavigatorDeviceController = class extends Destroyable {
1247
1253
  super();
1248
1254
  this.webRTCApiProvider = webRTCApiProvider;
1249
1255
  this.deviceChangeHandler = () => {
1250
- logger$26.debug("[DeviceController] Device change detected");
1256
+ logger$28.debug("[DeviceController] Device change detected");
1251
1257
  this.enumerateDevices();
1252
1258
  };
1253
1259
  this._devicesState$ = this.createBehaviorSubject(initialDevicesState);
@@ -1312,13 +1318,13 @@ var NavigatorDeviceController = class extends Destroyable {
1312
1318
  return this.cachedObservable("videoInputDevices$", () => this._devicesState$.pipe((0, rxjs.map)((state) => state.videoinput), (0, rxjs.distinctUntilChanged)(), (0, rxjs.takeUntil)(this.destroyed$)));
1313
1319
  }
1314
1320
  get selectedAudioInputDevice$() {
1315
- return this.cachedObservable("selectedAudioInputDevice$", () => this._selectedDevicesState$.asObservable().pipe((0, rxjs.map)((state) => state.audioinput), (0, rxjs.distinctUntilChanged)(), (0, rxjs.takeUntil)(this.destroyed$), (0, rxjs.tap)((info) => logger$26.debug("[DeviceController] Selected audio input device changed:", info))));
1321
+ return this.cachedObservable("selectedAudioInputDevice$", () => this._selectedDevicesState$.asObservable().pipe((0, rxjs.map)((state) => state.audioinput), (0, rxjs.distinctUntilChanged)(), (0, rxjs.takeUntil)(this.destroyed$), (0, rxjs.tap)((info) => logger$28.debug("[DeviceController] Selected audio input device changed:", info))));
1316
1322
  }
1317
1323
  get selectedAudioOutputDevice$() {
1318
- return this.cachedObservable("selectedAudioOutputDevice$", () => this._selectedDevicesState$.asObservable().pipe((0, rxjs.map)((state) => state.audiooutput), (0, rxjs.distinctUntilChanged)(), (0, rxjs.takeUntil)(this.destroyed$), (0, rxjs.tap)((info) => logger$26.debug("[DeviceController] Selected audio output device changed:", info))));
1324
+ return this.cachedObservable("selectedAudioOutputDevice$", () => this._selectedDevicesState$.asObservable().pipe((0, rxjs.map)((state) => state.audiooutput), (0, rxjs.distinctUntilChanged)(), (0, rxjs.takeUntil)(this.destroyed$), (0, rxjs.tap)((info) => logger$28.debug("[DeviceController] Selected audio output device changed:", info))));
1319
1325
  }
1320
1326
  get selectedVideoInputDevice$() {
1321
- return this.cachedObservable("selectedVideoInputDevice$", () => this._selectedDevicesState$.asObservable().pipe((0, rxjs.map)((state) => state.videoinput), (0, rxjs.distinctUntilChanged)(), (0, rxjs.takeUntil)(this.destroyed$), (0, rxjs.tap)((info) => logger$26.debug("[DeviceController] Selected video input device changed:", info))));
1327
+ return this.cachedObservable("selectedVideoInputDevice$", () => this._selectedDevicesState$.asObservable().pipe((0, rxjs.map)((state) => state.videoinput), (0, rxjs.distinctUntilChanged)(), (0, rxjs.takeUntil)(this.destroyed$), (0, rxjs.tap)((info) => logger$28.debug("[DeviceController] Selected video input device changed:", info))));
1322
1328
  }
1323
1329
  get selectedAudioInputDevice() {
1324
1330
  if (this._audioInputDisabled$.value) return null;
@@ -1393,7 +1399,7 @@ var NavigatorDeviceController = class extends Destroyable {
1393
1399
  if (device) this.persistDeviceSelection("audioinput", device);
1394
1400
  }
1395
1401
  selectVideoInputDevice(device) {
1396
- logger$26.debug("[DeviceController] Setting selected video input device:", device);
1402
+ logger$28.debug("[DeviceController] Setting selected video input device:", device);
1397
1403
  if (this._videoInputDisabled$.value && device) this._videoInputDisabled$.next(false);
1398
1404
  const previous = this._selectedDevicesState$.value.videoinput;
1399
1405
  if (previous && previous.deviceId !== device?.deviceId) this._deviceHistory.push("videoinput", previous);
@@ -1450,7 +1456,7 @@ var NavigatorDeviceController = class extends Destroyable {
1450
1456
  }
1451
1457
  const fromHistory = this._deviceHistory.findInHistory(kind, devices);
1452
1458
  if (fromHistory) {
1453
- logger$26.debug(`[DeviceController] Device disappeared, falling back to history: ${fromHistory.label}`);
1459
+ logger$28.debug(`[DeviceController] Device disappeared, falling back to history: ${fromHistory.label}`);
1454
1460
  this.emitDeviceRecovered(kind, selected, fromHistory, "device_disconnected");
1455
1461
  return fromHistory;
1456
1462
  }
@@ -1503,7 +1509,7 @@ var NavigatorDeviceController = class extends Destroyable {
1503
1509
  try {
1504
1510
  await this._storageManager.setItem(DEVICE_STORAGE_KEYS[kind], stored, "local");
1505
1511
  } catch (error) {
1506
- logger$26.error(`[DeviceController] Failed to persist device selection for ${kind}:`, error);
1512
+ logger$28.error(`[DeviceController] Failed to persist device selection for ${kind}:`, error);
1507
1513
  }
1508
1514
  }
1509
1515
  async loadPersistedDevices() {
@@ -1519,7 +1525,7 @@ var NavigatorDeviceController = class extends Destroyable {
1519
1525
  [kind]: stored
1520
1526
  };
1521
1527
  } catch (error) {
1522
- logger$26.error(`[DeviceController] Failed to load persisted device for ${kind}:`, error);
1528
+ logger$28.error(`[DeviceController] Failed to load persisted device for ${kind}:`, error);
1523
1529
  }
1524
1530
  }
1525
1531
  /** Clears device history, persisted selections, and re-enumerates devices. */
@@ -1537,7 +1543,7 @@ var NavigatorDeviceController = class extends Destroyable {
1537
1543
  this.disableDeviceMonitoring();
1538
1544
  this.webRTCApiProvider.mediaDevices.addEventListener("devicechange", this.deviceChangeHandler);
1539
1545
  if (PreferencesContainer.instance.devicePollingInterval > 0) this._devicesPoolingSubscription = (0, rxjs.interval)(PreferencesContainer.instance.devicePollingInterval).subscribe(() => {
1540
- logger$26.debug("[DeviceController] Polling devices due to interval");
1546
+ logger$28.debug("[DeviceController] Polling devices due to interval");
1541
1547
  this.enumerateDevices();
1542
1548
  });
1543
1549
  this.enumerateDevices();
@@ -1563,13 +1569,13 @@ var NavigatorDeviceController = class extends Destroyable {
1563
1569
  videoinput: []
1564
1570
  });
1565
1571
  this._devicesState$.next(devicesByKind);
1566
- logger$26.debug("[DeviceController] Devices enumerated:", {
1572
+ logger$28.debug("[DeviceController] Devices enumerated:", {
1567
1573
  audioInputs: devicesByKind.audioinput.length,
1568
1574
  audioOutputs: devicesByKind.audiooutput.length,
1569
1575
  videoInputs: devicesByKind.videoinput.length
1570
1576
  });
1571
1577
  } catch (error) {
1572
- logger$26.error("[DeviceController] Failed to enumerate devices:", error);
1578
+ logger$28.error("[DeviceController] Failed to enumerate devices:", error);
1573
1579
  this._errors$.next(toError(error));
1574
1580
  }
1575
1581
  }
@@ -1585,7 +1591,7 @@ var NavigatorDeviceController = class extends Destroyable {
1585
1591
  stream.getTracks().forEach((t) => t.stop());
1586
1592
  return capabilities;
1587
1593
  } catch (error) {
1588
- logger$26.error("[DeviceController] Failed to get device capabilities:", error);
1594
+ logger$28.error("[DeviceController] Failed to get device capabilities:", error);
1589
1595
  this._errors$.next(toError(error));
1590
1596
  throw error;
1591
1597
  }
@@ -1836,7 +1842,7 @@ var DependencyContainer = class {
1836
1842
 
1837
1843
  //#endregion
1838
1844
  //#region src/controllers/CryptoController.ts
1839
- const logger$25 = require_operators.getLogger();
1845
+ const logger$27 = require_operators.getLogger();
1840
1846
  const DPOP_DB_NAME = "sw-dpop";
1841
1847
  const DPOP_DB_VERSION = 1;
1842
1848
  const DPOP_STORE_NAME = "keys";
@@ -1895,7 +1901,7 @@ async function loadKeyPairFromDB() {
1895
1901
  tx.oncomplete = () => db.close();
1896
1902
  });
1897
1903
  } catch (error) {
1898
- logger$25.warn("[DPoP] Failed to load key pair from IndexedDB:", error);
1904
+ logger$27.warn("[DPoP] Failed to load key pair from IndexedDB:", error);
1899
1905
  return null;
1900
1906
  }
1901
1907
  }
@@ -1915,7 +1921,7 @@ async function saveKeyPairToDB(keyPair) {
1915
1921
  };
1916
1922
  });
1917
1923
  } catch (error) {
1918
- logger$25.warn("[DPoP] Failed to save key pair to IndexedDB:", error);
1924
+ logger$27.warn("[DPoP] Failed to save key pair to IndexedDB:", error);
1919
1925
  }
1920
1926
  }
1921
1927
  async function deleteKeyPairFromDB() {
@@ -1934,7 +1940,7 @@ async function deleteKeyPairFromDB() {
1934
1940
  };
1935
1941
  });
1936
1942
  } catch (error) {
1937
- logger$25.warn("[DPoP] Failed to delete key pair from IndexedDB:", error);
1943
+ logger$27.warn("[DPoP] Failed to delete key pair from IndexedDB:", error);
1938
1944
  }
1939
1945
  }
1940
1946
  /**
@@ -1994,13 +2000,13 @@ var CryptoController = class {
1994
2000
  this._publicJwk = await crypto.subtle.exportKey("jwk", stored.publicKey);
1995
2001
  this._fingerprint = await computeJwkThumbprint(this._publicJwk);
1996
2002
  this._initialized = true;
1997
- logger$25.debug("[DPoP] Key pair restored from IndexedDB, fingerprint:", this._fingerprint);
2003
+ logger$27.debug("[DPoP] Key pair restored from IndexedDB, fingerprint:", this._fingerprint);
1998
2004
  return this._fingerprint;
1999
2005
  } catch (error) {
2000
- logger$25.warn("[DPoP] Stored key pair unusable, generating new one:", error);
2006
+ logger$27.warn("[DPoP] Stored key pair unusable, generating new one:", error);
2001
2007
  await deleteKeyPairFromDB();
2002
2008
  }
2003
- logger$25.debug("[DPoP] Generating RSA key pair");
2009
+ logger$27.debug("[DPoP] Generating RSA key pair");
2004
2010
  this._keyPair = await crypto.subtle.generateKey({
2005
2011
  name: "RSASSA-PKCS1-v1_5",
2006
2012
  modulusLength: 2048,
@@ -2015,7 +2021,7 @@ var CryptoController = class {
2015
2021
  this._fingerprint = await computeJwkThumbprint(this._publicJwk);
2016
2022
  this._initialized = true;
2017
2023
  await saveKeyPairToDB(this._keyPair);
2018
- logger$25.debug("[DPoP] Key pair generated and persisted, fingerprint:", this._fingerprint);
2024
+ logger$27.debug("[DPoP] Key pair generated and persisted, fingerprint:", this._fingerprint);
2019
2025
  return this._fingerprint;
2020
2026
  }
2021
2027
  /**
@@ -2081,7 +2087,7 @@ var CryptoController = class {
2081
2087
  this._fingerprint = null;
2082
2088
  this._initialized = false;
2083
2089
  deleteKeyPairFromDB();
2084
- logger$25.debug("[DPoP] Controller destroyed");
2090
+ logger$27.debug("[DPoP] Controller destroyed");
2085
2091
  }
2086
2092
  get publicJwk() {
2087
2093
  if (!this._publicJwk) throw new require_operators.DPoPInitError("CryptoController not initialized. Call init() first.");
@@ -2104,7 +2110,7 @@ var CryptoController = class {
2104
2110
 
2105
2111
  //#endregion
2106
2112
  //#region src/controllers/NetworkMonitor.ts
2107
- const logger$24 = require_operators.getLogger();
2113
+ const logger$26 = require_operators.getLogger();
2108
2114
  /**
2109
2115
  * Safely check whether we are running in a browser environment
2110
2116
  * with `window` and the relevant event targets.
@@ -2161,7 +2167,7 @@ var NetworkMonitor = class extends Destroyable {
2161
2167
  }
2162
2168
  attachListeners() {
2163
2169
  if (!hasBrowserNetworkEvents()) {
2164
- logger$24.debug("NetworkMonitor: no browser environment detected, skipping event listeners");
2170
+ logger$26.debug("NetworkMonitor: no browser environment detected, skipping event listeners");
2165
2171
  return;
2166
2172
  }
2167
2173
  window.addEventListener("online", this._onOnline);
@@ -2169,7 +2175,7 @@ var NetworkMonitor = class extends Destroyable {
2169
2175
  const connection = getNetworkConnection();
2170
2176
  if (connection) connection.addEventListener("change", this._onConnectionChange);
2171
2177
  this._listenersAttached = true;
2172
- logger$24.debug("NetworkMonitor: event listeners attached");
2178
+ logger$26.debug("NetworkMonitor: event listeners attached");
2173
2179
  }
2174
2180
  removeListeners() {
2175
2181
  if (!this._listenersAttached) return;
@@ -2180,10 +2186,10 @@ var NetworkMonitor = class extends Destroyable {
2180
2186
  if (connection) connection.removeEventListener("change", this._onConnectionChange);
2181
2187
  }
2182
2188
  this._listenersAttached = false;
2183
- logger$24.debug("NetworkMonitor: event listeners removed");
2189
+ logger$26.debug("NetworkMonitor: event listeners removed");
2184
2190
  }
2185
2191
  handleOnline() {
2186
- logger$24.info("NetworkMonitor: browser went online");
2192
+ logger$26.info("NetworkMonitor: browser went online");
2187
2193
  this._isOnline$.next(true);
2188
2194
  this._networkChange$.next({
2189
2195
  type: "online",
@@ -2192,7 +2198,7 @@ var NetworkMonitor = class extends Destroyable {
2192
2198
  });
2193
2199
  }
2194
2200
  handleOffline() {
2195
- logger$24.info("NetworkMonitor: browser went offline");
2201
+ logger$26.info("NetworkMonitor: browser went offline");
2196
2202
  this._isOnline$.next(false);
2197
2203
  this._networkChange$.next({
2198
2204
  type: "offline",
@@ -2201,7 +2207,7 @@ var NetworkMonitor = class extends Destroyable {
2201
2207
  }
2202
2208
  handleConnectionChange() {
2203
2209
  const networkType = getNetworkType();
2204
- logger$24.info(`NetworkMonitor: connection changed — effectiveType=${networkType ?? "unknown"}`);
2210
+ logger$26.info(`NetworkMonitor: connection changed — effectiveType=${networkType ?? "unknown"}`);
2205
2211
  this._networkChange$.next({
2206
2212
  type: "connection_change",
2207
2213
  timestamp: Date.now(),
@@ -2316,7 +2322,7 @@ function getNavigatorMediaDevices() {
2316
2322
 
2317
2323
  //#endregion
2318
2324
  //#region src/controllers/PreflightRunner.ts
2319
- const logger$23 = require_operators.getLogger();
2325
+ const logger$25 = require_operators.getLogger();
2320
2326
  const DEFAULT_MEDIA_TEST_DURATION_S = 10;
2321
2327
  const ICE_GATHERING_TIMEOUT_MS = 1e4;
2322
2328
  const SIGNALING_RTT_TIMEOUT_MS = 5e3;
@@ -2365,7 +2371,7 @@ var PreflightRunner = class extends Destroyable {
2365
2371
  if (!this._options.skipMediaTest) try {
2366
2372
  bandwidth = await this.testMediaBandwidth(destination);
2367
2373
  } catch (error) {
2368
- logger$23.warn("[PreflightRunner] Media bandwidth test failed:", error);
2374
+ logger$25.warn("[PreflightRunner] Media bandwidth test failed:", error);
2369
2375
  warnings.push("Media bandwidth test failed");
2370
2376
  }
2371
2377
  return {
@@ -2377,7 +2383,7 @@ var PreflightRunner = class extends Destroyable {
2377
2383
  warnings
2378
2384
  };
2379
2385
  } catch (error) {
2380
- logger$23.error("[PreflightRunner] Preflight test failed:", error);
2386
+ logger$25.error("[PreflightRunner] Preflight test failed:", error);
2381
2387
  throw new require_operators.PreflightError("preflight", error instanceof Error ? error : new Error(String(error)));
2382
2388
  } finally {
2383
2389
  this.destroy();
@@ -2408,7 +2414,7 @@ var PreflightRunner = class extends Destroyable {
2408
2414
  if (track.kind === "video" && track.readyState === "live") videoWorking = true;
2409
2415
  }
2410
2416
  } catch (error) {
2411
- logger$23.warn("[PreflightRunner] Device test failed:", error);
2417
+ logger$25.warn("[PreflightRunner] Device test failed:", error);
2412
2418
  } finally {
2413
2419
  if (audioStream) audioStream.getTracks().forEach((t) => t.stop());
2414
2420
  }
@@ -2466,7 +2472,7 @@ var PreflightRunner = class extends Destroyable {
2466
2472
  rttMs
2467
2473
  };
2468
2474
  } catch (error) {
2469
- logger$23.warn("[PreflightRunner] ICE connectivity test failed:", error);
2475
+ logger$25.warn("[PreflightRunner] ICE connectivity test failed:", error);
2470
2476
  return {
2471
2477
  type: "failed",
2472
2478
  turnReachable: false,
@@ -2513,7 +2519,7 @@ var PreflightRunner = class extends Destroyable {
2513
2519
 
2514
2520
  //#endregion
2515
2521
  //#region src/controllers/VisibilityController.ts
2516
- const logger$22 = require_operators.getLogger();
2522
+ const logger$24 = require_operators.getLogger();
2517
2523
  /**
2518
2524
  * Checks whether the document visibility API is available.
2519
2525
  */
@@ -2550,8 +2556,8 @@ var VisibilityController = class extends Destroyable {
2550
2556
  this._boundHandler = this._handleVisibilityChange.bind(this);
2551
2557
  if (this._hasVisibilityApi) {
2552
2558
  document.addEventListener("visibilitychange", this._boundHandler);
2553
- logger$22.debug("VisibilityController: listening for visibilitychange events");
2554
- } else logger$22.debug("VisibilityController: document visibility API not available, defaulting to visible");
2559
+ logger$24.debug("VisibilityController: listening for visibilitychange events");
2560
+ } else logger$24.debug("VisibilityController: document visibility API not available, defaulting to visible");
2555
2561
  }
2556
2562
  /**
2557
2563
  * Observable of the current visibility state.
@@ -2576,7 +2582,7 @@ var VisibilityController = class extends Destroyable {
2576
2582
  destroy() {
2577
2583
  if (this._hasVisibilityApi) {
2578
2584
  document.removeEventListener("visibilitychange", this._boundHandler);
2579
- logger$22.debug("VisibilityController: removed visibilitychange listener");
2585
+ logger$24.debug("VisibilityController: removed visibilitychange listener");
2580
2586
  }
2581
2587
  super.destroy();
2582
2588
  }
@@ -2594,7 +2600,7 @@ var VisibilityController = class extends Destroyable {
2594
2600
  timestamp: Date.now()
2595
2601
  };
2596
2602
  this._visibilityChange$.next(changeEvent);
2597
- logger$22.debug("VisibilityController: visibility changed", {
2603
+ logger$24.debug("VisibilityController: visibility changed", {
2598
2604
  from: previousState,
2599
2605
  to: newState
2600
2606
  });
@@ -2797,7 +2803,7 @@ const RPCEventAckResponse = (id) => makeRPCResponse({
2797
2803
 
2798
2804
  //#endregion
2799
2805
  //#region src/managers/AttachManager.ts
2800
- const logger$21 = require_operators.getLogger();
2806
+ const logger$23 = require_operators.getLogger();
2801
2807
  var AttachManager = class {
2802
2808
  constructor(storage, deviceController, reconnectCallsTimeout, attachKey) {
2803
2809
  this.storage = storage;
@@ -2818,7 +2824,7 @@ var AttachManager = class {
2818
2824
  try {
2819
2825
  return await this.storage.getItem(this.attachKey) ?? {};
2820
2826
  } catch (error) {
2821
- logger$21.warn("[AttachManager] Failed to retrieve attached calls from storage", error);
2827
+ logger$23.warn("[AttachManager] Failed to retrieve attached calls from storage", error);
2822
2828
  return {};
2823
2829
  }
2824
2830
  }
@@ -2826,7 +2832,7 @@ var AttachManager = class {
2826
2832
  try {
2827
2833
  await this.storage.setItem(this.attachKey, attached);
2828
2834
  } catch (error) {
2829
- logger$21.warn("[AttachManager] Failed to write attached calls to storage", error);
2835
+ logger$23.warn("[AttachManager] Failed to write attached calls to storage", error);
2830
2836
  }
2831
2837
  }
2832
2838
  /**
@@ -2845,7 +2851,7 @@ var AttachManager = class {
2845
2851
  }
2846
2852
  async attach(call) {
2847
2853
  if (!call.to) {
2848
- logger$21.warn("[AttachManager] Skip attach for calls with no destination");
2854
+ logger$23.warn("[AttachManager] Skip attach for calls with no destination");
2849
2855
  return;
2850
2856
  }
2851
2857
  const destination = call.to;
@@ -2898,15 +2904,15 @@ var AttachManager = class {
2898
2904
  callId,
2899
2905
  ...options
2900
2906
  });
2901
- logger$21.info(`[AttachManager] Reattached call ${callId} (attempt ${attempt})`);
2907
+ logger$23.info(`[AttachManager] Reattached call ${callId} (attempt ${attempt})`);
2902
2908
  succeeded = true;
2903
2909
  break;
2904
2910
  } catch (error) {
2905
- logger$21.warn(`[AttachManager] Reattach attempt ${attempt}/3 failed for call ${callId}:`, error);
2911
+ logger$23.warn(`[AttachManager] Reattach attempt ${attempt}/3 failed for call ${callId}:`, error);
2906
2912
  if (attempt < 3) await new Promise((r) => setTimeout(r, (attempt + 1) * 1e3));
2907
2913
  }
2908
2914
  if (!succeeded) {
2909
- logger$21.warn(`[AttachManager] Reattach failed after 3 attempts for call ${callId}, removing reference`);
2915
+ logger$23.warn(`[AttachManager] Reattach failed after 3 attempts for call ${callId}, removing reference`);
2910
2916
  await this.detach({
2911
2917
  id: callId,
2912
2918
  mediaDirections: attachment.mediaDirections
@@ -3220,7 +3226,7 @@ function toggleHandraiseMethod(is) {
3220
3226
 
3221
3227
  //#endregion
3222
3228
  //#region src/core/entities/Participant.ts
3223
- const logger$20 = require_operators.getLogger();
3229
+ const logger$22 = require_operators.getLogger();
3224
3230
  const initialState = {};
3225
3231
  /**
3226
3232
  * Represents a participant in a call.
@@ -3272,15 +3278,35 @@ var Participant = class extends Destroyable {
3272
3278
  get deaf$() {
3273
3279
  return this.cachedObservable("deaf$", () => this._state$.pipe((0, rxjs_operators.map)((state) => state.deaf), (0, rxjs_operators.distinctUntilChanged)()));
3274
3280
  }
3275
- /** Observable of the participant's microphone input volume. */
3281
+ /**
3282
+ * Observable of the participant's **server-side** microphone input volume
3283
+ * as reported by the mix engine. This is gain applied on the bridged audio
3284
+ * leg (FreeSWITCH channel read volume), NOT the local browser mic. For a
3285
+ * local PC mic control, see {@link Call.setLocalMicrophoneGain}.
3286
+ *
3287
+ * @see {@link setAudioInputVolume}
3288
+ */
3276
3289
  get inputVolume$() {
3277
3290
  return this.cachedObservable("inputVolume$", () => this._state$.pipe((0, rxjs_operators.map)((state) => state.input_volume), (0, rxjs_operators.distinctUntilChanged)()));
3278
3291
  }
3279
- /** Observable of the participant's speaker output volume. */
3292
+ /**
3293
+ * Observable of the participant's **server-side** speaker output volume as
3294
+ * reported by the mix engine (FreeSWITCH channel write volume). NOT the
3295
+ * local HTML `<audio>` element volume — set that on your own element.
3296
+ *
3297
+ * @see {@link setAudioOutputVolume}
3298
+ */
3280
3299
  get outputVolume$() {
3281
3300
  return this.cachedObservable("outputVolume$", () => this._state$.pipe((0, rxjs_operators.map)((state) => state.output_volume), (0, rxjs_operators.distinctUntilChanged)()));
3282
3301
  }
3283
- /** Observable of the microphone input sensitivity level. */
3302
+ /**
3303
+ * Observable of the **conference-only** microphone energy/gate sensitivity
3304
+ * level for this member. Routes through the conferencing mix engine and has
3305
+ * no effect on 1:1 WebRTC calls. Populated from `member.updated` events for
3306
+ * conference members.
3307
+ *
3308
+ * @see {@link setAudioInputSensitivity}
3309
+ */
3284
3310
  get inputSensitivity$() {
3285
3311
  return this.cachedObservable("inputSensitivity$", () => this._state$.pipe((0, rxjs_operators.map)((state) => state.input_sensitivity), (0, rxjs_operators.distinctUntilChanged)()));
3286
3312
  }
@@ -3368,15 +3394,25 @@ var Participant = class extends Destroyable {
3368
3394
  get deaf() {
3369
3395
  return this._state$.value.deaf ?? false;
3370
3396
  }
3371
- /** Current microphone input volume level, or `undefined` if not set. */
3397
+ /**
3398
+ * Current **server-side** microphone input volume as reported by the mix
3399
+ * engine, or `undefined` if not set. Not the local PC mic — see
3400
+ * {@link Call.setLocalMicrophoneGain} for browser-side control.
3401
+ */
3372
3402
  get inputVolume() {
3373
3403
  return this._state$.value.input_volume;
3374
3404
  }
3375
- /** Current speaker output volume level, or `undefined` if not set. */
3405
+ /**
3406
+ * Current **server-side** speaker output volume from the mix engine, or
3407
+ * `undefined` if not set. Not the local `<audio>` element volume.
3408
+ */
3376
3409
  get outputVolume() {
3377
3410
  return this._state$.value.output_volume;
3378
3411
  }
3379
- /** Current microphone input sensitivity level, or `undefined` if not set. */
3412
+ /**
3413
+ * Current **conference-only** microphone sensitivity/gate level, or
3414
+ * `undefined` if not set. Applies only to conference members.
3415
+ */
3380
3416
  get inputSensitivity() {
3381
3417
  return this._state$.value.input_sensitivity;
3382
3418
  }
@@ -3480,19 +3516,44 @@ var Participant = class extends Destroyable {
3480
3516
  async toggleLowbitrate() {
3481
3517
  throw new require_operators.UnimplementedError();
3482
3518
  }
3483
- /** Sets the microphone input sensitivity level. */
3519
+ /**
3520
+ * Adjusts the **conference-only** microphone energy gate / sensitivity level
3521
+ * for this member. Routes through the conferencing mix engine
3522
+ * (`signalwire.conferencing member.set_input_sensitivity`) and has no effect
3523
+ * on 1:1 WebRTC calls — for those, use browser audio constraints via
3524
+ * {@link Call.setNoiseSuppression} / {@link Call.setAutoGainControl}.
3525
+ *
3526
+ * This is **not** a local PC mic gain control; it only changes how the
3527
+ * server-side mixer decides to open the mic gate on this participant.
3528
+ *
3529
+ * @param value - Sensitivity level as understood by the conference engine
3530
+ * (integer, larger values are more sensitive).
3531
+ */
3484
3532
  async setAudioInputSensitivity(value) {
3485
3533
  await this.executeMethod(this.id, "call.microphone.sensitivity.set", { sensitivity: value });
3486
3534
  }
3487
3535
  /**
3488
- * Sets the microphone input volume level.
3536
+ * Sets the **server-side** microphone volume on this participant's bridged
3537
+ * call leg. Applies a multiplier to the audio flowing through the mix
3538
+ * engine (FreeSWITCH channel read volume) — changes what other participants
3539
+ * hear, not what the local browser captures.
3540
+ *
3541
+ * For local PC mic gain, use {@link Call.setLocalMicrophoneGain} instead.
3542
+ *
3489
3543
  * @param value - Volume level (0-100).
3490
3544
  */
3491
3545
  async setAudioInputVolume(value) {
3492
3546
  await this.executeMethod(this.id, "call.microphone.volume.set", { volume: value });
3493
3547
  }
3494
3548
  /**
3495
- * Sets the speaker output volume level.
3549
+ * Sets the **server-side** speaker volume on this participant's bridged call
3550
+ * leg (FreeSWITCH channel write volume) — what this participant hears from
3551
+ * the mix before it reaches their client.
3552
+ *
3553
+ * For local playback volume (the `<audio>` element the consumer attaches
3554
+ * `remoteStream` to), set `audioElement.volume` directly in the consumer's
3555
+ * code.
3556
+ *
3496
3557
  * @param value - Volume level (0-100).
3497
3558
  */
3498
3559
  async setAudioOutputVolume(value) {
@@ -3598,7 +3659,7 @@ var SelfParticipant = class extends Participant {
3598
3659
  try {
3599
3660
  await this.vertoManager.addScreenMedia();
3600
3661
  } catch (error) {
3601
- logger$20.error("[Participant.startScreenShare] Screen share error:", error);
3662
+ logger$22.error("[Participant.startScreenShare] Screen share error:", error);
3602
3663
  }
3603
3664
  }
3604
3665
  /** Observable of the current screen share status. */
@@ -3618,7 +3679,7 @@ var SelfParticipant = class extends Participant {
3618
3679
  try {
3619
3680
  await this.vertoManager.addInputDevice(options);
3620
3681
  } catch (error) {
3621
- logger$20.error("[Participant.startScreenShare] Screen share error:", error);
3682
+ logger$22.error("[Participant.startScreenShare] Screen share error:", error);
3622
3683
  }
3623
3684
  }
3624
3685
  /** Removes an additional media input device by ID. */
@@ -3680,7 +3741,7 @@ var SelfParticipant = class extends Participant {
3680
3741
  */
3681
3742
  exitStudioModeIfActive() {
3682
3743
  if (this._studioAudio$.value) {
3683
- logger$20.debug("[SelfParticipant] Exiting studio audio mode due to individual flag toggle");
3744
+ logger$22.debug("[SelfParticipant] Exiting studio audio mode due to individual flag toggle");
3684
3745
  this._studioAudio$.next(false);
3685
3746
  }
3686
3747
  }
@@ -3704,7 +3765,7 @@ var SelfParticipant = class extends Participant {
3704
3765
  try {
3705
3766
  await super.mute();
3706
3767
  } catch (error) {
3707
- logger$20.warn("[Participant.toggleAudioInput] Server Error while muting audio input, proceeding with local toggle anyway", error);
3768
+ logger$22.warn("[Participant.toggleAudioInput] Server Error while muting audio input, proceeding with local toggle anyway", error);
3708
3769
  } finally {
3709
3770
  this.vertoManager.muteMainAudioInputDevice();
3710
3771
  }
@@ -3714,7 +3775,7 @@ var SelfParticipant = class extends Participant {
3714
3775
  try {
3715
3776
  await super.unmute();
3716
3777
  } catch (error) {
3717
- logger$20.warn("[Participant.toggleAudioInput] Server Error while unmuting audio input, proceeding with local toggle anyway", error);
3778
+ logger$22.warn("[Participant.toggleAudioInput] Server Error while unmuting audio input, proceeding with local toggle anyway", error);
3718
3779
  } finally {
3719
3780
  await this.vertoManager.unmuteMainAudioInputDevice();
3720
3781
  }
@@ -3724,7 +3785,7 @@ var SelfParticipant = class extends Participant {
3724
3785
  try {
3725
3786
  await super.muteVideo();
3726
3787
  } catch (error) {
3727
- logger$20.warn("[Participant.toggleVideoInput] Server Error while muting video input, proceeding with local toggle anyway", error);
3788
+ logger$22.warn("[Participant.toggleVideoInput] Server Error while muting video input, proceeding with local toggle anyway", error);
3728
3789
  } finally {
3729
3790
  this.vertoManager.muteMainVideoInputDevice();
3730
3791
  }
@@ -3734,7 +3795,7 @@ var SelfParticipant = class extends Participant {
3734
3795
  try {
3735
3796
  await super.unmuteVideo();
3736
3797
  } catch (error) {
3737
- logger$20.warn("[Participant.toggleVideoInput] Server Error while unmuting video input, proceeding with local toggle anyway", error);
3798
+ logger$22.warn("[Participant.toggleVideoInput] Server Error while unmuting video input, proceeding with local toggle anyway", error);
3738
3799
  } finally {
3739
3800
  await this.vertoManager.unmuteMainVideoInputDevice();
3740
3801
  }
@@ -3828,7 +3889,7 @@ function isLayoutChangedPayload(value) {
3828
3889
 
3829
3890
  //#endregion
3830
3891
  //#region src/managers/CallEventsManager.ts
3831
- const logger$19 = require_operators.getLogger();
3892
+ const logger$21 = require_operators.getLogger();
3832
3893
  const initialSessionState = {};
3833
3894
  /** @internal */
3834
3895
  var CallEventsManager = class extends Destroyable {
@@ -3932,7 +3993,7 @@ var CallEventsManager = class extends Destroyable {
3932
3993
  }
3933
3994
  initSubscriptions() {
3934
3995
  this.subscribeTo(this.callJoinedEvent$, (callJoinedEvent) => {
3935
- logger$19.debug("[CallEventsManager] Handling call.joined event for call/session IDs:", {
3996
+ logger$21.debug("[CallEventsManager] Handling call.joined event for call/session IDs:", {
3936
3997
  callId: callJoinedEvent.call_id,
3937
3998
  roomSessionId: callJoinedEvent.room_session_id
3938
3999
  });
@@ -3959,19 +4020,19 @@ var CallEventsManager = class extends Destroyable {
3959
4020
  if (this._self$.value?.capabilities.setLayout) this.updateLayouts();
3960
4021
  });
3961
4022
  this.subscribeTo(this.memberUpdates$, (member) => {
3962
- logger$19.debug("[CallEventsManager] Handling member update event for member ID:", member);
4023
+ logger$21.debug("[CallEventsManager] Handling member update event for member ID:", member);
3963
4024
  this.upsertParticipant(member);
3964
4025
  });
3965
4026
  this.subscribeTo(this.webRtcCallSession.memberLeft$, (memberLeftEvent) => {
3966
- logger$19.debug("[CallEventsManager] Handling member.left event for member ID:", memberLeftEvent.member.member_id);
4027
+ logger$21.debug("[CallEventsManager] Handling member.left event for member ID:", memberLeftEvent.member.member_id);
3967
4028
  const participants = { ...this._participants$.value };
3968
4029
  if (memberLeftEvent.member.member_id in participants) {
3969
4030
  delete participants[memberLeftEvent.member.member_id];
3970
4031
  this._participants$.next(participants);
3971
- } else logger$19.warn(`[CallEventsManager] Received member.left event for unknown member ID: ${memberLeftEvent.member.member_id}`);
4032
+ } else logger$21.warn(`[CallEventsManager] Received member.left event for unknown member ID: ${memberLeftEvent.member.member_id}`);
3972
4033
  });
3973
4034
  this.subscribeTo(this.webRtcCallSession.callUpdated$, (callUpdatedEvent) => {
3974
- logger$19.debug("[CallEventsManager] Handling call.updated event:", callUpdatedEvent);
4035
+ logger$21.debug("[CallEventsManager] Handling call.updated event:", callUpdatedEvent);
3975
4036
  const roomSession = callUpdatedEvent.room_session;
3976
4037
  this._sessionState$.next({
3977
4038
  ...this._sessionState$.value,
@@ -3986,7 +4047,7 @@ var CallEventsManager = class extends Destroyable {
3986
4047
  });
3987
4048
  });
3988
4049
  this.subscribeTo(this.layoutChangedEvent$, (layoutChangedEvent) => {
3989
- logger$19.debug("[CallEventsManager] Handling layout.changed event:", layoutChangedEvent);
4050
+ logger$21.debug("[CallEventsManager] Handling layout.changed event:", layoutChangedEvent);
3990
4051
  this._sessionState$.next({
3991
4052
  ...this._sessionState$.value,
3992
4053
  layout_name: layoutChangedEvent.id,
@@ -3996,10 +4057,10 @@ var CallEventsManager = class extends Destroyable {
3996
4057
  });
3997
4058
  }
3998
4059
  updateParticipantPositions(layoutChangedEvent) {
3999
- if (Object.keys(this._participants$.value).length > 0 && !layoutChangedEvent.layers.some((layer) => !!layer.member_id)) logger$19.warn("[CallEventsManager] No layers with member_id found in layout.changed event. Nothing to update.");
4060
+ if (Object.keys(this._participants$.value).length > 0 && !layoutChangedEvent.layers.some((layer) => !!layer.member_id)) logger$21.warn("[CallEventsManager] No layers with member_id found in layout.changed event. Nothing to update.");
4000
4061
  layoutChangedEvent.layers.filter((layer) => !!layer.member_id).filter((layer) => {
4001
4062
  if (!(layer.member_id in this._participants$.value)) {
4002
- logger$19.warn(`[CallEventsManager] Skipping layout layer for unknown member_id: ${layer.member_id}`);
4063
+ logger$21.warn(`[CallEventsManager] Skipping layout layer for unknown member_id: ${layer.member_id}`);
4003
4064
  return false;
4004
4065
  }
4005
4066
  return true;
@@ -4022,7 +4083,7 @@ var CallEventsManager = class extends Destroyable {
4022
4083
  layouts: response.result.layouts
4023
4084
  });
4024
4085
  }).catch((error) => {
4025
- logger$19.error("[CallEventsManager] Error fetching layouts:", error);
4086
+ logger$21.error("[CallEventsManager] Error fetching layouts:", error);
4026
4087
  });
4027
4088
  }
4028
4089
  updateParticipants(members) {
@@ -4038,7 +4099,7 @@ var CallEventsManager = class extends Destroyable {
4038
4099
  }
4039
4100
  const participant = this._participants$.value[member.member_id];
4040
4101
  const oldValue = participant.value;
4041
- logger$19.debug("[CallEventsManager] Updating participant:", member.member_id, {
4102
+ logger$21.debug("[CallEventsManager] Updating participant:", member.member_id, {
4042
4103
  oldValue,
4043
4104
  newValue: member
4044
4105
  });
@@ -4051,17 +4112,17 @@ var CallEventsManager = class extends Destroyable {
4051
4112
  }
4052
4113
  get callJoinedEvent$() {
4053
4114
  return this.cachedObservable("callJoinedEvent$", () => this.webRtcCallSession.callEvent$.pipe((0, rxjs.filter)(isCallJoinedPayload), (0, rxjs.tap)((event) => {
4054
- logger$19.debug("[CallEventsManager] Call joined event:", event);
4115
+ logger$21.debug("[CallEventsManager] Call joined event:", event);
4055
4116
  })));
4056
4117
  }
4057
4118
  get layoutChangedEvent$() {
4058
4119
  return this.cachedObservable("layoutChangedEvent$", () => this.webRtcCallSession.callEvent$.pipe(require_operators.filterAs(isLayoutChangedPayload, "layout"), (0, rxjs.tap)((event) => {
4059
- logger$19.debug("[CallEventsManager] Layout changed event:", event);
4120
+ logger$21.debug("[CallEventsManager] Layout changed event:", event);
4060
4121
  })));
4061
4122
  }
4062
4123
  get memberUpdates$() {
4063
4124
  return this.cachedObservable("memberUpdates$", () => (0, rxjs.merge)(this.webRtcCallSession.memberJoined$, this.webRtcCallSession.memberUpdated$, this.webRtcCallSession.memberTalking$).pipe((0, rxjs.map)((event) => event.member), (0, rxjs.tap)((event) => {
4064
- logger$19.debug("[CallEventsManager] Member update event:", event);
4125
+ logger$21.debug("[CallEventsManager] Member update event:", event);
4065
4126
  })));
4066
4127
  }
4067
4128
  destroy() {
@@ -4317,7 +4378,7 @@ function appendStereoParams(fmtpLine, maxBitrate) {
4317
4378
 
4318
4379
  //#endregion
4319
4380
  //#region src/controllers/ICEGatheringController.ts
4320
- const logger$18 = require_operators.getLogger();
4381
+ const logger$20 = require_operators.getLogger();
4321
4382
  var ICEGatheringController = class extends Destroyable {
4322
4383
  constructor(peerConnection, peerConnectionControllerNegotiating$, options = {}) {
4323
4384
  super();
@@ -4325,23 +4386,23 @@ var ICEGatheringController = class extends Destroyable {
4325
4386
  this.peerConnectionControllerNegotiating$ = peerConnectionControllerNegotiating$;
4326
4387
  this.onicegatheringstatechangeHandler = () => {
4327
4388
  const { iceGatheringState } = this.peerConnection;
4328
- logger$18.debug(`[ICEGatheringController] ICE gathering state changed to: ${iceGatheringState}`);
4389
+ logger$20.debug(`[ICEGatheringController] ICE gathering state changed to: ${iceGatheringState}`);
4329
4390
  if (iceGatheringState === "gathering") this._iceCandidatesState.next({
4330
4391
  state: "gathering",
4331
4392
  validSDP: false
4332
4393
  });
4333
4394
  };
4334
4395
  this.onicecandidateHandler = (event) => {
4335
- logger$18.debug("[ICEGatheringController] ICE candidate event received:", event.candidate);
4396
+ logger$20.debug("[ICEGatheringController] ICE candidate event received:", event.candidate);
4336
4397
  this.removeTimer("iceCandidateTimer");
4337
4398
  if (event.candidate) this.iceCandidateTimer = setTimeout(() => {
4338
4399
  if (this.peerConnection.iceGatheringState !== "complete") {
4339
- logger$18.warn("[ICEGatheringController] ICE candidate timeout, using current SDP");
4400
+ logger$20.warn("[ICEGatheringController] ICE candidate timeout, using current SDP");
4340
4401
  this.handleICECandidateTimeout();
4341
4402
  }
4342
4403
  }, this.iceCandidateTimeout);
4343
4404
  else {
4344
- logger$18.debug("[ICEGatheringController] ICE gathering completed: null candidate received");
4405
+ logger$20.debug("[ICEGatheringController] ICE gathering completed: null candidate received");
4345
4406
  this.removeTimer("iceGatheringTimer");
4346
4407
  this.handleICEGatheringComplete();
4347
4408
  }
@@ -4359,7 +4420,7 @@ var ICEGatheringController = class extends Destroyable {
4359
4420
  this.setupEventListeners();
4360
4421
  this.iceGatheringTimer = setTimeout(() => {
4361
4422
  if (this.peerConnection.iceGatheringState !== "complete") {
4362
- logger$18.warn("[ICEGatheringController] ICE gathering timeout, using current SDP");
4423
+ logger$20.warn("[ICEGatheringController] ICE gathering timeout, using current SDP");
4363
4424
  this.handleICEGatheringTimeout();
4364
4425
  }
4365
4426
  }, this.iceGatheringTimeout);
@@ -4386,9 +4447,9 @@ var ICEGatheringController = class extends Destroyable {
4386
4447
  this.relayOnly = value;
4387
4448
  }
4388
4449
  handleICEGatheringComplete() {
4389
- logger$18.debug("[ICEGatheringController] Handling ICE gathering complete");
4390
- logger$18.debug(`[ICEGatheringController] Checking ICE gathering state: ${this.peerConnection.iceGatheringState}`);
4391
- logger$18.debug("[ICEGatheringController] ICE gathering complete");
4450
+ logger$20.debug("[ICEGatheringController] Handling ICE gathering complete");
4451
+ logger$20.debug(`[ICEGatheringController] Checking ICE gathering state: ${this.peerConnection.iceGatheringState}`);
4452
+ logger$20.debug("[ICEGatheringController] ICE gathering complete");
4392
4453
  this._iceCandidatesState.next({
4393
4454
  state: "complete",
4394
4455
  validSDP: this.hasValidLocalDescriptionSDP
@@ -4404,21 +4465,21 @@ var ICEGatheringController = class extends Destroyable {
4404
4465
  this.removeTimer("iceGatheringTimer");
4405
4466
  const validSDP = this.hasValidLocalDescriptionSDP;
4406
4467
  if (validSDP) {
4407
- logger$18.debug("[ICEGatheringController] Local SDP is valid");
4468
+ logger$20.debug("[ICEGatheringController] Local SDP is valid");
4408
4469
  this._iceCandidatesState.next({
4409
4470
  state: "timeout",
4410
4471
  validSDP
4411
4472
  });
4412
4473
  this.stopGathering();
4413
- } else logger$18.debug("### ICE gathering timeout\n", this.peerConnection.localDescription?.sdp);
4474
+ } else logger$20.debug("### ICE gathering timeout\n", this.peerConnection.localDescription?.sdp);
4414
4475
  }
4415
4476
  handleICECandidateTimeout() {
4416
4477
  if (this.iceCandidateTimer) this.removeTimer("iceCandidateTimer");
4417
- logger$18.warn("[ICEGatheringController] ICE candidate timeout");
4478
+ logger$20.warn("[ICEGatheringController] ICE candidate timeout");
4418
4479
  const validSDP = this.hasValidLocalDescriptionSDP;
4419
4480
  if (!validSDP && !this.relayOnly) this.restartICEGatheringWithRelayOnly();
4420
4481
  else {
4421
- logger$18.debug("[ICEGatheringController] Using current SDP due to ICE candidate timeout");
4482
+ logger$20.debug("[ICEGatheringController] Using current SDP due to ICE candidate timeout");
4422
4483
  this._iceCandidatesState.next({
4423
4484
  state: "timeout",
4424
4485
  validSDP
@@ -4427,7 +4488,7 @@ var ICEGatheringController = class extends Destroyable {
4427
4488
  }
4428
4489
  }
4429
4490
  restartICEGatheringWithRelayOnly() {
4430
- logger$18.debug("[ICEGatheringController] Restarting ICE gathering with relay-only candidates");
4491
+ logger$20.debug("[ICEGatheringController] Restarting ICE gathering with relay-only candidates");
4431
4492
  this.relayOnly = true;
4432
4493
  this.peerConnection.setConfiguration({
4433
4494
  ...this.peerConnection.getConfiguration(),
@@ -4442,7 +4503,7 @@ var ICEGatheringController = class extends Destroyable {
4442
4503
  }
4443
4504
  }
4444
4505
  clearAllTimers() {
4445
- logger$18.debug("[ICEGatheringController] Clearing all timers");
4506
+ logger$20.debug("[ICEGatheringController] Clearing all timers");
4446
4507
  this.removeTimer("iceGatheringTimer");
4447
4508
  this.removeTimer("iceCandidateTimer");
4448
4509
  }
@@ -4451,16 +4512,179 @@ var ICEGatheringController = class extends Destroyable {
4451
4512
  this.peerConnection.removeEventListener("icecandidate", this.onicecandidateHandler);
4452
4513
  }
4453
4514
  destroy() {
4454
- logger$18.debug("[ICEGatheringController] Destroying ICEGatheringController");
4515
+ logger$20.debug("[ICEGatheringController] Destroying ICEGatheringController");
4455
4516
  this.clearAllTimers();
4456
4517
  this.removeEventListeners();
4457
4518
  super.destroy();
4458
4519
  }
4459
4520
  };
4460
4521
 
4522
+ //#endregion
4523
+ //#region src/controllers/LocalAudioPipeline.ts
4524
+ const logger$19 = require_operators.getLogger();
4525
+ /**
4526
+ * Web Audio pipeline for the local microphone stream.
4527
+ *
4528
+ * Wraps the raw mic `MediaStreamTrack` in a graph of:
4529
+ *
4530
+ * ```
4531
+ * MediaStreamAudioSourceNode → GainNode → AnalyserNode → MediaStreamAudioDestinationNode
4532
+ * ```
4533
+ *
4534
+ * The {@link outputTrack} from the destination node is what callers should
4535
+ * attach to the `RTCRtpSender` in place of the raw mic track. The same
4536
+ * destination track is reused across input changes (device switch, mute /
4537
+ * unmute track replacement) so the sender reference stays stable — only the
4538
+ * source end of the graph is rebuilt.
4539
+ *
4540
+ * The pipeline owns a single {@link AudioContext}. Callers must invoke
4541
+ * {@link destroy} to release it when the call ends.
4542
+ */
4543
+ var LocalAudioPipeline = class extends Destroyable {
4544
+ constructor(options = {}) {
4545
+ super();
4546
+ this._inputSource = null;
4547
+ this._inputStream = null;
4548
+ this._lastSpokeAt = 0;
4549
+ this._gain$ = this.createBehaviorSubject(1);
4550
+ this._pttMultiplier = 1;
4551
+ this._audioContext = (options.audioContextFactory ?? (() => new AudioContext()))();
4552
+ this._gainNode = this._audioContext.createGain();
4553
+ this._analyser = this._audioContext.createAnalyser();
4554
+ this._analyser.fftSize = 2048;
4555
+ this._analyser.smoothingTimeConstant = .3;
4556
+ this._analyserBuffer = new Uint8Array(new ArrayBuffer(this._analyser.fftSize));
4557
+ this._destination = this._audioContext.createMediaStreamDestination();
4558
+ this._gainNode.connect(this._analyser);
4559
+ this._analyser.connect(this._destination);
4560
+ this._speakingThreshold = options.speakingThreshold ?? VAD_THRESHOLD;
4561
+ this._speakingHoldMs = options.speakingHoldMs ?? VAD_HOLD_MS;
4562
+ this._pollIntervalMs = options.pollIntervalMs ?? AUDIO_LEVEL_POLL_INTERVAL_MS;
4563
+ const initial = options.initialGain ?? 1;
4564
+ this._gain$.next(initial);
4565
+ this.applyEffectiveGain();
4566
+ }
4567
+ /** Observable of the current gain value (0..2). */
4568
+ get gain$() {
4569
+ return this._gain$.asObservable();
4570
+ }
4571
+ /** Current gain value (0..2). */
4572
+ get gain() {
4573
+ return this._gain$.value;
4574
+ }
4575
+ /**
4576
+ * Processed output track to attach to the RTCRtpSender. Stable reference
4577
+ * across input changes, so `sender.replaceTrack(pipeline.outputTrack)` only
4578
+ * needs to be called once.
4579
+ */
4580
+ get outputTrack() {
4581
+ const [track] = this._destination.stream.getAudioTracks();
4582
+ return track;
4583
+ }
4584
+ /**
4585
+ * Root-mean-square audio level of the input signal, 0..1. Emits on a fixed
4586
+ * interval (~30fps by default).
4587
+ */
4588
+ get level$() {
4589
+ return this.deferEmission((0, rxjs.interval)(this._pollIntervalMs, rxjs.animationFrameScheduler).pipe((0, rxjs.map)(() => this.computeLevel())));
4590
+ }
4591
+ /**
4592
+ * Boolean VAD derived from {@link level$}. True while level ≥ threshold or
4593
+ * during the hold window after the last frame that crossed the threshold.
4594
+ */
4595
+ get speaking$() {
4596
+ return this.deferEmission(this.level$.pipe((0, rxjs.map)((level) => this.evaluateSpeaking(level)), (0, rxjs.distinctUntilChanged)()));
4597
+ }
4598
+ /**
4599
+ * Set gain multiplier applied to the input signal. 0 = silence,
4600
+ * 1 = unity, 2 = 2x. Values are clamped to [0, 2]. The effective gain on
4601
+ * the graph also respects the current PTT state.
4602
+ */
4603
+ setGain(value) {
4604
+ const clamped = Math.max(0, Math.min(2, value));
4605
+ this._gain$.next(clamped);
4606
+ this.applyEffectiveGain();
4607
+ }
4608
+ /**
4609
+ * Silence the graph when `active = false`, otherwise restore the configured
4610
+ * gain. Use this from a PTT handler: released → `false`, held → `true`.
4611
+ * Orthogonal to {@link setGain} — once PTT returns to active, the last
4612
+ * configured gain reappears.
4613
+ */
4614
+ setPTTActive(active) {
4615
+ this._pttMultiplier = active ? 1 : 0;
4616
+ this.applyEffectiveGain();
4617
+ }
4618
+ applyEffectiveGain() {
4619
+ this._gainNode.gain.value = this._gain$.value * this._pttMultiplier;
4620
+ }
4621
+ /**
4622
+ * Wire a new raw mic track as the pipeline's input. Replaces any previous
4623
+ * input source and reconnects the graph so {@link outputTrack} continues
4624
+ * to emit the processed audio. Pass `null` to disconnect the input (the
4625
+ * output track stays alive but emits silence).
4626
+ *
4627
+ * Also resumes the underlying AudioContext on attach — Chrome creates it
4628
+ * in a suspended state and the graph won't process (the destination
4629
+ * track emits silence) until resume() succeeds.
4630
+ */
4631
+ setInputTrack(track) {
4632
+ if (this._inputSource) {
4633
+ try {
4634
+ this._inputSource.disconnect();
4635
+ } catch (error) {
4636
+ logger$19.debug("[LocalAudioPipeline] input disconnect warning:", error);
4637
+ }
4638
+ this._inputSource = null;
4639
+ }
4640
+ if (this._inputStream) this._inputStream = null;
4641
+ if (!track) return;
4642
+ this._inputStream = new MediaStream([track]);
4643
+ this._inputSource = this._audioContext.createMediaStreamSource(this._inputStream);
4644
+ this._inputSource.connect(this._gainNode);
4645
+ if (this._audioContext.state === "suspended") this._audioContext.resume().catch((error) => {
4646
+ logger$19.warn("[LocalAudioPipeline] AudioContext resume failed:", error);
4647
+ });
4648
+ }
4649
+ destroy() {
4650
+ if (this._inputSource) {
4651
+ try {
4652
+ this._inputSource.disconnect();
4653
+ } catch {}
4654
+ this._inputSource = null;
4655
+ }
4656
+ try {
4657
+ this._gainNode.disconnect();
4658
+ this._analyser.disconnect();
4659
+ } catch {}
4660
+ this._audioContext.close().catch((error) => {
4661
+ logger$19.debug("[LocalAudioPipeline] audio context close warning:", error);
4662
+ });
4663
+ super.destroy();
4664
+ }
4665
+ computeLevel() {
4666
+ if (!this._inputSource) return 0;
4667
+ this._analyser.getByteTimeDomainData(this._analyserBuffer);
4668
+ let sum = 0;
4669
+ for (const sample of this._analyserBuffer) {
4670
+ const normalized = (sample - 128) / 128;
4671
+ sum += normalized * normalized;
4672
+ }
4673
+ return Math.sqrt(sum / this._analyserBuffer.length);
4674
+ }
4675
+ evaluateSpeaking(level) {
4676
+ const now = Date.now();
4677
+ if (level >= this._speakingThreshold) {
4678
+ this._lastSpokeAt = now;
4679
+ return true;
4680
+ }
4681
+ return now - this._lastSpokeAt < this._speakingHoldMs;
4682
+ }
4683
+ };
4684
+
4461
4685
  //#endregion
4462
4686
  //#region src/controllers/LocalStreamController.ts
4463
- const logger$17 = require_operators.getLogger();
4687
+ const logger$18 = require_operators.getLogger();
4464
4688
  var LocalStreamController = class extends Destroyable {
4465
4689
  constructor(options) {
4466
4690
  super();
@@ -4498,26 +4722,26 @@ var LocalStreamController = class extends Destroyable {
4498
4722
  * Build the local media stream based on the provided options.
4499
4723
  */
4500
4724
  async buildLocalStream() {
4501
- logger$17.debug("[LocalStreamController] Building local media stream.");
4725
+ logger$18.debug("[LocalStreamController] Building local media stream.");
4502
4726
  let stream;
4503
4727
  if (this.options.inputAudioStream ?? this.options.inputVideoStream) {
4504
4728
  const tracks = [...this.options.inputAudioStream?.getTracks() ?? [], ...this.options.inputVideoStream?.getTracks() ?? []];
4505
4729
  stream = new MediaStream(tracks);
4506
4730
  } else if (this.options.propose === "screenshare") {
4507
- logger$17.debug("[LocalStreamController] Requesting display media for screen sharing with audio:", Boolean(this.options.inputAudioDeviceConstraints));
4731
+ logger$18.debug("[LocalStreamController] Requesting display media for screen sharing with audio:", Boolean(this.options.inputAudioDeviceConstraints));
4508
4732
  stream = await this.options.getDisplayMedia({
4509
4733
  video: true,
4510
4734
  audio: Boolean(this.options.inputAudioDeviceConstraints)
4511
4735
  });
4512
- logger$17.debug("[LocalStreamController] Screen share media obtained:", stream);
4736
+ logger$18.debug("[LocalStreamController] Screen share media obtained:", stream);
4513
4737
  } else {
4514
4738
  const constraints = {
4515
4739
  audio: this.options.inputAudioDeviceConstraints,
4516
4740
  video: this.options.inputVideoDeviceConstraints
4517
4741
  };
4518
- logger$17.debug("[LocalStreamController] Requesting user media with constraints:", constraints);
4742
+ logger$18.debug("[LocalStreamController] Requesting user media with constraints:", constraints);
4519
4743
  stream = await this.options.getUserMedia(constraints);
4520
- logger$17.debug("[LocalStreamController] User media obtained:", stream);
4744
+ logger$18.debug("[LocalStreamController] User media obtained:", stream);
4521
4745
  }
4522
4746
  this._localStream$.next(stream);
4523
4747
  return stream;
@@ -4534,7 +4758,7 @@ var LocalStreamController = class extends Destroyable {
4534
4758
  this._localStream$.next(localStream);
4535
4759
  if (track.kind === "video") this._localVideoTracks$.next(localStream.getVideoTracks());
4536
4760
  else this._localAudioTracks$.next(localStream.getAudioTracks());
4537
- logger$17.debug(`[LocalStreamController] ${track.kind} track added:`, track.id);
4761
+ logger$18.debug(`[LocalStreamController] ${track.kind} track added:`, track.id);
4538
4762
  return localStream;
4539
4763
  }
4540
4764
  /**
@@ -4546,7 +4770,7 @@ var LocalStreamController = class extends Destroyable {
4546
4770
  const stream = this._localStream$.value;
4547
4771
  const track = stream?.getTracks().find((t) => t.id === trackId);
4548
4772
  if (!track) {
4549
- logger$17.debug(`[LocalStreamController] track not found: ${trackId}`);
4773
+ logger$18.debug(`[LocalStreamController] track not found: ${trackId}`);
4550
4774
  return;
4551
4775
  }
4552
4776
  track.removeEventListener("ended", this.mediaTrackEndedHandler);
@@ -4555,7 +4779,7 @@ var LocalStreamController = class extends Destroyable {
4555
4779
  this._localStream$.next(stream);
4556
4780
  if (track.kind === "video") this._localVideoTracks$.next(stream?.getVideoTracks() ?? []);
4557
4781
  else this._localAudioTracks$.next(stream?.getAudioTracks() ?? []);
4558
- logger$17.debug(`[LocalStreamController] ${track.kind} track removed:`, trackId);
4782
+ logger$18.debug(`[LocalStreamController] ${track.kind} track removed:`, trackId);
4559
4783
  return track;
4560
4784
  }
4561
4785
  /**
@@ -4590,7 +4814,7 @@ var LocalStreamController = class extends Destroyable {
4590
4814
  */
4591
4815
  stopAllTracks() {
4592
4816
  this._localStream$.value?.getTracks().forEach((track) => {
4593
- logger$17.debug(`[LocalStreamController] Stopping local track: ${track.kind}`);
4817
+ logger$18.debug(`[LocalStreamController] Stopping local track: ${track.kind}`);
4594
4818
  track.removeEventListener("ended", this.mediaTrackEndedHandler);
4595
4819
  track.stop();
4596
4820
  });
@@ -4606,7 +4830,7 @@ var LocalStreamController = class extends Destroyable {
4606
4830
 
4607
4831
  //#endregion
4608
4832
  //#region src/controllers/TransceiverController.ts
4609
- const logger$16 = require_operators.getLogger();
4833
+ const logger$17 = require_operators.getLogger();
4610
4834
  const getDirection = (send, recv) => {
4611
4835
  if (send && recv) return "sendrecv";
4612
4836
  else if (send && !recv) return "sendonly";
@@ -4708,7 +4932,7 @@ var TransceiverController = class extends Destroyable {
4708
4932
  sendEncodings: isAudio ? void 0 : this.sendEncodings,
4709
4933
  streams: direction === "recvonly" ? void 0 : [localStream]
4710
4934
  };
4711
- logger$16.debug(`[TransceiverController] Setting up transceiver sender for local ${track.kind} track:`, {
4935
+ logger$17.debug(`[TransceiverController] Setting up transceiver sender for local ${track.kind} track:`, {
4712
4936
  transceiver,
4713
4937
  transceiverParams
4714
4938
  });
@@ -4716,11 +4940,11 @@ var TransceiverController = class extends Destroyable {
4716
4940
  await transceiver.sender.replaceTrack(track);
4717
4941
  transceiver.direction = transceiverParams.direction;
4718
4942
  if (transceiverParams.streams?.some((stream) => Boolean(stream))) {
4719
- logger$16.debug(`[TransceiverController] Setting streams for transceiver sender for local ${track.kind} track:`, transceiverParams.streams);
4943
+ logger$17.debug(`[TransceiverController] Setting streams for transceiver sender for local ${track.kind} track:`, transceiverParams.streams);
4720
4944
  transceiver.sender.setStreams(...transceiverParams.streams);
4721
4945
  }
4722
4946
  } else {
4723
- logger$16.debug(`[TransceiverController] Adding new transceiver for local ${track.kind} track:`, track.id);
4947
+ logger$17.debug(`[TransceiverController] Adding new transceiver for local ${track.kind} track:`, track.id);
4724
4948
  this.peerConnection.addTransceiver(track, transceiverParams);
4725
4949
  }
4726
4950
  }
@@ -4734,13 +4958,13 @@ var TransceiverController = class extends Destroyable {
4734
4958
  if (options.updateTransceiverDirection) transceiver.direction = "inactive";
4735
4959
  }
4736
4960
  } catch (error) {
4737
- logger$16.error("[TransceiverController] stopTrackSender error", kind, error);
4961
+ logger$17.error("[TransceiverController] stopTrackSender error", kind, error);
4738
4962
  this.options.onError?.(new require_operators.MediaTrackError("stopTrackSender", kind, error));
4739
4963
  }
4740
4964
  }
4741
4965
  async restoreTrackSender(kind) {
4742
4966
  try {
4743
- logger$16.debug("[TransceiverController] restoreTrackSender called", kind);
4967
+ logger$17.debug("[TransceiverController] restoreTrackSender called", kind);
4744
4968
  const constraints = {};
4745
4969
  const transceivers = this.transceiverByKind(kind);
4746
4970
  for (const transceiver of transceivers) {
@@ -4750,23 +4974,23 @@ var TransceiverController = class extends Destroyable {
4750
4974
  if (trackKind === "audio" || trackKind === "video") constraints[trackKind] = this.getConstraintsFor(trackKind);
4751
4975
  }
4752
4976
  }
4753
- logger$16.debug("[TransceiverController] restoreTrackSender constraints:", constraints);
4977
+ logger$17.debug("[TransceiverController] restoreTrackSender constraints:", constraints);
4754
4978
  if (Object.keys(constraints).length === 0) {
4755
- logger$16.warn("[TransceiverController] restoreTrackSender: no tracks need restoration", kind);
4979
+ logger$17.warn("[TransceiverController] restoreTrackSender: no tracks need restoration", kind);
4756
4980
  return;
4757
4981
  }
4758
4982
  const newTracks = (await this.options.getUserMedia(constraints)).getTracks();
4759
- logger$16.debug("[TransceiverController] restoreTrackSender new tracks:", newTracks);
4983
+ logger$17.debug("[TransceiverController] restoreTrackSender new tracks:", newTracks);
4760
4984
  for (const newTrack of newTracks) {
4761
4985
  this.options.localStreamController.addTrack(newTrack);
4762
4986
  const trackKind = newTrack.kind;
4763
4987
  const transceiverOfKind = this.transceiverByKind(trackKind)[0];
4764
4988
  transceiverOfKind.direction = trackKind === "audio" ? this.audioDirection : this.videoDirection;
4765
- logger$16.debug("[TransceiverController] restoreTrackSender setting direction for", trackKind, transceiverOfKind.direction);
4989
+ logger$17.debug("[TransceiverController] restoreTrackSender setting direction for", trackKind, transceiverOfKind.direction);
4766
4990
  await transceiverOfKind.sender.replaceTrack(newTrack);
4767
4991
  }
4768
4992
  } catch (error) {
4769
- logger$16.error("[TransceiverController] restoreTrackSender error", kind, error);
4993
+ logger$17.error("[TransceiverController] restoreTrackSender error", kind, error);
4770
4994
  this.options.onError?.(new require_operators.MediaTrackError("restoreTrackSender", kind, error));
4771
4995
  }
4772
4996
  }
@@ -4807,14 +5031,14 @@ var TransceiverController = class extends Destroyable {
4807
5031
  };
4808
5032
  try {
4809
5033
  await track.applyConstraints(constraintsToApply);
4810
- logger$16.debug(`[TransceiverController] Updated ${kind} sender constraints:`, constraintsToApply);
4811
- logger$16.debug(`[TransceiverController] Updated ${kind} sender constraints:`, track.getConstraints());
5034
+ logger$17.debug(`[TransceiverController] Updated ${kind} sender constraints:`, constraintsToApply);
5035
+ logger$17.debug(`[TransceiverController] Updated ${kind} sender constraints:`, track.getConstraints());
4812
5036
  } catch (error) {
4813
- logger$16.warn(`[TransceiverController] applyConstraints failed for ${kind} track ${track.id}, attempting track replacement fallback:`, error);
5037
+ logger$17.warn(`[TransceiverController] applyConstraints failed for ${kind} track ${track.id}, attempting track replacement fallback:`, error);
4814
5038
  try {
4815
5039
  await this.replaceTrackFallback(sender, track, kind, constraintsToApply);
4816
5040
  } catch (fallbackError) {
4817
- logger$16.warn(`[TransceiverController] Track replacement fallback also failed for ${kind} track:`, fallbackError);
5041
+ logger$17.warn(`[TransceiverController] Track replacement fallback also failed for ${kind} track:`, fallbackError);
4818
5042
  this.options.onError?.(new require_operators.MediaTrackError("updateSendersConstraints", kind, fallbackError));
4819
5043
  }
4820
5044
  }
@@ -4842,7 +5066,7 @@ var TransceiverController = class extends Destroyable {
4842
5066
  if (!newTrack) throw new require_operators.MediaTrackError("replaceTrackFallback", kind, /* @__PURE__ */ new Error("getUserMedia returned no track of the requested kind"));
4843
5067
  await sender.replaceTrack(newTrack);
4844
5068
  this.options.localStreamController.addTrack(newTrack);
4845
- logger$16.debug(`[TransceiverController] Track replacement fallback succeeded for ${kind}. New track: ${newTrack.id}`);
5069
+ logger$17.debug(`[TransceiverController] Track replacement fallback succeeded for ${kind}. New track: ${newTrack.id}`);
4846
5070
  }
4847
5071
  getMediaDirections() {
4848
5072
  if (this.peerConnection.connectionState === "connected") return this.peerConnection.getTransceivers().reduce((acc, transceiver) => {
@@ -4872,7 +5096,7 @@ var TransceiverController = class extends Destroyable {
4872
5096
 
4873
5097
  //#endregion
4874
5098
  //#region src/controllers/RTCPeerConnectionController.ts
4875
- const logger$15 = require_operators.getLogger();
5099
+ const logger$16 = require_operators.getLogger();
4876
5100
  var RTCPeerConnectionController = class extends Destroyable {
4877
5101
  constructor(options = {}, remoteSessionDescription, deviceController) {
4878
5102
  super();
@@ -4888,43 +5112,43 @@ var RTCPeerConnectionController = class extends Destroyable {
4888
5112
  this.oniceconnectionstatechangeHandler = () => {
4889
5113
  if (this.peerConnection) {
4890
5114
  const { iceConnectionState } = this.peerConnection;
4891
- logger$15.debug(`[RTCPeerConnectionController] ICE connection state changed to: ${iceConnectionState}`);
5115
+ logger$16.debug(`[RTCPeerConnectionController] ICE connection state changed to: ${iceConnectionState}`);
4892
5116
  this._iceConnectionState$.next(this.peerConnection.iceConnectionState);
4893
5117
  }
4894
5118
  };
4895
5119
  this.onconnectionstatechangeHandler = () => {
4896
5120
  if (this.peerConnection) {
4897
5121
  const { connectionState } = this.peerConnection;
4898
- logger$15.debug(`[RTCPeerConnectionController] Connection state changed to: ${connectionState}`);
5122
+ logger$16.debug(`[RTCPeerConnectionController] Connection state changed to: ${connectionState}`);
4899
5123
  if (connectionState === "connected") this.removeConnectionTimer();
4900
5124
  this._connectionState$.next(this.peerConnection.connectionState);
4901
5125
  }
4902
5126
  };
4903
5127
  this.onsignalingstatechangeHandler = () => {
4904
- logger$15.debug(`[RTCPeerConnectionController] Signaling state changed to: ${this.peerConnection?.signalingState}`);
5128
+ logger$16.debug(`[RTCPeerConnectionController] Signaling state changed to: ${this.peerConnection?.signalingState}`);
4905
5129
  };
4906
5130
  this.onicegatheringstatechangeHandler = () => {
4907
5131
  if (this.peerConnection) this._iceGatheringState$.next(this.peerConnection.iceGatheringState);
4908
5132
  };
4909
5133
  this.onnegotiationneededHandler = (event) => {
4910
- logger$15.debug("[RTCPeerConnectionController] Negotiation needed event received.", event);
5134
+ logger$16.debug("[RTCPeerConnectionController] Negotiation needed event received.", event);
4911
5135
  this.negotiationNeeded$.next();
4912
5136
  };
4913
5137
  this.updateSelectedInputDevice = async (kind, deviceInfo) => {
4914
5138
  try {
4915
5139
  const { localStream } = this;
4916
5140
  if (!localStream) {
4917
- logger$15.warn("[RTCPeerConnectionController] No local stream available to update input device.");
5141
+ logger$16.warn("[RTCPeerConnectionController] No local stream available to update input device.");
4918
5142
  return;
4919
5143
  }
4920
- logger$15.debug(`[RTCPeerConnectionController] Updating selected ${kind} input device:`, localStream.getTracks());
5144
+ logger$16.debug(`[RTCPeerConnectionController] Updating selected ${kind} input device:`, localStream.getTracks());
4921
5145
  const track = localStream.getTracks().find((track$1) => track$1.kind === kind);
4922
5146
  if (track) {
4923
5147
  this.transceiverController?.stopTrackSender(kind);
4924
5148
  this.localStream?.removeTrack(track);
4925
- logger$15.debug(`[RTCPeerConnectionController] Stopped existing ${kind} track: ${track.id}`, localStream.getTracks());
5149
+ logger$16.debug(`[RTCPeerConnectionController] Stopped existing ${kind} track: ${track.id}`, localStream.getTracks());
4926
5150
  if (!deviceInfo) {
4927
- logger$15.debug(`[RTCPeerConnectionController] ${kind} input device selected: none`);
5151
+ logger$16.debug(`[RTCPeerConnectionController] ${kind} input device selected: none`);
4928
5152
  return;
4929
5153
  }
4930
5154
  const streamTrack = (await this.getUserMedia({ [kind]: {
@@ -4932,15 +5156,15 @@ var RTCPeerConnectionController = class extends Destroyable {
4932
5156
  ...this.deviceController.deviceInfoToConstraints(deviceInfo)
4933
5157
  } })).getTracks().find((t) => t.kind === kind);
4934
5158
  if (streamTrack) {
4935
- logger$15.debug(`[RTCPeerConnectionController] Adding new ${kind} track: ${streamTrack.id}`);
5159
+ logger$16.debug(`[RTCPeerConnectionController] Adding new ${kind} track: ${streamTrack.id}`);
4936
5160
  this.localStream?.addTrack(streamTrack);
4937
5161
  await this.transceiverController?.replaceSenderTrack(kind, streamTrack);
4938
- logger$15.debug(`[RTCPeerConnectionController] Added new ${kind} track: ${streamTrack.id}`, this.localStream?.getTracks());
5162
+ logger$16.debug(`[RTCPeerConnectionController] Added new ${kind} track: ${streamTrack.id}`, this.localStream?.getTracks());
4939
5163
  }
4940
5164
  }
4941
- logger$15.debug(`[RTCPeerConnectionController] ${kind} input device selected:`, deviceInfo?.label);
5165
+ logger$16.debug(`[RTCPeerConnectionController] ${kind} input device selected:`, deviceInfo?.label);
4942
5166
  } catch (error) {
4943
- logger$15.error(`[RTCPeerConnectionController] Failed to select ${kind} input device:`, error);
5167
+ logger$16.error(`[RTCPeerConnectionController] Failed to select ${kind} input device:`, error);
4944
5168
  this._errors$.next(toError(error));
4945
5169
  throw error;
4946
5170
  }
@@ -4957,6 +5181,7 @@ var RTCPeerConnectionController = class extends Destroyable {
4957
5181
  this._remoteDescription$ = this.createReplaySubject(1);
4958
5182
  this._remoteStream$ = this.createBehaviorSubject(null);
4959
5183
  this._remoteOfferMediaDirections = null;
5184
+ this._localAudioPipeline = null;
4960
5185
  this.deviceController = deviceController ?? {};
4961
5186
  this.id = options.callId ?? (0, uuid.v4)();
4962
5187
  this._type = remoteSessionDescription ? "answer" : "offer";
@@ -5177,15 +5402,15 @@ var RTCPeerConnectionController = class extends Destroyable {
5177
5402
  this.setupPeerConnection();
5178
5403
  this.subscribeTo(this.negotiationNeeded$.pipe((0, rxjs.auditTime)(0), (0, rxjs.exhaustMap)(async () => this.startNegotiation())), {
5179
5404
  next: () => {
5180
- logger$15.debug("[RTCPeerConnectionController] Start Negotiation completed successfully");
5405
+ logger$16.debug("[RTCPeerConnectionController] Start Negotiation completed successfully");
5181
5406
  },
5182
5407
  error: (error) => {
5183
- logger$15.error("[RTCPeerConnectionController] Start Negotiation error:", error);
5408
+ logger$16.error("[RTCPeerConnectionController] Start Negotiation error:", error);
5184
5409
  this._errors$.next(toError(error));
5185
5410
  }
5186
5411
  });
5187
5412
  this.subscribeTo((0, rxjs.merge)(this.deviceController.selectedAudioInputDevice$.pipe((0, rxjs.map)((deviceInfo) => ["audio", deviceInfo])), this.deviceController.selectedVideoInputDevice$.pipe((0, rxjs.map)((deviceInfo) => ["video", deviceInfo]))).pipe((0, rxjs.skipWhile)(() => !this.localStreamController.localStream)), async ([kind, deviceInfo]) => {
5188
- logger$15.debug(`[RTCPeerConnectionController] Selected input device changed for:`, {
5413
+ logger$16.debug(`[RTCPeerConnectionController] Selected input device changed for:`, {
5189
5414
  kind,
5190
5415
  deviceInfo
5191
5416
  });
@@ -5202,7 +5427,7 @@ var RTCPeerConnectionController = class extends Destroyable {
5202
5427
  this._initialized$.next(true);
5203
5428
  }
5204
5429
  } catch (error) {
5205
- logger$15.error("[RTCPeerConnectionController] Initialization error:", error);
5430
+ logger$16.error("[RTCPeerConnectionController] Initialization error:", error);
5206
5431
  this._errors$.next(toError(error));
5207
5432
  this.destroy();
5208
5433
  }
@@ -5234,22 +5459,22 @@ var RTCPeerConnectionController = class extends Destroyable {
5234
5459
  }
5235
5460
  async startNegotiation() {
5236
5461
  if (this.isNegotiating) {
5237
- logger$15.debug("[RTCPeerConnectionController] Negotiation already in progress, skipping.");
5462
+ logger$16.debug("[RTCPeerConnectionController] Negotiation already in progress, skipping.");
5238
5463
  return;
5239
5464
  }
5240
5465
  this.setupEventListeners();
5241
5466
  if (this.type === "answer") {
5242
- logger$15.debug("[RTCPeerConnectionController] This is an answer type still, skipping offer creation.");
5467
+ logger$16.debug("[RTCPeerConnectionController] This is an answer type still, skipping offer creation.");
5243
5468
  return;
5244
5469
  }
5245
5470
  this._isNegotiating$.next(true);
5246
- logger$15.debug("[RTCPeerConnectionController] Starting negotiation.");
5471
+ logger$16.debug("[RTCPeerConnectionController] Starting negotiation.");
5247
5472
  try {
5248
5473
  const { offerOptions } = this;
5249
- logger$15.debug("[RTCPeerConnectionController] Creating offer with options:", offerOptions);
5474
+ logger$16.debug("[RTCPeerConnectionController] Creating offer with options:", offerOptions);
5250
5475
  await this.createOffer(offerOptions);
5251
5476
  } catch (error) {
5252
- logger$15.error("[RTCPeerConnectionController] Error during negotiation:", error);
5477
+ logger$16.error("[RTCPeerConnectionController] Error during negotiation:", error);
5253
5478
  this._errors$.next(toError(error));
5254
5479
  }
5255
5480
  }
@@ -5265,14 +5490,14 @@ var RTCPeerConnectionController = class extends Destroyable {
5265
5490
  let readyToConnect = status !== "failed";
5266
5491
  try {
5267
5492
  if (status === "received" && sdp) {
5268
- logger$15.debug("[RTCPeerConnectionController] Received answer SDP:", sdp);
5493
+ logger$16.debug("[RTCPeerConnectionController] Received answer SDP:", sdp);
5269
5494
  await this._setRemoteDescription({
5270
5495
  type: "answer",
5271
5496
  sdp
5272
5497
  });
5273
5498
  }
5274
5499
  } catch (error) {
5275
- logger$15.error("[RTCPeerConnectionController] Error updating answer status:", error);
5500
+ logger$16.error("[RTCPeerConnectionController] Error updating answer status:", error);
5276
5501
  this._errors$.next(toError(error));
5277
5502
  readyToConnect = false;
5278
5503
  } finally {
@@ -5291,7 +5516,7 @@ var RTCPeerConnectionController = class extends Destroyable {
5291
5516
  await this.handleOfferReceived();
5292
5517
  break;
5293
5518
  case "failed":
5294
- logger$15.error("[RTCPeerConnectionController] Offer failed to be processed by remote.");
5519
+ logger$16.error("[RTCPeerConnectionController] Offer failed to be processed by remote.");
5295
5520
  break;
5296
5521
  case "sent":
5297
5522
  default:
@@ -5323,7 +5548,7 @@ var RTCPeerConnectionController = class extends Destroyable {
5323
5548
  }
5324
5549
  await this.setupLocalTracks();
5325
5550
  const { answerOptions } = this;
5326
- logger$15.debug("[RTCPeerConnectionController] Creating inbound answer with options:", answerOptions);
5551
+ logger$16.debug("[RTCPeerConnectionController] Creating inbound answer with options:", answerOptions);
5327
5552
  await this.createAnswer(answerOptions);
5328
5553
  }
5329
5554
  async handleOfferReceived() {
@@ -5331,7 +5556,7 @@ var RTCPeerConnectionController = class extends Destroyable {
5331
5556
  this._isNegotiating$.next(true);
5332
5557
  await this._setRemoteDescription(this.sdpInit);
5333
5558
  const { answerOptions } = this;
5334
- logger$15.debug("[RTCPeerConnectionController] Creating answer with options:", answerOptions);
5559
+ logger$16.debug("[RTCPeerConnectionController] Creating answer with options:", answerOptions);
5335
5560
  await this.createAnswer(answerOptions);
5336
5561
  }
5337
5562
  readyToConnect() {
@@ -5339,7 +5564,7 @@ var RTCPeerConnectionController = class extends Destroyable {
5339
5564
  this.connectionTimer = setTimeout(() => {
5340
5565
  this.removeConnectionTimer();
5341
5566
  if (this.peerConnection?.connectionState !== "connected") {
5342
- logger$15.debug("[RTCPeerConnectionController] Connection timeout, restarting ICE gathering with relay only.");
5567
+ logger$16.debug("[RTCPeerConnectionController] Connection timeout, restarting ICE gathering with relay only.");
5343
5568
  this.iceGatheringController.restartICEGatheringWithRelayOnly();
5344
5569
  }
5345
5570
  }, this.connectionTimeout);
@@ -5361,14 +5586,14 @@ var RTCPeerConnectionController = class extends Destroyable {
5361
5586
  const stereo = this.options.stereo ?? PreferencesContainer.instance.stereoAudio;
5362
5587
  if (preferredAudioCodecs.length > 0 || preferredVideoCodecs.length > 0) {
5363
5588
  result = setCodecPreferences(result, preferredAudioCodecs, preferredVideoCodecs);
5364
- logger$15.debug("[RTCPeerConnectionController] Applied codec preferences to SDP", {
5589
+ logger$16.debug("[RTCPeerConnectionController] Applied codec preferences to SDP", {
5365
5590
  preferredAudioCodecs,
5366
5591
  preferredVideoCodecs
5367
5592
  });
5368
5593
  }
5369
5594
  if (stereo) {
5370
5595
  result = enableStereoOpus(result);
5371
- logger$15.debug("[RTCPeerConnectionController] Applied stereo Opus to SDP");
5596
+ logger$16.debug("[RTCPeerConnectionController] Applied stereo Opus to SDP");
5372
5597
  }
5373
5598
  return Promise.resolve(result);
5374
5599
  }
@@ -5424,25 +5649,25 @@ var RTCPeerConnectionController = class extends Destroyable {
5424
5649
  ...this.peerConnection.getConfiguration(),
5425
5650
  iceTransportPolicy: "relay"
5426
5651
  });
5427
- logger$15.debug("[RTCPeerConnectionController] ICE transport policy set to relay-only");
5652
+ logger$16.debug("[RTCPeerConnectionController] ICE transport policy set to relay-only");
5428
5653
  } catch (error) {
5429
- logger$15.warn("[RTCPeerConnectionController] Failed to set relay-only policy:", error);
5654
+ logger$16.warn("[RTCPeerConnectionController] Failed to set relay-only policy:", error);
5430
5655
  }
5431
5656
  this.setupEventListeners();
5432
5657
  this._isNegotiating$.next(true);
5433
- logger$15.debug(`[RTCPeerConnectionController] Triggering ICE restart${relayOnly ? " (relay-only)" : ""}.`);
5658
+ logger$16.debug(`[RTCPeerConnectionController] Triggering ICE restart${relayOnly ? " (relay-only)" : ""}.`);
5434
5659
  try {
5435
5660
  const offer = await this.peerConnection.createOffer({ iceRestart: true });
5436
5661
  await this.setLocalDescription(offer);
5437
5662
  } catch (error) {
5438
- logger$15.error("[RTCPeerConnectionController] ICE restart offer failed:", error);
5663
+ logger$16.error("[RTCPeerConnectionController] ICE restart offer failed:", error);
5439
5664
  this._errors$.next(toError(error));
5440
5665
  this.negotiationEnded();
5441
5666
  if (policyChanged) this.restoreIceTransportPolicy();
5442
5667
  throw error;
5443
5668
  }
5444
5669
  if (policyChanged) (0, rxjs.firstValueFrom)((0, rxjs.race)(this._iceGatheringState$.pipe((0, rxjs.filter)((state) => state === "complete"), (0, rxjs.take)(1)), (0, rxjs.timer)(ICE_GATHERING_COMPLETE_TIMEOUT_MS).pipe((0, rxjs.map)(() => "timeout")))).then(() => this.restoreIceTransportPolicy()).catch((error) => {
5445
- logger$15.warn("[RTCPeerConnectionController] Error waiting for ICE gathering to complete:", error);
5670
+ logger$16.warn("[RTCPeerConnectionController] Error waiting for ICE gathering to complete:", error);
5446
5671
  this.restoreIceTransportPolicy();
5447
5672
  });
5448
5673
  }
@@ -5452,9 +5677,9 @@ var RTCPeerConnectionController = class extends Destroyable {
5452
5677
  ...this.peerConnection.getConfiguration(),
5453
5678
  iceTransportPolicy: this.options.relayOnly ? "relay" : "all"
5454
5679
  });
5455
- logger$15.debug("[RTCPeerConnectionController] ICE transport policy restored");
5680
+ logger$16.debug("[RTCPeerConnectionController] ICE transport policy restored");
5456
5681
  } catch (error) {
5457
- logger$15.warn("[RTCPeerConnectionController] Failed to restore ICE transport policy:", error);
5682
+ logger$16.warn("[RTCPeerConnectionController] Failed to restore ICE transport policy:", error);
5458
5683
  }
5459
5684
  }
5460
5685
  /**
@@ -5466,13 +5691,13 @@ var RTCPeerConnectionController = class extends Destroyable {
5466
5691
  await this.setupRemoteTracks();
5467
5692
  }
5468
5693
  async setupLocalTracks() {
5469
- logger$15.debug("[RTCPeerConnectionController] Setting up local tracks/transceivers.");
5694
+ logger$16.debug("[RTCPeerConnectionController] Setting up local tracks/transceivers.");
5470
5695
  const localStream = this.localStream ?? await this.localStreamController.buildLocalStream();
5471
5696
  if (this.transceiverController?.useAddStream ?? false) {
5472
- logger$15.warn("[RTCPeerConnectionController] Using deprecated addStream API to add local stream.");
5697
+ logger$16.warn("[RTCPeerConnectionController] Using deprecated addStream API to add local stream.");
5473
5698
  this.peerConnection?.addStream(localStream);
5474
5699
  if (!this.isNegotiating) {
5475
- logger$15.debug("[RTCPeerConnectionController] Forcing negotiationneeded after local tracks setup.");
5700
+ logger$16.debug("[RTCPeerConnectionController] Forcing negotiationneeded after local tracks setup.");
5476
5701
  this.negotiationNeeded$.next();
5477
5702
  }
5478
5703
  return;
@@ -5488,7 +5713,7 @@ var RTCPeerConnectionController = class extends Destroyable {
5488
5713
  const transceivers = (kind === "audio" ? this.transceiverController?.audioTransceivers : this.transceiverController?.videoTransceivers) ?? [];
5489
5714
  await this.transceiverController?.setupTransceiverSender(track, localStream, transceivers[index]);
5490
5715
  } else {
5491
- logger$15.debug(`[RTCPeerConnectionController] Using addTrack for local ${kind} track:`, track.id);
5716
+ logger$16.debug(`[RTCPeerConnectionController] Using addTrack for local ${kind} track:`, track.id);
5492
5717
  this.peerConnection?.addTrack(track, localStream);
5493
5718
  }
5494
5719
  }
@@ -5505,7 +5730,7 @@ var RTCPeerConnectionController = class extends Destroyable {
5505
5730
  async setupRemoteTracks() {
5506
5731
  if (!this.peerConnection) throw new require_operators.DependencyError("RTCPeerConnection is not initialized");
5507
5732
  this.peerConnection.ontrack = (event) => {
5508
- logger$15.debug("[RTCPeerConnectionController] Remote track received:", event.track.kind);
5733
+ logger$16.debug("[RTCPeerConnectionController] Remote track received:", event.track.kind);
5509
5734
  if (event.streams[0]) this._remoteStream$.next(event.streams[0]);
5510
5735
  else {
5511
5736
  const existingTracks = this._remoteStream$.value?.getTracks() ?? [];
@@ -5517,6 +5742,46 @@ var RTCPeerConnectionController = class extends Destroyable {
5517
5742
  }
5518
5743
  async restoreTrackSender(kind) {
5519
5744
  await this.transceiverController?.restoreTrackSender(kind);
5745
+ if (kind !== "video" && this._localAudioPipeline) await this.applyLocalAudioPipelineToSender();
5746
+ }
5747
+ /**
5748
+ * Return the lazily-created {@link LocalAudioPipeline}, constructing it on
5749
+ * first access. On creation the current audio sender's track is routed
5750
+ * through the pipeline (input → gain → analyser → destination) and the
5751
+ * sender is switched to emit the processed track. Returns `null` when no
5752
+ * audio sender exists yet (pre-negotiation).
5753
+ */
5754
+ ensureLocalAudioPipeline() {
5755
+ if (this._localAudioPipeline) return this._localAudioPipeline;
5756
+ if (!this.peerConnection) return null;
5757
+ try {
5758
+ this._localAudioPipeline = new LocalAudioPipeline();
5759
+ } catch (error) {
5760
+ logger$16.warn("[RTCPeerConnectionController] Failed to create LocalAudioPipeline:", error);
5761
+ return null;
5762
+ }
5763
+ this.subscribeTo(this.localStreamController.localAudioTracks$, () => {
5764
+ this.applyLocalAudioPipelineToSender();
5765
+ });
5766
+ this.applyLocalAudioPipelineToSender();
5767
+ return this._localAudioPipeline;
5768
+ }
5769
+ /** The active LocalAudioPipeline, or null if it hasn't been created yet. */
5770
+ get localAudioPipeline() {
5771
+ return this._localAudioPipeline;
5772
+ }
5773
+ async applyLocalAudioPipelineToSender() {
5774
+ if (!this._localAudioPipeline || !this.peerConnection) return;
5775
+ const [raw] = this.localStreamController.localAudioTracks;
5776
+ this._localAudioPipeline.setInputTrack(raw ?? null);
5777
+ const [audioTransceiver] = this.transceiverController?.audioTransceivers ?? [];
5778
+ const sender = audioTransceiver?.sender ?? this.peerConnection.getSenders().find((s) => s.track?.kind === "audio");
5779
+ if (!sender || !raw) return;
5780
+ try {
5781
+ await sender.replaceTrack(this._localAudioPipeline.outputTrack);
5782
+ } catch (error) {
5783
+ logger$16.warn("[RTCPeerConnectionController] Failed to route audio sender through pipeline:", error);
5784
+ }
5520
5785
  }
5521
5786
  /**
5522
5787
  * Add a local media track to the peer connection.
@@ -5531,9 +5796,9 @@ var RTCPeerConnectionController = class extends Destroyable {
5531
5796
  try {
5532
5797
  const localStream = this.localStreamController.addTrack(track);
5533
5798
  this.peerConnection.addTrack(track, localStream);
5534
- logger$15.debug(`[RTCPeerConnectionController] ${track.kind} track added:`, track.id);
5799
+ logger$16.debug(`[RTCPeerConnectionController] ${track.kind} track added:`, track.id);
5535
5800
  } catch (error) {
5536
- logger$15.error(`[RTCPeerConnectionController] Failed to add ${track.kind} track:`, error);
5801
+ logger$16.error(`[RTCPeerConnectionController] Failed to add ${track.kind} track:`, error);
5537
5802
  this._errors$.next(toError(error));
5538
5803
  throw error;
5539
5804
  }
@@ -5550,15 +5815,15 @@ var RTCPeerConnectionController = class extends Destroyable {
5550
5815
  }
5551
5816
  const sender = this.peerConnection.getSenders().find((sender$1) => sender$1.track?.id === trackId);
5552
5817
  if (!sender) {
5553
- logger$15.debug(`[RTCPeerConnectionController] track not found: ${trackId}`);
5818
+ logger$16.debug(`[RTCPeerConnectionController] track not found: ${trackId}`);
5554
5819
  return;
5555
5820
  }
5556
5821
  try {
5557
5822
  this.peerConnection.removeTrack(sender);
5558
5823
  this.localStreamController.removeTrack(trackId);
5559
- logger$15.debug(`[RTCPeerConnectionController] ${sender.track?.kind} track removed:`, trackId);
5824
+ logger$16.debug(`[RTCPeerConnectionController] ${sender.track?.kind} track removed:`, trackId);
5560
5825
  } catch (error) {
5561
- logger$15.error(`[RTCPeerConnectionController] Failed to remove ${sender.track?.kind} track:`, error);
5826
+ logger$16.error(`[RTCPeerConnectionController] Failed to remove ${sender.track?.kind} track:`, error);
5562
5827
  this._errors$.next(toError(error));
5563
5828
  throw error;
5564
5829
  }
@@ -5585,7 +5850,7 @@ var RTCPeerConnectionController = class extends Destroyable {
5585
5850
  async replaceAudioTrackWithConstraints(constraints) {
5586
5851
  const senders = this.peerConnection?.getSenders().filter((s) => s.track?.kind === "audio" && s.track.readyState === "live");
5587
5852
  if (!senders || senders.length === 0) {
5588
- logger$15.warn("[RTCPeerConnectionController] No live audio sender to replace");
5853
+ logger$16.warn("[RTCPeerConnectionController] No live audio sender to replace");
5589
5854
  return;
5590
5855
  }
5591
5856
  for (const sender of senders) {
@@ -5603,7 +5868,7 @@ var RTCPeerConnectionController = class extends Destroyable {
5603
5868
  const newTrack = (await this.getUserMedia({ audio: mergedConstraints })).getAudioTracks()[0];
5604
5869
  await sender.replaceTrack(newTrack);
5605
5870
  this.localStreamController.addTrack(newTrack);
5606
- logger$15.debug(`[RTCPeerConnectionController] Audio track replaced for server-pushed params. New track: ${newTrack.id}`);
5871
+ logger$16.debug(`[RTCPeerConnectionController] Audio track replaced for server-pushed params. New track: ${newTrack.id}`);
5607
5872
  }
5608
5873
  }
5609
5874
  /**
@@ -5611,9 +5876,11 @@ var RTCPeerConnectionController = class extends Destroyable {
5611
5876
  * Completes all observables to prevent memory leaks.
5612
5877
  */
5613
5878
  destroy() {
5614
- logger$15.debug(`[RTCPeerConnectionController] Destroying RTCPeerConnectionController. ${this.propose}`);
5879
+ logger$16.debug(`[RTCPeerConnectionController] Destroying RTCPeerConnectionController. ${this.propose}`);
5615
5880
  this.removeConnectionTimer();
5616
5881
  this._iceGatheringController?.destroy();
5882
+ this._localAudioPipeline?.destroy();
5883
+ this._localAudioPipeline = null;
5617
5884
  this.localStreamController.destroy();
5618
5885
  this.transceiverController?.destroy();
5619
5886
  if (this.peerConnection) {
@@ -5635,7 +5902,7 @@ var RTCPeerConnectionController = class extends Destroyable {
5635
5902
  }
5636
5903
  stopRemoteTracks() {
5637
5904
  this._remoteStream$.value?.getTracks().forEach((track) => {
5638
- logger$15.debug(`[RTCPeerConnectionController] Stopping remote track: ${track.kind}`);
5905
+ logger$16.debug(`[RTCPeerConnectionController] Stopping remote track: ${track.kind}`);
5639
5906
  track.stop();
5640
5907
  });
5641
5908
  }
@@ -5652,7 +5919,7 @@ var RTCPeerConnectionController = class extends Destroyable {
5652
5919
  ...params,
5653
5920
  sdp: finalRemote
5654
5921
  };
5655
- logger$15.debug("[RTCPeerConnectionController] Setting remote description:", answer);
5922
+ logger$16.debug("[RTCPeerConnectionController] Setting remote description:", answer);
5656
5923
  return this.peerConnection.setRemoteDescription(answer);
5657
5924
  }
5658
5925
  };
@@ -5690,7 +5957,7 @@ function isVertoPingInnerParams(value) {
5690
5957
 
5691
5958
  //#endregion
5692
5959
  //#region src/managers/VertoManager.ts
5693
- const logger$14 = require_operators.getLogger();
5960
+ const logger$15 = require_operators.getLogger();
5694
5961
  var VertoManager = class extends Destroyable {
5695
5962
  constructor(callSession) {
5696
5963
  super();
@@ -5729,7 +5996,7 @@ var WebRTCVertoManager = class extends VertoManager {
5729
5996
  try {
5730
5997
  await this.executeVerto(vertoModifyMessage);
5731
5998
  } catch (error) {
5732
- logger$14.warn("[WebRTCManager] Call might already be disconnected, error sending Verto hold:", error);
5999
+ logger$15.warn("[WebRTCManager] Call might already be disconnected, error sending Verto hold:", error);
5733
6000
  throw error;
5734
6001
  }
5735
6002
  }
@@ -5742,7 +6009,7 @@ var WebRTCVertoManager = class extends VertoManager {
5742
6009
  try {
5743
6010
  await this.executeVerto(vertoModifyMessage);
5744
6011
  } catch (error) {
5745
- logger$14.warn("[WebRTCManager] Call might already be disconnected, error sending Verto unhold:", error);
6012
+ logger$15.warn("[WebRTCManager] Call might already be disconnected, error sending Verto unhold:", error);
5746
6013
  throw error;
5747
6014
  }
5748
6015
  }
@@ -5795,7 +6062,7 @@ var WebRTCVertoManager = class extends VertoManager {
5795
6062
  if (event.member_id) this.setSelfIdIfNull(event.member_id);
5796
6063
  });
5797
6064
  this.subscribeTo(this.vertoMedia$, (event) => {
5798
- logger$14.debug("[WebRTCManager] Received Verto media event (early media SDP):", event);
6065
+ logger$15.debug("[WebRTCManager] Received Verto media event (early media SDP):", event);
5799
6066
  this._signalingStatus$.next("ringing");
5800
6067
  const { sdp, callID } = event;
5801
6068
  this._rtcPeerConnectionsMap.get(callID)?.updateAnswerStatus({
@@ -5804,7 +6071,7 @@ var WebRTCVertoManager = class extends VertoManager {
5804
6071
  });
5805
6072
  });
5806
6073
  this.subscribeTo(this.vertoAnswer$, (event) => {
5807
- logger$14.debug("[WebRTCManager] Received Verto answer event:", event);
6074
+ logger$15.debug("[WebRTCManager] Received Verto answer event:", event);
5808
6075
  this._signalingStatus$.next("connecting");
5809
6076
  const { sdp, callID } = event;
5810
6077
  this._rtcPeerConnectionsMap.get(callID)?.updateAnswerStatus({
@@ -5813,7 +6080,7 @@ var WebRTCVertoManager = class extends VertoManager {
5813
6080
  });
5814
6081
  });
5815
6082
  this.subscribeTo(this.vertoMediaParams$, (event) => {
5816
- logger$14.debug("[WebRTCManager] Received Verto mediaParams event:", event);
6083
+ logger$15.debug("[WebRTCManager] Received Verto mediaParams event:", event);
5817
6084
  const { mediaParams, callID } = event;
5818
6085
  const rtcPeerConnController = this._rtcPeerConnectionsMap.get(callID);
5819
6086
  const { audio, video } = mediaParams;
@@ -5827,7 +6094,7 @@ var WebRTCVertoManager = class extends VertoManager {
5827
6094
  timestamp: Date.now()
5828
6095
  });
5829
6096
  } catch (error) {
5830
- logger$14.warn("[WebRTCManager] Error applying server-pushed media params:", error);
6097
+ logger$15.warn("[WebRTCManager] Error applying server-pushed media params:", error);
5831
6098
  this.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
5832
6099
  }
5833
6100
  })();
@@ -5849,13 +6116,13 @@ var WebRTCVertoManager = class extends VertoManager {
5849
6116
  */
5850
6117
  setNodeIdIfNull(nodeId) {
5851
6118
  if (!this._nodeId$.value && nodeId) {
5852
- logger$14.debug(`[WebRTCManager] Early node_id set: ${nodeId}`);
6119
+ logger$15.debug(`[WebRTCManager] Early node_id set: ${nodeId}`);
5853
6120
  this._nodeId$.next(nodeId);
5854
6121
  }
5855
6122
  }
5856
6123
  setSelfIdIfNull(selfId) {
5857
6124
  if (!this._selfId$.value && selfId) {
5858
- logger$14.debug(`[WebRTCManager] Early selfId set: ${selfId}`);
6125
+ logger$15.debug(`[WebRTCManager] Early selfId set: ${selfId}`);
5859
6126
  this._selfId$.next(selfId);
5860
6127
  }
5861
6128
  }
@@ -5864,7 +6131,7 @@ var WebRTCVertoManager = class extends VertoManager {
5864
6131
  const vertoPongMessage = VertoPong({ ...vertoPing });
5865
6132
  await this.executeVerto(vertoPongMessage);
5866
6133
  } catch (error) {
5867
- logger$14.warn("[WebRTCManager] Call might disconnect, error sending Verto pong:", error);
6134
+ logger$15.warn("[WebRTCManager] Call might disconnect, error sending Verto pong:", error);
5868
6135
  this.onError?.(new require_operators.VertoPongError(error));
5869
6136
  }
5870
6137
  }
@@ -5874,7 +6141,7 @@ var WebRTCVertoManager = class extends VertoManager {
5874
6141
  if (audio) await this.mainPeerConnection.updateSendersConstraints("audio", audio);
5875
6142
  if (video) await this.mainPeerConnection.updateSendersConstraints("video", video);
5876
6143
  } catch (error) {
5877
- logger$14.warn("[WebRTCManager] Error updating media constraints:", error);
6144
+ logger$15.warn("[WebRTCManager] Error updating media constraints:", error);
5878
6145
  this.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
5879
6146
  throw error;
5880
6147
  }
@@ -5904,20 +6171,20 @@ var WebRTCVertoManager = class extends VertoManager {
5904
6171
  try {
5905
6172
  const pc = this.mainPeerConnection.peerConnection;
5906
6173
  if (!pc) {
5907
- logger$14.warn("[WebRTCManager] No peer connection for keyframe request");
6174
+ logger$15.warn("[WebRTCManager] No peer connection for keyframe request");
5908
6175
  return;
5909
6176
  }
5910
6177
  const videoReceiver = pc.getReceivers().find((r) => r.track.kind === "video");
5911
6178
  if (!videoReceiver) {
5912
- logger$14.warn("[WebRTCManager] No video receiver for keyframe request");
6179
+ logger$15.warn("[WebRTCManager] No video receiver for keyframe request");
5913
6180
  return;
5914
6181
  }
5915
6182
  if (typeof videoReceiver.requestKeyFrame === "function") {
5916
6183
  videoReceiver.requestKeyFrame();
5917
- logger$14.debug("[WebRTCManager] Keyframe requested via RTCRtpReceiver.requestKeyFrame()");
5918
- } else logger$14.debug("[WebRTCManager] requestKeyFrame() not supported, skipping");
6184
+ logger$15.debug("[WebRTCManager] Keyframe requested via RTCRtpReceiver.requestKeyFrame()");
6185
+ } else logger$15.debug("[WebRTCManager] requestKeyFrame() not supported, skipping");
5919
6186
  } catch (error) {
5920
- logger$14.warn("[WebRTCManager] Keyframe request failed (non-fatal):", error);
6187
+ logger$15.warn("[WebRTCManager] Keyframe request failed (non-fatal):", error);
5921
6188
  }
5922
6189
  }
5923
6190
  /**
@@ -5935,13 +6202,13 @@ var WebRTCVertoManager = class extends VertoManager {
5935
6202
  try {
5936
6203
  const controller = this.mainPeerConnection;
5937
6204
  if (!controller.peerConnection) {
5938
- logger$14.warn("[WebRTCManager] No peer connection for ICE restart");
6205
+ logger$15.warn("[WebRTCManager] No peer connection for ICE restart");
5939
6206
  return;
5940
6207
  }
5941
6208
  await controller.triggerIceRestart(relayOnly);
5942
- logger$14.info(`[WebRTCManager] ICE restart initiated${relayOnly ? " (relay-only)" : ""}`);
6209
+ logger$15.info(`[WebRTCManager] ICE restart initiated${relayOnly ? " (relay-only)" : ""}`);
5943
6210
  } catch (error) {
5944
- logger$14.error("[WebRTCManager] ICE restart failed:", error);
6211
+ logger$15.error("[WebRTCManager] ICE restart failed:", error);
5945
6212
  throw error;
5946
6213
  }
5947
6214
  }
@@ -5959,13 +6226,13 @@ var WebRTCVertoManager = class extends VertoManager {
5959
6226
  const entries = Array.from(this._rtcPeerConnectionsMap.entries());
5960
6227
  for (const [id, controller] of entries) try {
5961
6228
  if (!controller.peerConnection) {
5962
- logger$14.debug(`[WebRTCManager] No peer connection for leg ${id}, skipping ICE restart`);
6229
+ logger$15.debug(`[WebRTCManager] No peer connection for leg ${id}, skipping ICE restart`);
5963
6230
  continue;
5964
6231
  }
5965
6232
  await controller.triggerIceRestart(relayOnly);
5966
- logger$14.info(`[WebRTCManager] ICE restart initiated for leg ${id}${relayOnly ? " (relay-only)" : ""}`);
6233
+ logger$15.info(`[WebRTCManager] ICE restart initiated for leg ${id}${relayOnly ? " (relay-only)" : ""}`);
5967
6234
  } catch (error) {
5968
- logger$14.warn(`[WebRTCManager] ICE restart failed for leg ${id}:`, error);
6235
+ logger$15.warn(`[WebRTCManager] ICE restart failed for leg ${id}:`, error);
5969
6236
  }
5970
6237
  }
5971
6238
  /**
@@ -5977,7 +6244,7 @@ var WebRTCVertoManager = class extends VertoManager {
5977
6244
  requestKeyframeAll() {
5978
6245
  for (const [id, controller] of this._rtcPeerConnectionsMap) {
5979
6246
  if (controller.isScreenShare) {
5980
- logger$14.debug(`[WebRTCManager] Skipping keyframe for send-only screen share leg ${id}`);
6247
+ logger$15.debug(`[WebRTCManager] Skipping keyframe for send-only screen share leg ${id}`);
5981
6248
  continue;
5982
6249
  }
5983
6250
  try {
@@ -5987,10 +6254,10 @@ var WebRTCVertoManager = class extends VertoManager {
5987
6254
  if (!videoReceiver) continue;
5988
6255
  if (typeof videoReceiver.requestKeyFrame === "function") {
5989
6256
  videoReceiver.requestKeyFrame();
5990
- logger$14.debug(`[WebRTCManager] Keyframe requested for leg ${id}`);
6257
+ logger$15.debug(`[WebRTCManager] Keyframe requested for leg ${id}`);
5991
6258
  }
5992
6259
  } catch (error) {
5993
- logger$14.warn(`[WebRTCManager] Keyframe request failed for leg ${id} (non-fatal):`, error);
6260
+ logger$15.warn(`[WebRTCManager] Keyframe request failed for leg ${id} (non-fatal):`, error);
5994
6261
  }
5995
6262
  }
5996
6263
  }
@@ -6051,7 +6318,7 @@ var WebRTCVertoManager = class extends VertoManager {
6051
6318
  default:
6052
6319
  }
6053
6320
  } catch (error) {
6054
- logger$14.error(`[WebRTCManager] Error sending Verto ${vertoMethod}:`, error);
6321
+ logger$15.error(`[WebRTCManager] Error sending Verto ${vertoMethod}:`, error);
6055
6322
  this.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
6056
6323
  if (vertoMethod === "verto.modify") this.onModifyFailed?.();
6057
6324
  }
@@ -6066,7 +6333,7 @@ var WebRTCVertoManager = class extends VertoManager {
6066
6333
  sdp
6067
6334
  });
6068
6335
  } catch (error) {
6069
- logger$14.warn("[WebRTCManager] Error processing modify response:", error);
6336
+ logger$15.warn("[WebRTCManager] Error processing modify response:", error);
6070
6337
  const modifyError = error instanceof Error ? error : new Error(String(error), { cause: error });
6071
6338
  this.onError?.(modifyError);
6072
6339
  }
@@ -6078,7 +6345,7 @@ var WebRTCVertoManager = class extends VertoManager {
6078
6345
  this._nodeId$.next(require_operators.getValueFrom(response, "result.node_id") ?? null);
6079
6346
  const memberId = require_operators.getValueFrom(response, "result.result.result.memberID") ?? null;
6080
6347
  const callId = require_operators.getValueFrom(response, "result.result.result.callID") ?? null;
6081
- logger$14.debug("[WebRTCManager] Verto invite response:", {
6348
+ logger$15.debug("[WebRTCManager] Verto invite response:", {
6082
6349
  callId,
6083
6350
  memberId,
6084
6351
  response
@@ -6088,14 +6355,14 @@ var WebRTCVertoManager = class extends VertoManager {
6088
6355
  if (callId) {
6089
6356
  this.webRtcCallSession.addCallId(callId);
6090
6357
  this.attachManager.attach(this.buildAttachableCall(callId));
6091
- } else logger$14.warn("[WebRTCManager] Cannot attach call, missing callId:", {
6358
+ } else logger$15.warn("[WebRTCManager] Cannot attach call, missing callId:", {
6092
6359
  nodeId: this.nodeId,
6093
6360
  callId
6094
6361
  });
6095
- logger$14.info("[WebRTCManager] Verto invite successful");
6096
- logger$14.debug(`[WebRTCManager] nodeid: ${this._nodeId$.value}, selfId: ${this._selfId$.value}`);
6362
+ logger$15.info("[WebRTCManager] Verto invite successful");
6363
+ logger$15.debug(`[WebRTCManager] nodeid: ${this._nodeId$.value}, selfId: ${this._selfId$.value}`);
6097
6364
  } else {
6098
- logger$14.error("[WebRTCManager] Verto invite failed:", response);
6365
+ logger$15.error("[WebRTCManager] Verto invite failed:", response);
6099
6366
  const inviteError = response.error ? new require_operators.JSONRPCError(response.error.code, response.error.message, response.error.data) : /* @__PURE__ */ new Error("Verto invite failed: unexpected response");
6100
6367
  this.onError?.(inviteError);
6101
6368
  }
@@ -6140,17 +6407,17 @@ var WebRTCVertoManager = class extends VertoManager {
6140
6407
  if (options.initOffer) this.handleInboundAnswer(rtcPeerConnController);
6141
6408
  }
6142
6409
  async handleInboundAnswer(rtcPeerConnController) {
6143
- logger$14.debug("[WebRTCManager] Waiting for inbound call to be accepted or rejected");
6410
+ logger$15.debug("[WebRTCManager] Waiting for inbound call to be accepted or rejected");
6144
6411
  const vertoByeOrAccepted = await (0, rxjs.firstValueFrom)((0, rxjs.race)(this.vertoBye$, this.webRtcCallSession.answered$).pipe((0, rxjs.takeUntil)(this.destroyed$))).catch(() => null);
6145
6412
  if (vertoByeOrAccepted === null) {
6146
- logger$14.debug("[WebRTCManager] Inbound answer handler aborted (destroyed).");
6413
+ logger$15.debug("[WebRTCManager] Inbound answer handler aborted (destroyed).");
6147
6414
  return;
6148
6415
  }
6149
6416
  if (isVertoByeMessage(vertoByeOrAccepted)) {
6150
- logger$14.info("[WebRTCManager] Inbound call ended by remote before answer.");
6417
+ logger$15.info("[WebRTCManager] Inbound call ended by remote before answer.");
6151
6418
  this.callSession?.destroy();
6152
6419
  } else if (!vertoByeOrAccepted) {
6153
- logger$14.info("[WebRTCManager] Inbound call rejected by user.");
6420
+ logger$15.info("[WebRTCManager] Inbound call rejected by user.");
6154
6421
  try {
6155
6422
  await this.bye("USER_BUSY");
6156
6423
  } finally {
@@ -6158,19 +6425,19 @@ var WebRTCVertoManager = class extends VertoManager {
6158
6425
  this.callSession?.destroy();
6159
6426
  }
6160
6427
  } else {
6161
- logger$14.debug("[WebRTCManager] Inbound call accepted, creating SDP answer");
6428
+ logger$15.debug("[WebRTCManager] Inbound call accepted, creating SDP answer");
6162
6429
  const answerOptions = this.webRtcCallSession.answerMediaOptions;
6163
6430
  try {
6164
6431
  await rtcPeerConnController.acceptInbound(answerOptions);
6165
6432
  } catch (error) {
6166
- logger$14.error("[WebRTCManager] Error creating inbound answer:", error);
6433
+ logger$15.error("[WebRTCManager] Error creating inbound answer:", error);
6167
6434
  this.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
6168
6435
  }
6169
6436
  }
6170
6437
  }
6171
6438
  setupVertoAttachHandler() {
6172
6439
  this.subscribeTo(this.vertoAttach$, async (vertoAttach) => {
6173
- logger$14.debug("[WebRTCManager] Received Verto attach event for existing call:", vertoAttach);
6440
+ logger$15.debug("[WebRTCManager] Received Verto attach event for existing call:", vertoAttach);
6174
6441
  const { callID } = vertoAttach;
6175
6442
  await this.attachManager.attach({
6176
6443
  nodeId: this.nodeId ?? void 0,
@@ -6239,17 +6506,17 @@ var WebRTCVertoManager = class extends VertoManager {
6239
6506
  };
6240
6507
  }
6241
6508
  async sendLocalDescriptionOnceAccepted(vertoMessageRequest, rtcPeerConnectionController) {
6242
- logger$14.debug("[WebRTCManager] Waiting for call to be accepted or ended before sending answer");
6509
+ logger$15.debug("[WebRTCManager] Waiting for call to be accepted or ended before sending answer");
6243
6510
  const vertoByeOrAccepted = await (0, rxjs.firstValueFrom)((0, rxjs.race)(this.vertoBye$, this.webRtcCallSession.answered$).pipe((0, rxjs.takeUntil)(this.destroyed$))).catch(() => null);
6244
6511
  if (vertoByeOrAccepted === null) {
6245
- logger$14.debug("[WebRTCManager] Destroyed while waiting for call acceptance");
6512
+ logger$15.debug("[WebRTCManager] Destroyed while waiting for call acceptance");
6246
6513
  return;
6247
6514
  }
6248
6515
  if (isVertoByeMessage(vertoByeOrAccepted)) {
6249
- logger$14.info("[WebRTCManager] Call ended before answer was sent.");
6516
+ logger$15.info("[WebRTCManager] Call ended before answer was sent.");
6250
6517
  this.callSession?.destroy();
6251
6518
  } else if (!vertoByeOrAccepted) {
6252
- logger$14.info("[WebRTCManager] Call was not accepted, sending verto.bye.");
6519
+ logger$15.info("[WebRTCManager] Call was not accepted, sending verto.bye.");
6253
6520
  try {
6254
6521
  await this.bye("USER_BUSY");
6255
6522
  } finally {
@@ -6257,14 +6524,14 @@ var WebRTCVertoManager = class extends VertoManager {
6257
6524
  this.callSession?.destroy();
6258
6525
  }
6259
6526
  } else {
6260
- logger$14.debug("[WebRTCManager] Call accepted, sending answer");
6527
+ logger$15.debug("[WebRTCManager] Call accepted, sending answer");
6261
6528
  try {
6262
6529
  this._signalingStatus$.next("connecting");
6263
6530
  await this.sendLocalDescription(vertoMessageRequest, rtcPeerConnectionController);
6264
6531
  await rtcPeerConnectionController.updateAnswerStatus({ status: "sent" });
6265
6532
  await this.attachManager.attach(this.buildAttachableCall());
6266
6533
  } catch (error) {
6267
- logger$14.error("[WebRTCManager] Error sending Verto answer:", error);
6534
+ logger$15.error("[WebRTCManager] Error sending Verto answer:", error);
6268
6535
  this.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
6269
6536
  await rtcPeerConnectionController.updateAnswerStatus({ status: "failed" });
6270
6537
  }
@@ -6305,6 +6572,14 @@ var WebRTCVertoManager = class extends VertoManager {
6305
6572
  async unmuteMainVideoInputDevice() {
6306
6573
  return this.mainPeerConnection.restoreTrackSender("video");
6307
6574
  }
6575
+ /** Get or lazily create the local audio pipeline for the main peer connection. */
6576
+ ensureLocalAudioPipeline() {
6577
+ return this.mainPeerConnection.ensureLocalAudioPipeline();
6578
+ }
6579
+ /** The currently-active local audio pipeline, or null if it hasn't been created. */
6580
+ get localAudioPipeline() {
6581
+ return this.mainPeerConnection.localAudioPipeline;
6582
+ }
6308
6583
  async addInputDevice(options = {
6309
6584
  audio: false,
6310
6585
  video: true
@@ -6355,10 +6630,10 @@ var WebRTCVertoManager = class extends VertoManager {
6355
6630
  });
6356
6631
  await (0, rxjs.firstValueFrom)(rtcPeerConnController.connectionState$.pipe((0, rxjs.filter)((state) => state === "connected"), (0, rxjs.take)(1), (0, rxjs.timeout)(this._screenShareTimeoutMs), (0, rxjs.takeUntil)(this.destroyed$)));
6357
6632
  this._screenShareStatus$.next("started");
6358
- logger$14.info("[WebRTCManager] Screen share started successfully.");
6633
+ logger$15.info("[WebRTCManager] Screen share started successfully.");
6359
6634
  return rtcPeerConnController.id;
6360
6635
  } catch (error) {
6361
- logger$14.warn("[WebRTCManager] Error initializing additional peer connection:", error);
6636
+ logger$15.warn("[WebRTCManager] Error initializing additional peer connection:", error);
6362
6637
  this.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
6363
6638
  if (rtcPeerConnController) rtcPeerConnController.destroy();
6364
6639
  this._screenShareStatus$.next("none");
@@ -6377,9 +6652,9 @@ var WebRTCVertoManager = class extends VertoManager {
6377
6652
  if (removeTrack) return this.mainPeerConnection.stopTrackSender(removeTrack, { updateTransceiverDirection: true });
6378
6653
  }
6379
6654
  async removeScreenMedia() {
6380
- if (!["starting", "started"].includes(this._screenShareStatus$.value)) logger$14.warn("[WebRTCManager] No active screen share to stop.");
6655
+ if (!["starting", "started"].includes(this._screenShareStatus$.value)) logger$15.warn("[WebRTCManager] No active screen share to stop.");
6381
6656
  if (!this._screenShareId) {
6382
- logger$14.debug("[WebRTCManager] No screen share peer connection found.");
6657
+ logger$15.debug("[WebRTCManager] No screen share peer connection found.");
6383
6658
  return;
6384
6659
  }
6385
6660
  this._screenShareStatus$.next("stopping");
@@ -6408,7 +6683,7 @@ var WebRTCVertoManager = class extends VertoManager {
6408
6683
  dialogParams: this.dialogParams(rtcPeerConnController)
6409
6684
  }));
6410
6685
  } catch (error) {
6411
- logger$14.warn("[WebRTCManager] Call might already be disconnected, error sending Verto bye:", error);
6686
+ logger$15.warn("[WebRTCManager] Call might already be disconnected, error sending Verto bye:", error);
6412
6687
  throw error;
6413
6688
  }
6414
6689
  }
@@ -6426,7 +6701,7 @@ var WebRTCVertoManager = class extends VertoManager {
6426
6701
  try {
6427
6702
  await this.executeVerto(vertoInfoMessage);
6428
6703
  } catch (error) {
6429
- logger$14.warn("[WebRTCManager] Error sending DTMF digits:", error);
6704
+ logger$15.warn("[WebRTCManager] Error sending DTMF digits:", error);
6430
6705
  throw error;
6431
6706
  }
6432
6707
  }
@@ -6437,10 +6712,10 @@ var WebRTCVertoManager = class extends VertoManager {
6437
6712
  action: "transfer"
6438
6713
  });
6439
6714
  try {
6440
- logger$14.debug("[WebRTCManager] Transferring call with options:", options);
6715
+ logger$15.debug("[WebRTCManager] Transferring call with options:", options);
6441
6716
  await this.executeVerto(message);
6442
6717
  } catch (error) {
6443
- logger$14.error("[WebRTCManager] Error transferring call:", error);
6718
+ logger$15.error("[WebRTCManager] Error transferring call:", error);
6444
6719
  throw error;
6445
6720
  }
6446
6721
  }
@@ -6454,6 +6729,76 @@ var WebRTCVertoManager = class extends VertoManager {
6454
6729
  }
6455
6730
  };
6456
6731
 
6732
+ //#endregion
6733
+ //#region src/controllers/RemoteAudioMeter.ts
6734
+ const logger$14 = require_operators.getLogger();
6735
+ /**
6736
+ * Read-only audio level meter for a remote MediaStream. Attaches an
6737
+ * AnalyserNode to a MediaStreamAudioSourceNode so it observes the stream
6738
+ * without affecting the caller's playback path (no GainNode, no destination).
6739
+ *
6740
+ * The server delivers all remote audio as a single mixed stream — there is
6741
+ * no per-participant demux — so this meter reports the aggregate remote
6742
+ * level, not per-member.
6743
+ */
6744
+ var RemoteAudioMeter = class extends Destroyable {
6745
+ constructor(options = {}) {
6746
+ super();
6747
+ this._source = null;
6748
+ this._stream = null;
6749
+ this._audioContext = (options.audioContextFactory ?? (() => new AudioContext()))();
6750
+ this._analyser = this._audioContext.createAnalyser();
6751
+ this._analyser.fftSize = 2048;
6752
+ this._analyser.smoothingTimeConstant = .3;
6753
+ this._analyserBuffer = new Uint8Array(new ArrayBuffer(this._analyser.fftSize));
6754
+ this._pollIntervalMs = options.pollIntervalMs ?? AUDIO_LEVEL_POLL_INTERVAL_MS;
6755
+ }
6756
+ /** RMS level of the remote audio, 0..1. 0 when no stream is attached. */
6757
+ get level$() {
6758
+ return this.deferEmission((0, rxjs.interval)(this._pollIntervalMs, rxjs.animationFrameScheduler).pipe((0, rxjs.map)(() => this.computeLevel())));
6759
+ }
6760
+ /**
6761
+ * Attach (or replace) the MediaStream whose audio track is being metered.
6762
+ * Pass null to detach without destroying the meter.
6763
+ */
6764
+ setStream(stream) {
6765
+ if (this._source) {
6766
+ try {
6767
+ this._source.disconnect();
6768
+ } catch (error) {
6769
+ logger$14.debug("[RemoteAudioMeter] source disconnect warning:", error);
6770
+ }
6771
+ this._source = null;
6772
+ this._stream = null;
6773
+ }
6774
+ if (!stream || stream.getAudioTracks().length === 0) return;
6775
+ this._stream = new MediaStream(stream.getAudioTracks());
6776
+ this._source = this._audioContext.createMediaStreamSource(this._stream);
6777
+ }
6778
+ destroy() {
6779
+ if (this._source) {
6780
+ try {
6781
+ this._source.disconnect();
6782
+ } catch {}
6783
+ this._source = null;
6784
+ }
6785
+ this._audioContext.close().catch((error) => {
6786
+ logger$14.debug("[RemoteAudioMeter] audio context close warning:", error);
6787
+ });
6788
+ super.destroy();
6789
+ }
6790
+ computeLevel() {
6791
+ if (!this._source) return 0;
6792
+ this._analyser.getByteTimeDomainData(this._analyserBuffer);
6793
+ let sum = 0;
6794
+ for (const sample of this._analyserBuffer) {
6795
+ const normalized = (sample - 128) / 128;
6796
+ sum += normalized * normalized;
6797
+ }
6798
+ return Math.sqrt(sum / this._analyserBuffer.length);
6799
+ }
6800
+ };
6801
+
6457
6802
  //#endregion
6458
6803
  //#region src/controllers/RTCStatsMonitor.ts
6459
6804
  /**
@@ -7270,6 +7615,8 @@ var WebRTCCall = class extends Destroyable {
7270
7615
  this._bandwidthConstrained$ = this.createBehaviorSubject(false);
7271
7616
  this._mediaParamsUpdated$ = this.createSubject();
7272
7617
  this._customSubscriptions = /* @__PURE__ */ new Map();
7618
+ this._pushToTalkEnabled = false;
7619
+ this._remoteAudioMeter = null;
7273
7620
  this.id = options.callId ?? (0, uuid.v4)();
7274
7621
  this.to = options.to;
7275
7622
  this._userVariables$.next({
@@ -8041,11 +8388,129 @@ var WebRTCCall = class extends Destroyable {
8041
8388
  async transfer(options) {
8042
8389
  return this.vertoManager.transfer(options);
8043
8390
  }
8391
+ /**
8392
+ * Set the local microphone gain as a percentage applied before transmission.
8393
+ *
8394
+ * - `0` = silent
8395
+ * - `100` = unity (no change, default)
8396
+ * - `200` = 2× digital boost (max; expect clipping / noise amplification)
8397
+ *
8398
+ * Values are clamped to [0, 200]. Engages the local audio pipeline on
8399
+ * first use (one-time cost).
8400
+ *
8401
+ * Note: this is a **digital** multiplier applied in a Web Audio GainNode
8402
+ * between your mic track and the RTCRtpSender — it does not change the
8403
+ * physical mic's hardware sensitivity. Browsers' autoGainControl can
8404
+ * fight the setting; call {@link setAutoGainControl}(false) for
8405
+ * predictable behaviour.
8406
+ *
8407
+ * @param value - Gain percentage (0..200; 100 = unity).
8408
+ */
8409
+ setLocalMicrophoneGain(value) {
8410
+ const pipeline = this.vertoManager.ensureLocalAudioPipeline();
8411
+ if (!pipeline) {
8412
+ logger$11.warn("[Call] setLocalMicrophoneGain: audio pipeline unavailable");
8413
+ return;
8414
+ }
8415
+ const percent = Math.max(0, Math.min(200, value));
8416
+ pipeline.setGain(percent / 100);
8417
+ }
8418
+ /** Observable of the current local microphone gain (0..200, where 100 = unity). */
8419
+ get localMicrophoneGain$() {
8420
+ const pipeline = this.vertoManager.ensureLocalAudioPipeline();
8421
+ if (!pipeline) return (0, rxjs.of)(100).pipe((0, rxjs.takeUntil)(this._destroyed$));
8422
+ return pipeline.gain$.pipe((0, rxjs.map)((multiplier) => multiplier * 100), (0, rxjs.takeUntil)(this._destroyed$));
8423
+ }
8424
+ /**
8425
+ * Observable of the RMS audio level of the local microphone, 0..1.
8426
+ * Emits at ~30fps while a mic track is active. Engages the local audio
8427
+ * pipeline on first subscription.
8428
+ */
8429
+ get localAudioLevel$() {
8430
+ return this.vertoManager.ensureLocalAudioPipeline()?.level$ ?? (0, rxjs.of)(0).pipe((0, rxjs.takeUntil)(this._destroyed$));
8431
+ }
8432
+ /**
8433
+ * Observable that is `true` while the local participant is speaking
8434
+ * (RMS level above the VAD threshold, with hold time to avoid flicker).
8435
+ */
8436
+ get localSpeaking$() {
8437
+ return this.vertoManager.ensureLocalAudioPipeline()?.speaking$ ?? (0, rxjs.of)(false).pipe((0, rxjs.takeUntil)(this._destroyed$));
8438
+ }
8439
+ /**
8440
+ * Enable push-to-talk: while {@link setPushToTalkActive} has been called
8441
+ * with `false`, the microphone gain is forced to 0; calling
8442
+ * {@link setPushToTalkActive} with `true` restores the configured gain.
8443
+ * Use this instead of mute/unmute for instant talk/silence transitions
8444
+ * because it doesn't rebuild the track.
8445
+ *
8446
+ * This method installs the pipeline but does not attach any keyboard
8447
+ * listener — consumers bind the key themselves and call
8448
+ * {@link setPushToTalkActive} on keydown/keyup.
8449
+ */
8450
+ enablePushToTalk() {
8451
+ const pipeline = this.vertoManager.ensureLocalAudioPipeline();
8452
+ if (!pipeline) {
8453
+ logger$11.warn("[Call] enablePushToTalk: audio pipeline unavailable");
8454
+ return;
8455
+ }
8456
+ pipeline.setPTTActive(false);
8457
+ this._pushToTalkEnabled = true;
8458
+ }
8459
+ /** Disable push-to-talk; mic gain returns to the configured value. */
8460
+ disablePushToTalk() {
8461
+ this.vertoManager.localAudioPipeline?.setPTTActive(true);
8462
+ this._pushToTalkEnabled = false;
8463
+ }
8464
+ /**
8465
+ * While push-to-talk is enabled, sets the talk state. `true` = transmitting,
8466
+ * `false` = silent. No-op if push-to-talk has not been enabled.
8467
+ */
8468
+ setPushToTalkActive(active) {
8469
+ if (!this._pushToTalkEnabled) return;
8470
+ this.vertoManager.localAudioPipeline?.setPTTActive(active);
8471
+ }
8472
+ /**
8473
+ * Toggle echo cancellation on the local mic at runtime. Applied via
8474
+ * `track.applyConstraints`; browsers that don't honour runtime constraints
8475
+ * (notably iOS Safari) fall back to re-acquiring the track with the new
8476
+ * constraint set and plumbing the replacement through the local audio
8477
+ * pipeline if one is active.
8478
+ */
8479
+ async setEchoCancellation(enabled) {
8480
+ await this.vertoManager.updateMediaConstraints({ audio: { echoCancellation: enabled } });
8481
+ }
8482
+ /** Toggle browser noise suppression on the local mic at runtime. */
8483
+ async setNoiseSuppression(enabled) {
8484
+ await this.vertoManager.updateMediaConstraints({ audio: { noiseSuppression: enabled } });
8485
+ }
8486
+ /** Toggle browser automatic gain control on the local mic at runtime. */
8487
+ async setAutoGainControl(enabled) {
8488
+ await this.vertoManager.updateMediaConstraints({ audio: { autoGainControl: enabled } });
8489
+ }
8490
+ /**
8491
+ * Observable of the aggregate remote audio level, 0..1 RMS. The server
8492
+ * delivers a single mixed audio stream for all remote participants — this
8493
+ * meter reports that mix. Per-participant audio is not available client-side.
8494
+ *
8495
+ * Engages a shared AudioContext on first subscription (cheap — one
8496
+ * AnalyserNode, no GainNode, no destination) so it does not affect the
8497
+ * caller's audio element playback.
8498
+ */
8499
+ get remoteAudioLevel$() {
8500
+ this._remoteAudioMeter ??= new RemoteAudioMeter();
8501
+ const meter = this._remoteAudioMeter;
8502
+ this.subscribeTo(this.vertoManager.remoteStream$, (stream) => {
8503
+ meter.setStream(stream);
8504
+ });
8505
+ return meter.level$.pipe((0, rxjs.takeUntil)(this._destroyed$));
8506
+ }
8044
8507
  /** Destroys the call, releasing all resources and subscriptions. */
8045
8508
  destroy() {
8046
8509
  if (this._status$.value === "destroyed") return;
8047
8510
  this._status$.next("destroyed");
8048
8511
  this.stopResilienceSubsystems();
8512
+ this._remoteAudioMeter?.destroy();
8513
+ this._remoteAudioMeter = null;
8049
8514
  this.vertoManager.destroy();
8050
8515
  this.callEventsManager.destroy();
8051
8516
  super.destroy();
@@ -10696,6 +11161,36 @@ var SignalWire = class extends Destroyable {
10696
11161
  selectAudioOutputDevice(device) {
10697
11162
  this._deviceController.selectAudioOutputDevice(device);
10698
11163
  }
11164
+ /**
11165
+ * Apply the currently selected audio output device to an HTMLMediaElement
11166
+ * (e.g. the `<audio>` or `<video>` element the consumer attached the
11167
+ * remote stream to). Uses `HTMLMediaElement.setSinkId` under the hood.
11168
+ * Returns a `Promise<boolean>`: `true` if the sink was applied,
11169
+ * `false` if the browser doesn't support `setSinkId` or no device is
11170
+ * selected.
11171
+ *
11172
+ * @example
11173
+ * ```ts
11174
+ * audioEl.srcObject = call.remoteStream;
11175
+ * await client.applySelectedAudioOutputDevice(audioEl);
11176
+ * ```
11177
+ */
11178
+ async applySelectedAudioOutputDevice(element) {
11179
+ const device = this._deviceController.selectedAudioOutputDevice;
11180
+ if (!device?.deviceId) return false;
11181
+ const withSink = element;
11182
+ if (typeof withSink.setSinkId !== "function") {
11183
+ logger$1.warn("[SignalWire] setSinkId not supported on this element / browser");
11184
+ return false;
11185
+ }
11186
+ try {
11187
+ await withSink.setSinkId(device.deviceId);
11188
+ return true;
11189
+ } catch (error) {
11190
+ logger$1.warn("[SignalWire] Failed to apply audio output device:", error);
11191
+ return false;
11192
+ }
11193
+ }
10699
11194
  /** Starts monitoring for media device changes (connect/disconnect). */
10700
11195
  enableDeviceMonitoring() {
10701
11196
  this._deviceController.enableDeviceMonitoring();