@signalwire/js 4.0.0-beta.11 → 4.0.0-beta.12

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.mjs CHANGED
@@ -1,6 +1,6 @@
1
- import { A as StorageNotAvailableError, B as VertoPongError, C as OverconstrainedFallbackError, D as RequestError, E as RecoveryError, F as UnexpectedError, H as WebSocketTimeoutError, I as UnimplementedError, L as ValidationError, M as StorageWriteError, N as TokenRefreshError, O as RequestTimeoutError, P as TransportConnectionError, R as VertoAttachHandlerError, S as MessageParseError, T as RPCTimeoutError, V as WebSocketConnectionError, _ as DeviceTokenError, a as filterNull, b as JSONRPCError, c as setLogLevel, d as CallCreateError, f as CollectionFetchError, g as DeserializationError, h as DependencyError, i as getValueFrom, j as StorageReadError, k as SerializationError, l as setLogger, m as DPoPInitError, n as filterAs, o as getLogger, p as ConversationError, s as setDebugOptions, t as throwOnRPCError, u as AuthStateHandlerError, v as InvalidCredentialsError, w as PreflightError, x as MediaTrackError, y as InvalidParams, z as VertoInviteHandlerError } from "./operators-B1xH6k06.mjs";
1
+ import { A as StorageNotAvailableError, B as VertoPongError, C as OverconstrainedFallbackError, D as RequestError, E as RecoveryError, F as UnexpectedError, H as WebSocketTimeoutError, I as UnimplementedError, L as ValidationError, M as StorageWriteError, N as TokenRefreshError, O as RequestTimeoutError, P as TransportConnectionError, R as VertoAttachHandlerError, S as MessageParseError, T as RPCTimeoutError, V as WebSocketConnectionError, _ as DeviceTokenError, a as filterNull, b as JSONRPCError, c as setLogLevel, d as CallCreateError, f as CollectionFetchError, g as DeserializationError, h as DependencyError, i as getValueFrom, j as StorageReadError, k as SerializationError, l as setLogger, m as DPoPInitError, n as filterAs, o as getLogger, p as ConversationError, s as setDebugOptions, t as throwOnRPCError, u as AuthStateHandlerError, v as InvalidCredentialsError, w as PreflightError, x as MediaTrackError, y as InvalidParams, z as VertoInviteHandlerError } from "./operators-CX_lCCJm.mjs";
2
2
  import { jwtDecode } from "jwt-decode";
3
- import { BehaviorSubject, EMPTY, NEVER, Observable, ReplaySubject, Subject, TimeoutError, asapScheduler, auditTime, catchError, combineLatest, debounceTime, defer, distinctUntilChanged, exhaustMap, filter, first, firstValueFrom, from, interval, lastValueFrom, map, merge, mergeMap, observeOn, of, pipe, race, scan, share, shareReplay, skip, skipWhile, startWith, switchMap, take, takeUntil, tap, throwError, timeout, timer, toArray, withLatestFrom } from "rxjs";
3
+ import { BehaviorSubject, EMPTY, NEVER, Observable, ReplaySubject, Subject, TimeoutError, animationFrameScheduler, asapScheduler, auditTime, catchError, combineLatest, debounceTime, defer, distinctUntilChanged, exhaustMap, filter, first, firstValueFrom, from, interval, lastValueFrom, map, merge, mergeMap, observeOn, of, pipe, race, scan, share, shareReplay, skip, skipWhile, startWith, switchMap, take, takeUntil, tap, throwError, timeout, timer, toArray, withLatestFrom } from "rxjs";
4
4
  import { v4 } from "uuid";
5
5
  import { distinctUntilChanged as distinctUntilChanged$1, map as map$1 } from "rxjs/operators";
6
6
 
@@ -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 = getLogger();
134
+ const logger$30 = getLogger();
135
135
  const GET_PARAMS = {
136
136
  method: "GET",
137
137
  headers: { Accept: "application/json" }
@@ -143,7 +143,7 @@ const POST_PARAMS = {
143
143
  "Content-Type": "application/json"
144
144
  }
145
145
  };
146
- var HTTPRequestController = class HTTPRequestController {
146
+ var HTTPRequestController = class HTTPRequestController extends Destroyable {
147
147
  static {
148
148
  this.defaultMaxRetries = 3;
149
149
  }
@@ -164,11 +164,12 @@ var HTTPRequestController = class HTTPRequestController {
164
164
  ]);
165
165
  }
166
166
  constructor(baseURL, getCredential, options = {}) {
167
+ super();
167
168
  this.baseURL = baseURL;
168
169
  this.getCredential = getCredential;
169
- this._responses$ = new Subject();
170
- this._errors$ = new Subject();
171
- this._status$ = new BehaviorSubject("idle");
170
+ this._responses$ = this.createSubject();
171
+ this._errors$ = this.createSubject();
172
+ this._status$ = this.createBehaviorSubject("idle");
172
173
  this.maxRetries = options.maxRetries ?? HTTPRequestController.defaultMaxRetries;
173
174
  this.retryDelayMin = options.retryDelayMin ?? HTTPRequestController.defaultRetryDelayMinMs;
174
175
  this.retryDelayMax = options.retryDelayMax ?? HTTPRequestController.defaultRetryDelayMaxMs;
@@ -194,7 +195,7 @@ var HTTPRequestController = class HTTPRequestController {
194
195
  this._responses$.next(response);
195
196
  return response;
196
197
  } catch (error) {
197
- logger$28.error("[HTTPRequestController] Request error:", error);
198
+ logger$30.error("[HTTPRequestController] Request error:", error);
198
199
  this._status$.next("error");
199
200
  const err = error instanceof Error ? error : new Error("HTTP request failed", { cause: error });
200
201
  this._errors$.next(err);
@@ -221,7 +222,7 @@ var HTTPRequestController = class HTTPRequestController {
221
222
  const url = this.buildURL(request.url);
222
223
  const headers = this.buildHeaders(request.headers);
223
224
  const timeout$1 = request.timeout ?? this.requestTimeout;
224
- logger$28.debug("[HTTPRequestController] Executing request:", {
225
+ logger$30.debug("[HTTPRequestController] Executing request:", {
225
226
  method: request.method,
226
227
  url,
227
228
  headers: Object.keys(headers).reduce((acc, key) => {
@@ -241,7 +242,7 @@ var HTTPRequestController = class HTTPRequestController {
241
242
  });
242
243
  clearTimeout(timeoutId);
243
244
  const httpResponse = await this.convertResponse(response);
244
- logger$28.debug("[HTTPRequestController] Response received:", {
245
+ logger$30.debug("[HTTPRequestController] Response received:", {
245
246
  status: response.status,
246
247
  statusText: response.statusText,
247
248
  headers: [...response.headers.entries()],
@@ -251,7 +252,7 @@ var HTTPRequestController = class HTTPRequestController {
251
252
  } catch (error) {
252
253
  clearTimeout(timeoutId);
253
254
  if (error instanceof Error && error.name === "AbortError") throw new RequestTimeoutError(`Request timeout after ${timeout$1}ms`, { cause: error });
254
- logger$28.error("[HTTPRequestController] Request failed:", error);
255
+ logger$30.error("[HTTPRequestController] Request failed:", error);
255
256
  throw error;
256
257
  }
257
258
  }
@@ -265,8 +266,8 @@ var HTTPRequestController = class HTTPRequestController {
265
266
  const credential = this.getCredential();
266
267
  if (credential.token) {
267
268
  headers.Authorization = `Bearer ${credential.token}`;
268
- logger$28.debug("[HTTPRequestController] Using Bearer token auth, token length:", credential.token.length);
269
- } 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");
270
271
  return headers;
271
272
  }
272
273
  /**
@@ -478,6 +479,18 @@ const DEFAULT_ICE_DISCONNECTED_GRACE_PERIOD_MS = 3e3;
478
479
  const DEFAULT_ICE_RESTART_TIMEOUT_MS$1 = 5e3;
479
480
  /** Maximum recovery attempts before emitting 'max_attempts_reached'. */
480
481
  const DEFAULT_MAX_RECOVERY_ATTEMPTS = 3;
482
+ /** Upper bound in ms for waiting on iceGatheringState === 'complete' after an ICE restart. */
483
+ const ICE_GATHERING_COMPLETE_TIMEOUT_MS = 1e4;
484
+ /** Upper bound in ms for waiting on RTCPeerConnection.connectionState === 'connected' after a recovery ICE restart. */
485
+ const PEER_CONNECTION_RECOVERY_WAIT_MS = 5e3;
486
+ /** Polling interval in ms while waiting for RTCPeerConnection.connectionState to transition. */
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;
481
494
  /** Whether to persist device selections to storage by default. */
482
495
  const DEFAULT_PERSIST_DEVICE_SELECTION = true;
483
496
  /** Whether to auto-apply device changes to active calls by default. */
@@ -526,7 +539,7 @@ function fromMsToSec(milliseconds) {
526
539
 
527
540
  //#endregion
528
541
  //#region src/containers/PreferencesContainer.ts
529
- const logger$27 = getLogger();
542
+ const logger$29 = getLogger();
530
543
  var PreferencesContainer = class PreferencesContainer {
531
544
  static get instance() {
532
545
  this._instance ??= new PreferencesContainer();
@@ -1188,7 +1201,7 @@ var ClientPreferences = class {
1188
1201
  if (!this._storage) return;
1189
1202
  const data = collectStoredPreferences();
1190
1203
  this._storage.setItem(PREFERENCES_STORAGE_KEY, data, "local").catch((error) => {
1191
- logger$27.error(`[ClientPreferences] Failed to save preferences: ${String(error)}`);
1204
+ logger$29.error(`[ClientPreferences] Failed to save preferences: ${String(error)}`);
1192
1205
  });
1193
1206
  }
1194
1207
  /** Loads preferences from storage and applies them to the container. */
@@ -1197,7 +1210,7 @@ var ClientPreferences = class {
1197
1210
  this._storage.getItem(PREFERENCES_STORAGE_KEY, "local").then((stored) => {
1198
1211
  if (stored) applyStoredPreferences(stored);
1199
1212
  }).catch((error) => {
1200
- logger$27.error(`[ClientPreferences] Failed to load preferences: ${String(error)}`);
1213
+ logger$29.error(`[ClientPreferences] Failed to load preferences: ${String(error)}`);
1201
1214
  });
1202
1215
  }
1203
1216
  };
@@ -1218,7 +1231,7 @@ function toError(value) {
1218
1231
 
1219
1232
  //#endregion
1220
1233
  //#region src/controllers/NavigatorDeviceController.ts
1221
- const logger$26 = getLogger();
1234
+ const logger$28 = getLogger();
1222
1235
  /** Maps a device kind to its storage key. */
1223
1236
  const DEVICE_STORAGE_KEYS = {
1224
1237
  audioinput: DEVICE_STORAGE_KEY_AUDIO_INPUT,
@@ -1240,7 +1253,7 @@ var NavigatorDeviceController = class extends Destroyable {
1240
1253
  super();
1241
1254
  this.webRTCApiProvider = webRTCApiProvider;
1242
1255
  this.deviceChangeHandler = () => {
1243
- logger$26.debug("[DeviceController] Device change detected");
1256
+ logger$28.debug("[DeviceController] Device change detected");
1244
1257
  this.enumerateDevices();
1245
1258
  };
1246
1259
  this._devicesState$ = this.createBehaviorSubject(initialDevicesState);
@@ -1305,13 +1318,13 @@ var NavigatorDeviceController = class extends Destroyable {
1305
1318
  return this.cachedObservable("videoInputDevices$", () => this._devicesState$.pipe(map((state) => state.videoinput), distinctUntilChanged(), takeUntil(this.destroyed$)));
1306
1319
  }
1307
1320
  get selectedAudioInputDevice$() {
1308
- return this.cachedObservable("selectedAudioInputDevice$", () => this._selectedDevicesState$.asObservable().pipe(map((state) => state.audioinput), distinctUntilChanged(), takeUntil(this.destroyed$), tap((info) => logger$26.debug("[DeviceController] Selected audio input device changed:", info))));
1321
+ return this.cachedObservable("selectedAudioInputDevice$", () => this._selectedDevicesState$.asObservable().pipe(map((state) => state.audioinput), distinctUntilChanged(), takeUntil(this.destroyed$), tap((info) => logger$28.debug("[DeviceController] Selected audio input device changed:", info))));
1309
1322
  }
1310
1323
  get selectedAudioOutputDevice$() {
1311
- return this.cachedObservable("selectedAudioOutputDevice$", () => this._selectedDevicesState$.asObservable().pipe(map((state) => state.audiooutput), distinctUntilChanged(), takeUntil(this.destroyed$), tap((info) => logger$26.debug("[DeviceController] Selected audio output device changed:", info))));
1324
+ return this.cachedObservable("selectedAudioOutputDevice$", () => this._selectedDevicesState$.asObservable().pipe(map((state) => state.audiooutput), distinctUntilChanged(), takeUntil(this.destroyed$), tap((info) => logger$28.debug("[DeviceController] Selected audio output device changed:", info))));
1312
1325
  }
1313
1326
  get selectedVideoInputDevice$() {
1314
- return this.cachedObservable("selectedVideoInputDevice$", () => this._selectedDevicesState$.asObservable().pipe(map((state) => state.videoinput), distinctUntilChanged(), takeUntil(this.destroyed$), tap((info) => logger$26.debug("[DeviceController] Selected video input device changed:", info))));
1327
+ return this.cachedObservable("selectedVideoInputDevice$", () => this._selectedDevicesState$.asObservable().pipe(map((state) => state.videoinput), distinctUntilChanged(), takeUntil(this.destroyed$), tap((info) => logger$28.debug("[DeviceController] Selected video input device changed:", info))));
1315
1328
  }
1316
1329
  get selectedAudioInputDevice() {
1317
1330
  if (this._audioInputDisabled$.value) return null;
@@ -1386,7 +1399,7 @@ var NavigatorDeviceController = class extends Destroyable {
1386
1399
  if (device) this.persistDeviceSelection("audioinput", device);
1387
1400
  }
1388
1401
  selectVideoInputDevice(device) {
1389
- logger$26.debug("[DeviceController] Setting selected video input device:", device);
1402
+ logger$28.debug("[DeviceController] Setting selected video input device:", device);
1390
1403
  if (this._videoInputDisabled$.value && device) this._videoInputDisabled$.next(false);
1391
1404
  const previous = this._selectedDevicesState$.value.videoinput;
1392
1405
  if (previous && previous.deviceId !== device?.deviceId) this._deviceHistory.push("videoinput", previous);
@@ -1443,7 +1456,7 @@ var NavigatorDeviceController = class extends Destroyable {
1443
1456
  }
1444
1457
  const fromHistory = this._deviceHistory.findInHistory(kind, devices);
1445
1458
  if (fromHistory) {
1446
- 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}`);
1447
1460
  this.emitDeviceRecovered(kind, selected, fromHistory, "device_disconnected");
1448
1461
  return fromHistory;
1449
1462
  }
@@ -1496,7 +1509,7 @@ var NavigatorDeviceController = class extends Destroyable {
1496
1509
  try {
1497
1510
  await this._storageManager.setItem(DEVICE_STORAGE_KEYS[kind], stored, "local");
1498
1511
  } catch (error) {
1499
- 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);
1500
1513
  }
1501
1514
  }
1502
1515
  async loadPersistedDevices() {
@@ -1512,7 +1525,7 @@ var NavigatorDeviceController = class extends Destroyable {
1512
1525
  [kind]: stored
1513
1526
  };
1514
1527
  } catch (error) {
1515
- 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);
1516
1529
  }
1517
1530
  }
1518
1531
  /** Clears device history, persisted selections, and re-enumerates devices. */
@@ -1530,7 +1543,7 @@ var NavigatorDeviceController = class extends Destroyable {
1530
1543
  this.disableDeviceMonitoring();
1531
1544
  this.webRTCApiProvider.mediaDevices.addEventListener("devicechange", this.deviceChangeHandler);
1532
1545
  if (PreferencesContainer.instance.devicePollingInterval > 0) this._devicesPoolingSubscription = interval(PreferencesContainer.instance.devicePollingInterval).subscribe(() => {
1533
- logger$26.debug("[DeviceController] Polling devices due to interval");
1546
+ logger$28.debug("[DeviceController] Polling devices due to interval");
1534
1547
  this.enumerateDevices();
1535
1548
  });
1536
1549
  this.enumerateDevices();
@@ -1556,13 +1569,13 @@ var NavigatorDeviceController = class extends Destroyable {
1556
1569
  videoinput: []
1557
1570
  });
1558
1571
  this._devicesState$.next(devicesByKind);
1559
- logger$26.debug("[DeviceController] Devices enumerated:", {
1572
+ logger$28.debug("[DeviceController] Devices enumerated:", {
1560
1573
  audioInputs: devicesByKind.audioinput.length,
1561
1574
  audioOutputs: devicesByKind.audiooutput.length,
1562
1575
  videoInputs: devicesByKind.videoinput.length
1563
1576
  });
1564
1577
  } catch (error) {
1565
- logger$26.error("[DeviceController] Failed to enumerate devices:", error);
1578
+ logger$28.error("[DeviceController] Failed to enumerate devices:", error);
1566
1579
  this._errors$.next(toError(error));
1567
1580
  }
1568
1581
  }
@@ -1578,7 +1591,7 @@ var NavigatorDeviceController = class extends Destroyable {
1578
1591
  stream.getTracks().forEach((t) => t.stop());
1579
1592
  return capabilities;
1580
1593
  } catch (error) {
1581
- logger$26.error("[DeviceController] Failed to get device capabilities:", error);
1594
+ logger$28.error("[DeviceController] Failed to get device capabilities:", error);
1582
1595
  this._errors$.next(toError(error));
1583
1596
  throw error;
1584
1597
  }
@@ -1730,15 +1743,15 @@ var DependencyContainer = class {
1730
1743
  this._baseURL = this.apiHost;
1731
1744
  this._credential = {};
1732
1745
  }
1733
- get subscriberId() {
1734
- return this.subscriber.id;
1746
+ get userId() {
1747
+ return this.user.id;
1735
1748
  }
1736
- get subscriber() {
1737
- if (!this._subscriber) throw new DependencyError("Subscriber");
1738
- return this._subscriber;
1749
+ get user() {
1750
+ if (!this._user) throw new DependencyError("User");
1751
+ return this._user;
1739
1752
  }
1740
- set subscriber(subscriber) {
1741
- this._subscriber = subscriber;
1753
+ set user(user) {
1754
+ this._user = user;
1742
1755
  }
1743
1756
  get storage() {
1744
1757
  if (!this._storageManager) {
@@ -1784,16 +1797,16 @@ var DependencyContainer = class {
1784
1797
  this._deviceController = void 0;
1785
1798
  }
1786
1799
  get authorizationStateKey() {
1787
- return `sw:${this.subscriberId}:as`;
1800
+ return `sw:${this.userId}:as`;
1788
1801
  }
1789
1802
  get protocolKey() {
1790
- return `sw:${this.subscriberId}:pt`;
1803
+ return `sw:${this.userId}:pt`;
1791
1804
  }
1792
1805
  get attachedCallsKey() {
1793
- return `sw:${this.subscriberId}:att`;
1806
+ return `sw:${this.userId}:att`;
1794
1807
  }
1795
- getSubscriberFromAddressId() {
1796
- return this.subscriber.addresses[0]?.id ?? "";
1808
+ getUserFromAddressId() {
1809
+ return this.user.addresses[0]?.id ?? "";
1797
1810
  }
1798
1811
  set baseURL(baseURL) {
1799
1812
  this._baseURL = baseURL;
@@ -1829,7 +1842,7 @@ var DependencyContainer = class {
1829
1842
 
1830
1843
  //#endregion
1831
1844
  //#region src/controllers/CryptoController.ts
1832
- const logger$25 = getLogger();
1845
+ const logger$27 = getLogger();
1833
1846
  const DPOP_DB_NAME = "sw-dpop";
1834
1847
  const DPOP_DB_VERSION = 1;
1835
1848
  const DPOP_STORE_NAME = "keys";
@@ -1888,7 +1901,7 @@ async function loadKeyPairFromDB() {
1888
1901
  tx.oncomplete = () => db.close();
1889
1902
  });
1890
1903
  } catch (error) {
1891
- 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);
1892
1905
  return null;
1893
1906
  }
1894
1907
  }
@@ -1908,7 +1921,7 @@ async function saveKeyPairToDB(keyPair) {
1908
1921
  };
1909
1922
  });
1910
1923
  } catch (error) {
1911
- 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);
1912
1925
  }
1913
1926
  }
1914
1927
  async function deleteKeyPairFromDB() {
@@ -1927,7 +1940,7 @@ async function deleteKeyPairFromDB() {
1927
1940
  };
1928
1941
  });
1929
1942
  } catch (error) {
1930
- 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);
1931
1944
  }
1932
1945
  }
1933
1946
  /**
@@ -1987,13 +2000,13 @@ var CryptoController = class {
1987
2000
  this._publicJwk = await crypto.subtle.exportKey("jwk", stored.publicKey);
1988
2001
  this._fingerprint = await computeJwkThumbprint(this._publicJwk);
1989
2002
  this._initialized = true;
1990
- 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);
1991
2004
  return this._fingerprint;
1992
2005
  } catch (error) {
1993
- 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);
1994
2007
  await deleteKeyPairFromDB();
1995
2008
  }
1996
- logger$25.debug("[DPoP] Generating RSA key pair");
2009
+ logger$27.debug("[DPoP] Generating RSA key pair");
1997
2010
  this._keyPair = await crypto.subtle.generateKey({
1998
2011
  name: "RSASSA-PKCS1-v1_5",
1999
2012
  modulusLength: 2048,
@@ -2008,7 +2021,7 @@ var CryptoController = class {
2008
2021
  this._fingerprint = await computeJwkThumbprint(this._publicJwk);
2009
2022
  this._initialized = true;
2010
2023
  await saveKeyPairToDB(this._keyPair);
2011
- 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);
2012
2025
  return this._fingerprint;
2013
2026
  }
2014
2027
  /**
@@ -2074,7 +2087,7 @@ var CryptoController = class {
2074
2087
  this._fingerprint = null;
2075
2088
  this._initialized = false;
2076
2089
  deleteKeyPairFromDB();
2077
- logger$25.debug("[DPoP] Controller destroyed");
2090
+ logger$27.debug("[DPoP] Controller destroyed");
2078
2091
  }
2079
2092
  get publicJwk() {
2080
2093
  if (!this._publicJwk) throw new DPoPInitError("CryptoController not initialized. Call init() first.");
@@ -2097,7 +2110,7 @@ var CryptoController = class {
2097
2110
 
2098
2111
  //#endregion
2099
2112
  //#region src/controllers/NetworkMonitor.ts
2100
- const logger$24 = getLogger();
2113
+ const logger$26 = getLogger();
2101
2114
  /**
2102
2115
  * Safely check whether we are running in a browser environment
2103
2116
  * with `window` and the relevant event targets.
@@ -2154,7 +2167,7 @@ var NetworkMonitor = class extends Destroyable {
2154
2167
  }
2155
2168
  attachListeners() {
2156
2169
  if (!hasBrowserNetworkEvents()) {
2157
- logger$24.debug("NetworkMonitor: no browser environment detected, skipping event listeners");
2170
+ logger$26.debug("NetworkMonitor: no browser environment detected, skipping event listeners");
2158
2171
  return;
2159
2172
  }
2160
2173
  window.addEventListener("online", this._onOnline);
@@ -2162,7 +2175,7 @@ var NetworkMonitor = class extends Destroyable {
2162
2175
  const connection = getNetworkConnection();
2163
2176
  if (connection) connection.addEventListener("change", this._onConnectionChange);
2164
2177
  this._listenersAttached = true;
2165
- logger$24.debug("NetworkMonitor: event listeners attached");
2178
+ logger$26.debug("NetworkMonitor: event listeners attached");
2166
2179
  }
2167
2180
  removeListeners() {
2168
2181
  if (!this._listenersAttached) return;
@@ -2173,10 +2186,10 @@ var NetworkMonitor = class extends Destroyable {
2173
2186
  if (connection) connection.removeEventListener("change", this._onConnectionChange);
2174
2187
  }
2175
2188
  this._listenersAttached = false;
2176
- logger$24.debug("NetworkMonitor: event listeners removed");
2189
+ logger$26.debug("NetworkMonitor: event listeners removed");
2177
2190
  }
2178
2191
  handleOnline() {
2179
- logger$24.info("NetworkMonitor: browser went online");
2192
+ logger$26.info("NetworkMonitor: browser went online");
2180
2193
  this._isOnline$.next(true);
2181
2194
  this._networkChange$.next({
2182
2195
  type: "online",
@@ -2185,7 +2198,7 @@ var NetworkMonitor = class extends Destroyable {
2185
2198
  });
2186
2199
  }
2187
2200
  handleOffline() {
2188
- logger$24.info("NetworkMonitor: browser went offline");
2201
+ logger$26.info("NetworkMonitor: browser went offline");
2189
2202
  this._isOnline$.next(false);
2190
2203
  this._networkChange$.next({
2191
2204
  type: "offline",
@@ -2194,7 +2207,7 @@ var NetworkMonitor = class extends Destroyable {
2194
2207
  }
2195
2208
  handleConnectionChange() {
2196
2209
  const networkType = getNetworkType();
2197
- logger$24.info(`NetworkMonitor: connection changed — effectiveType=${networkType ?? "unknown"}`);
2210
+ logger$26.info(`NetworkMonitor: connection changed — effectiveType=${networkType ?? "unknown"}`);
2198
2211
  this._networkChange$.next({
2199
2212
  type: "connection_change",
2200
2213
  timestamp: Date.now(),
@@ -2309,7 +2322,7 @@ function getNavigatorMediaDevices() {
2309
2322
 
2310
2323
  //#endregion
2311
2324
  //#region src/controllers/PreflightRunner.ts
2312
- const logger$23 = getLogger();
2325
+ const logger$25 = getLogger();
2313
2326
  const DEFAULT_MEDIA_TEST_DURATION_S = 10;
2314
2327
  const ICE_GATHERING_TIMEOUT_MS = 1e4;
2315
2328
  const SIGNALING_RTT_TIMEOUT_MS = 5e3;
@@ -2358,7 +2371,7 @@ var PreflightRunner = class extends Destroyable {
2358
2371
  if (!this._options.skipMediaTest) try {
2359
2372
  bandwidth = await this.testMediaBandwidth(destination);
2360
2373
  } catch (error) {
2361
- logger$23.warn("[PreflightRunner] Media bandwidth test failed:", error);
2374
+ logger$25.warn("[PreflightRunner] Media bandwidth test failed:", error);
2362
2375
  warnings.push("Media bandwidth test failed");
2363
2376
  }
2364
2377
  return {
@@ -2370,7 +2383,7 @@ var PreflightRunner = class extends Destroyable {
2370
2383
  warnings
2371
2384
  };
2372
2385
  } catch (error) {
2373
- logger$23.error("[PreflightRunner] Preflight test failed:", error);
2386
+ logger$25.error("[PreflightRunner] Preflight test failed:", error);
2374
2387
  throw new PreflightError("preflight", error instanceof Error ? error : new Error(String(error)));
2375
2388
  } finally {
2376
2389
  this.destroy();
@@ -2401,7 +2414,7 @@ var PreflightRunner = class extends Destroyable {
2401
2414
  if (track.kind === "video" && track.readyState === "live") videoWorking = true;
2402
2415
  }
2403
2416
  } catch (error) {
2404
- logger$23.warn("[PreflightRunner] Device test failed:", error);
2417
+ logger$25.warn("[PreflightRunner] Device test failed:", error);
2405
2418
  } finally {
2406
2419
  if (audioStream) audioStream.getTracks().forEach((t) => t.stop());
2407
2420
  }
@@ -2459,7 +2472,7 @@ var PreflightRunner = class extends Destroyable {
2459
2472
  rttMs
2460
2473
  };
2461
2474
  } catch (error) {
2462
- logger$23.warn("[PreflightRunner] ICE connectivity test failed:", error);
2475
+ logger$25.warn("[PreflightRunner] ICE connectivity test failed:", error);
2463
2476
  return {
2464
2477
  type: "failed",
2465
2478
  turnReachable: false,
@@ -2506,7 +2519,7 @@ var PreflightRunner = class extends Destroyable {
2506
2519
 
2507
2520
  //#endregion
2508
2521
  //#region src/controllers/VisibilityController.ts
2509
- const logger$22 = getLogger();
2522
+ const logger$24 = getLogger();
2510
2523
  /**
2511
2524
  * Checks whether the document visibility API is available.
2512
2525
  */
@@ -2543,8 +2556,8 @@ var VisibilityController = class extends Destroyable {
2543
2556
  this._boundHandler = this._handleVisibilityChange.bind(this);
2544
2557
  if (this._hasVisibilityApi) {
2545
2558
  document.addEventListener("visibilitychange", this._boundHandler);
2546
- logger$22.debug("VisibilityController: listening for visibilitychange events");
2547
- } 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");
2548
2561
  }
2549
2562
  /**
2550
2563
  * Observable of the current visibility state.
@@ -2569,7 +2582,7 @@ var VisibilityController = class extends Destroyable {
2569
2582
  destroy() {
2570
2583
  if (this._hasVisibilityApi) {
2571
2584
  document.removeEventListener("visibilitychange", this._boundHandler);
2572
- logger$22.debug("VisibilityController: removed visibilitychange listener");
2585
+ logger$24.debug("VisibilityController: removed visibilitychange listener");
2573
2586
  }
2574
2587
  super.destroy();
2575
2588
  }
@@ -2587,7 +2600,7 @@ var VisibilityController = class extends Destroyable {
2587
2600
  timestamp: Date.now()
2588
2601
  };
2589
2602
  this._visibilityChange$.next(changeEvent);
2590
- logger$22.debug("VisibilityController: visibility changed", {
2603
+ logger$24.debug("VisibilityController: visibility changed", {
2591
2604
  from: previousState,
2592
2605
  to: newState
2593
2606
  });
@@ -2619,14 +2632,14 @@ var Fetchable = class extends Destroyable {
2619
2632
  };
2620
2633
 
2621
2634
  //#endregion
2622
- //#region src/core/entities/Subscriber.ts
2635
+ //#region src/core/entities/User.ts
2623
2636
  /**
2624
- * Authenticated subscriber profile.
2637
+ * Authenticated user profile.
2625
2638
  *
2626
2639
  * Fetched automatically when a {@link SignalWire} connects.
2627
2640
  * Contains identity, contact, and organization details.
2628
2641
  */
2629
- var Subscriber = class extends Fetchable {
2642
+ var User = class extends Fetchable {
2630
2643
  constructor(http) {
2631
2644
  super("/api/fabric/subscriber/info", http);
2632
2645
  }
@@ -2790,20 +2803,18 @@ const RPCEventAckResponse = (id) => makeRPCResponse({
2790
2803
 
2791
2804
  //#endregion
2792
2805
  //#region src/managers/AttachManager.ts
2793
- const logger$21 = getLogger();
2806
+ const logger$23 = getLogger();
2794
2807
  var AttachManager = class {
2795
2808
  constructor(storage, deviceController, reconnectCallsTimeout, attachKey) {
2796
2809
  this.storage = storage;
2797
2810
  this.deviceController = deviceController;
2798
2811
  this.reconnectCallsTimeout = reconnectCallsTimeout;
2799
2812
  this.attachKey = attachKey;
2813
+ this.writeQueue = Promise.resolve();
2800
2814
  }
2801
2815
  async detachAll() {
2802
- const attached = await this.readAttached();
2803
- for (const callId of Object.keys(attached)) await this.detach({
2804
- id: callId,
2805
- nodeId: attached[callId].nodeId,
2806
- mediaDirections: attached[callId].mediaDirections
2816
+ await this.mutate((attached) => {
2817
+ return {};
2807
2818
  });
2808
2819
  }
2809
2820
  setSession(session) {
@@ -2813,7 +2824,7 @@ var AttachManager = class {
2813
2824
  try {
2814
2825
  return await this.storage.getItem(this.attachKey) ?? {};
2815
2826
  } catch (error) {
2816
- 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);
2817
2828
  return {};
2818
2829
  }
2819
2830
  }
@@ -2821,34 +2832,50 @@ var AttachManager = class {
2821
2832
  try {
2822
2833
  await this.storage.setItem(this.attachKey, attached);
2823
2834
  } catch (error) {
2824
- 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);
2825
2836
  }
2826
2837
  }
2838
+ /**
2839
+ * Serialize a read-modify-write operation against the attached-calls
2840
+ * storage. The mutator receives the current state and returns the new
2841
+ * state. Concurrent calls queue behind the in-flight one so writes never
2842
+ * interleave.
2843
+ */
2844
+ async mutate(mutator) {
2845
+ const next = this.writeQueue.then(async () => {
2846
+ const updated = await mutator(await this.readAttached());
2847
+ await this.writeAttached(updated);
2848
+ });
2849
+ this.writeQueue = next.catch(() => void 0);
2850
+ return next;
2851
+ }
2827
2852
  async attach(call) {
2828
2853
  if (!call.to) {
2829
- logger$21.warn("[AttachManager] Skip attach for calls with no destination");
2854
+ logger$23.warn("[AttachManager] Skip attach for calls with no destination");
2830
2855
  return;
2831
2856
  }
2857
+ const destination = call.to;
2832
2858
  const attachment = {
2833
2859
  nodeId: call.nodeId,
2834
- destination: call.to,
2860
+ destination,
2835
2861
  mediaDirections: call.mediaDirections,
2836
2862
  audioInputDevice: call.mediaDirections.audio !== "inactive" ? this.deviceController.selectedAudioInputDevice : null,
2837
2863
  videoInputDevice: call.mediaDirections.video !== "inactive" ? this.deviceController.selectedVideoInputDevice : null,
2838
2864
  attachedAt: Date.now()
2839
2865
  };
2840
- const updated = {
2841
- ...await this.readAttached(),
2866
+ await this.mutate((attached) => ({
2867
+ ...attached,
2842
2868
  [call.id]: attachment
2843
- };
2844
- await this.writeAttached(updated);
2869
+ }));
2845
2870
  }
2846
2871
  async detach(call) {
2847
- const { [call.id]: _, ...remaining } = await this.readAttached();
2848
- await this.writeAttached(remaining);
2872
+ await this.mutate((attached) => {
2873
+ const { [call.id]: _, ...remaining } = attached;
2874
+ return remaining;
2875
+ });
2849
2876
  }
2850
2877
  async flush() {
2851
- await this.writeAttached({});
2878
+ await this.mutate(() => ({}));
2852
2879
  }
2853
2880
  /**
2854
2881
  * Reattach to previously active calls by sending verto.invite with
@@ -2877,15 +2904,15 @@ var AttachManager = class {
2877
2904
  callId,
2878
2905
  ...options
2879
2906
  });
2880
- logger$21.info(`[AttachManager] Reattached call ${callId} (attempt ${attempt})`);
2907
+ logger$23.info(`[AttachManager] Reattached call ${callId} (attempt ${attempt})`);
2881
2908
  succeeded = true;
2882
2909
  break;
2883
2910
  } catch (error) {
2884
- 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);
2885
2912
  if (attempt < 3) await new Promise((r) => setTimeout(r, (attempt + 1) * 1e3));
2886
2913
  }
2887
2914
  if (!succeeded) {
2888
- 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`);
2889
2916
  await this.detach({
2890
2917
  id: callId,
2891
2918
  mediaDirections: attachment.mediaDirections
@@ -2920,20 +2947,31 @@ var AttachManager = class {
2920
2947
  };
2921
2948
  }
2922
2949
  /**
2923
- * Consume stored attachment data for a pending call (used by session-level
2924
- * verto.attach handler as a future path when server supports it).
2950
+ * Look up stored attachment data for a call id and return CallOptions
2951
+ * suitable for rehydrating a reattached call. Returns undefined when no
2952
+ * matching entry exists in storage.
2953
+ *
2954
+ * Used by the session-level verto.attach handler when the server pushes
2955
+ * an attach event for a call the client doesn't have an object for yet
2956
+ * (e.g. after a reload).
2925
2957
  */
2926
- consumePendingAttachment(_callId) {}
2927
- async detachExpired() {
2958
+ async consumePendingAttachment(callId) {
2928
2959
  const attached = await this.readAttached();
2960
+ if (!Object.hasOwn(attached, callId)) return;
2961
+ return this.buildCallOptions(attached[callId]);
2962
+ }
2963
+ async detachExpired() {
2929
2964
  const now = Date.now();
2930
2965
  const timeout$1 = this.reconnectCallsTimeout;
2931
- const expired = Object.entries(attached).filter(([, attachment]) => now - attachment.attachedAt > timeout$1);
2932
- if (expired.length > 0) {
2966
+ await this.mutate((attached) => {
2933
2967
  const remaining = { ...attached };
2934
- for (const [callId] of expired) delete remaining[callId];
2935
- await this.writeAttached(remaining);
2936
- }
2968
+ let changed = false;
2969
+ for (const [callId, attachment] of Object.entries(attached)) if (now - attachment.attachedAt > timeout$1) {
2970
+ delete remaining[callId];
2971
+ changed = true;
2972
+ }
2973
+ return changed ? remaining : attached;
2974
+ });
2937
2975
  }
2938
2976
  };
2939
2977
 
@@ -3188,7 +3226,7 @@ function toggleHandraiseMethod(is) {
3188
3226
 
3189
3227
  //#endregion
3190
3228
  //#region src/core/entities/Participant.ts
3191
- const logger$20 = getLogger();
3229
+ const logger$22 = getLogger();
3192
3230
  const initialState = {};
3193
3231
  /**
3194
3232
  * Represents a participant in a call.
@@ -3240,15 +3278,35 @@ var Participant = class extends Destroyable {
3240
3278
  get deaf$() {
3241
3279
  return this.cachedObservable("deaf$", () => this._state$.pipe(map$1((state) => state.deaf), distinctUntilChanged$1()));
3242
3280
  }
3243
- /** 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
+ */
3244
3289
  get inputVolume$() {
3245
3290
  return this.cachedObservable("inputVolume$", () => this._state$.pipe(map$1((state) => state.input_volume), distinctUntilChanged$1()));
3246
3291
  }
3247
- /** 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
+ */
3248
3299
  get outputVolume$() {
3249
3300
  return this.cachedObservable("outputVolume$", () => this._state$.pipe(map$1((state) => state.output_volume), distinctUntilChanged$1()));
3250
3301
  }
3251
- /** 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
+ */
3252
3310
  get inputSensitivity$() {
3253
3311
  return this.cachedObservable("inputSensitivity$", () => this._state$.pipe(map$1((state) => state.input_sensitivity), distinctUntilChanged$1()));
3254
3312
  }
@@ -3276,9 +3334,9 @@ var Participant = class extends Destroyable {
3276
3334
  get meta$() {
3277
3335
  return this.cachedObservable("meta$", () => this._state$.pipe(map$1((state) => state.meta), distinctUntilChanged$1()));
3278
3336
  }
3279
- /** Observable of the participant's subscriber ID. */
3280
- get subscriberId$() {
3281
- return this.cachedObservable("subscriberId$", () => this._state$.pipe(map$1((state) => state.subscriber_id), distinctUntilChanged$1()));
3337
+ /** Observable of the participant's user ID. */
3338
+ get userId$() {
3339
+ return this.cachedObservable("userId$", () => this._state$.pipe(map$1((state) => state.subscriber_id), distinctUntilChanged$1()));
3282
3340
  }
3283
3341
  /** Observable of the participant's address ID. */
3284
3342
  get addressId$() {
@@ -3336,15 +3394,25 @@ var Participant = class extends Destroyable {
3336
3394
  get deaf() {
3337
3395
  return this._state$.value.deaf ?? false;
3338
3396
  }
3339
- /** 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
+ */
3340
3402
  get inputVolume() {
3341
3403
  return this._state$.value.input_volume;
3342
3404
  }
3343
- /** 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
+ */
3344
3409
  get outputVolume() {
3345
3410
  return this._state$.value.output_volume;
3346
3411
  }
3347
- /** 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
+ */
3348
3416
  get inputSensitivity() {
3349
3417
  return this._state$.value.input_sensitivity;
3350
3418
  }
@@ -3372,8 +3440,8 @@ var Participant = class extends Destroyable {
3372
3440
  get meta() {
3373
3441
  return this._state$.value.meta;
3374
3442
  }
3375
- /** Subscriber ID of this participant, or `undefined` if not available. */
3376
- get subscriberId() {
3443
+ /** User ID of this participant, or `undefined` if not available. */
3444
+ get userId() {
3377
3445
  return this._state$.value.subscriber_id;
3378
3446
  }
3379
3447
  /** Address ID of this participant, or `undefined` if not available. */
@@ -3448,19 +3516,44 @@ var Participant = class extends Destroyable {
3448
3516
  async toggleLowbitrate() {
3449
3517
  throw new UnimplementedError();
3450
3518
  }
3451
- /** 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
+ */
3452
3532
  async setAudioInputSensitivity(value) {
3453
3533
  await this.executeMethod(this.id, "call.microphone.sensitivity.set", { sensitivity: value });
3454
3534
  }
3455
3535
  /**
3456
- * 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
+ *
3457
3543
  * @param value - Volume level (0-100).
3458
3544
  */
3459
3545
  async setAudioInputVolume(value) {
3460
3546
  await this.executeMethod(this.id, "call.microphone.volume.set", { volume: value });
3461
3547
  }
3462
3548
  /**
3463
- * 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
+ *
3464
3557
  * @param value - Volume level (0-100).
3465
3558
  */
3466
3559
  async setAudioOutputVolume(value) {
@@ -3566,7 +3659,7 @@ var SelfParticipant = class extends Participant {
3566
3659
  try {
3567
3660
  await this.vertoManager.addScreenMedia();
3568
3661
  } catch (error) {
3569
- logger$20.error("[Participant.startScreenShare] Screen share error:", error);
3662
+ logger$22.error("[Participant.startScreenShare] Screen share error:", error);
3570
3663
  }
3571
3664
  }
3572
3665
  /** Observable of the current screen share status. */
@@ -3586,7 +3679,7 @@ var SelfParticipant = class extends Participant {
3586
3679
  try {
3587
3680
  await this.vertoManager.addInputDevice(options);
3588
3681
  } catch (error) {
3589
- logger$20.error("[Participant.startScreenShare] Screen share error:", error);
3682
+ logger$22.error("[Participant.startScreenShare] Screen share error:", error);
3590
3683
  }
3591
3684
  }
3592
3685
  /** Removes an additional media input device by ID. */
@@ -3648,7 +3741,7 @@ var SelfParticipant = class extends Participant {
3648
3741
  */
3649
3742
  exitStudioModeIfActive() {
3650
3743
  if (this._studioAudio$.value) {
3651
- 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");
3652
3745
  this._studioAudio$.next(false);
3653
3746
  }
3654
3747
  }
@@ -3672,7 +3765,7 @@ var SelfParticipant = class extends Participant {
3672
3765
  try {
3673
3766
  await super.mute();
3674
3767
  } catch (error) {
3675
- 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);
3676
3769
  } finally {
3677
3770
  this.vertoManager.muteMainAudioInputDevice();
3678
3771
  }
@@ -3682,7 +3775,7 @@ var SelfParticipant = class extends Participant {
3682
3775
  try {
3683
3776
  await super.unmute();
3684
3777
  } catch (error) {
3685
- 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);
3686
3779
  } finally {
3687
3780
  await this.vertoManager.unmuteMainAudioInputDevice();
3688
3781
  }
@@ -3692,7 +3785,7 @@ var SelfParticipant = class extends Participant {
3692
3785
  try {
3693
3786
  await super.muteVideo();
3694
3787
  } catch (error) {
3695
- 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);
3696
3789
  } finally {
3697
3790
  this.vertoManager.muteMainVideoInputDevice();
3698
3791
  }
@@ -3702,7 +3795,7 @@ var SelfParticipant = class extends Participant {
3702
3795
  try {
3703
3796
  await super.unmuteVideo();
3704
3797
  } catch (error) {
3705
- 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);
3706
3799
  } finally {
3707
3800
  await this.vertoManager.unmuteMainVideoInputDevice();
3708
3801
  }
@@ -3796,7 +3889,7 @@ function isLayoutChangedPayload(value) {
3796
3889
 
3797
3890
  //#endregion
3798
3891
  //#region src/managers/CallEventsManager.ts
3799
- const logger$19 = getLogger();
3892
+ const logger$21 = getLogger();
3800
3893
  const initialSessionState = {};
3801
3894
  /** @internal */
3802
3895
  var CallEventsManager = class extends Destroyable {
@@ -3900,7 +3993,7 @@ var CallEventsManager = class extends Destroyable {
3900
3993
  }
3901
3994
  initSubscriptions() {
3902
3995
  this.subscribeTo(this.callJoinedEvent$, (callJoinedEvent) => {
3903
- 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:", {
3904
3997
  callId: callJoinedEvent.call_id,
3905
3998
  roomSessionId: callJoinedEvent.room_session_id
3906
3999
  });
@@ -3927,19 +4020,19 @@ var CallEventsManager = class extends Destroyable {
3927
4020
  if (this._self$.value?.capabilities.setLayout) this.updateLayouts();
3928
4021
  });
3929
4022
  this.subscribeTo(this.memberUpdates$, (member) => {
3930
- 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);
3931
4024
  this.upsertParticipant(member);
3932
4025
  });
3933
4026
  this.subscribeTo(this.webRtcCallSession.memberLeft$, (memberLeftEvent) => {
3934
- 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);
3935
4028
  const participants = { ...this._participants$.value };
3936
4029
  if (memberLeftEvent.member.member_id in participants) {
3937
4030
  delete participants[memberLeftEvent.member.member_id];
3938
4031
  this._participants$.next(participants);
3939
- } 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}`);
3940
4033
  });
3941
4034
  this.subscribeTo(this.webRtcCallSession.callUpdated$, (callUpdatedEvent) => {
3942
- logger$19.debug("[CallEventsManager] Handling call.updated event:", callUpdatedEvent);
4035
+ logger$21.debug("[CallEventsManager] Handling call.updated event:", callUpdatedEvent);
3943
4036
  const roomSession = callUpdatedEvent.room_session;
3944
4037
  this._sessionState$.next({
3945
4038
  ...this._sessionState$.value,
@@ -3954,7 +4047,7 @@ var CallEventsManager = class extends Destroyable {
3954
4047
  });
3955
4048
  });
3956
4049
  this.subscribeTo(this.layoutChangedEvent$, (layoutChangedEvent) => {
3957
- logger$19.debug("[CallEventsManager] Handling layout.changed event:", layoutChangedEvent);
4050
+ logger$21.debug("[CallEventsManager] Handling layout.changed event:", layoutChangedEvent);
3958
4051
  this._sessionState$.next({
3959
4052
  ...this._sessionState$.value,
3960
4053
  layout_name: layoutChangedEvent.id,
@@ -3964,10 +4057,10 @@ var CallEventsManager = class extends Destroyable {
3964
4057
  });
3965
4058
  }
3966
4059
  updateParticipantPositions(layoutChangedEvent) {
3967
- 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.");
3968
4061
  layoutChangedEvent.layers.filter((layer) => !!layer.member_id).filter((layer) => {
3969
4062
  if (!(layer.member_id in this._participants$.value)) {
3970
- 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}`);
3971
4064
  return false;
3972
4065
  }
3973
4066
  return true;
@@ -3990,7 +4083,7 @@ var CallEventsManager = class extends Destroyable {
3990
4083
  layouts: response.result.layouts
3991
4084
  });
3992
4085
  }).catch((error) => {
3993
- logger$19.error("[CallEventsManager] Error fetching layouts:", error);
4086
+ logger$21.error("[CallEventsManager] Error fetching layouts:", error);
3994
4087
  });
3995
4088
  }
3996
4089
  updateParticipants(members) {
@@ -4006,7 +4099,7 @@ var CallEventsManager = class extends Destroyable {
4006
4099
  }
4007
4100
  const participant = this._participants$.value[member.member_id];
4008
4101
  const oldValue = participant.value;
4009
- logger$19.debug("[CallEventsManager] Updating participant:", member.member_id, {
4102
+ logger$21.debug("[CallEventsManager] Updating participant:", member.member_id, {
4010
4103
  oldValue,
4011
4104
  newValue: member
4012
4105
  });
@@ -4019,17 +4112,17 @@ var CallEventsManager = class extends Destroyable {
4019
4112
  }
4020
4113
  get callJoinedEvent$() {
4021
4114
  return this.cachedObservable("callJoinedEvent$", () => this.webRtcCallSession.callEvent$.pipe(filter(isCallJoinedPayload), tap((event) => {
4022
- logger$19.debug("[CallEventsManager] Call joined event:", event);
4115
+ logger$21.debug("[CallEventsManager] Call joined event:", event);
4023
4116
  })));
4024
4117
  }
4025
4118
  get layoutChangedEvent$() {
4026
4119
  return this.cachedObservable("layoutChangedEvent$", () => this.webRtcCallSession.callEvent$.pipe(filterAs(isLayoutChangedPayload, "layout"), tap((event) => {
4027
- logger$19.debug("[CallEventsManager] Layout changed event:", event);
4120
+ logger$21.debug("[CallEventsManager] Layout changed event:", event);
4028
4121
  })));
4029
4122
  }
4030
4123
  get memberUpdates$() {
4031
4124
  return this.cachedObservable("memberUpdates$", () => merge(this.webRtcCallSession.memberJoined$, this.webRtcCallSession.memberUpdated$, this.webRtcCallSession.memberTalking$).pipe(map((event) => event.member), tap((event) => {
4032
- logger$19.debug("[CallEventsManager] Member update event:", event);
4125
+ logger$21.debug("[CallEventsManager] Member update event:", event);
4033
4126
  })));
4034
4127
  }
4035
4128
  destroy() {
@@ -4285,7 +4378,7 @@ function appendStereoParams(fmtpLine, maxBitrate) {
4285
4378
 
4286
4379
  //#endregion
4287
4380
  //#region src/controllers/ICEGatheringController.ts
4288
- const logger$18 = getLogger();
4381
+ const logger$20 = getLogger();
4289
4382
  var ICEGatheringController = class extends Destroyable {
4290
4383
  constructor(peerConnection, peerConnectionControllerNegotiating$, options = {}) {
4291
4384
  super();
@@ -4293,23 +4386,23 @@ var ICEGatheringController = class extends Destroyable {
4293
4386
  this.peerConnectionControllerNegotiating$ = peerConnectionControllerNegotiating$;
4294
4387
  this.onicegatheringstatechangeHandler = () => {
4295
4388
  const { iceGatheringState } = this.peerConnection;
4296
- logger$18.debug(`[ICEGatheringController] ICE gathering state changed to: ${iceGatheringState}`);
4389
+ logger$20.debug(`[ICEGatheringController] ICE gathering state changed to: ${iceGatheringState}`);
4297
4390
  if (iceGatheringState === "gathering") this._iceCandidatesState.next({
4298
4391
  state: "gathering",
4299
4392
  validSDP: false
4300
4393
  });
4301
4394
  };
4302
4395
  this.onicecandidateHandler = (event) => {
4303
- logger$18.debug("[ICEGatheringController] ICE candidate event received:", event.candidate);
4396
+ logger$20.debug("[ICEGatheringController] ICE candidate event received:", event.candidate);
4304
4397
  this.removeTimer("iceCandidateTimer");
4305
4398
  if (event.candidate) this.iceCandidateTimer = setTimeout(() => {
4306
4399
  if (this.peerConnection.iceGatheringState !== "complete") {
4307
- logger$18.warn("[ICEGatheringController] ICE candidate timeout, using current SDP");
4400
+ logger$20.warn("[ICEGatheringController] ICE candidate timeout, using current SDP");
4308
4401
  this.handleICECandidateTimeout();
4309
4402
  }
4310
4403
  }, this.iceCandidateTimeout);
4311
4404
  else {
4312
- logger$18.debug("[ICEGatheringController] ICE gathering completed: null candidate received");
4405
+ logger$20.debug("[ICEGatheringController] ICE gathering completed: null candidate received");
4313
4406
  this.removeTimer("iceGatheringTimer");
4314
4407
  this.handleICEGatheringComplete();
4315
4408
  }
@@ -4327,7 +4420,7 @@ var ICEGatheringController = class extends Destroyable {
4327
4420
  this.setupEventListeners();
4328
4421
  this.iceGatheringTimer = setTimeout(() => {
4329
4422
  if (this.peerConnection.iceGatheringState !== "complete") {
4330
- logger$18.warn("[ICEGatheringController] ICE gathering timeout, using current SDP");
4423
+ logger$20.warn("[ICEGatheringController] ICE gathering timeout, using current SDP");
4331
4424
  this.handleICEGatheringTimeout();
4332
4425
  }
4333
4426
  }, this.iceGatheringTimeout);
@@ -4354,9 +4447,9 @@ var ICEGatheringController = class extends Destroyable {
4354
4447
  this.relayOnly = value;
4355
4448
  }
4356
4449
  handleICEGatheringComplete() {
4357
- logger$18.debug("[ICEGatheringController] Handling ICE gathering complete");
4358
- logger$18.debug(`[ICEGatheringController] Checking ICE gathering state: ${this.peerConnection.iceGatheringState}`);
4359
- 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");
4360
4453
  this._iceCandidatesState.next({
4361
4454
  state: "complete",
4362
4455
  validSDP: this.hasValidLocalDescriptionSDP
@@ -4372,21 +4465,21 @@ var ICEGatheringController = class extends Destroyable {
4372
4465
  this.removeTimer("iceGatheringTimer");
4373
4466
  const validSDP = this.hasValidLocalDescriptionSDP;
4374
4467
  if (validSDP) {
4375
- logger$18.debug("[ICEGatheringController] Local SDP is valid");
4468
+ logger$20.debug("[ICEGatheringController] Local SDP is valid");
4376
4469
  this._iceCandidatesState.next({
4377
4470
  state: "timeout",
4378
4471
  validSDP
4379
4472
  });
4380
4473
  this.stopGathering();
4381
- } 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);
4382
4475
  }
4383
4476
  handleICECandidateTimeout() {
4384
4477
  if (this.iceCandidateTimer) this.removeTimer("iceCandidateTimer");
4385
- logger$18.warn("[ICEGatheringController] ICE candidate timeout");
4478
+ logger$20.warn("[ICEGatheringController] ICE candidate timeout");
4386
4479
  const validSDP = this.hasValidLocalDescriptionSDP;
4387
4480
  if (!validSDP && !this.relayOnly) this.restartICEGatheringWithRelayOnly();
4388
4481
  else {
4389
- 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");
4390
4483
  this._iceCandidatesState.next({
4391
4484
  state: "timeout",
4392
4485
  validSDP
@@ -4395,13 +4488,13 @@ var ICEGatheringController = class extends Destroyable {
4395
4488
  }
4396
4489
  }
4397
4490
  restartICEGatheringWithRelayOnly() {
4398
- logger$18.debug("[ICEGatheringController] Restarting ICE gathering with relay-only candidates");
4491
+ logger$20.debug("[ICEGatheringController] Restarting ICE gathering with relay-only candidates");
4399
4492
  this.relayOnly = true;
4400
4493
  this.peerConnection.setConfiguration({
4401
4494
  ...this.peerConnection.getConfiguration(),
4402
4495
  iceTransportPolicy: "relay"
4403
4496
  });
4404
- if (!(this.peerConnection.connectionState === "connected")) this.peerConnection.restartIce();
4497
+ this.peerConnection.restartIce();
4405
4498
  }
4406
4499
  removeTimer(timer$1) {
4407
4500
  if (this[timer$1]) {
@@ -4410,7 +4503,7 @@ var ICEGatheringController = class extends Destroyable {
4410
4503
  }
4411
4504
  }
4412
4505
  clearAllTimers() {
4413
- logger$18.debug("[ICEGatheringController] Clearing all timers");
4506
+ logger$20.debug("[ICEGatheringController] Clearing all timers");
4414
4507
  this.removeTimer("iceGatheringTimer");
4415
4508
  this.removeTimer("iceCandidateTimer");
4416
4509
  }
@@ -4419,16 +4512,179 @@ var ICEGatheringController = class extends Destroyable {
4419
4512
  this.peerConnection.removeEventListener("icecandidate", this.onicecandidateHandler);
4420
4513
  }
4421
4514
  destroy() {
4422
- logger$18.debug("[ICEGatheringController] Destroying ICEGatheringController");
4515
+ logger$20.debug("[ICEGatheringController] Destroying ICEGatheringController");
4423
4516
  this.clearAllTimers();
4424
4517
  this.removeEventListeners();
4425
4518
  super.destroy();
4426
4519
  }
4427
4520
  };
4428
4521
 
4522
+ //#endregion
4523
+ //#region src/controllers/LocalAudioPipeline.ts
4524
+ const logger$19 = 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(interval(this._pollIntervalMs, animationFrameScheduler).pipe(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(map((level) => this.evaluateSpeaking(level)), 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
+
4429
4685
  //#endregion
4430
4686
  //#region src/controllers/LocalStreamController.ts
4431
- const logger$17 = getLogger();
4687
+ const logger$18 = getLogger();
4432
4688
  var LocalStreamController = class extends Destroyable {
4433
4689
  constructor(options) {
4434
4690
  super();
@@ -4466,26 +4722,26 @@ var LocalStreamController = class extends Destroyable {
4466
4722
  * Build the local media stream based on the provided options.
4467
4723
  */
4468
4724
  async buildLocalStream() {
4469
- logger$17.debug("[LocalStreamController] Building local media stream.");
4725
+ logger$18.debug("[LocalStreamController] Building local media stream.");
4470
4726
  let stream;
4471
4727
  if (this.options.inputAudioStream ?? this.options.inputVideoStream) {
4472
4728
  const tracks = [...this.options.inputAudioStream?.getTracks() ?? [], ...this.options.inputVideoStream?.getTracks() ?? []];
4473
4729
  stream = new MediaStream(tracks);
4474
4730
  } else if (this.options.propose === "screenshare") {
4475
- 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));
4476
4732
  stream = await this.options.getDisplayMedia({
4477
4733
  video: true,
4478
4734
  audio: Boolean(this.options.inputAudioDeviceConstraints)
4479
4735
  });
4480
- logger$17.debug("[LocalStreamController] Screen share media obtained:", stream);
4736
+ logger$18.debug("[LocalStreamController] Screen share media obtained:", stream);
4481
4737
  } else {
4482
4738
  const constraints = {
4483
4739
  audio: this.options.inputAudioDeviceConstraints,
4484
4740
  video: this.options.inputVideoDeviceConstraints
4485
4741
  };
4486
- logger$17.debug("[LocalStreamController] Requesting user media with constraints:", constraints);
4742
+ logger$18.debug("[LocalStreamController] Requesting user media with constraints:", constraints);
4487
4743
  stream = await this.options.getUserMedia(constraints);
4488
- logger$17.debug("[LocalStreamController] User media obtained:", stream);
4744
+ logger$18.debug("[LocalStreamController] User media obtained:", stream);
4489
4745
  }
4490
4746
  this._localStream$.next(stream);
4491
4747
  return stream;
@@ -4502,7 +4758,7 @@ var LocalStreamController = class extends Destroyable {
4502
4758
  this._localStream$.next(localStream);
4503
4759
  if (track.kind === "video") this._localVideoTracks$.next(localStream.getVideoTracks());
4504
4760
  else this._localAudioTracks$.next(localStream.getAudioTracks());
4505
- logger$17.debug(`[LocalStreamController] ${track.kind} track added:`, track.id);
4761
+ logger$18.debug(`[LocalStreamController] ${track.kind} track added:`, track.id);
4506
4762
  return localStream;
4507
4763
  }
4508
4764
  /**
@@ -4514,7 +4770,7 @@ var LocalStreamController = class extends Destroyable {
4514
4770
  const stream = this._localStream$.value;
4515
4771
  const track = stream?.getTracks().find((t) => t.id === trackId);
4516
4772
  if (!track) {
4517
- logger$17.debug(`[LocalStreamController] track not found: ${trackId}`);
4773
+ logger$18.debug(`[LocalStreamController] track not found: ${trackId}`);
4518
4774
  return;
4519
4775
  }
4520
4776
  track.removeEventListener("ended", this.mediaTrackEndedHandler);
@@ -4523,7 +4779,7 @@ var LocalStreamController = class extends Destroyable {
4523
4779
  this._localStream$.next(stream);
4524
4780
  if (track.kind === "video") this._localVideoTracks$.next(stream?.getVideoTracks() ?? []);
4525
4781
  else this._localAudioTracks$.next(stream?.getAudioTracks() ?? []);
4526
- logger$17.debug(`[LocalStreamController] ${track.kind} track removed:`, trackId);
4782
+ logger$18.debug(`[LocalStreamController] ${track.kind} track removed:`, trackId);
4527
4783
  return track;
4528
4784
  }
4529
4785
  /**
@@ -4558,7 +4814,7 @@ var LocalStreamController = class extends Destroyable {
4558
4814
  */
4559
4815
  stopAllTracks() {
4560
4816
  this._localStream$.value?.getTracks().forEach((track) => {
4561
- logger$17.debug(`[LocalStreamController] Stopping local track: ${track.kind}`);
4817
+ logger$18.debug(`[LocalStreamController] Stopping local track: ${track.kind}`);
4562
4818
  track.removeEventListener("ended", this.mediaTrackEndedHandler);
4563
4819
  track.stop();
4564
4820
  });
@@ -4574,7 +4830,7 @@ var LocalStreamController = class extends Destroyable {
4574
4830
 
4575
4831
  //#endregion
4576
4832
  //#region src/controllers/TransceiverController.ts
4577
- const logger$16 = getLogger();
4833
+ const logger$17 = getLogger();
4578
4834
  const getDirection = (send, recv) => {
4579
4835
  if (send && recv) return "sendrecv";
4580
4836
  else if (send && !recv) return "sendonly";
@@ -4676,7 +4932,7 @@ var TransceiverController = class extends Destroyable {
4676
4932
  sendEncodings: isAudio ? void 0 : this.sendEncodings,
4677
4933
  streams: direction === "recvonly" ? void 0 : [localStream]
4678
4934
  };
4679
- 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:`, {
4680
4936
  transceiver,
4681
4937
  transceiverParams
4682
4938
  });
@@ -4684,11 +4940,11 @@ var TransceiverController = class extends Destroyable {
4684
4940
  await transceiver.sender.replaceTrack(track);
4685
4941
  transceiver.direction = transceiverParams.direction;
4686
4942
  if (transceiverParams.streams?.some((stream) => Boolean(stream))) {
4687
- 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);
4688
4944
  transceiver.sender.setStreams(...transceiverParams.streams);
4689
4945
  }
4690
4946
  } else {
4691
- 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);
4692
4948
  this.peerConnection.addTransceiver(track, transceiverParams);
4693
4949
  }
4694
4950
  }
@@ -4702,13 +4958,13 @@ var TransceiverController = class extends Destroyable {
4702
4958
  if (options.updateTransceiverDirection) transceiver.direction = "inactive";
4703
4959
  }
4704
4960
  } catch (error) {
4705
- logger$16.error("[TransceiverController] stopTrackSender error", kind, error);
4961
+ logger$17.error("[TransceiverController] stopTrackSender error", kind, error);
4706
4962
  this.options.onError?.(new MediaTrackError("stopTrackSender", kind, error));
4707
4963
  }
4708
4964
  }
4709
4965
  async restoreTrackSender(kind) {
4710
4966
  try {
4711
- logger$16.debug("[TransceiverController] restoreTrackSender called", kind);
4967
+ logger$17.debug("[TransceiverController] restoreTrackSender called", kind);
4712
4968
  const constraints = {};
4713
4969
  const transceivers = this.transceiverByKind(kind);
4714
4970
  for (const transceiver of transceivers) {
@@ -4718,23 +4974,23 @@ var TransceiverController = class extends Destroyable {
4718
4974
  if (trackKind === "audio" || trackKind === "video") constraints[trackKind] = this.getConstraintsFor(trackKind);
4719
4975
  }
4720
4976
  }
4721
- logger$16.debug("[TransceiverController] restoreTrackSender constraints:", constraints);
4977
+ logger$17.debug("[TransceiverController] restoreTrackSender constraints:", constraints);
4722
4978
  if (Object.keys(constraints).length === 0) {
4723
- logger$16.warn("[TransceiverController] restoreTrackSender: no tracks need restoration", kind);
4979
+ logger$17.warn("[TransceiverController] restoreTrackSender: no tracks need restoration", kind);
4724
4980
  return;
4725
4981
  }
4726
4982
  const newTracks = (await this.options.getUserMedia(constraints)).getTracks();
4727
- logger$16.debug("[TransceiverController] restoreTrackSender new tracks:", newTracks);
4983
+ logger$17.debug("[TransceiverController] restoreTrackSender new tracks:", newTracks);
4728
4984
  for (const newTrack of newTracks) {
4729
4985
  this.options.localStreamController.addTrack(newTrack);
4730
4986
  const trackKind = newTrack.kind;
4731
4987
  const transceiverOfKind = this.transceiverByKind(trackKind)[0];
4732
4988
  transceiverOfKind.direction = trackKind === "audio" ? this.audioDirection : this.videoDirection;
4733
- logger$16.debug("[TransceiverController] restoreTrackSender setting direction for", trackKind, transceiverOfKind.direction);
4989
+ logger$17.debug("[TransceiverController] restoreTrackSender setting direction for", trackKind, transceiverOfKind.direction);
4734
4990
  await transceiverOfKind.sender.replaceTrack(newTrack);
4735
4991
  }
4736
4992
  } catch (error) {
4737
- logger$16.error("[TransceiverController] restoreTrackSender error", kind, error);
4993
+ logger$17.error("[TransceiverController] restoreTrackSender error", kind, error);
4738
4994
  this.options.onError?.(new MediaTrackError("restoreTrackSender", kind, error));
4739
4995
  }
4740
4996
  }
@@ -4775,14 +5031,14 @@ var TransceiverController = class extends Destroyable {
4775
5031
  };
4776
5032
  try {
4777
5033
  await track.applyConstraints(constraintsToApply);
4778
- logger$16.debug(`[TransceiverController] Updated ${kind} sender constraints:`, constraintsToApply);
4779
- 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());
4780
5036
  } catch (error) {
4781
- 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);
4782
5038
  try {
4783
5039
  await this.replaceTrackFallback(sender, track, kind, constraintsToApply);
4784
5040
  } catch (fallbackError) {
4785
- 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);
4786
5042
  this.options.onError?.(new MediaTrackError("updateSendersConstraints", kind, fallbackError));
4787
5043
  }
4788
5044
  }
@@ -4810,7 +5066,7 @@ var TransceiverController = class extends Destroyable {
4810
5066
  if (!newTrack) throw new MediaTrackError("replaceTrackFallback", kind, /* @__PURE__ */ new Error("getUserMedia returned no track of the requested kind"));
4811
5067
  await sender.replaceTrack(newTrack);
4812
5068
  this.options.localStreamController.addTrack(newTrack);
4813
- 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}`);
4814
5070
  }
4815
5071
  getMediaDirections() {
4816
5072
  if (this.peerConnection.connectionState === "connected") return this.peerConnection.getTransceivers().reduce((acc, transceiver) => {
@@ -4840,7 +5096,7 @@ var TransceiverController = class extends Destroyable {
4840
5096
 
4841
5097
  //#endregion
4842
5098
  //#region src/controllers/RTCPeerConnectionController.ts
4843
- const logger$15 = getLogger();
5099
+ const logger$16 = getLogger();
4844
5100
  var RTCPeerConnectionController = class extends Destroyable {
4845
5101
  constructor(options = {}, remoteSessionDescription, deviceController) {
4846
5102
  super();
@@ -4856,43 +5112,43 @@ var RTCPeerConnectionController = class extends Destroyable {
4856
5112
  this.oniceconnectionstatechangeHandler = () => {
4857
5113
  if (this.peerConnection) {
4858
5114
  const { iceConnectionState } = this.peerConnection;
4859
- logger$15.debug(`[RTCPeerConnectionController] ICE connection state changed to: ${iceConnectionState}`);
5115
+ logger$16.debug(`[RTCPeerConnectionController] ICE connection state changed to: ${iceConnectionState}`);
4860
5116
  this._iceConnectionState$.next(this.peerConnection.iceConnectionState);
4861
5117
  }
4862
5118
  };
4863
5119
  this.onconnectionstatechangeHandler = () => {
4864
5120
  if (this.peerConnection) {
4865
5121
  const { connectionState } = this.peerConnection;
4866
- logger$15.debug(`[RTCPeerConnectionController] Connection state changed to: ${connectionState}`);
5122
+ logger$16.debug(`[RTCPeerConnectionController] Connection state changed to: ${connectionState}`);
4867
5123
  if (connectionState === "connected") this.removeConnectionTimer();
4868
5124
  this._connectionState$.next(this.peerConnection.connectionState);
4869
5125
  }
4870
5126
  };
4871
5127
  this.onsignalingstatechangeHandler = () => {
4872
- logger$15.debug(`[RTCPeerConnectionController] Signaling state changed to: ${this.peerConnection?.signalingState}`);
5128
+ logger$16.debug(`[RTCPeerConnectionController] Signaling state changed to: ${this.peerConnection?.signalingState}`);
4873
5129
  };
4874
5130
  this.onicegatheringstatechangeHandler = () => {
4875
5131
  if (this.peerConnection) this._iceGatheringState$.next(this.peerConnection.iceGatheringState);
4876
5132
  };
4877
5133
  this.onnegotiationneededHandler = (event) => {
4878
- logger$15.debug("[RTCPeerConnectionController] Negotiation needed event received.", event);
5134
+ logger$16.debug("[RTCPeerConnectionController] Negotiation needed event received.", event);
4879
5135
  this.negotiationNeeded$.next();
4880
5136
  };
4881
5137
  this.updateSelectedInputDevice = async (kind, deviceInfo) => {
4882
5138
  try {
4883
5139
  const { localStream } = this;
4884
5140
  if (!localStream) {
4885
- 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.");
4886
5142
  return;
4887
5143
  }
4888
- logger$15.debug(`[RTCPeerConnectionController] Updating selected ${kind} input device:`, localStream.getTracks());
5144
+ logger$16.debug(`[RTCPeerConnectionController] Updating selected ${kind} input device:`, localStream.getTracks());
4889
5145
  const track = localStream.getTracks().find((track$1) => track$1.kind === kind);
4890
5146
  if (track) {
4891
5147
  this.transceiverController?.stopTrackSender(kind);
4892
- this.localStream?.removeTrack(track);
4893
- logger$15.debug(`[RTCPeerConnectionController] Stopped existing ${kind} track: ${track.id}`, localStream.getTracks());
5148
+ this.localStreamController.removeTrack(track.id);
5149
+ logger$16.debug(`[RTCPeerConnectionController] Stopped existing ${kind} track: ${track.id}`, localStream.getTracks());
4894
5150
  if (!deviceInfo) {
4895
- logger$15.debug(`[RTCPeerConnectionController] ${kind} input device selected: none`);
5151
+ logger$16.debug(`[RTCPeerConnectionController] ${kind} input device selected: none`);
4896
5152
  return;
4897
5153
  }
4898
5154
  const streamTrack = (await this.getUserMedia({ [kind]: {
@@ -4900,15 +5156,15 @@ var RTCPeerConnectionController = class extends Destroyable {
4900
5156
  ...this.deviceController.deviceInfoToConstraints(deviceInfo)
4901
5157
  } })).getTracks().find((t) => t.kind === kind);
4902
5158
  if (streamTrack) {
4903
- logger$15.debug(`[RTCPeerConnectionController] Adding new ${kind} track: ${streamTrack.id}`);
4904
- this.localStream?.addTrack(streamTrack);
5159
+ logger$16.debug(`[RTCPeerConnectionController] Adding new ${kind} track: ${streamTrack.id}`);
5160
+ this.localStreamController.addTrack(streamTrack);
4905
5161
  await this.transceiverController?.replaceSenderTrack(kind, streamTrack);
4906
- 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());
4907
5163
  }
4908
5164
  }
4909
- logger$15.debug(`[RTCPeerConnectionController] ${kind} input device selected:`, deviceInfo?.label);
5165
+ logger$16.debug(`[RTCPeerConnectionController] ${kind} input device selected:`, deviceInfo?.label);
4910
5166
  } catch (error) {
4911
- logger$15.error(`[RTCPeerConnectionController] Failed to select ${kind} input device:`, error);
5167
+ logger$16.error(`[RTCPeerConnectionController] Failed to select ${kind} input device:`, error);
4912
5168
  this._errors$.next(toError(error));
4913
5169
  throw error;
4914
5170
  }
@@ -4925,6 +5181,7 @@ var RTCPeerConnectionController = class extends Destroyable {
4925
5181
  this._remoteDescription$ = this.createReplaySubject(1);
4926
5182
  this._remoteStream$ = this.createBehaviorSubject(null);
4927
5183
  this._remoteOfferMediaDirections = null;
5184
+ this._localAudioPipeline = null;
4928
5185
  this.deviceController = deviceController ?? {};
4929
5186
  this.id = options.callId ?? v4();
4930
5187
  this._type = remoteSessionDescription ? "answer" : "offer";
@@ -5145,15 +5402,15 @@ var RTCPeerConnectionController = class extends Destroyable {
5145
5402
  this.setupPeerConnection();
5146
5403
  this.subscribeTo(this.negotiationNeeded$.pipe(auditTime(0), exhaustMap(async () => this.startNegotiation())), {
5147
5404
  next: () => {
5148
- logger$15.debug("[RTCPeerConnectionController] Start Negotiation completed successfully");
5405
+ logger$16.debug("[RTCPeerConnectionController] Start Negotiation completed successfully");
5149
5406
  },
5150
5407
  error: (error) => {
5151
- logger$15.error("[RTCPeerConnectionController] Start Negotiation error:", error);
5408
+ logger$16.error("[RTCPeerConnectionController] Start Negotiation error:", error);
5152
5409
  this._errors$.next(toError(error));
5153
5410
  }
5154
5411
  });
5155
5412
  this.subscribeTo(merge(this.deviceController.selectedAudioInputDevice$.pipe(map((deviceInfo) => ["audio", deviceInfo])), this.deviceController.selectedVideoInputDevice$.pipe(map((deviceInfo) => ["video", deviceInfo]))).pipe(skipWhile(() => !this.localStreamController.localStream)), async ([kind, deviceInfo]) => {
5156
- logger$15.debug(`[RTCPeerConnectionController] Selected input device changed for:`, {
5413
+ logger$16.debug(`[RTCPeerConnectionController] Selected input device changed for:`, {
5157
5414
  kind,
5158
5415
  deviceInfo
5159
5416
  });
@@ -5170,7 +5427,7 @@ var RTCPeerConnectionController = class extends Destroyable {
5170
5427
  this._initialized$.next(true);
5171
5428
  }
5172
5429
  } catch (error) {
5173
- logger$15.error("[RTCPeerConnectionController] Initialization error:", error);
5430
+ logger$16.error("[RTCPeerConnectionController] Initialization error:", error);
5174
5431
  this._errors$.next(toError(error));
5175
5432
  this.destroy();
5176
5433
  }
@@ -5202,22 +5459,22 @@ var RTCPeerConnectionController = class extends Destroyable {
5202
5459
  }
5203
5460
  async startNegotiation() {
5204
5461
  if (this.isNegotiating) {
5205
- logger$15.debug("[RTCPeerConnectionController] Negotiation already in progress, skipping.");
5462
+ logger$16.debug("[RTCPeerConnectionController] Negotiation already in progress, skipping.");
5206
5463
  return;
5207
5464
  }
5208
5465
  this.setupEventListeners();
5209
5466
  if (this.type === "answer") {
5210
- 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.");
5211
5468
  return;
5212
5469
  }
5213
5470
  this._isNegotiating$.next(true);
5214
- logger$15.debug("[RTCPeerConnectionController] Starting negotiation.");
5471
+ logger$16.debug("[RTCPeerConnectionController] Starting negotiation.");
5215
5472
  try {
5216
5473
  const { offerOptions } = this;
5217
- logger$15.debug("[RTCPeerConnectionController] Creating offer with options:", offerOptions);
5474
+ logger$16.debug("[RTCPeerConnectionController] Creating offer with options:", offerOptions);
5218
5475
  await this.createOffer(offerOptions);
5219
5476
  } catch (error) {
5220
- logger$15.error("[RTCPeerConnectionController] Error during negotiation:", error);
5477
+ logger$16.error("[RTCPeerConnectionController] Error during negotiation:", error);
5221
5478
  this._errors$.next(toError(error));
5222
5479
  }
5223
5480
  }
@@ -5233,14 +5490,14 @@ var RTCPeerConnectionController = class extends Destroyable {
5233
5490
  let readyToConnect = status !== "failed";
5234
5491
  try {
5235
5492
  if (status === "received" && sdp) {
5236
- logger$15.debug("[RTCPeerConnectionController] Received answer SDP:", sdp);
5493
+ logger$16.debug("[RTCPeerConnectionController] Received answer SDP:", sdp);
5237
5494
  await this._setRemoteDescription({
5238
5495
  type: "answer",
5239
5496
  sdp
5240
5497
  });
5241
5498
  }
5242
5499
  } catch (error) {
5243
- logger$15.error("[RTCPeerConnectionController] Error updating answer status:", error);
5500
+ logger$16.error("[RTCPeerConnectionController] Error updating answer status:", error);
5244
5501
  this._errors$.next(toError(error));
5245
5502
  readyToConnect = false;
5246
5503
  } finally {
@@ -5259,7 +5516,7 @@ var RTCPeerConnectionController = class extends Destroyable {
5259
5516
  await this.handleOfferReceived();
5260
5517
  break;
5261
5518
  case "failed":
5262
- logger$15.error("[RTCPeerConnectionController] Offer failed to be processed by remote.");
5519
+ logger$16.error("[RTCPeerConnectionController] Offer failed to be processed by remote.");
5263
5520
  break;
5264
5521
  case "sent":
5265
5522
  default:
@@ -5291,7 +5548,7 @@ var RTCPeerConnectionController = class extends Destroyable {
5291
5548
  }
5292
5549
  await this.setupLocalTracks();
5293
5550
  const { answerOptions } = this;
5294
- logger$15.debug("[RTCPeerConnectionController] Creating inbound answer with options:", answerOptions);
5551
+ logger$16.debug("[RTCPeerConnectionController] Creating inbound answer with options:", answerOptions);
5295
5552
  await this.createAnswer(answerOptions);
5296
5553
  }
5297
5554
  async handleOfferReceived() {
@@ -5299,7 +5556,7 @@ var RTCPeerConnectionController = class extends Destroyable {
5299
5556
  this._isNegotiating$.next(true);
5300
5557
  await this._setRemoteDescription(this.sdpInit);
5301
5558
  const { answerOptions } = this;
5302
- logger$15.debug("[RTCPeerConnectionController] Creating answer with options:", answerOptions);
5559
+ logger$16.debug("[RTCPeerConnectionController] Creating answer with options:", answerOptions);
5303
5560
  await this.createAnswer(answerOptions);
5304
5561
  }
5305
5562
  readyToConnect() {
@@ -5307,7 +5564,7 @@ var RTCPeerConnectionController = class extends Destroyable {
5307
5564
  this.connectionTimer = setTimeout(() => {
5308
5565
  this.removeConnectionTimer();
5309
5566
  if (this.peerConnection?.connectionState !== "connected") {
5310
- 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.");
5311
5568
  this.iceGatheringController.restartICEGatheringWithRelayOnly();
5312
5569
  }
5313
5570
  }, this.connectionTimeout);
@@ -5329,14 +5586,14 @@ var RTCPeerConnectionController = class extends Destroyable {
5329
5586
  const stereo = this.options.stereo ?? PreferencesContainer.instance.stereoAudio;
5330
5587
  if (preferredAudioCodecs.length > 0 || preferredVideoCodecs.length > 0) {
5331
5588
  result = setCodecPreferences(result, preferredAudioCodecs, preferredVideoCodecs);
5332
- logger$15.debug("[RTCPeerConnectionController] Applied codec preferences to SDP", {
5589
+ logger$16.debug("[RTCPeerConnectionController] Applied codec preferences to SDP", {
5333
5590
  preferredAudioCodecs,
5334
5591
  preferredVideoCodecs
5335
5592
  });
5336
5593
  }
5337
5594
  if (stereo) {
5338
5595
  result = enableStereoOpus(result);
5339
- logger$15.debug("[RTCPeerConnectionController] Applied stereo Opus to SDP");
5596
+ logger$16.debug("[RTCPeerConnectionController] Applied stereo Opus to SDP");
5340
5597
  }
5341
5598
  return Promise.resolve(result);
5342
5599
  }
@@ -5370,9 +5627,6 @@ var RTCPeerConnectionController = class extends Destroyable {
5370
5627
  negotiationEnded() {
5371
5628
  this._isNegotiating$.next(false);
5372
5629
  }
5373
- restarIce() {
5374
- this.peerConnection?.restartIce();
5375
- }
5376
5630
  /**
5377
5631
  * Trigger an ICE restart through the existing negotiation pipeline.
5378
5632
  *
@@ -5395,24 +5649,27 @@ var RTCPeerConnectionController = class extends Destroyable {
5395
5649
  ...this.peerConnection.getConfiguration(),
5396
5650
  iceTransportPolicy: "relay"
5397
5651
  });
5398
- logger$15.debug("[RTCPeerConnectionController] ICE transport policy set to relay-only");
5652
+ logger$16.debug("[RTCPeerConnectionController] ICE transport policy set to relay-only");
5399
5653
  } catch (error) {
5400
- logger$15.warn("[RTCPeerConnectionController] Failed to set relay-only policy:", error);
5654
+ logger$16.warn("[RTCPeerConnectionController] Failed to set relay-only policy:", error);
5401
5655
  }
5402
5656
  this.setupEventListeners();
5403
5657
  this._isNegotiating$.next(true);
5404
- logger$15.debug(`[RTCPeerConnectionController] Triggering ICE restart${relayOnly ? " (relay-only)" : ""}.`);
5658
+ logger$16.debug(`[RTCPeerConnectionController] Triggering ICE restart${relayOnly ? " (relay-only)" : ""}.`);
5405
5659
  try {
5406
5660
  const offer = await this.peerConnection.createOffer({ iceRestart: true });
5407
5661
  await this.setLocalDescription(offer);
5408
5662
  } catch (error) {
5409
- logger$15.error("[RTCPeerConnectionController] ICE restart offer failed:", error);
5663
+ logger$16.error("[RTCPeerConnectionController] ICE restart offer failed:", error);
5410
5664
  this._errors$.next(toError(error));
5411
5665
  this.negotiationEnded();
5412
5666
  if (policyChanged) this.restoreIceTransportPolicy();
5413
5667
  throw error;
5414
5668
  }
5415
- if (policyChanged) this.restoreIceTransportPolicy();
5669
+ if (policyChanged) firstValueFrom(race(this._iceGatheringState$.pipe(filter((state) => state === "complete"), take(1)), timer(ICE_GATHERING_COMPLETE_TIMEOUT_MS).pipe(map(() => "timeout")))).then(() => this.restoreIceTransportPolicy()).catch((error) => {
5670
+ logger$16.warn("[RTCPeerConnectionController] Error waiting for ICE gathering to complete:", error);
5671
+ this.restoreIceTransportPolicy();
5672
+ });
5416
5673
  }
5417
5674
  restoreIceTransportPolicy() {
5418
5675
  try {
@@ -5420,9 +5677,9 @@ var RTCPeerConnectionController = class extends Destroyable {
5420
5677
  ...this.peerConnection.getConfiguration(),
5421
5678
  iceTransportPolicy: this.options.relayOnly ? "relay" : "all"
5422
5679
  });
5423
- logger$15.debug("[RTCPeerConnectionController] ICE transport policy restored");
5680
+ logger$16.debug("[RTCPeerConnectionController] ICE transport policy restored");
5424
5681
  } catch (error) {
5425
- logger$15.warn("[RTCPeerConnectionController] Failed to restore ICE transport policy:", error);
5682
+ logger$16.warn("[RTCPeerConnectionController] Failed to restore ICE transport policy:", error);
5426
5683
  }
5427
5684
  }
5428
5685
  /**
@@ -5434,13 +5691,13 @@ var RTCPeerConnectionController = class extends Destroyable {
5434
5691
  await this.setupRemoteTracks();
5435
5692
  }
5436
5693
  async setupLocalTracks() {
5437
- logger$15.debug("[RTCPeerConnectionController] Setting up local tracks/transceivers.");
5694
+ logger$16.debug("[RTCPeerConnectionController] Setting up local tracks/transceivers.");
5438
5695
  const localStream = this.localStream ?? await this.localStreamController.buildLocalStream();
5439
5696
  if (this.transceiverController?.useAddStream ?? false) {
5440
- 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.");
5441
5698
  this.peerConnection?.addStream(localStream);
5442
5699
  if (!this.isNegotiating) {
5443
- logger$15.debug("[RTCPeerConnectionController] Forcing negotiationneeded after local tracks setup.");
5700
+ logger$16.debug("[RTCPeerConnectionController] Forcing negotiationneeded after local tracks setup.");
5444
5701
  this.negotiationNeeded$.next();
5445
5702
  }
5446
5703
  return;
@@ -5456,7 +5713,7 @@ var RTCPeerConnectionController = class extends Destroyable {
5456
5713
  const transceivers = (kind === "audio" ? this.transceiverController?.audioTransceivers : this.transceiverController?.videoTransceivers) ?? [];
5457
5714
  await this.transceiverController?.setupTransceiverSender(track, localStream, transceivers[index]);
5458
5715
  } else {
5459
- 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);
5460
5717
  this.peerConnection?.addTrack(track, localStream);
5461
5718
  }
5462
5719
  }
@@ -5473,7 +5730,7 @@ var RTCPeerConnectionController = class extends Destroyable {
5473
5730
  async setupRemoteTracks() {
5474
5731
  if (!this.peerConnection) throw new DependencyError("RTCPeerConnection is not initialized");
5475
5732
  this.peerConnection.ontrack = (event) => {
5476
- logger$15.debug("[RTCPeerConnectionController] Remote track received:", event.track.kind);
5733
+ logger$16.debug("[RTCPeerConnectionController] Remote track received:", event.track.kind);
5477
5734
  if (event.streams[0]) this._remoteStream$.next(event.streams[0]);
5478
5735
  else {
5479
5736
  const existingTracks = this._remoteStream$.value?.getTracks() ?? [];
@@ -5485,6 +5742,45 @@ var RTCPeerConnectionController = class extends Destroyable {
5485
5742
  }
5486
5743
  async restoreTrackSender(kind) {
5487
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.at(0);
5776
+ this._localAudioPipeline.setInputTrack(raw ?? null);
5777
+ const sender = (this.transceiverController?.audioTransceivers.at(0))?.sender ?? this.peerConnection.getSenders().find((s) => s.track?.kind === "audio");
5778
+ if (!sender || !raw) return;
5779
+ try {
5780
+ await sender.replaceTrack(this._localAudioPipeline.outputTrack);
5781
+ } catch (error) {
5782
+ logger$16.warn("[RTCPeerConnectionController] Failed to route audio sender through pipeline:", error);
5783
+ }
5488
5784
  }
5489
5785
  /**
5490
5786
  * Add a local media track to the peer connection.
@@ -5499,9 +5795,9 @@ var RTCPeerConnectionController = class extends Destroyable {
5499
5795
  try {
5500
5796
  const localStream = this.localStreamController.addTrack(track);
5501
5797
  this.peerConnection.addTrack(track, localStream);
5502
- logger$15.debug(`[RTCPeerConnectionController] ${track.kind} track added:`, track.id);
5798
+ logger$16.debug(`[RTCPeerConnectionController] ${track.kind} track added:`, track.id);
5503
5799
  } catch (error) {
5504
- logger$15.error(`[RTCPeerConnectionController] Failed to add ${track.kind} track:`, error);
5800
+ logger$16.error(`[RTCPeerConnectionController] Failed to add ${track.kind} track:`, error);
5505
5801
  this._errors$.next(toError(error));
5506
5802
  throw error;
5507
5803
  }
@@ -5518,15 +5814,15 @@ var RTCPeerConnectionController = class extends Destroyable {
5518
5814
  }
5519
5815
  const sender = this.peerConnection.getSenders().find((sender$1) => sender$1.track?.id === trackId);
5520
5816
  if (!sender) {
5521
- logger$15.debug(`[RTCPeerConnectionController] track not found: ${trackId}`);
5817
+ logger$16.debug(`[RTCPeerConnectionController] track not found: ${trackId}`);
5522
5818
  return;
5523
5819
  }
5524
5820
  try {
5525
5821
  this.peerConnection.removeTrack(sender);
5526
5822
  this.localStreamController.removeTrack(trackId);
5527
- logger$15.debug(`[RTCPeerConnectionController] ${sender.track?.kind} track removed:`, trackId);
5823
+ logger$16.debug(`[RTCPeerConnectionController] ${sender.track?.kind} track removed:`, trackId);
5528
5824
  } catch (error) {
5529
- logger$15.error(`[RTCPeerConnectionController] Failed to remove ${sender.track?.kind} track:`, error);
5825
+ logger$16.error(`[RTCPeerConnectionController] Failed to remove ${sender.track?.kind} track:`, error);
5530
5826
  this._errors$.next(toError(error));
5531
5827
  throw error;
5532
5828
  }
@@ -5553,7 +5849,7 @@ var RTCPeerConnectionController = class extends Destroyable {
5553
5849
  async replaceAudioTrackWithConstraints(constraints) {
5554
5850
  const senders = this.peerConnection?.getSenders().filter((s) => s.track?.kind === "audio" && s.track.readyState === "live");
5555
5851
  if (!senders || senders.length === 0) {
5556
- logger$15.warn("[RTCPeerConnectionController] No live audio sender to replace");
5852
+ logger$16.warn("[RTCPeerConnectionController] No live audio sender to replace");
5557
5853
  return;
5558
5854
  }
5559
5855
  for (const sender of senders) {
@@ -5571,7 +5867,7 @@ var RTCPeerConnectionController = class extends Destroyable {
5571
5867
  const newTrack = (await this.getUserMedia({ audio: mergedConstraints })).getAudioTracks()[0];
5572
5868
  await sender.replaceTrack(newTrack);
5573
5869
  this.localStreamController.addTrack(newTrack);
5574
- logger$15.debug(`[RTCPeerConnectionController] Audio track replaced for server-pushed params. New track: ${newTrack.id}`);
5870
+ logger$16.debug(`[RTCPeerConnectionController] Audio track replaced for server-pushed params. New track: ${newTrack.id}`);
5575
5871
  }
5576
5872
  }
5577
5873
  /**
@@ -5579,9 +5875,11 @@ var RTCPeerConnectionController = class extends Destroyable {
5579
5875
  * Completes all observables to prevent memory leaks.
5580
5876
  */
5581
5877
  destroy() {
5582
- logger$15.debug(`[RTCPeerConnectionController] Destroying RTCPeerConnectionController. ${this.propose}`);
5878
+ logger$16.debug(`[RTCPeerConnectionController] Destroying RTCPeerConnectionController. ${this.propose}`);
5583
5879
  this.removeConnectionTimer();
5584
5880
  this._iceGatheringController?.destroy();
5881
+ this._localAudioPipeline?.destroy();
5882
+ this._localAudioPipeline = null;
5585
5883
  this.localStreamController.destroy();
5586
5884
  this.transceiverController?.destroy();
5587
5885
  if (this.peerConnection) {
@@ -5603,7 +5901,7 @@ var RTCPeerConnectionController = class extends Destroyable {
5603
5901
  }
5604
5902
  stopRemoteTracks() {
5605
5903
  this._remoteStream$.value?.getTracks().forEach((track) => {
5606
- logger$15.debug(`[RTCPeerConnectionController] Stopping remote track: ${track.kind}`);
5904
+ logger$16.debug(`[RTCPeerConnectionController] Stopping remote track: ${track.kind}`);
5607
5905
  track.stop();
5608
5906
  });
5609
5907
  }
@@ -5620,7 +5918,7 @@ var RTCPeerConnectionController = class extends Destroyable {
5620
5918
  ...params,
5621
5919
  sdp: finalRemote
5622
5920
  };
5623
- logger$15.debug("[RTCPeerConnectionController] Setting remote description:", answer);
5921
+ logger$16.debug("[RTCPeerConnectionController] Setting remote description:", answer);
5624
5922
  return this.peerConnection.setRemoteDescription(answer);
5625
5923
  }
5626
5924
  };
@@ -5658,7 +5956,24 @@ function isVertoPingInnerParams(value) {
5658
5956
 
5659
5957
  //#endregion
5660
5958
  //#region src/managers/VertoManager.ts
5661
- const logger$14 = getLogger();
5959
+ const logger$15 = getLogger();
5960
+ /**
5961
+ * Decide what value goes on the `node_id` field of a `webrtc.verto` envelope.
5962
+ *
5963
+ * - **Reattach invite:** must carry the persisted nodeId so the server routes
5964
+ * the new connection to the FreeSWITCH instance that holds the existing call.
5965
+ * - **Fresh invite, caller-supplied `CallOptions.nodeId`:** carry the explicit
5966
+ * value as a steering hint (dev/staging traffic pinning). Server may honour
5967
+ * or ignore for placement reasons.
5968
+ * - **Fresh invite, no caller nodeId:** strip to `''` = "server picks".
5969
+ * - **Non-invite frames** (verto.modify, verto.bye, etc.): always carry the
5970
+ * current `_nodeId$.value` so the frame targets the node hosting the call.
5971
+ *
5972
+ * Pure function — exported for unit testing.
5973
+ */
5974
+ function resolveInviteNodeId(args) {
5975
+ return args.isInvite && !args.reattach && !args.explicitNodeId ? "" : args.currentNodeId ?? "";
5976
+ }
5662
5977
  var VertoManager = class extends Destroyable {
5663
5978
  constructor(callSession) {
5664
5979
  super();
@@ -5697,7 +6012,7 @@ var WebRTCVertoManager = class extends VertoManager {
5697
6012
  try {
5698
6013
  await this.executeVerto(vertoModifyMessage);
5699
6014
  } catch (error) {
5700
- logger$14.warn("[WebRTCManager] Call might already be disconnected, error sending Verto hold:", error);
6015
+ logger$15.warn("[WebRTCManager] Call might already be disconnected, error sending Verto hold:", error);
5701
6016
  throw error;
5702
6017
  }
5703
6018
  }
@@ -5710,7 +6025,7 @@ var WebRTCVertoManager = class extends VertoManager {
5710
6025
  try {
5711
6026
  await this.executeVerto(vertoModifyMessage);
5712
6027
  } catch (error) {
5713
- logger$14.warn("[WebRTCManager] Call might already be disconnected, error sending Verto unhold:", error);
6028
+ logger$15.warn("[WebRTCManager] Call might already be disconnected, error sending Verto unhold:", error);
5714
6029
  throw error;
5715
6030
  }
5716
6031
  }
@@ -5763,7 +6078,7 @@ var WebRTCVertoManager = class extends VertoManager {
5763
6078
  if (event.member_id) this.setSelfIdIfNull(event.member_id);
5764
6079
  });
5765
6080
  this.subscribeTo(this.vertoMedia$, (event) => {
5766
- logger$14.debug("[WebRTCManager] Received Verto media event (early media SDP):", event);
6081
+ logger$15.debug("[WebRTCManager] Received Verto media event (early media SDP):", event);
5767
6082
  this._signalingStatus$.next("ringing");
5768
6083
  const { sdp, callID } = event;
5769
6084
  this._rtcPeerConnectionsMap.get(callID)?.updateAnswerStatus({
@@ -5772,7 +6087,7 @@ var WebRTCVertoManager = class extends VertoManager {
5772
6087
  });
5773
6088
  });
5774
6089
  this.subscribeTo(this.vertoAnswer$, (event) => {
5775
- logger$14.debug("[WebRTCManager] Received Verto answer event:", event);
6090
+ logger$15.debug("[WebRTCManager] Received Verto answer event:", event);
5776
6091
  this._signalingStatus$.next("connecting");
5777
6092
  const { sdp, callID } = event;
5778
6093
  this._rtcPeerConnectionsMap.get(callID)?.updateAnswerStatus({
@@ -5781,7 +6096,7 @@ var WebRTCVertoManager = class extends VertoManager {
5781
6096
  });
5782
6097
  });
5783
6098
  this.subscribeTo(this.vertoMediaParams$, (event) => {
5784
- logger$14.debug("[WebRTCManager] Received Verto mediaParams event:", event);
6099
+ logger$15.debug("[WebRTCManager] Received Verto mediaParams event:", event);
5785
6100
  const { mediaParams, callID } = event;
5786
6101
  const rtcPeerConnController = this._rtcPeerConnectionsMap.get(callID);
5787
6102
  const { audio, video } = mediaParams;
@@ -5795,7 +6110,7 @@ var WebRTCVertoManager = class extends VertoManager {
5795
6110
  timestamp: Date.now()
5796
6111
  });
5797
6112
  } catch (error) {
5798
- logger$14.warn("[WebRTCManager] Error applying server-pushed media params:", error);
6113
+ logger$15.warn("[WebRTCManager] Error applying server-pushed media params:", error);
5799
6114
  this.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
5800
6115
  }
5801
6116
  })();
@@ -5817,13 +6132,13 @@ var WebRTCVertoManager = class extends VertoManager {
5817
6132
  */
5818
6133
  setNodeIdIfNull(nodeId) {
5819
6134
  if (!this._nodeId$.value && nodeId) {
5820
- logger$14.debug(`[WebRTCManager] Early node_id set: ${nodeId}`);
6135
+ logger$15.debug(`[WebRTCManager] Early node_id set: ${nodeId}`);
5821
6136
  this._nodeId$.next(nodeId);
5822
6137
  }
5823
6138
  }
5824
6139
  setSelfIdIfNull(selfId) {
5825
6140
  if (!this._selfId$.value && selfId) {
5826
- logger$14.debug(`[WebRTCManager] Early selfId set: ${selfId}`);
6141
+ logger$15.debug(`[WebRTCManager] Early selfId set: ${selfId}`);
5827
6142
  this._selfId$.next(selfId);
5828
6143
  }
5829
6144
  }
@@ -5832,7 +6147,7 @@ var WebRTCVertoManager = class extends VertoManager {
5832
6147
  const vertoPongMessage = VertoPong({ ...vertoPing });
5833
6148
  await this.executeVerto(vertoPongMessage);
5834
6149
  } catch (error) {
5835
- logger$14.warn("[WebRTCManager] Call might disconnect, error sending Verto pong:", error);
6150
+ logger$15.warn("[WebRTCManager] Call might disconnect, error sending Verto pong:", error);
5836
6151
  this.onError?.(new VertoPongError(error));
5837
6152
  }
5838
6153
  }
@@ -5842,7 +6157,7 @@ var WebRTCVertoManager = class extends VertoManager {
5842
6157
  if (audio) await this.mainPeerConnection.updateSendersConstraints("audio", audio);
5843
6158
  if (video) await this.mainPeerConnection.updateSendersConstraints("video", video);
5844
6159
  } catch (error) {
5845
- logger$14.warn("[WebRTCManager] Error updating media constraints:", error);
6160
+ logger$15.warn("[WebRTCManager] Error updating media constraints:", error);
5846
6161
  this.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
5847
6162
  throw error;
5848
6163
  }
@@ -5872,20 +6187,20 @@ var WebRTCVertoManager = class extends VertoManager {
5872
6187
  try {
5873
6188
  const pc = this.mainPeerConnection.peerConnection;
5874
6189
  if (!pc) {
5875
- logger$14.warn("[WebRTCManager] No peer connection for keyframe request");
6190
+ logger$15.warn("[WebRTCManager] No peer connection for keyframe request");
5876
6191
  return;
5877
6192
  }
5878
6193
  const videoReceiver = pc.getReceivers().find((r) => r.track.kind === "video");
5879
6194
  if (!videoReceiver) {
5880
- logger$14.warn("[WebRTCManager] No video receiver for keyframe request");
6195
+ logger$15.warn("[WebRTCManager] No video receiver for keyframe request");
5881
6196
  return;
5882
6197
  }
5883
6198
  if (typeof videoReceiver.requestKeyFrame === "function") {
5884
6199
  videoReceiver.requestKeyFrame();
5885
- logger$14.debug("[WebRTCManager] Keyframe requested via RTCRtpReceiver.requestKeyFrame()");
5886
- } else logger$14.debug("[WebRTCManager] requestKeyFrame() not supported, skipping");
6200
+ logger$15.debug("[WebRTCManager] Keyframe requested via RTCRtpReceiver.requestKeyFrame()");
6201
+ } else logger$15.debug("[WebRTCManager] requestKeyFrame() not supported, skipping");
5887
6202
  } catch (error) {
5888
- logger$14.warn("[WebRTCManager] Keyframe request failed (non-fatal):", error);
6203
+ logger$15.warn("[WebRTCManager] Keyframe request failed (non-fatal):", error);
5889
6204
  }
5890
6205
  }
5891
6206
  /**
@@ -5903,13 +6218,13 @@ var WebRTCVertoManager = class extends VertoManager {
5903
6218
  try {
5904
6219
  const controller = this.mainPeerConnection;
5905
6220
  if (!controller.peerConnection) {
5906
- logger$14.warn("[WebRTCManager] No peer connection for ICE restart");
6221
+ logger$15.warn("[WebRTCManager] No peer connection for ICE restart");
5907
6222
  return;
5908
6223
  }
5909
6224
  await controller.triggerIceRestart(relayOnly);
5910
- logger$14.info(`[WebRTCManager] ICE restart initiated${relayOnly ? " (relay-only)" : ""}`);
6225
+ logger$15.info(`[WebRTCManager] ICE restart initiated${relayOnly ? " (relay-only)" : ""}`);
5911
6226
  } catch (error) {
5912
- logger$14.error("[WebRTCManager] ICE restart failed:", error);
6227
+ logger$15.error("[WebRTCManager] ICE restart failed:", error);
5913
6228
  throw error;
5914
6229
  }
5915
6230
  }
@@ -5927,13 +6242,13 @@ var WebRTCVertoManager = class extends VertoManager {
5927
6242
  const entries = Array.from(this._rtcPeerConnectionsMap.entries());
5928
6243
  for (const [id, controller] of entries) try {
5929
6244
  if (!controller.peerConnection) {
5930
- logger$14.debug(`[WebRTCManager] No peer connection for leg ${id}, skipping ICE restart`);
6245
+ logger$15.debug(`[WebRTCManager] No peer connection for leg ${id}, skipping ICE restart`);
5931
6246
  continue;
5932
6247
  }
5933
6248
  await controller.triggerIceRestart(relayOnly);
5934
- logger$14.info(`[WebRTCManager] ICE restart initiated for leg ${id}${relayOnly ? " (relay-only)" : ""}`);
6249
+ logger$15.info(`[WebRTCManager] ICE restart initiated for leg ${id}${relayOnly ? " (relay-only)" : ""}`);
5935
6250
  } catch (error) {
5936
- logger$14.warn(`[WebRTCManager] ICE restart failed for leg ${id}:`, error);
6251
+ logger$15.warn(`[WebRTCManager] ICE restart failed for leg ${id}:`, error);
5937
6252
  }
5938
6253
  }
5939
6254
  /**
@@ -5945,7 +6260,7 @@ var WebRTCVertoManager = class extends VertoManager {
5945
6260
  requestKeyframeAll() {
5946
6261
  for (const [id, controller] of this._rtcPeerConnectionsMap) {
5947
6262
  if (controller.isScreenShare) {
5948
- logger$14.debug(`[WebRTCManager] Skipping keyframe for send-only screen share leg ${id}`);
6263
+ logger$15.debug(`[WebRTCManager] Skipping keyframe for send-only screen share leg ${id}`);
5949
6264
  continue;
5950
6265
  }
5951
6266
  try {
@@ -5955,10 +6270,10 @@ var WebRTCVertoManager = class extends VertoManager {
5955
6270
  if (!videoReceiver) continue;
5956
6271
  if (typeof videoReceiver.requestKeyFrame === "function") {
5957
6272
  videoReceiver.requestKeyFrame();
5958
- logger$14.debug(`[WebRTCManager] Keyframe requested for leg ${id}`);
6273
+ logger$15.debug(`[WebRTCManager] Keyframe requested for leg ${id}`);
5959
6274
  }
5960
6275
  } catch (error) {
5961
- logger$14.warn(`[WebRTCManager] Keyframe request failed for leg ${id} (non-fatal):`, error);
6276
+ logger$15.warn(`[WebRTCManager] Keyframe request failed for leg ${id} (non-fatal):`, error);
5962
6277
  }
5963
6278
  }
5964
6279
  }
@@ -6019,7 +6334,7 @@ var WebRTCVertoManager = class extends VertoManager {
6019
6334
  default:
6020
6335
  }
6021
6336
  } catch (error) {
6022
- logger$14.error(`[WebRTCManager] Error sending Verto ${vertoMethod}:`, error);
6337
+ logger$15.error(`[WebRTCManager] Error sending Verto ${vertoMethod}:`, error);
6023
6338
  this.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
6024
6339
  if (vertoMethod === "verto.modify") this.onModifyFailed?.();
6025
6340
  }
@@ -6034,7 +6349,7 @@ var WebRTCVertoManager = class extends VertoManager {
6034
6349
  sdp
6035
6350
  });
6036
6351
  } catch (error) {
6037
- logger$14.warn("[WebRTCManager] Error processing modify response:", error);
6352
+ logger$15.warn("[WebRTCManager] Error processing modify response:", error);
6038
6353
  const modifyError = error instanceof Error ? error : new Error(String(error), { cause: error });
6039
6354
  this.onError?.(modifyError);
6040
6355
  }
@@ -6046,7 +6361,7 @@ var WebRTCVertoManager = class extends VertoManager {
6046
6361
  this._nodeId$.next(getValueFrom(response, "result.node_id") ?? null);
6047
6362
  const memberId = getValueFrom(response, "result.result.result.memberID") ?? null;
6048
6363
  const callId = getValueFrom(response, "result.result.result.callID") ?? null;
6049
- logger$14.debug("[WebRTCManager] Verto invite response:", {
6364
+ logger$15.debug("[WebRTCManager] Verto invite response:", {
6050
6365
  callId,
6051
6366
  memberId,
6052
6367
  response
@@ -6056,14 +6371,14 @@ var WebRTCVertoManager = class extends VertoManager {
6056
6371
  if (callId) {
6057
6372
  this.webRtcCallSession.addCallId(callId);
6058
6373
  this.attachManager.attach(this.buildAttachableCall(callId));
6059
- } else logger$14.warn("[WebRTCManager] Cannot attach call, missing callId:", {
6374
+ } else logger$15.warn("[WebRTCManager] Cannot attach call, missing callId:", {
6060
6375
  nodeId: this.nodeId,
6061
6376
  callId
6062
6377
  });
6063
- logger$14.info("[WebRTCManager] Verto invite successful");
6064
- logger$14.debug(`[WebRTCManager] nodeid: ${this._nodeId$.value}, selfId: ${this._selfId$.value}`);
6378
+ logger$15.info("[WebRTCManager] Verto invite successful");
6379
+ logger$15.debug(`[WebRTCManager] nodeid: ${this._nodeId$.value}, selfId: ${this._selfId$.value}`);
6065
6380
  } else {
6066
- logger$14.error("[WebRTCManager] Verto invite failed:", response);
6381
+ logger$15.error("[WebRTCManager] Verto invite failed:", response);
6067
6382
  const inviteError = response.error ? new JSONRPCError(response.error.code, response.error.message, response.error.data) : /* @__PURE__ */ new Error("Verto invite failed: unexpected response");
6068
6383
  this.onError?.(inviteError);
6069
6384
  }
@@ -6108,17 +6423,17 @@ var WebRTCVertoManager = class extends VertoManager {
6108
6423
  if (options.initOffer) this.handleInboundAnswer(rtcPeerConnController);
6109
6424
  }
6110
6425
  async handleInboundAnswer(rtcPeerConnController) {
6111
- logger$14.debug("[WebRTCManager] Waiting for inbound call to be accepted or rejected");
6426
+ logger$15.debug("[WebRTCManager] Waiting for inbound call to be accepted or rejected");
6112
6427
  const vertoByeOrAccepted = await firstValueFrom(race(this.vertoBye$, this.webRtcCallSession.answered$).pipe(takeUntil(this.destroyed$))).catch(() => null);
6113
6428
  if (vertoByeOrAccepted === null) {
6114
- logger$14.debug("[WebRTCManager] Inbound answer handler aborted (destroyed).");
6429
+ logger$15.debug("[WebRTCManager] Inbound answer handler aborted (destroyed).");
6115
6430
  return;
6116
6431
  }
6117
6432
  if (isVertoByeMessage(vertoByeOrAccepted)) {
6118
- logger$14.info("[WebRTCManager] Inbound call ended by remote before answer.");
6433
+ logger$15.info("[WebRTCManager] Inbound call ended by remote before answer.");
6119
6434
  this.callSession?.destroy();
6120
6435
  } else if (!vertoByeOrAccepted) {
6121
- logger$14.info("[WebRTCManager] Inbound call rejected by user.");
6436
+ logger$15.info("[WebRTCManager] Inbound call rejected by user.");
6122
6437
  try {
6123
6438
  await this.bye("USER_BUSY");
6124
6439
  } finally {
@@ -6126,19 +6441,19 @@ var WebRTCVertoManager = class extends VertoManager {
6126
6441
  this.callSession?.destroy();
6127
6442
  }
6128
6443
  } else {
6129
- logger$14.debug("[WebRTCManager] Inbound call accepted, creating SDP answer");
6444
+ logger$15.debug("[WebRTCManager] Inbound call accepted, creating SDP answer");
6130
6445
  const answerOptions = this.webRtcCallSession.answerMediaOptions;
6131
6446
  try {
6132
6447
  await rtcPeerConnController.acceptInbound(answerOptions);
6133
6448
  } catch (error) {
6134
- logger$14.error("[WebRTCManager] Error creating inbound answer:", error);
6449
+ logger$15.error("[WebRTCManager] Error creating inbound answer:", error);
6135
6450
  this.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
6136
6451
  }
6137
6452
  }
6138
6453
  }
6139
6454
  setupVertoAttachHandler() {
6140
6455
  this.subscribeTo(this.vertoAttach$, async (vertoAttach) => {
6141
- logger$14.debug("[WebRTCManager] Received Verto attach event for existing call:", vertoAttach);
6456
+ logger$15.debug("[WebRTCManager] Received Verto attach event for existing call:", vertoAttach);
6142
6457
  const { callID } = vertoAttach;
6143
6458
  await this.attachManager.attach({
6144
6459
  nodeId: this.nodeId ?? void 0,
@@ -6198,26 +6513,29 @@ var WebRTCVertoManager = class extends VertoManager {
6198
6513
  else if (rtcPeerConnController.isAdditionalDevice) subscribe.push(...PreferencesContainer.instance.inviteSubscribeAdditionalDevice);
6199
6514
  else if (rtcPeerConnController.isScreenShare) subscribe.push(...PreferencesContainer.instance.inviteSubscribeScreenshare);
6200
6515
  }
6201
- const isInvite = isVertoInviteMessage(vertoMessage);
6202
- const isReattach = isInvite && this.webRtcCallSession.options.reattach;
6203
6516
  return {
6204
6517
  callID: rtcPeerConnController.id,
6205
- node_id: isInvite && !isReattach ? "" : this._nodeId$.value ?? "",
6518
+ node_id: resolveInviteNodeId({
6519
+ isInvite: isVertoInviteMessage(vertoMessage),
6520
+ reattach: this.webRtcCallSession.options.reattach === true,
6521
+ explicitNodeId: this.webRtcCallSession.options.nodeId,
6522
+ currentNodeId: this._nodeId$.value
6523
+ }),
6206
6524
  subscribe
6207
6525
  };
6208
6526
  }
6209
6527
  async sendLocalDescriptionOnceAccepted(vertoMessageRequest, rtcPeerConnectionController) {
6210
- logger$14.debug("[WebRTCManager] Waiting for call to be accepted or ended before sending answer");
6528
+ logger$15.debug("[WebRTCManager] Waiting for call to be accepted or ended before sending answer");
6211
6529
  const vertoByeOrAccepted = await firstValueFrom(race(this.vertoBye$, this.webRtcCallSession.answered$).pipe(takeUntil(this.destroyed$))).catch(() => null);
6212
6530
  if (vertoByeOrAccepted === null) {
6213
- logger$14.debug("[WebRTCManager] Destroyed while waiting for call acceptance");
6531
+ logger$15.debug("[WebRTCManager] Destroyed while waiting for call acceptance");
6214
6532
  return;
6215
6533
  }
6216
6534
  if (isVertoByeMessage(vertoByeOrAccepted)) {
6217
- logger$14.info("[WebRTCManager] Call ended before answer was sent.");
6535
+ logger$15.info("[WebRTCManager] Call ended before answer was sent.");
6218
6536
  this.callSession?.destroy();
6219
6537
  } else if (!vertoByeOrAccepted) {
6220
- logger$14.info("[WebRTCManager] Call was not accepted, sending verto.bye.");
6538
+ logger$15.info("[WebRTCManager] Call was not accepted, sending verto.bye.");
6221
6539
  try {
6222
6540
  await this.bye("USER_BUSY");
6223
6541
  } finally {
@@ -6225,14 +6543,14 @@ var WebRTCVertoManager = class extends VertoManager {
6225
6543
  this.callSession?.destroy();
6226
6544
  }
6227
6545
  } else {
6228
- logger$14.debug("[WebRTCManager] Call accepted, sending answer");
6546
+ logger$15.debug("[WebRTCManager] Call accepted, sending answer");
6229
6547
  try {
6230
6548
  this._signalingStatus$.next("connecting");
6231
6549
  await this.sendLocalDescription(vertoMessageRequest, rtcPeerConnectionController);
6232
6550
  await rtcPeerConnectionController.updateAnswerStatus({ status: "sent" });
6233
6551
  await this.attachManager.attach(this.buildAttachableCall());
6234
6552
  } catch (error) {
6235
- logger$14.error("[WebRTCManager] Error sending Verto answer:", error);
6553
+ logger$15.error("[WebRTCManager] Error sending Verto answer:", error);
6236
6554
  this.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
6237
6555
  await rtcPeerConnectionController.updateAnswerStatus({ status: "failed" });
6238
6556
  }
@@ -6273,6 +6591,14 @@ var WebRTCVertoManager = class extends VertoManager {
6273
6591
  async unmuteMainVideoInputDevice() {
6274
6592
  return this.mainPeerConnection.restoreTrackSender("video");
6275
6593
  }
6594
+ /** Get or lazily create the local audio pipeline for the main peer connection. */
6595
+ ensureLocalAudioPipeline() {
6596
+ return this.mainPeerConnection.ensureLocalAudioPipeline();
6597
+ }
6598
+ /** The currently-active local audio pipeline, or null if it hasn't been created. */
6599
+ get localAudioPipeline() {
6600
+ return this.mainPeerConnection.localAudioPipeline;
6601
+ }
6276
6602
  async addInputDevice(options = {
6277
6603
  audio: false,
6278
6604
  video: true
@@ -6323,10 +6649,10 @@ var WebRTCVertoManager = class extends VertoManager {
6323
6649
  });
6324
6650
  await firstValueFrom(rtcPeerConnController.connectionState$.pipe(filter((state) => state === "connected"), take(1), timeout(this._screenShareTimeoutMs), takeUntil(this.destroyed$)));
6325
6651
  this._screenShareStatus$.next("started");
6326
- logger$14.info("[WebRTCManager] Screen share started successfully.");
6652
+ logger$15.info("[WebRTCManager] Screen share started successfully.");
6327
6653
  return rtcPeerConnController.id;
6328
6654
  } catch (error) {
6329
- logger$14.warn("[WebRTCManager] Error initializing additional peer connection:", error);
6655
+ logger$15.warn("[WebRTCManager] Error initializing additional peer connection:", error);
6330
6656
  this.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
6331
6657
  if (rtcPeerConnController) rtcPeerConnController.destroy();
6332
6658
  this._screenShareStatus$.next("none");
@@ -6345,9 +6671,9 @@ var WebRTCVertoManager = class extends VertoManager {
6345
6671
  if (removeTrack) return this.mainPeerConnection.stopTrackSender(removeTrack, { updateTransceiverDirection: true });
6346
6672
  }
6347
6673
  async removeScreenMedia() {
6348
- if (!["starting", "started"].includes(this._screenShareStatus$.value)) logger$14.warn("[WebRTCManager] No active screen share to stop.");
6674
+ if (!["starting", "started"].includes(this._screenShareStatus$.value)) logger$15.warn("[WebRTCManager] No active screen share to stop.");
6349
6675
  if (!this._screenShareId) {
6350
- logger$14.debug("[WebRTCManager] No screen share peer connection found.");
6676
+ logger$15.debug("[WebRTCManager] No screen share peer connection found.");
6351
6677
  return;
6352
6678
  }
6353
6679
  this._screenShareStatus$.next("stopping");
@@ -6376,7 +6702,7 @@ var WebRTCVertoManager = class extends VertoManager {
6376
6702
  dialogParams: this.dialogParams(rtcPeerConnController)
6377
6703
  }));
6378
6704
  } catch (error) {
6379
- logger$14.warn("[WebRTCManager] Call might already be disconnected, error sending Verto bye:", error);
6705
+ logger$15.warn("[WebRTCManager] Call might already be disconnected, error sending Verto bye:", error);
6380
6706
  throw error;
6381
6707
  }
6382
6708
  }
@@ -6394,7 +6720,7 @@ var WebRTCVertoManager = class extends VertoManager {
6394
6720
  try {
6395
6721
  await this.executeVerto(vertoInfoMessage);
6396
6722
  } catch (error) {
6397
- logger$14.warn("[WebRTCManager] Error sending DTMF digits:", error);
6723
+ logger$15.warn("[WebRTCManager] Error sending DTMF digits:", error);
6398
6724
  throw error;
6399
6725
  }
6400
6726
  }
@@ -6405,10 +6731,10 @@ var WebRTCVertoManager = class extends VertoManager {
6405
6731
  action: "transfer"
6406
6732
  });
6407
6733
  try {
6408
- logger$14.debug("[WebRTCManager] Transferring call with options:", options);
6734
+ logger$15.debug("[WebRTCManager] Transferring call with options:", options);
6409
6735
  await this.executeVerto(message);
6410
6736
  } catch (error) {
6411
- logger$14.error("[WebRTCManager] Error transferring call:", error);
6737
+ logger$15.error("[WebRTCManager] Error transferring call:", error);
6412
6738
  throw error;
6413
6739
  }
6414
6740
  }
@@ -6422,6 +6748,76 @@ var WebRTCVertoManager = class extends VertoManager {
6422
6748
  }
6423
6749
  };
6424
6750
 
6751
+ //#endregion
6752
+ //#region src/controllers/RemoteAudioMeter.ts
6753
+ const logger$14 = getLogger();
6754
+ /**
6755
+ * Read-only audio level meter for a remote MediaStream. Attaches an
6756
+ * AnalyserNode to a MediaStreamAudioSourceNode so it observes the stream
6757
+ * without affecting the caller's playback path (no GainNode, no destination).
6758
+ *
6759
+ * The server delivers all remote audio as a single mixed stream — there is
6760
+ * no per-participant demux — so this meter reports the aggregate remote
6761
+ * level, not per-member.
6762
+ */
6763
+ var RemoteAudioMeter = class extends Destroyable {
6764
+ constructor(options = {}) {
6765
+ super();
6766
+ this._source = null;
6767
+ this._stream = null;
6768
+ this._audioContext = (options.audioContextFactory ?? (() => new AudioContext()))();
6769
+ this._analyser = this._audioContext.createAnalyser();
6770
+ this._analyser.fftSize = 2048;
6771
+ this._analyser.smoothingTimeConstant = .3;
6772
+ this._analyserBuffer = new Uint8Array(new ArrayBuffer(this._analyser.fftSize));
6773
+ this._pollIntervalMs = options.pollIntervalMs ?? AUDIO_LEVEL_POLL_INTERVAL_MS;
6774
+ }
6775
+ /** RMS level of the remote audio, 0..1. 0 when no stream is attached. */
6776
+ get level$() {
6777
+ return this.deferEmission(interval(this._pollIntervalMs, animationFrameScheduler).pipe(map(() => this.computeLevel())));
6778
+ }
6779
+ /**
6780
+ * Attach (or replace) the MediaStream whose audio track is being metered.
6781
+ * Pass null to detach without destroying the meter.
6782
+ */
6783
+ setStream(stream) {
6784
+ if (this._source) {
6785
+ try {
6786
+ this._source.disconnect();
6787
+ } catch (error) {
6788
+ logger$14.debug("[RemoteAudioMeter] source disconnect warning:", error);
6789
+ }
6790
+ this._source = null;
6791
+ this._stream = null;
6792
+ }
6793
+ if (!stream || stream.getAudioTracks().length === 0) return;
6794
+ this._stream = new MediaStream(stream.getAudioTracks());
6795
+ this._source = this._audioContext.createMediaStreamSource(this._stream);
6796
+ }
6797
+ destroy() {
6798
+ if (this._source) {
6799
+ try {
6800
+ this._source.disconnect();
6801
+ } catch {}
6802
+ this._source = null;
6803
+ }
6804
+ this._audioContext.close().catch((error) => {
6805
+ logger$14.debug("[RemoteAudioMeter] audio context close warning:", error);
6806
+ });
6807
+ super.destroy();
6808
+ }
6809
+ computeLevel() {
6810
+ if (!this._source) return 0;
6811
+ this._analyser.getByteTimeDomainData(this._analyserBuffer);
6812
+ let sum = 0;
6813
+ for (const sample of this._analyserBuffer) {
6814
+ const normalized = (sample - 128) / 128;
6815
+ sum += normalized * normalized;
6816
+ }
6817
+ return Math.sqrt(sum / this._analyserBuffer.length);
6818
+ }
6819
+ };
6820
+
6425
6821
  //#endregion
6426
6822
  //#region src/controllers/RTCStatsMonitor.ts
6427
6823
  /**
@@ -6584,11 +6980,11 @@ var RTCStatsMonitor = class extends Destroyable {
6584
6980
  let availableOutgoingBitrate;
6585
6981
  report.forEach((stat) => {
6586
6982
  if (isInboundRtpStat(stat)) if (stat.kind === "audio") {
6587
- audioPacketsReceived += stat.packetsReceived ?? this.lastAudioPacketsReceived;
6983
+ audioPacketsReceived += stat.packetsReceived ?? 0;
6588
6984
  audioPacketsLost += stat.packetsLost ?? 0;
6589
6985
  audioJitter = Math.max(audioJitter, (stat.jitter ?? 0) * 1e3);
6590
6986
  } else {
6591
- videoPacketsReceived += stat.packetsReceived ?? this.lastVideoPacketsReceived;
6987
+ videoPacketsReceived += stat.packetsReceived ?? 0;
6592
6988
  videoPacketsLost += stat.packetsLost ?? 0;
6593
6989
  }
6594
6990
  if (isCandidatePairStat(stat) && stat.state === "succeeded" && stat.nominated) {
@@ -7238,6 +7634,8 @@ var WebRTCCall = class extends Destroyable {
7238
7634
  this._bandwidthConstrained$ = this.createBehaviorSubject(false);
7239
7635
  this._mediaParamsUpdated$ = this.createSubject();
7240
7636
  this._customSubscriptions = /* @__PURE__ */ new Map();
7637
+ this._pushToTalkEnabled = false;
7638
+ this._remoteAudioMeter = null;
7241
7639
  this.id = options.callId ?? v4();
7242
7640
  this.to = options.to;
7243
7641
  this._userVariables$.next({
@@ -7636,10 +8034,10 @@ var WebRTCCall = class extends Destroyable {
7636
8034
  try {
7637
8035
  if (this.vertoManager.requestIceRestartAll) await this.vertoManager.requestIceRestartAll(relayOnly);
7638
8036
  else await this.vertoManager.requestIceRestart?.(relayOnly);
7639
- return true;
7640
8037
  } catch {
7641
8038
  return false;
7642
8039
  }
8040
+ return this.waitForPeerConnectionConnected();
7643
8041
  },
7644
8042
  disableVideo: () => {
7645
8043
  try {
@@ -7731,6 +8129,27 @@ var WebRTCCall = class extends Destroyable {
7731
8129
  }
7732
8130
  }
7733
8131
  /**
8132
+ * Wait for the underlying RTCPeerConnection to reach 'connected' after
8133
+ * triggering an ICE restart. Resolves true on success, false on failure
8134
+ * or if the state doesn't transition within the configured timeout.
8135
+ *
8136
+ * Polls connectionState directly because the recovery manager already
8137
+ * wraps this call in its own withTimeout(); a separate listener-based
8138
+ * implementation would race the outer timeout in subtle ways.
8139
+ */
8140
+ async waitForPeerConnectionConnected() {
8141
+ const pc = this.rtcPeerConnection;
8142
+ if (!pc) return false;
8143
+ const deadline = Date.now() + PEER_CONNECTION_RECOVERY_WAIT_MS;
8144
+ for (;;) {
8145
+ const state = pc.connectionState;
8146
+ if (state === "connected") return true;
8147
+ if (state === "failed" || state === "closed") return false;
8148
+ if (Date.now() >= deadline) return false;
8149
+ await new Promise((resolve) => setTimeout(resolve, PEER_CONNECTION_RECOVERY_POLL_MS));
8150
+ }
8151
+ }
8152
+ /**
7734
8153
  * @internal Stop and destroy resilience subsystems (on disconnect/destroy).
7735
8154
  * Clears references so they can be re-created on reconnect.
7736
8155
  */
@@ -7867,8 +8286,13 @@ var WebRTCCall = class extends Destroyable {
7867
8286
  const cached = this._customSubscriptions.get(eventType);
7868
8287
  if (cached) return cached;
7869
8288
  const filtered$ = this.callSessionEvents$.pipe(filter((event) => event.event_type === eventType), map((event) => JSON.parse(JSON.stringify(event))), takeUntil(this._destroyed$));
8289
+ this._sendVertoSubscribe(eventType).then(() => {
8290
+ this._customSubscriptions.set(eventType, filtered$);
8291
+ }, (error) => {
8292
+ this._customSubscriptions.delete(eventType);
8293
+ logger$11.warn(`[Call] verto.subscribe for '${eventType}' failed, not caching:`, error);
8294
+ });
7870
8295
  this._customSubscriptions.set(eventType, filtered$);
7871
- this._sendVertoSubscribe(eventType);
7872
8296
  return filtered$;
7873
8297
  }
7874
8298
  get webrtcMessages$() {
@@ -7983,37 +8407,156 @@ var WebRTCCall = class extends Destroyable {
7983
8407
  async transfer(options) {
7984
8408
  return this.vertoManager.transfer(options);
7985
8409
  }
8410
+ /**
8411
+ * Set the local microphone gain as a percentage applied before transmission.
8412
+ *
8413
+ * - `0` = silent
8414
+ * - `100` = unity (no change, default)
8415
+ * - `200` = 2× digital boost (max; expect clipping / noise amplification)
8416
+ *
8417
+ * Values are clamped to [0, 200]. Engages the local audio pipeline on
8418
+ * first use (one-time cost).
8419
+ *
8420
+ * Note: this is a **digital** multiplier applied in a Web Audio GainNode
8421
+ * between your mic track and the RTCRtpSender — it does not change the
8422
+ * physical mic's hardware sensitivity. Browsers' autoGainControl can
8423
+ * fight the setting; call {@link setAutoGainControl}(false) for
8424
+ * predictable behaviour.
8425
+ *
8426
+ * @param value - Gain percentage (0..200; 100 = unity).
8427
+ */
8428
+ setLocalMicrophoneGain(value) {
8429
+ const pipeline = this.vertoManager.ensureLocalAudioPipeline();
8430
+ if (!pipeline) {
8431
+ logger$11.warn("[Call] setLocalMicrophoneGain: audio pipeline unavailable");
8432
+ return;
8433
+ }
8434
+ const percent = Math.max(0, Math.min(200, value));
8435
+ pipeline.setGain(percent / 100);
8436
+ }
8437
+ /** Observable of the current local microphone gain (0..200, where 100 = unity). */
8438
+ get localMicrophoneGain$() {
8439
+ const pipeline = this.vertoManager.ensureLocalAudioPipeline();
8440
+ if (!pipeline) return of(100).pipe(takeUntil(this._destroyed$));
8441
+ return this.publicCachedObservable("localMicrophoneGain$", () => pipeline.gain$.pipe(map((multiplier) => multiplier * 100), takeUntil(this._destroyed$)));
8442
+ }
8443
+ /**
8444
+ * Observable of the RMS audio level of the local microphone, 0..1.
8445
+ * Emits at ~30fps while a mic track is active. Engages the local audio
8446
+ * pipeline on first subscription.
8447
+ */
8448
+ get localAudioLevel$() {
8449
+ const pipeline = this.vertoManager.ensureLocalAudioPipeline();
8450
+ if (!pipeline) return of(0).pipe(takeUntil(this._destroyed$));
8451
+ return this.publicCachedObservable("localAudioLevel$", () => pipeline.level$.pipe(takeUntil(this._destroyed$), share()));
8452
+ }
8453
+ /**
8454
+ * Observable that is `true` while the local participant is speaking
8455
+ * (RMS level above the VAD threshold, with hold time to avoid flicker).
8456
+ */
8457
+ get localSpeaking$() {
8458
+ const pipeline = this.vertoManager.ensureLocalAudioPipeline();
8459
+ if (!pipeline) return of(false).pipe(takeUntil(this._destroyed$));
8460
+ return this.publicCachedObservable("localSpeaking$", () => pipeline.speaking$.pipe(takeUntil(this._destroyed$), share()));
8461
+ }
8462
+ /**
8463
+ * Enable push-to-talk: while {@link setPushToTalkActive} has been called
8464
+ * with `false`, the microphone gain is forced to 0; calling
8465
+ * {@link setPushToTalkActive} with `true` restores the configured gain.
8466
+ * Use this instead of mute/unmute for instant talk/silence transitions
8467
+ * because it doesn't rebuild the track.
8468
+ *
8469
+ * This method installs the pipeline but does not attach any keyboard
8470
+ * listener — consumers bind the key themselves and call
8471
+ * {@link setPushToTalkActive} on keydown/keyup.
8472
+ */
8473
+ enablePushToTalk() {
8474
+ const pipeline = this.vertoManager.ensureLocalAudioPipeline();
8475
+ if (!pipeline) {
8476
+ logger$11.warn("[Call] enablePushToTalk: audio pipeline unavailable");
8477
+ return;
8478
+ }
8479
+ pipeline.setPTTActive(false);
8480
+ this._pushToTalkEnabled = true;
8481
+ }
8482
+ /** Disable push-to-talk; mic gain returns to the configured value. */
8483
+ disablePushToTalk() {
8484
+ this.vertoManager.localAudioPipeline?.setPTTActive(true);
8485
+ this._pushToTalkEnabled = false;
8486
+ }
8487
+ /**
8488
+ * While push-to-talk is enabled, sets the talk state. `true` = transmitting,
8489
+ * `false` = silent. No-op if push-to-talk has not been enabled.
8490
+ */
8491
+ setPushToTalkActive(active) {
8492
+ if (!this._pushToTalkEnabled) return;
8493
+ this.vertoManager.localAudioPipeline?.setPTTActive(active);
8494
+ }
8495
+ /**
8496
+ * Toggle echo cancellation on the local mic at runtime. Applied via
8497
+ * `track.applyConstraints`; browsers that don't honour runtime constraints
8498
+ * (notably iOS Safari) fall back to re-acquiring the track with the new
8499
+ * constraint set and plumbing the replacement through the local audio
8500
+ * pipeline if one is active.
8501
+ */
8502
+ async setEchoCancellation(enabled) {
8503
+ await this.vertoManager.updateMediaConstraints({ audio: { echoCancellation: enabled } });
8504
+ }
8505
+ /** Toggle browser noise suppression on the local mic at runtime. */
8506
+ async setNoiseSuppression(enabled) {
8507
+ await this.vertoManager.updateMediaConstraints({ audio: { noiseSuppression: enabled } });
8508
+ }
8509
+ /** Toggle browser automatic gain control on the local mic at runtime. */
8510
+ async setAutoGainControl(enabled) {
8511
+ await this.vertoManager.updateMediaConstraints({ audio: { autoGainControl: enabled } });
8512
+ }
8513
+ /**
8514
+ * Observable of the aggregate remote audio level, 0..1 RMS. The server
8515
+ * delivers a single mixed audio stream for all remote participants — this
8516
+ * meter reports that mix. Per-participant audio is not available client-side.
8517
+ *
8518
+ * Engages a shared AudioContext on first subscription (cheap — one
8519
+ * AnalyserNode, no GainNode, no destination) so it does not affect the
8520
+ * caller's audio element playback.
8521
+ */
8522
+ get remoteAudioLevel$() {
8523
+ return this.publicCachedObservable("remoteAudioLevel$", () => {
8524
+ this._remoteAudioMeter ??= new RemoteAudioMeter();
8525
+ const meter = this._remoteAudioMeter;
8526
+ this.subscribeTo(this.vertoManager.remoteStream$, (stream) => {
8527
+ meter.setStream(stream);
8528
+ });
8529
+ return meter.level$.pipe(takeUntil(this._destroyed$), share());
8530
+ });
8531
+ }
7986
8532
  /** Destroys the call, releasing all resources and subscriptions. */
7987
8533
  destroy() {
7988
8534
  if (this._status$.value === "destroyed") return;
7989
8535
  this._status$.next("destroyed");
7990
8536
  this.stopResilienceSubsystems();
8537
+ this._remoteAudioMeter?.destroy();
8538
+ this._remoteAudioMeter = null;
7991
8539
  this.vertoManager.destroy();
7992
8540
  this.callEventsManager.destroy();
7993
8541
  super.destroy();
7994
8542
  }
7995
8543
  /**
7996
8544
  * @internal Send a verto.subscribe message to add an event type to the
7997
- * server's subscription list for this call. Best-effort failures are
7998
- * logged but don't prevent the filtered observable from being returned.
7999
- */
8000
- _sendVertoSubscribe(eventType) {
8001
- try {
8002
- const message = VertoSubscribe({
8003
- sessid: this.id,
8004
- eventChannel: [eventType]
8005
- });
8006
- const params = {
8007
- callID: this.id,
8008
- node_id: this.vertoManager.nodeId ?? "",
8009
- message
8010
- };
8011
- this.clientSession.execute(WebrtcVerto(params)).catch((error) => {
8012
- logger$11.warn(`[Call] verto.subscribe for '${eventType}' failed (non-fatal):`, error);
8013
- });
8014
- } catch (error) {
8015
- logger$11.warn(`[Call] Failed to send verto.subscribe for '${eventType}':`, error);
8016
- }
8545
+ * server's subscription list for this call. Returns the underlying RPC
8546
+ * promise so callers can decide whether to cache the observable on success
8547
+ * or retry on failure.
8548
+ */
8549
+ async _sendVertoSubscribe(eventType) {
8550
+ const message = VertoSubscribe({
8551
+ sessid: this.id,
8552
+ eventChannel: [eventType]
8553
+ });
8554
+ const params = {
8555
+ callID: this.id,
8556
+ node_id: this.vertoManager.nodeId ?? "",
8557
+ message
8558
+ };
8559
+ await this.clientSession.execute(WebrtcVerto(params));
8017
8560
  }
8018
8561
  };
8019
8562
 
@@ -8030,11 +8573,21 @@ function inferCallErrorKind(error) {
8030
8573
  if (error instanceof WebSocketConnectionError || error instanceof TransportConnectionError) return "network";
8031
8574
  return "internal";
8032
8575
  }
8576
+ /** JSON-RPC error codes that ClientSessionManager treats as recoverable at the
8577
+ * session level. Surfacing one of these against an in-flight call should not
8578
+ * destroy the call, because the session will reauthenticate and any pending
8579
+ * RPC can then be retried. */
8580
+ const RECOVERABLE_RPC_CODES = new Set([
8581
+ RPC_ERROR_REQUESTER_VALIDATION_FAILED,
8582
+ RPC_ERROR_AUTHENTICATION_FAILED,
8583
+ RPC_ERROR_INVALID_PARAMS
8584
+ ]);
8033
8585
  /** Determines whether an error should be fatal (destroy the call). */
8034
8586
  function isFatalError(error) {
8035
8587
  if (error instanceof VertoPongError) return false;
8036
8588
  if (error instanceof MediaTrackError) return false;
8037
8589
  if (error instanceof RPCTimeoutError) return false;
8590
+ if (error instanceof JSONRPCError && RECOVERABLE_RPC_CODES.has(error.code)) return false;
8038
8591
  return true;
8039
8592
  }
8040
8593
  /**
@@ -8537,7 +9090,7 @@ var ClientSessionManager = class extends Destroyable {
8537
9090
  this.attachManager = attachManager;
8538
9091
  this.dpopManager = dpopManager;
8539
9092
  this.callCreateTimeout = 6e3;
8540
- this.agent = `signalwire-typescript-sdk/1.0.0`;
9093
+ this.agent = `signalwire-js/4.0.0`;
8541
9094
  this.eventAcks = true;
8542
9095
  this.authorizationState$ = this.createReplaySubject(1);
8543
9096
  this.connectVersion = {
@@ -8549,7 +9102,7 @@ var ClientSessionManager = class extends Destroyable {
8549
9102
  this._errors$ = this.createReplaySubject(1);
8550
9103
  this._authState$ = this.createBehaviorSubject({ kind: "unauthenticated" });
8551
9104
  this._wasClientBound = false;
8552
- this._subscriberInfo$ = this.createBehaviorSubject(null);
9105
+ this._userInfo$ = this.createBehaviorSubject(null);
8553
9106
  this._calls$ = this.createBehaviorSubject({});
8554
9107
  this._iceServers$ = this.createBehaviorSubject([]);
8555
9108
  attachManager.setSession(this);
@@ -8562,11 +9115,11 @@ var ClientSessionManager = class extends Destroyable {
8562
9115
  get incomingCalls() {
8563
9116
  return Object.values(this._calls$.value).filter((call) => call.direction === "inbound");
8564
9117
  }
8565
- get subscriberInfo$() {
8566
- return this._subscriberInfo$.asObservable();
9118
+ get userInfo$() {
9119
+ return this._userInfo$.asObservable();
8567
9120
  }
8568
- get subscriberInfo() {
8569
- return this._subscriberInfo$.value;
9121
+ get userInfo() {
9122
+ return this._userInfo$.value;
8570
9123
  }
8571
9124
  get calls$() {
8572
9125
  return this.cachedObservable("calls$", () => this._calls$.pipe(map((calls) => Object.values(calls))));
@@ -8871,7 +9424,6 @@ var ClientSessionManager = class extends Destroyable {
8871
9424
  displayDirection: invite.display_direction,
8872
9425
  userVariables: invite.userVariables
8873
9426
  });
8874
- await firstValueFrom(callSession.status$);
8875
9427
  this._calls$.next({
8876
9428
  [`${callSession.id}`]: callSession,
8877
9429
  ...this._calls$.value
@@ -8892,7 +9444,7 @@ var ClientSessionManager = class extends Destroyable {
8892
9444
  logger$8.debug(`[Session] Verto attach for existing call ${callID}, deferring to per-call handler`);
8893
9445
  return;
8894
9446
  }
8895
- const storedOptions = this.attachManager.consumePendingAttachment(callID);
9447
+ const storedOptions = await this.attachManager.consumePendingAttachment(callID);
8896
9448
  logger$8.debug(`[Session] Creating reattached call for callID: ${callID}`);
8897
9449
  const callSession = await this.createCall({
8898
9450
  nodeId: attach.node_id,
@@ -8904,7 +9456,6 @@ var ClientSessionManager = class extends Destroyable {
8904
9456
  reattach: true,
8905
9457
  ...storedOptions
8906
9458
  });
8907
- await firstValueFrom(callSession.status$);
8908
9459
  this._calls$.next({
8909
9460
  [`${callSession.id}`]: callSession,
8910
9461
  ...this._calls$.value
@@ -9013,22 +9564,22 @@ var ConversationMessageCollection = class extends EntityCollection {
9013
9564
  }
9014
9565
  };
9015
9566
  var ConversationsManager = class {
9016
- constructor(clientSession, http, getSubscriberAddressId, onError) {
9567
+ constructor(clientSession, http, getUserAddressId, onError) {
9017
9568
  this.clientSession = clientSession;
9018
9569
  this.http = http;
9019
- this.getSubscriberAddressId = getSubscriberAddressId;
9570
+ this.getUserAddressId = getUserAddressId;
9020
9571
  this.onError = onError;
9021
9572
  this.groupIds = /* @__PURE__ */ new Map();
9022
9573
  }
9023
9574
  async join(addressId) {
9024
- const subscriberFromAddressId = this.getSubscriberAddressId();
9575
+ const userFromAddressId = this.getUserAddressId();
9025
9576
  try {
9026
9577
  const response = await this.http.request({
9027
9578
  ...POST_PARAMS,
9028
9579
  url: `/api/fabric/conversations/join`,
9029
9580
  body: JSON.stringify({
9030
- from_fabric_address_id: subscriberFromAddressId,
9031
- fabric_address_ids: [addressId, subscriberFromAddressId]
9581
+ from_fabric_address_id: userFromAddressId,
9582
+ fabric_address_ids: [addressId, userFromAddressId]
9032
9583
  })
9033
9584
  });
9034
9585
  if (response.ok && !!response.body) {
@@ -9050,14 +9601,14 @@ var ConversationsManager = class {
9050
9601
  }
9051
9602
  async sendText(text, destinationAddressId) {
9052
9603
  const groupId = this.groupIds.get(destinationAddressId) ?? await this.join(destinationAddressId);
9053
- const subscriberFromAddressId = this.getSubscriberAddressId();
9604
+ const userFromAddressId = this.getUserAddressId();
9054
9605
  try {
9055
9606
  if ((await this.http.request({
9056
9607
  ...POST_PARAMS,
9057
9608
  url: "/api/fabric/messages",
9058
9609
  body: JSON.stringify({
9059
9610
  group_id: groupId,
9060
- from_fabric_address_id: subscriberFromAddressId,
9611
+ from_fabric_address_id: userFromAddressId,
9061
9612
  text
9062
9613
  })
9063
9614
  })).ok) return;
@@ -9129,17 +9680,17 @@ var DeviceTokenManager = class extends Destroyable {
9129
9680
  return this._effectiveExpireIn;
9130
9681
  }
9131
9682
  /**
9132
- * Activates the Client Bound SAT flow when the subscriber's token has
9683
+ * Activates the Client Bound SAT flow when the user's token has
9133
9684
  * `sat:refresh` scope.
9134
9685
  *
9135
9686
  * Steps:
9136
- * 1. Check subscriber's `sat_claims` for `sat:refresh` scope
9687
+ * 1. Check user's `sat_claims` for `sat:refresh` scope
9137
9688
  * 2. Call `/api/fabric/subscriber/devices/token` with a DPoP proof
9138
9689
  * 3. Reauthenticate the session with the Client Bound SAT + DPoP proof
9139
9690
  * 4. Emit token to trigger the reactive refresh pipeline
9140
9691
  */
9141
- async activate(subscriber, session, updateCredential) {
9142
- const { satClaims } = subscriber;
9692
+ async activate(user, session, updateCredential) {
9693
+ const { satClaims } = user;
9143
9694
  if (!satClaims?.scope?.includes(SAT_REFRESH_SCOPE)) {
9144
9695
  logger$6.debug("[DeviceToken] No sat:refresh scope, skipping Client Bound SAT activation");
9145
9696
  return;
@@ -9422,8 +9973,8 @@ const isEmptyArray = (a) => {
9422
9973
  };
9423
9974
 
9424
9975
  //#endregion
9425
- //#region src/utils/warnup.ts
9426
- const warnup = (observable) => {
9976
+ //#region src/utils/warmup.ts
9977
+ const warmup = (observable) => {
9427
9978
  observable.pipe(take(1)).subscribe();
9428
9979
  };
9429
9980
 
@@ -9471,7 +10022,7 @@ var DirectoryManager = class extends Destroyable {
9471
10022
  return address;
9472
10023
  }));
9473
10024
  if (observable) {
9474
- warnup(observable);
10025
+ warmup(observable);
9475
10026
  this._observableRegistry.set(id, observable);
9476
10027
  }
9477
10028
  this._addressesInstances.set(id, address);
@@ -9623,10 +10174,9 @@ var WebSocketController = class WebSocketController extends Destroyable {
9623
10174
  else this._status$.next("disconnected");
9624
10175
  }
9625
10176
  reconnect() {
9626
- if (this.shouldReconnect) {
9627
- this._status$.next("reconnecting");
9628
- this.scheduleReconnection();
9629
- } else this._status$.next("disconnected");
10177
+ this.shouldReconnect = true;
10178
+ this._status$.next("reconnecting");
10179
+ this.scheduleReconnection();
9630
10180
  }
9631
10181
  send(data) {
9632
10182
  if (this._status$.value === "connected" && this.socket?.readyState === 1) {
@@ -9983,7 +10533,7 @@ var SignalWire = class extends Destroyable {
9983
10533
  constructor(credentialProvider, options = {}) {
9984
10534
  super();
9985
10535
  this.preferences = new ClientPreferences();
9986
- this._subscriber$ = this.createBehaviorSubject(void 0);
10536
+ this._user$ = this.createBehaviorSubject(void 0);
9987
10537
  this._directory$ = this.createBehaviorSubject(void 0);
9988
10538
  this._isConnected$ = this.createBehaviorSubject(false);
9989
10539
  this._isRegistered$ = this.createBehaviorSubject(false);
@@ -10123,7 +10673,7 @@ var SignalWire = class extends Destroyable {
10123
10673
  if (this._deps.persistSession) this._deps.storage.setItem("sw:cached_credential", credential, "local");
10124
10674
  }
10125
10675
  async init() {
10126
- this._subscriber$.next(new Subscriber(this._deps.http));
10676
+ this._user$.next(new User(this._deps.http));
10127
10677
  if (!this._options.skipConnection) await this.connect();
10128
10678
  if (!this._options.reconnectAttachedCalls && this._attachManager) await this._attachManager.flush();
10129
10679
  if (!this._options.skipRegister) try {
@@ -10185,14 +10735,15 @@ var SignalWire = class extends Destroyable {
10185
10735
  * `'reconnecting'`, `'disconnecting'`, or `'disconnected'`.
10186
10736
  */
10187
10737
  async connect() {
10738
+ await this.teardownTransportAndSession();
10188
10739
  try {
10189
- const subscriber = this._subscriber$.value;
10190
- if (!subscriber) throw new UnexpectedError("Subscriber not initialized before connect");
10191
- if (!await firstValueFrom(subscriber.fetched$)) throw new UnexpectedError("Failed to fetch subscriber information - fetched$ emitted false");
10192
- this._deps.subscriber = subscriber;
10740
+ const user = this._user$.value;
10741
+ if (!user) throw new UnexpectedError("User not initialized before connect");
10742
+ if (!await firstValueFrom(user.fetched$)) throw new UnexpectedError("Failed to fetch user information - fetched$ emitted false");
10743
+ this._deps.user = user;
10193
10744
  } catch (error) {
10194
- logger$1.error(`[SignalWire] Failed to fetch subscriber information: ${error instanceof Error ? error.message : "Unknown error"}. This usually means the subscriber token is invalid or expired.`);
10195
- throw new UnexpectedError("Error fetching subscriber information", { cause: error });
10745
+ logger$1.error(`[SignalWire] Failed to fetch user information: ${error instanceof Error ? error.message : "Unknown error"}. This usually means the user token is invalid or expired.`);
10746
+ throw new UnexpectedError("Error fetching user information", { cause: error });
10196
10747
  }
10197
10748
  const errorHandler = (error) => {
10198
10749
  this._errors$.next(error);
@@ -10226,7 +10777,7 @@ var SignalWire = class extends Destroyable {
10226
10777
  logger$1.debug("[SignalWire] Developer refresh disabled — Client Bound SAT activation starting");
10227
10778
  }
10228
10779
  this._deviceTokenManager = new DeviceTokenManager(this._dpopManager, this._deps.http, (error) => this._errors$.next(error), () => this._deps.credential);
10229
- await this._deviceTokenManager.activate(this._deps.subscriber, this._clientSession, (cred) => {
10780
+ await this._deviceTokenManager.activate(this._deps.user, this._clientSession, (cred) => {
10230
10781
  this._deps.credential = {
10231
10782
  ...this._deps.credential,
10232
10783
  ...cred
@@ -10236,7 +10787,7 @@ var SignalWire = class extends Destroyable {
10236
10787
  this.subscribeTo(this._clientSession.authenticated$.pipe(skip(1), filter(Boolean)), async () => {
10237
10788
  try {
10238
10789
  if (this._deviceTokenManager) {
10239
- await this._deviceTokenManager.activate(this._deps.subscriber, this._clientSession, (cred) => {
10790
+ await this._deviceTokenManager.activate(this._deps.user, this._clientSession, (cred) => {
10240
10791
  this._deps.credential = {
10241
10792
  ...this._deps.credential,
10242
10793
  ...cred
@@ -10249,15 +10800,15 @@ var SignalWire = class extends Destroyable {
10249
10800
  this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
10250
10801
  }
10251
10802
  try {
10252
- logger$1.debug("[SignalWire] Re-registering subscriber after reconnect");
10803
+ logger$1.debug("[SignalWire] Re-registering user after reconnect");
10253
10804
  await this.register();
10254
- logger$1.debug("[SignalWire] Subscriber re-registered successfully after reconnect");
10805
+ logger$1.debug("[SignalWire] User re-registered successfully after reconnect");
10255
10806
  } catch (error) {
10256
10807
  logger$1.error("[SignalWire] Re-registration failed after reconnect:", error);
10257
10808
  this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
10258
10809
  }
10259
10810
  });
10260
- const conversationManager = new ConversationsManager(this._clientSession, this._deps.http, () => this._deps.getSubscriberFromAddressId(), errorHandler);
10811
+ const conversationManager = new ConversationsManager(this._clientSession, this._deps.http, () => this._deps.getUserFromAddressId(), errorHandler);
10261
10812
  const directory = new DirectoryManager(this._deps.http, this._clientSession, conversationManager, errorHandler);
10262
10813
  this._directory$.next(directory);
10263
10814
  this._clientSession.setDirectory(directory);
@@ -10268,22 +10819,22 @@ var SignalWire = class extends Destroyable {
10268
10819
  });
10269
10820
  }
10270
10821
  /**
10271
- * Observable that emits the {@link Subscriber} profile once fetched,
10822
+ * Observable that emits the {@link User} profile once fetched,
10272
10823
  * or `undefined` before authentication completes.
10273
10824
  *
10274
10825
  * @example
10275
10826
  * ```ts
10276
- * client.subscriber$.subscribe(sub => {
10277
- * if (sub) console.log('Logged in as', sub.email);
10827
+ * client.user$.subscribe(u => {
10828
+ * if (u) console.log('Logged in as', u.email);
10278
10829
  * });
10279
10830
  * ```
10280
10831
  */
10281
- get subscriber$() {
10282
- return this.deferEmission(this._subscriber$.asObservable());
10832
+ get user$() {
10833
+ return this.deferEmission(this._user$.asObservable());
10283
10834
  }
10284
- /** Current subscriber snapshot, or `undefined` if not yet authenticated. */
10285
- get subscriber() {
10286
- return this._subscriber$.value;
10835
+ /** Current user snapshot, or `undefined` if not yet authenticated. */
10836
+ get user() {
10837
+ return this._user$.value;
10287
10838
  }
10288
10839
  /**
10289
10840
  * Observable that emits the {@link Directory} instance once the client is connected,
@@ -10307,11 +10858,11 @@ var SignalWire = class extends Destroyable {
10307
10858
  get directory() {
10308
10859
  return this._directory$.value;
10309
10860
  }
10310
- /** Observable that emits when the subscriber registration state changes. */
10861
+ /** Observable that emits when the user registration state changes. */
10311
10862
  get isRegistered$() {
10312
10863
  return this.deferEmission(this._isRegistered$.asObservable());
10313
10864
  }
10314
- /** Whether the subscriber is currently registered. */
10865
+ /** Whether the user is currently registered. */
10315
10866
  get isRegistered() {
10316
10867
  return this._isRegistered$.value;
10317
10868
  }
@@ -10416,15 +10967,35 @@ var SignalWire = class extends Destroyable {
10416
10967
  this._refreshTimerId = void 0;
10417
10968
  }
10418
10969
  this._diagnosticsCollector?.record("connection", "disconnected");
10419
- await this._clientSession.disconnect();
10420
- this._clientSession.destroy();
10970
+ await this.teardownTransportAndSession();
10421
10971
  this._isConnected$.next(false);
10422
10972
  }
10973
+ /**
10974
+ * Tear down the current transport / session / attach manager. Safe to call
10975
+ * when nothing has been initialized yet (e.g. first connect()).
10976
+ */
10977
+ async teardownTransportAndSession() {
10978
+ const session = this._clientSession;
10979
+ const transport = this._transport;
10980
+ if (session) {
10981
+ try {
10982
+ await session.disconnect();
10983
+ } catch (error) {
10984
+ logger$1.warn("[SignalWire] Error disconnecting previous session:", error);
10985
+ }
10986
+ session.destroy();
10987
+ }
10988
+ if (transport) transport.destroy();
10989
+ this._clientSession = void 0;
10990
+ this._publicSession = void 0;
10991
+ this._transport = void 0;
10992
+ this._attachManager = void 0;
10993
+ }
10423
10994
  async waitAuthentication() {
10424
10995
  await firstValueFrom(this.ready$.pipe(filter((ready$1) => ready$1 === true)));
10425
10996
  }
10426
10997
  /**
10427
- * Registers the subscriber as online to receive inbound calls and events.
10998
+ * Registers the user as online to receive inbound calls and events.
10428
10999
  *
10429
11000
  * Waits for authentication to complete before sending the registration.
10430
11001
  * If the initial attempt fails, reauthentication is attempted automatically.
@@ -10439,26 +11010,31 @@ var SignalWire = class extends Destroyable {
10439
11010
  params: {}
10440
11011
  }));
10441
11012
  this._isRegistered$.next(true);
11013
+ return;
10442
11014
  } catch (error) {
10443
- logger$1.debug("[SignalWire] Failed to register subscriber, trying reauthentication...");
10444
- if (this._deps.credential.token) this._clientSession.reauthenticate(this._deps.credential.token).then(async () => {
11015
+ if (!this._deps.credential.token) {
11016
+ this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
11017
+ throw error;
11018
+ }
11019
+ logger$1.debug("[SignalWire] Failed to register user, trying reauthentication...");
11020
+ try {
11021
+ await this._clientSession.reauthenticate(this._deps.credential.token);
10445
11022
  logger$1.debug("[SignalWire] Reauthentication successful, retrying register()");
10446
11023
  await this._transport.execute(RPCExecute({
10447
11024
  method: "subscriber.online",
10448
11025
  params: {}
10449
11026
  }));
10450
11027
  this._isRegistered$.next(true);
10451
- }).catch((reauthError) => {
11028
+ } catch (reauthError) {
10452
11029
  logger$1.error("[SignalWire] Reauthentication failed during register():", reauthError);
10453
- const registerError = new InvalidCredentialsError("Failed to register subscriber, and reauthentication attempt also failed. Please check your credentials.", { cause: reauthError instanceof Error ? reauthError : new Error(String(reauthError), { cause: reauthError }) });
11030
+ const registerError = new InvalidCredentialsError("Failed to register user, and reauthentication attempt also failed. Please check your credentials.", { cause: reauthError instanceof Error ? reauthError : new Error(String(reauthError), { cause: reauthError }) });
10454
11031
  this._errors$.next(registerError);
10455
- });
10456
- this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
10457
- throw error;
11032
+ throw registerError;
11033
+ }
10458
11034
  }
10459
11035
  }
10460
11036
  /**
10461
- * Unregisters the subscriber, going offline for inbound calls.
11037
+ * Unregisters the user, going offline for inbound calls.
10462
11038
  *
10463
11039
  * The WebSocket connection remains open; use {@link disconnect} to fully close it.
10464
11040
  */
@@ -10470,7 +11046,7 @@ var SignalWire = class extends Destroyable {
10470
11046
  }));
10471
11047
  this._isRegistered$.next(false);
10472
11048
  } catch (error) {
10473
- logger$1.error("[SignalWire] Failed to unregister subscriber:", error);
11049
+ logger$1.error("[SignalWire] Failed to unregister user:", error);
10474
11050
  this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
10475
11051
  throw error;
10476
11052
  }
@@ -10610,6 +11186,36 @@ var SignalWire = class extends Destroyable {
10610
11186
  selectAudioOutputDevice(device) {
10611
11187
  this._deviceController.selectAudioOutputDevice(device);
10612
11188
  }
11189
+ /**
11190
+ * Apply the currently selected audio output device to an HTMLMediaElement
11191
+ * (e.g. the `<audio>` or `<video>` element the consumer attached the
11192
+ * remote stream to). Uses `HTMLMediaElement.setSinkId` under the hood.
11193
+ * Returns a `Promise<boolean>`: `true` if the sink was applied,
11194
+ * `false` if the browser doesn't support `setSinkId` or no device is
11195
+ * selected.
11196
+ *
11197
+ * @example
11198
+ * ```ts
11199
+ * audioEl.srcObject = call.remoteStream;
11200
+ * await client.applySelectedAudioOutputDevice(audioEl);
11201
+ * ```
11202
+ */
11203
+ async applySelectedAudioOutputDevice(element) {
11204
+ const device = this._deviceController.selectedAudioOutputDevice;
11205
+ if (!device?.deviceId) return false;
11206
+ const withSink = element;
11207
+ if (typeof withSink.setSinkId !== "function") {
11208
+ logger$1.warn("[SignalWire] setSinkId not supported on this element / browser");
11209
+ return false;
11210
+ }
11211
+ try {
11212
+ await withSink.setSinkId(device.deviceId);
11213
+ return true;
11214
+ } catch (error) {
11215
+ logger$1.warn("[SignalWire] Failed to apply audio output device:", error);
11216
+ return false;
11217
+ }
11218
+ }
10613
11219
  /** Starts monitoring for media device changes (connect/disconnect). */
10614
11220
  enableDeviceMonitoring() {
10615
11221
  this._deviceController.enableDeviceMonitoring();
@@ -10790,6 +11396,7 @@ var EmbedTokenCredentialProvider = class {
10790
11396
  try {
10791
11397
  const response = await fetch(url, {
10792
11398
  method: "POST",
11399
+ headers: { "Content-Type": "application/json" },
10793
11400
  body: JSON.stringify({ token: this.embedToken }),
10794
11401
  signal: controller.signal
10795
11402
  });
@@ -10896,5 +11503,5 @@ const emitReadyEvent = () => {
10896
11503
  emitReadyEvent();
10897
11504
 
10898
11505
  //#endregion
10899
- export { Address, CallCreateError, ClientPreferences, CollectionFetchError, DPoPInitError, DeviceTokenError, InvalidCredentialsError, MediaTrackError, MessageParseError, OverconstrainedFallbackError, Participant, PreflightError, RecoveryError, SelfCapabilities, SelfParticipant, SignalWire, StaticCredentialProvider, Subscriber, TokenRefreshError, UnexpectedError, VertoPongError, WebRTCCall, embeddableCall, isSelfParticipant, ready, setDebugOptions, setLogLevel, setLogger, version };
11506
+ export { Address, CallCreateError, ClientPreferences, CollectionFetchError, DPoPInitError, DeviceTokenError, EmbedTokenCredentialProvider, InvalidCredentialsError, MediaTrackError, MessageParseError, OverconstrainedFallbackError, Participant, PreflightError, RecoveryError, SelfCapabilities, SelfParticipant, SignalWire, StaticCredentialProvider, TokenRefreshError, UnexpectedError, User, VertoPongError, WebRTCCall, embeddableCall, getLogger, isSelfParticipant, ready, setDebugOptions, setLogLevel, setLogger, version };
10900
11507
  //# sourceMappingURL=index.mjs.map