@signalwire/js 4.0.0-dev-20260910191529 → 4.0.0-dev-20260911135809

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1,4 +1,4 @@
1
- const require_operators = require('./operators-Bn4Ij3VB.cjs');
1
+ const require_operators = require('./operators-B3G5z2B1.cjs');
2
2
  let jwt_decode = require("jwt-decode");
3
3
  let rxjs = require("rxjs");
4
4
  let uuid = require("uuid");
@@ -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$31 = require_operators.getLogger();
134
+ const logger$32 = require_operators.getLogger();
135
135
  const GET_PARAMS = {
136
136
  method: "GET",
137
137
  headers: { Accept: "application/json" }
@@ -195,7 +195,7 @@ var HTTPRequestController = class HTTPRequestController extends Destroyable {
195
195
  this._responses$.next(response);
196
196
  return response;
197
197
  } catch (error) {
198
- logger$31.error("[HTTPRequestController] Request error:", error);
198
+ logger$32.error("[HTTPRequestController] Request error:", error);
199
199
  this._status$.next("error");
200
200
  const err = error instanceof Error ? error : new Error("HTTP request failed", { cause: error });
201
201
  this._errors$.next(err);
@@ -222,7 +222,7 @@ var HTTPRequestController = class HTTPRequestController extends Destroyable {
222
222
  const url = this.buildURL(request.url);
223
223
  const headers = this.buildHeaders(request.headers);
224
224
  const timeout$4 = request.timeout ?? this.requestTimeout;
225
- logger$31.debug("[HTTPRequestController] Executing request:", {
225
+ logger$32.debug("[HTTPRequestController] Executing request:", {
226
226
  method: request.method,
227
227
  url,
228
228
  headers: Object.keys(headers).reduce((acc, key) => {
@@ -242,7 +242,7 @@ var HTTPRequestController = class HTTPRequestController extends Destroyable {
242
242
  });
243
243
  clearTimeout(timeoutId);
244
244
  const httpResponse = await this.convertResponse(response);
245
- logger$31.debug("[HTTPRequestController] Response received:", {
245
+ logger$32.debug("[HTTPRequestController] Response received:", {
246
246
  status: response.status,
247
247
  statusText: response.statusText,
248
248
  headers: [...response.headers.entries()],
@@ -252,7 +252,7 @@ var HTTPRequestController = class HTTPRequestController extends Destroyable {
252
252
  } catch (error) {
253
253
  clearTimeout(timeoutId);
254
254
  if (error instanceof Error && error.name === "AbortError") throw new require_operators.RequestTimeoutError(`Request timeout after ${timeout$4}ms`, { cause: error });
255
- logger$31.error("[HTTPRequestController] Request failed:", error);
255
+ logger$32.error("[HTTPRequestController] Request failed:", error);
256
256
  throw error;
257
257
  }
258
258
  }
@@ -266,8 +266,8 @@ var HTTPRequestController = class HTTPRequestController extends Destroyable {
266
266
  const credential = this.getCredential();
267
267
  if (credential.token) {
268
268
  headers.Authorization = `Bearer ${credential.token}`;
269
- logger$31.debug("[HTTPRequestController] Using Bearer token auth, token length:", credential.token.length);
270
- } else logger$31.warn("[HTTPRequestController] No credentials available for authentication");
269
+ logger$32.debug("[HTTPRequestController] Using Bearer token auth, token length:", credential.token.length);
270
+ } else logger$32.warn("[HTTPRequestController] No credentials available for authentication");
271
271
  return headers;
272
272
  }
273
273
  /**
@@ -415,7 +415,7 @@ function fromMsToSec(milliseconds) {
415
415
 
416
416
  //#endregion
417
417
  //#region src/containers/PreferencesContainer.ts
418
- const logger$30 = require_operators.getLogger();
418
+ const logger$31 = require_operators.getLogger();
419
419
  var PreferencesContainer = class PreferencesContainer {
420
420
  static get instance() {
421
421
  this._instance ??= new PreferencesContainer();
@@ -1077,7 +1077,7 @@ var ClientPreferences = class {
1077
1077
  if (!this._storage) return;
1078
1078
  const data = collectStoredPreferences();
1079
1079
  this._storage.setItem(require_operators.PREFERENCES_STORAGE_KEY, data, "local").catch((error) => {
1080
- logger$30.error(`[ClientPreferences] Failed to save preferences: ${String(error)}`);
1080
+ logger$31.error(`[ClientPreferences] Failed to save preferences: ${String(error)}`);
1081
1081
  });
1082
1082
  }
1083
1083
  /** Loads preferences from storage and applies them to the container. */
@@ -1086,7 +1086,7 @@ var ClientPreferences = class {
1086
1086
  this._storage.getItem(require_operators.PREFERENCES_STORAGE_KEY, "local").then((stored) => {
1087
1087
  if (stored) applyStoredPreferences(stored);
1088
1088
  }).catch((error) => {
1089
- logger$30.error(`[ClientPreferences] Failed to load preferences: ${String(error)}`);
1089
+ logger$31.error(`[ClientPreferences] Failed to load preferences: ${String(error)}`);
1090
1090
  });
1091
1091
  }
1092
1092
  };
@@ -1107,7 +1107,7 @@ function toError(value) {
1107
1107
 
1108
1108
  //#endregion
1109
1109
  //#region src/controllers/NavigatorDeviceController.ts
1110
- const logger$29 = require_operators.getLogger();
1110
+ const logger$30 = require_operators.getLogger();
1111
1111
  /** Maps a device kind to its storage key. */
1112
1112
  const DEVICE_STORAGE_KEYS = {
1113
1113
  audioinput: require_operators.DEVICE_STORAGE_KEY_AUDIO_INPUT,
@@ -1129,7 +1129,7 @@ var NavigatorDeviceController = class extends Destroyable {
1129
1129
  super();
1130
1130
  this.webRTCApiProvider = webRTCApiProvider;
1131
1131
  this.deviceChangeHandler = () => {
1132
- logger$29.debug("[DeviceController] Device change detected");
1132
+ logger$30.debug("[DeviceController] Device change detected");
1133
1133
  this.enumerateDevices();
1134
1134
  };
1135
1135
  this._devicesState$ = this.createBehaviorSubject(initialDevicesState);
@@ -1194,13 +1194,13 @@ var NavigatorDeviceController = class extends Destroyable {
1194
1194
  return this.cachedObservable("videoInputDevices$", () => this._devicesState$.pipe((0, rxjs.map)((state) => state.videoinput), (0, rxjs.distinctUntilChanged)(), (0, rxjs.takeUntil)(this.destroyed$)));
1195
1195
  }
1196
1196
  get selectedAudioInputDevice$() {
1197
- return this.cachedObservable("selectedAudioInputDevice$", () => this._selectedDevicesState$.asObservable().pipe((0, rxjs.map)((state) => state.audioinput), (0, rxjs.distinctUntilChanged)(), (0, rxjs.takeUntil)(this.destroyed$), (0, rxjs.tap)((info) => logger$29.debug("[DeviceController] Selected audio input device changed:", info))));
1197
+ return this.cachedObservable("selectedAudioInputDevice$", () => this._selectedDevicesState$.asObservable().pipe((0, rxjs.map)((state) => state.audioinput), (0, rxjs.distinctUntilChanged)(), (0, rxjs.takeUntil)(this.destroyed$), (0, rxjs.tap)((info) => logger$30.debug("[DeviceController] Selected audio input device changed:", info))));
1198
1198
  }
1199
1199
  get selectedAudioOutputDevice$() {
1200
- return this.cachedObservable("selectedAudioOutputDevice$", () => this._selectedDevicesState$.asObservable().pipe((0, rxjs.map)((state) => state.audiooutput), (0, rxjs.distinctUntilChanged)(), (0, rxjs.takeUntil)(this.destroyed$), (0, rxjs.tap)((info) => logger$29.debug("[DeviceController] Selected audio output device changed:", info))));
1200
+ return this.cachedObservable("selectedAudioOutputDevice$", () => this._selectedDevicesState$.asObservable().pipe((0, rxjs.map)((state) => state.audiooutput), (0, rxjs.distinctUntilChanged)(), (0, rxjs.takeUntil)(this.destroyed$), (0, rxjs.tap)((info) => logger$30.debug("[DeviceController] Selected audio output device changed:", info))));
1201
1201
  }
1202
1202
  get selectedVideoInputDevice$() {
1203
- return this.cachedObservable("selectedVideoInputDevice$", () => this._selectedDevicesState$.asObservable().pipe((0, rxjs.map)((state) => state.videoinput), (0, rxjs.distinctUntilChanged)(), (0, rxjs.takeUntil)(this.destroyed$), (0, rxjs.tap)((info) => logger$29.debug("[DeviceController] Selected video input device changed:", info))));
1203
+ return this.cachedObservable("selectedVideoInputDevice$", () => this._selectedDevicesState$.asObservable().pipe((0, rxjs.map)((state) => state.videoinput), (0, rxjs.distinctUntilChanged)(), (0, rxjs.takeUntil)(this.destroyed$), (0, rxjs.tap)((info) => logger$30.debug("[DeviceController] Selected video input device changed:", info))));
1204
1204
  }
1205
1205
  get selectedAudioInputDevice() {
1206
1206
  if (this._audioInputDisabled$.value) return null;
@@ -1275,7 +1275,7 @@ var NavigatorDeviceController = class extends Destroyable {
1275
1275
  if (device) this.persistDeviceSelection("audioinput", device);
1276
1276
  }
1277
1277
  selectVideoInputDevice(device) {
1278
- logger$29.debug("[DeviceController] Setting selected video input device:", device);
1278
+ logger$30.debug("[DeviceController] Setting selected video input device:", device);
1279
1279
  if (this._videoInputDisabled$.value && device) this._videoInputDisabled$.next(false);
1280
1280
  const previous = this._selectedDevicesState$.value.videoinput;
1281
1281
  if (previous && previous.deviceId !== device?.deviceId) this._deviceHistory.push("videoinput", previous);
@@ -1332,7 +1332,7 @@ var NavigatorDeviceController = class extends Destroyable {
1332
1332
  }
1333
1333
  const fromHistory = this._deviceHistory.findInHistory(kind, devices);
1334
1334
  if (fromHistory) {
1335
- logger$29.debug(`[DeviceController] Device disappeared, falling back to history: ${fromHistory.label}`);
1335
+ logger$30.debug(`[DeviceController] Device disappeared, falling back to history: ${fromHistory.label}`);
1336
1336
  this.emitDeviceRecovered(kind, selected, fromHistory, "device_disconnected");
1337
1337
  return fromHistory;
1338
1338
  }
@@ -1385,7 +1385,7 @@ var NavigatorDeviceController = class extends Destroyable {
1385
1385
  try {
1386
1386
  await this._storageManager.setItem(DEVICE_STORAGE_KEYS[kind], stored, "local");
1387
1387
  } catch (error) {
1388
- logger$29.error(`[DeviceController] Failed to persist device selection for ${kind}:`, error);
1388
+ logger$30.error(`[DeviceController] Failed to persist device selection for ${kind}:`, error);
1389
1389
  }
1390
1390
  }
1391
1391
  async loadPersistedDevices() {
@@ -1401,7 +1401,7 @@ var NavigatorDeviceController = class extends Destroyable {
1401
1401
  [kind]: stored
1402
1402
  };
1403
1403
  } catch (error) {
1404
- logger$29.error(`[DeviceController] Failed to load persisted device for ${kind}:`, error);
1404
+ logger$30.error(`[DeviceController] Failed to load persisted device for ${kind}:`, error);
1405
1405
  }
1406
1406
  }
1407
1407
  /** Clears device history, persisted selections, and re-enumerates devices. */
@@ -1419,7 +1419,7 @@ var NavigatorDeviceController = class extends Destroyable {
1419
1419
  this.disableDeviceMonitoring();
1420
1420
  this.webRTCApiProvider.mediaDevices.addEventListener("devicechange", this.deviceChangeHandler);
1421
1421
  if (PreferencesContainer.instance.devicePollingInterval > 0) this._devicesPoolingSubscription = (0, rxjs.interval)(PreferencesContainer.instance.devicePollingInterval).subscribe(() => {
1422
- logger$29.debug("[DeviceController] Polling devices due to interval");
1422
+ logger$30.debug("[DeviceController] Polling devices due to interval");
1423
1423
  this.enumerateDevices();
1424
1424
  });
1425
1425
  this.enumerateDevices();
@@ -1445,13 +1445,13 @@ var NavigatorDeviceController = class extends Destroyable {
1445
1445
  videoinput: []
1446
1446
  });
1447
1447
  this._devicesState$.next(devicesByKind);
1448
- logger$29.debug("[DeviceController] Devices enumerated:", {
1448
+ logger$30.debug("[DeviceController] Devices enumerated:", {
1449
1449
  audioInputs: devicesByKind.audioinput.length,
1450
1450
  audioOutputs: devicesByKind.audiooutput.length,
1451
1451
  videoInputs: devicesByKind.videoinput.length
1452
1452
  });
1453
1453
  } catch (error) {
1454
- logger$29.error("[DeviceController] Failed to enumerate devices:", error);
1454
+ logger$30.error("[DeviceController] Failed to enumerate devices:", error);
1455
1455
  this._errors$.next(toError(error));
1456
1456
  }
1457
1457
  }
@@ -1467,7 +1467,7 @@ var NavigatorDeviceController = class extends Destroyable {
1467
1467
  stream.getTracks().forEach((t) => t.stop());
1468
1468
  return capabilities;
1469
1469
  } catch (error) {
1470
- logger$29.error("[DeviceController] Failed to get device capabilities:", error);
1470
+ logger$30.error("[DeviceController] Failed to get device capabilities:", error);
1471
1471
  this._errors$.next(toError(error));
1472
1472
  throw error;
1473
1473
  }
@@ -1718,7 +1718,7 @@ var DependencyContainer = class {
1718
1718
 
1719
1719
  //#endregion
1720
1720
  //#region src/controllers/CryptoController.ts
1721
- const logger$28 = require_operators.getLogger();
1721
+ const logger$29 = require_operators.getLogger();
1722
1722
  const DPOP_DB_NAME = "sw-dpop";
1723
1723
  const DPOP_DB_VERSION = 1;
1724
1724
  const DPOP_STORE_NAME = "keys";
@@ -1777,7 +1777,7 @@ async function loadKeyPairFromDB() {
1777
1777
  tx.oncomplete = () => db.close();
1778
1778
  });
1779
1779
  } catch (error) {
1780
- logger$28.warn("[DPoP] Failed to load key pair from IndexedDB:", error);
1780
+ logger$29.warn("[DPoP] Failed to load key pair from IndexedDB:", error);
1781
1781
  return null;
1782
1782
  }
1783
1783
  }
@@ -1797,7 +1797,7 @@ async function saveKeyPairToDB(keyPair) {
1797
1797
  };
1798
1798
  });
1799
1799
  } catch (error) {
1800
- logger$28.warn("[DPoP] Failed to save key pair to IndexedDB:", error);
1800
+ logger$29.warn("[DPoP] Failed to save key pair to IndexedDB:", error);
1801
1801
  }
1802
1802
  }
1803
1803
  async function deleteKeyPairFromDB() {
@@ -1816,7 +1816,7 @@ async function deleteKeyPairFromDB() {
1816
1816
  };
1817
1817
  });
1818
1818
  } catch (error) {
1819
- logger$28.warn("[DPoP] Failed to delete key pair from IndexedDB:", error);
1819
+ logger$29.warn("[DPoP] Failed to delete key pair from IndexedDB:", error);
1820
1820
  }
1821
1821
  }
1822
1822
  /**
@@ -1876,13 +1876,13 @@ var CryptoController = class {
1876
1876
  this._publicJwk = await crypto.subtle.exportKey("jwk", stored.publicKey);
1877
1877
  this._fingerprint = await computeJwkThumbprint(this._publicJwk);
1878
1878
  this._initialized = true;
1879
- logger$28.debug("[DPoP] Key pair restored from IndexedDB, fingerprint:", this._fingerprint);
1879
+ logger$29.debug("[DPoP] Key pair restored from IndexedDB, fingerprint:", this._fingerprint);
1880
1880
  return this._fingerprint;
1881
1881
  } catch (error) {
1882
- logger$28.warn("[DPoP] Stored key pair unusable, generating new one:", error);
1882
+ logger$29.warn("[DPoP] Stored key pair unusable, generating new one:", error);
1883
1883
  await deleteKeyPairFromDB();
1884
1884
  }
1885
- logger$28.debug("[DPoP] Generating RSA key pair");
1885
+ logger$29.debug("[DPoP] Generating RSA key pair");
1886
1886
  this._keyPair = await crypto.subtle.generateKey({
1887
1887
  name: "RSASSA-PKCS1-v1_5",
1888
1888
  modulusLength: 2048,
@@ -1897,7 +1897,7 @@ var CryptoController = class {
1897
1897
  this._fingerprint = await computeJwkThumbprint(this._publicJwk);
1898
1898
  this._initialized = true;
1899
1899
  await saveKeyPairToDB(this._keyPair);
1900
- logger$28.debug("[DPoP] Key pair generated and persisted, fingerprint:", this._fingerprint);
1900
+ logger$29.debug("[DPoP] Key pair generated and persisted, fingerprint:", this._fingerprint);
1901
1901
  return this._fingerprint;
1902
1902
  }
1903
1903
  /**
@@ -1963,7 +1963,7 @@ var CryptoController = class {
1963
1963
  this._fingerprint = null;
1964
1964
  this._initialized = false;
1965
1965
  deleteKeyPairFromDB();
1966
- logger$28.debug("[DPoP] Controller destroyed");
1966
+ logger$29.debug("[DPoP] Controller destroyed");
1967
1967
  }
1968
1968
  get publicJwk() {
1969
1969
  if (!this._publicJwk) throw new require_operators.DPoPInitError("CryptoController not initialized. Call init() first.");
@@ -1986,7 +1986,7 @@ var CryptoController = class {
1986
1986
 
1987
1987
  //#endregion
1988
1988
  //#region src/controllers/NetworkMonitor.ts
1989
- const logger$27 = require_operators.getLogger();
1989
+ const logger$28 = require_operators.getLogger();
1990
1990
  /**
1991
1991
  * Safely check whether we are running in a browser environment
1992
1992
  * with `window` and the relevant event targets.
@@ -2043,7 +2043,7 @@ var NetworkMonitor = class extends Destroyable {
2043
2043
  }
2044
2044
  attachListeners() {
2045
2045
  if (!hasBrowserNetworkEvents()) {
2046
- logger$27.debug("NetworkMonitor: no browser environment detected, skipping event listeners");
2046
+ logger$28.debug("NetworkMonitor: no browser environment detected, skipping event listeners");
2047
2047
  return;
2048
2048
  }
2049
2049
  window.addEventListener("online", this._onOnline);
@@ -2051,7 +2051,7 @@ var NetworkMonitor = class extends Destroyable {
2051
2051
  const connection = getNetworkConnection();
2052
2052
  if (connection) connection.addEventListener("change", this._onConnectionChange);
2053
2053
  this._listenersAttached = true;
2054
- logger$27.debug("NetworkMonitor: event listeners attached");
2054
+ logger$28.debug("NetworkMonitor: event listeners attached");
2055
2055
  }
2056
2056
  removeListeners() {
2057
2057
  if (!this._listenersAttached) return;
@@ -2062,10 +2062,10 @@ var NetworkMonitor = class extends Destroyable {
2062
2062
  if (connection) connection.removeEventListener("change", this._onConnectionChange);
2063
2063
  }
2064
2064
  this._listenersAttached = false;
2065
- logger$27.debug("NetworkMonitor: event listeners removed");
2065
+ logger$28.debug("NetworkMonitor: event listeners removed");
2066
2066
  }
2067
2067
  handleOnline() {
2068
- logger$27.info("NetworkMonitor: browser went online");
2068
+ logger$28.info("NetworkMonitor: browser went online");
2069
2069
  this._isOnline$.next(true);
2070
2070
  this._networkChange$.next({
2071
2071
  type: "online",
@@ -2074,7 +2074,7 @@ var NetworkMonitor = class extends Destroyable {
2074
2074
  });
2075
2075
  }
2076
2076
  handleOffline() {
2077
- logger$27.info("NetworkMonitor: browser went offline");
2077
+ logger$28.info("NetworkMonitor: browser went offline");
2078
2078
  this._isOnline$.next(false);
2079
2079
  this._networkChange$.next({
2080
2080
  type: "offline",
@@ -2083,7 +2083,7 @@ var NetworkMonitor = class extends Destroyable {
2083
2083
  }
2084
2084
  handleConnectionChange() {
2085
2085
  const networkType = getNetworkType();
2086
- logger$27.info(`NetworkMonitor: connection changed — effectiveType=${networkType ?? "unknown"}`);
2086
+ logger$28.info(`NetworkMonitor: connection changed — effectiveType=${networkType ?? "unknown"}`);
2087
2087
  this._networkChange$.next({
2088
2088
  type: "connection_change",
2089
2089
  timestamp: Date.now(),
@@ -2198,7 +2198,7 @@ function getNavigatorMediaDevices() {
2198
2198
 
2199
2199
  //#endregion
2200
2200
  //#region src/controllers/PreflightRunner.ts
2201
- const logger$26 = require_operators.getLogger();
2201
+ const logger$27 = require_operators.getLogger();
2202
2202
  const DEFAULT_MEDIA_TEST_DURATION_S = 10;
2203
2203
  const ICE_GATHERING_TIMEOUT_MS = 1e4;
2204
2204
  const SIGNALING_RTT_TIMEOUT_MS = 5e3;
@@ -2247,7 +2247,7 @@ var PreflightRunner = class extends Destroyable {
2247
2247
  if (!this._options.skipMediaTest) try {
2248
2248
  bandwidth = await this.testMediaBandwidth(destination);
2249
2249
  } catch (error) {
2250
- logger$26.warn("[PreflightRunner] Media bandwidth test failed:", error);
2250
+ logger$27.warn("[PreflightRunner] Media bandwidth test failed:", error);
2251
2251
  warnings.push("Media bandwidth test failed");
2252
2252
  }
2253
2253
  return {
@@ -2259,7 +2259,7 @@ var PreflightRunner = class extends Destroyable {
2259
2259
  warnings
2260
2260
  };
2261
2261
  } catch (error) {
2262
- logger$26.error("[PreflightRunner] Preflight test failed:", error);
2262
+ logger$27.error("[PreflightRunner] Preflight test failed:", error);
2263
2263
  throw new require_operators.PreflightError("preflight", error instanceof Error ? error : new Error(String(error)));
2264
2264
  } finally {
2265
2265
  this.destroy();
@@ -2290,7 +2290,7 @@ var PreflightRunner = class extends Destroyable {
2290
2290
  if (track.kind === "video" && track.readyState === "live") videoWorking = true;
2291
2291
  }
2292
2292
  } catch (error) {
2293
- logger$26.warn("[PreflightRunner] Device test failed:", error);
2293
+ logger$27.warn("[PreflightRunner] Device test failed:", error);
2294
2294
  } finally {
2295
2295
  if (audioStream) audioStream.getTracks().forEach((t) => t.stop());
2296
2296
  }
@@ -2348,7 +2348,7 @@ var PreflightRunner = class extends Destroyable {
2348
2348
  rttMs
2349
2349
  };
2350
2350
  } catch (error) {
2351
- logger$26.warn("[PreflightRunner] ICE connectivity test failed:", error);
2351
+ logger$27.warn("[PreflightRunner] ICE connectivity test failed:", error);
2352
2352
  return {
2353
2353
  type: "failed",
2354
2354
  turnReachable: false,
@@ -2395,7 +2395,7 @@ var PreflightRunner = class extends Destroyable {
2395
2395
 
2396
2396
  //#endregion
2397
2397
  //#region src/controllers/VisibilityController.ts
2398
- const logger$25 = require_operators.getLogger();
2398
+ const logger$26 = require_operators.getLogger();
2399
2399
  /**
2400
2400
  * Checks whether the document visibility API is available.
2401
2401
  */
@@ -2432,8 +2432,8 @@ var VisibilityController = class extends Destroyable {
2432
2432
  this._boundHandler = this._handleVisibilityChange.bind(this);
2433
2433
  if (this._hasVisibilityApi) {
2434
2434
  document.addEventListener("visibilitychange", this._boundHandler);
2435
- logger$25.debug("VisibilityController: listening for visibilitychange events");
2436
- } else logger$25.debug("VisibilityController: document visibility API not available, defaulting to visible");
2435
+ logger$26.debug("VisibilityController: listening for visibilitychange events");
2436
+ } else logger$26.debug("VisibilityController: document visibility API not available, defaulting to visible");
2437
2437
  }
2438
2438
  /**
2439
2439
  * Observable of the current visibility state.
@@ -2458,7 +2458,7 @@ var VisibilityController = class extends Destroyable {
2458
2458
  destroy() {
2459
2459
  if (this._hasVisibilityApi) {
2460
2460
  document.removeEventListener("visibilitychange", this._boundHandler);
2461
- logger$25.debug("VisibilityController: removed visibilitychange listener");
2461
+ logger$26.debug("VisibilityController: removed visibilitychange listener");
2462
2462
  }
2463
2463
  super.destroy();
2464
2464
  }
@@ -2476,7 +2476,7 @@ var VisibilityController = class extends Destroyable {
2476
2476
  timestamp: Date.now()
2477
2477
  };
2478
2478
  this._visibilityChange$.next(changeEvent);
2479
- logger$25.debug("VisibilityController: visibility changed", {
2479
+ logger$26.debug("VisibilityController: visibility changed", {
2480
2480
  from: previousState,
2481
2481
  to: newState
2482
2482
  });
@@ -2720,7 +2720,7 @@ function isRequesterValidationError(error) {
2720
2720
 
2721
2721
  //#endregion
2722
2722
  //#region src/managers/AttachManager.ts
2723
- const logger$24 = require_operators.getLogger();
2723
+ const logger$25 = require_operators.getLogger();
2724
2724
  var AttachManager = class {
2725
2725
  constructor(storage, deviceController, reconnectCallsTimeout, attachKey, credentialRecovered) {
2726
2726
  this.storage = storage;
@@ -2742,7 +2742,7 @@ var AttachManager = class {
2742
2742
  try {
2743
2743
  return await this.storage.getItem(this.attachKey) ?? {};
2744
2744
  } catch (error) {
2745
- logger$24.warn("[AttachManager] Failed to retrieve attached calls from storage", error);
2745
+ logger$25.warn("[AttachManager] Failed to retrieve attached calls from storage", error);
2746
2746
  return {};
2747
2747
  }
2748
2748
  }
@@ -2750,7 +2750,7 @@ var AttachManager = class {
2750
2750
  try {
2751
2751
  await this.storage.setItem(this.attachKey, attached);
2752
2752
  } catch (error) {
2753
- logger$24.warn("[AttachManager] Failed to write attached calls to storage", error);
2753
+ logger$25.warn("[AttachManager] Failed to write attached calls to storage", error);
2754
2754
  }
2755
2755
  }
2756
2756
  /**
@@ -2769,7 +2769,7 @@ var AttachManager = class {
2769
2769
  }
2770
2770
  async attach(call) {
2771
2771
  if (!call.to) {
2772
- logger$24.warn("[AttachManager] Skip attach for calls with no destination");
2772
+ logger$25.warn("[AttachManager] Skip attach for calls with no destination");
2773
2773
  return;
2774
2774
  }
2775
2775
  const destination = call.to;
@@ -2829,25 +2829,25 @@ var AttachManager = class {
2829
2829
  callId,
2830
2830
  ...options
2831
2831
  });
2832
- logger$24.info(`[AttachManager] Reattached call ${callId} (attempt ${attempt})`);
2832
+ logger$25.info(`[AttachManager] Reattached call ${callId} (attempt ${attempt})`);
2833
2833
  succeeded = true;
2834
2834
  break;
2835
2835
  } catch (error) {
2836
- logger$24.warn(`[AttachManager] Reattach attempt ${attempt}/3 failed for call ${callId}:`, error);
2836
+ logger$25.warn(`[AttachManager] Reattach attempt ${attempt}/3 failed for call ${callId}:`, error);
2837
2837
  if (isRequesterValidationError(error)) {
2838
2838
  refusedOnCredentials = true;
2839
- logger$24.warn(`[AttachManager] Reattach of ${callId} was refused on credentials; not retrying.`);
2839
+ logger$25.warn(`[AttachManager] Reattach of ${callId} was refused on credentials; not retrying.`);
2840
2840
  break;
2841
2841
  }
2842
2842
  if (attempt < 3) await new Promise((r) => setTimeout(r, (attempt + 1) * 1e3));
2843
2843
  }
2844
2844
  if (!succeeded) if (this.credentialRecovered() && !refusedOnCredentials) {
2845
- logger$24.warn(`[AttachManager] Reattach of ${callId} was denied after a verified reauthentication, removing reference`);
2845
+ logger$25.warn(`[AttachManager] Reattach of ${callId} was denied after a verified reauthentication, removing reference`);
2846
2846
  await this.detach({
2847
2847
  id: callId,
2848
2848
  mediaDirections: attachment.mediaDirections
2849
2849
  });
2850
- } else logger$24.warn(`[AttachManager] Reattach failed for call ${callId}; keeping the reference (credential refused or never proven good)`);
2850
+ } else logger$25.warn(`[AttachManager] Reattach failed for call ${callId}; keeping the reference (credential refused or never proven good)`);
2851
2851
  }
2852
2852
  }
2853
2853
  /**
@@ -3160,7 +3160,7 @@ function toggleHandraiseMethod(is) {
3160
3160
 
3161
3161
  //#endregion
3162
3162
  //#region src/core/entities/Participant.ts
3163
- const logger$23 = require_operators.getLogger();
3163
+ const logger$24 = require_operators.getLogger();
3164
3164
  const initialState = {};
3165
3165
  /**
3166
3166
  * Represents a participant in a call.
@@ -3634,6 +3634,9 @@ var SelfParticipant = class extends Participant {
3634
3634
  *
3635
3635
  * The call is unaffected when acquisition fails.
3636
3636
  *
3637
+ * Known issue: stopping the share before its leg connects can leave this
3638
+ * promise pending until the call ends, so do not gate UI on it.
3639
+ *
3637
3640
  * @throws The raw `getDisplayMedia` error. A dismissed picker or a
3638
3641
  * permission denial rejects with a `NotAllowedError` `DOMException` —
3639
3642
  * inspect `error.name` to tell benign cancels apart from real failures.
@@ -3642,7 +3645,7 @@ var SelfParticipant = class extends Participant {
3642
3645
  try {
3643
3646
  await this.vertoManager.addScreenMedia();
3644
3647
  } catch (error) {
3645
- logger$23.error("[Participant.startScreenShare] Screen share error:", error);
3648
+ logger$24.error("[Participant.startScreenShare] Screen share error:", error);
3646
3649
  throw error;
3647
3650
  }
3648
3651
  }
@@ -3663,14 +3666,18 @@ var SelfParticipant = class extends Participant {
3663
3666
  *
3664
3667
  * The call is unaffected when acquisition fails.
3665
3668
  *
3669
+ * Known issue: removing the device before its leg connects can leave this
3670
+ * promise pending until the call ends, so do not gate UI on it.
3671
+ *
3666
3672
  * @throws The raw `getUserMedia` error (e.g. `NotAllowedError` on
3667
- * permission denial) — inspect `error.name` to decide how to react.
3673
+ * permission denial) — inspect `error.name` to decide how to react — or
3674
+ * `AuxiliaryLegTimeoutError` if the leg does not connect in time.
3668
3675
  */
3669
3676
  async addAdditionalDevice(options) {
3670
3677
  try {
3671
3678
  await this.vertoManager.addInputDevice(options);
3672
3679
  } catch (error) {
3673
- logger$23.error("[Participant.addAdditionalDevice] Additional device error:", error);
3680
+ logger$24.error("[Participant.addAdditionalDevice] Additional device error:", error);
3674
3681
  throw error;
3675
3682
  }
3676
3683
  }
@@ -3733,7 +3740,7 @@ var SelfParticipant = class extends Participant {
3733
3740
  */
3734
3741
  exitStudioModeIfActive() {
3735
3742
  if (this._studioAudio$.value) {
3736
- logger$23.debug("[SelfParticipant] Exiting studio audio mode due to individual flag toggle");
3743
+ logger$24.debug("[SelfParticipant] Exiting studio audio mode due to individual flag toggle");
3737
3744
  this._studioAudio$.next(false);
3738
3745
  }
3739
3746
  }
@@ -3757,7 +3764,7 @@ var SelfParticipant = class extends Participant {
3757
3764
  try {
3758
3765
  await super.mute();
3759
3766
  } catch (error) {
3760
- logger$23.warn("[Participant.toggleAudioInput] Server Error while muting audio input, proceeding with local toggle anyway", error);
3767
+ logger$24.warn("[Participant.toggleAudioInput] Server Error while muting audio input, proceeding with local toggle anyway", error);
3761
3768
  } finally {
3762
3769
  this.vertoManager.muteMainAudioInputDevice();
3763
3770
  }
@@ -3767,7 +3774,7 @@ var SelfParticipant = class extends Participant {
3767
3774
  try {
3768
3775
  await super.unmute();
3769
3776
  } catch (error) {
3770
- logger$23.warn("[Participant.toggleAudioInput] Server Error while unmuting audio input, proceeding with local toggle anyway", error);
3777
+ logger$24.warn("[Participant.toggleAudioInput] Server Error while unmuting audio input, proceeding with local toggle anyway", error);
3771
3778
  } finally {
3772
3779
  await this.vertoManager.unmuteMainAudioInputDevice();
3773
3780
  }
@@ -3777,7 +3784,7 @@ var SelfParticipant = class extends Participant {
3777
3784
  try {
3778
3785
  await super.muteVideo();
3779
3786
  } catch (error) {
3780
- logger$23.warn("[Participant.toggleVideoInput] Server Error while muting video input, proceeding with local toggle anyway", error);
3787
+ logger$24.warn("[Participant.toggleVideoInput] Server Error while muting video input, proceeding with local toggle anyway", error);
3781
3788
  } finally {
3782
3789
  this.vertoManager.muteMainVideoInputDevice();
3783
3790
  }
@@ -3787,7 +3794,7 @@ var SelfParticipant = class extends Participant {
3787
3794
  try {
3788
3795
  await super.unmuteVideo();
3789
3796
  } catch (error) {
3790
- logger$23.warn("[Participant.toggleVideoInput] Server Error while unmuting video input, proceeding with local toggle anyway", error);
3797
+ logger$24.warn("[Participant.toggleVideoInput] Server Error while unmuting video input, proceeding with local toggle anyway", error);
3791
3798
  } finally {
3792
3799
  await this.vertoManager.unmuteMainVideoInputDevice();
3793
3800
  }
@@ -3881,7 +3888,7 @@ function isLayoutChangedPayload(value) {
3881
3888
 
3882
3889
  //#endregion
3883
3890
  //#region src/managers/CallEventsManager.ts
3884
- const logger$22 = require_operators.getLogger();
3891
+ const logger$23 = require_operators.getLogger();
3885
3892
  const initialSessionState = {};
3886
3893
  /** @internal */
3887
3894
  var CallEventsManager = class extends Destroyable {
@@ -3985,7 +3992,7 @@ var CallEventsManager = class extends Destroyable {
3985
3992
  }
3986
3993
  initSubscriptions() {
3987
3994
  this.subscribeTo(this.callJoinedEvent$, (callJoinedEvent) => {
3988
- logger$22.debug("[CallEventsManager] Handling call.joined event for call/session IDs:", {
3995
+ logger$23.debug("[CallEventsManager] Handling call.joined event for call/session IDs:", {
3989
3996
  callId: callJoinedEvent.call_id,
3990
3997
  roomSessionId: callJoinedEvent.room_session_id
3991
3998
  });
@@ -4012,19 +4019,19 @@ var CallEventsManager = class extends Destroyable {
4012
4019
  if (this._self$.value?.capabilities.setLayout) this.updateLayouts();
4013
4020
  });
4014
4021
  this.subscribeTo(this.memberUpdates$, (member) => {
4015
- logger$22.debug("[CallEventsManager] Handling member update event for member ID:", member);
4022
+ logger$23.debug("[CallEventsManager] Handling member update event for member ID:", member);
4016
4023
  this.upsertParticipant(member);
4017
4024
  });
4018
4025
  this.subscribeTo(this.webRtcCallSession.memberLeft$, (memberLeftEvent) => {
4019
- logger$22.debug("[CallEventsManager] Handling member.left event for member ID:", memberLeftEvent.member.member_id);
4026
+ logger$23.debug("[CallEventsManager] Handling member.left event for member ID:", memberLeftEvent.member.member_id);
4020
4027
  const participants = { ...this._participants$.value };
4021
4028
  if (memberLeftEvent.member.member_id in participants) {
4022
4029
  delete participants[memberLeftEvent.member.member_id];
4023
4030
  this._participants$.next(participants);
4024
- } else logger$22.warn(`[CallEventsManager] Received member.left event for unknown member ID: ${memberLeftEvent.member.member_id}`);
4031
+ } else logger$23.warn(`[CallEventsManager] Received member.left event for unknown member ID: ${memberLeftEvent.member.member_id}`);
4025
4032
  });
4026
4033
  this.subscribeTo(this.webRtcCallSession.callUpdated$, (callUpdatedEvent) => {
4027
- logger$22.debug("[CallEventsManager] Handling call.updated event:", callUpdatedEvent);
4034
+ logger$23.debug("[CallEventsManager] Handling call.updated event:", callUpdatedEvent);
4028
4035
  const roomSession = callUpdatedEvent.room_session;
4029
4036
  this._sessionState$.next({
4030
4037
  ...this._sessionState$.value,
@@ -4039,7 +4046,7 @@ var CallEventsManager = class extends Destroyable {
4039
4046
  });
4040
4047
  });
4041
4048
  this.subscribeTo(this.layoutChangedEvent$, (layoutChangedEvent) => {
4042
- logger$22.debug("[CallEventsManager] Handling layout.changed event:", layoutChangedEvent);
4049
+ logger$23.debug("[CallEventsManager] Handling layout.changed event:", layoutChangedEvent);
4043
4050
  this._sessionState$.next({
4044
4051
  ...this._sessionState$.value,
4045
4052
  layout_name: layoutChangedEvent.id,
@@ -4049,10 +4056,10 @@ var CallEventsManager = class extends Destroyable {
4049
4056
  });
4050
4057
  }
4051
4058
  updateParticipantPositions(layoutChangedEvent) {
4052
- if (Object.keys(this._participants$.value).length > 0 && !layoutChangedEvent.layers.some((layer) => !!layer.member_id)) logger$22.warn("[CallEventsManager] No layers with member_id found in layout.changed event. Nothing to update.");
4059
+ if (Object.keys(this._participants$.value).length > 0 && !layoutChangedEvent.layers.some((layer) => !!layer.member_id)) logger$23.warn("[CallEventsManager] No layers with member_id found in layout.changed event. Nothing to update.");
4053
4060
  layoutChangedEvent.layers.filter((layer) => !!layer.member_id).filter((layer) => {
4054
4061
  if (!(layer.member_id in this._participants$.value)) {
4055
- logger$22.warn(`[CallEventsManager] Skipping layout layer for unknown member_id: ${layer.member_id}`);
4062
+ logger$23.warn(`[CallEventsManager] Skipping layout layer for unknown member_id: ${layer.member_id}`);
4056
4063
  return false;
4057
4064
  }
4058
4065
  return true;
@@ -4072,7 +4079,7 @@ var CallEventsManager = class extends Destroyable {
4072
4079
  this.webRtcCallSession.executeMethod(this.selfId, "call.layout.list", {}).then((response) => {
4073
4080
  const layouts = response.result?.layouts;
4074
4081
  if (!layouts) {
4075
- logger$22.warn("[CallEventsManager] Layout list response carried no layouts; keeping current layouts");
4082
+ logger$23.warn("[CallEventsManager] Layout list response carried no layouts; keeping current layouts");
4076
4083
  return;
4077
4084
  }
4078
4085
  this._sessionState$.next({
@@ -4080,7 +4087,7 @@ var CallEventsManager = class extends Destroyable {
4080
4087
  layouts
4081
4088
  });
4082
4089
  }).catch((error) => {
4083
- logger$22.error("[CallEventsManager] Error fetching layouts:", error);
4090
+ logger$23.error("[CallEventsManager] Error fetching layouts:", error);
4084
4091
  });
4085
4092
  }
4086
4093
  updateParticipants(members) {
@@ -4096,7 +4103,7 @@ var CallEventsManager = class extends Destroyable {
4096
4103
  }
4097
4104
  const participant = this._participants$.value[member.member_id];
4098
4105
  const oldValue = participant.value;
4099
- logger$22.debug("[CallEventsManager] Updating participant:", member.member_id, {
4106
+ logger$23.debug("[CallEventsManager] Updating participant:", member.member_id, {
4100
4107
  oldValue,
4101
4108
  newValue: member
4102
4109
  });
@@ -4109,17 +4116,17 @@ var CallEventsManager = class extends Destroyable {
4109
4116
  }
4110
4117
  get callJoinedEvent$() {
4111
4118
  return this.cachedObservable("callJoinedEvent$", () => this.webRtcCallSession.callEvent$.pipe((0, rxjs.filter)(isCallJoinedPayload), (0, rxjs.tap)((event) => {
4112
- logger$22.debug("[CallEventsManager] Call joined event:", event);
4119
+ logger$23.debug("[CallEventsManager] Call joined event:", event);
4113
4120
  })));
4114
4121
  }
4115
4122
  get layoutChangedEvent$() {
4116
4123
  return this.cachedObservable("layoutChangedEvent$", () => this.webRtcCallSession.callEvent$.pipe(require_operators.filterAs(isLayoutChangedPayload, "layout"), (0, rxjs.tap)((event) => {
4117
- logger$22.debug("[CallEventsManager] Layout changed event:", event);
4124
+ logger$23.debug("[CallEventsManager] Layout changed event:", event);
4118
4125
  })));
4119
4126
  }
4120
4127
  get memberUpdates$() {
4121
4128
  return this.cachedObservable("memberUpdates$", () => (0, rxjs.merge)(this.webRtcCallSession.memberJoined$, this.webRtcCallSession.memberUpdated$, this.webRtcCallSession.memberTalking$).pipe((0, rxjs.map)((event) => event.member), (0, rxjs.tap)((event) => {
4122
- logger$22.debug("[CallEventsManager] Member update event:", event);
4129
+ logger$23.debug("[CallEventsManager] Member update event:", event);
4123
4130
  })));
4124
4131
  }
4125
4132
  destroy() {
@@ -4138,6 +4145,111 @@ var CallEventsManager = class extends Destroyable {
4138
4145
  }
4139
4146
  };
4140
4147
 
4148
+ //#endregion
4149
+ //#region src/controllers/ConstraintFallbackHelper.ts
4150
+ /**
4151
+ * ConstraintFallbackHelper - Provides getUserMedia with automatic constraint
4152
+ * fallback on OverconstrainedError.
4153
+ *
4154
+ * When a specific device ID is requested, the helper tries progressively
4155
+ * looser constraints:
4156
+ * 1. `{ deviceId: { exact: deviceId } }` -- exact match
4157
+ * 2. `{ deviceId: deviceId }` -- preferred (browser may pick another)
4158
+ * 3. `{}` -- no constraint, browser default
4159
+ *
4160
+ * This prevents stale device IDs from blocking call setup.
4161
+ *
4162
+ * @see Section 5.8 and Section 11 of the Implementation Guide
4163
+ */
4164
+ const logger$22 = require_operators.getLogger();
4165
+ /**
4166
+ * Attempts getUserMedia with progressively looser constraints.
4167
+ *
4168
+ * The function tries three levels of constraint specificity for the given
4169
+ * device kind. Each level is only attempted if the previous one fails with
4170
+ * an OverconstrainedError. Non-OverconstrainedError failures (e.g.,
4171
+ * NotAllowedError) are thrown immediately without fallback.
4172
+ *
4173
+ * @param mediaDevices - Anything exposing `getUserMedia` (a full
4174
+ * `WebRTCMediaDevices`, or a shim wrapping one)
4175
+ * @param constraints - The full MediaStreamConstraints to use as a base
4176
+ * @param kind - Which track kind to apply fallback to ('audio' | 'video')
4177
+ * @param deviceId - The device ID to try (if undefined, calls getUserMedia as-is)
4178
+ * @returns The stream and the fallback level that succeeded
4179
+ * @throws When all fallback levels fail, or when a non-OverconstrainedError occurs
4180
+ */
4181
+ async function getUserMediaWithFallback(mediaDevices, constraints, kind, deviceId) {
4182
+ if (!deviceId) return {
4183
+ stream: await mediaDevices.getUserMedia(constraints),
4184
+ fallbackLevel: "default"
4185
+ };
4186
+ const baseConstraints = typeof constraints[kind] === "object" ? constraints[kind] : {};
4187
+ try {
4188
+ const exactConstraints = {
4189
+ ...constraints,
4190
+ [kind]: {
4191
+ ...baseConstraints,
4192
+ deviceId: { exact: deviceId }
4193
+ }
4194
+ };
4195
+ return {
4196
+ stream: await mediaDevices.getUserMedia(exactConstraints),
4197
+ fallbackLevel: "exact"
4198
+ };
4199
+ } catch (error) {
4200
+ if (!isOverconstrainedError(error)) throw error;
4201
+ logger$22.debug(`[ConstraintFallbackHelper] Exact constraint failed for ${kind}, trying preferred`, { deviceId });
4202
+ }
4203
+ try {
4204
+ const preferredConstraints = {
4205
+ ...constraints,
4206
+ [kind]: {
4207
+ ...baseConstraints,
4208
+ deviceId
4209
+ }
4210
+ };
4211
+ return {
4212
+ stream: await mediaDevices.getUserMedia(preferredConstraints),
4213
+ fallbackLevel: "preferred"
4214
+ };
4215
+ } catch (error) {
4216
+ if (!isOverconstrainedError(error)) throw error;
4217
+ logger$22.debug(`[ConstraintFallbackHelper] Preferred constraint failed for ${kind}, trying default`, { deviceId });
4218
+ }
4219
+ try {
4220
+ const defaultConstraints = {
4221
+ ...constraints,
4222
+ [kind]: { ...baseConstraints }
4223
+ };
4224
+ if (typeof defaultConstraints[kind] === "object") {
4225
+ const { deviceId: _removed, ...rest } = defaultConstraints[kind];
4226
+ defaultConstraints[kind] = rest;
4227
+ }
4228
+ const stream = await mediaDevices.getUserMedia(defaultConstraints);
4229
+ logger$22.warn(`[ConstraintFallbackHelper] Fell back to browser default for ${kind}`, { requestedDeviceId: deviceId });
4230
+ return {
4231
+ stream,
4232
+ fallbackLevel: "default"
4233
+ };
4234
+ } catch (error) {
4235
+ logger$22.error(`[ConstraintFallbackHelper] All fallback levels exhausted for ${kind}`, {
4236
+ deviceId,
4237
+ error
4238
+ });
4239
+ throw error;
4240
+ }
4241
+ }
4242
+ /**
4243
+ * Checks whether an error is an OverconstrainedError.
4244
+ *
4245
+ * Browsers may throw either a native OverconstrainedError or a DOMException
4246
+ * with a specific name.
4247
+ */
4248
+ function isOverconstrainedError(error) {
4249
+ if (error instanceof Error) return error.name === "OverconstrainedError" || error.name === "ConstraintNotSatisfiedError";
4250
+ return false;
4251
+ }
4252
+
4141
4253
  //#endregion
4142
4254
  //#region src/helpers/SDPHelper.ts
4143
4255
  /**
@@ -4693,6 +4805,7 @@ var LocalStreamController = class extends Destroyable {
4693
4805
  this._localAudioTracks$ = this.createBehaviorSubject([]);
4694
4806
  this._localVideoTracks$ = this.createBehaviorSubject([]);
4695
4807
  this._mediaTrackEnded$ = this.createSubject();
4808
+ this._trackOrigins = /* @__PURE__ */ new WeakMap();
4696
4809
  }
4697
4810
  get localStream$() {
4698
4811
  return this._localStream$.asObservable().pipe((0, rxjs.takeUntil)(this.destroyed$));
@@ -4715,6 +4828,22 @@ var LocalStreamController = class extends Destroyable {
4715
4828
  get localVideoTracks() {
4716
4829
  return this._localVideoTracks$.value;
4717
4830
  }
4831
+ tagTracks(tracks, origin) {
4832
+ for (const track of tracks) this._trackOrigins.set(track, origin);
4833
+ }
4834
+ setTrackOrigin(track, origin) {
4835
+ this._trackOrigins.set(track, origin);
4836
+ }
4837
+ getTrackOrigin(track) {
4838
+ return this._trackOrigins.get(track);
4839
+ }
4840
+ /**
4841
+ * Fail-safe: an unrecorded track reads as not-a-device-capture, so a missed
4842
+ * tagging site leaves media alone rather than destroying it.
4843
+ */
4844
+ isDeviceCapture(track) {
4845
+ return this._trackOrigins.get(track) === "device";
4846
+ }
4718
4847
  /**
4719
4848
  * Build the local media stream based on the provided options.
4720
4849
  */
@@ -4724,6 +4853,7 @@ var LocalStreamController = class extends Destroyable {
4724
4853
  if (this.options.inputAudioStream ?? this.options.inputVideoStream) {
4725
4854
  const tracks = [...this.options.inputAudioStream?.getTracks() ?? [], ...this.options.inputVideoStream?.getTracks() ?? []];
4726
4855
  stream = new MediaStream(tracks);
4856
+ this.tagTracks(tracks, "application");
4727
4857
  } else if (this.options.propose === "screenshare") {
4728
4858
  logger$19.debug("[LocalStreamController] Requesting display media for screen sharing with audio:", Boolean(this.options.inputAudioDeviceConstraints));
4729
4859
  stream = await this.options.getDisplayMedia({
@@ -4731,6 +4861,7 @@ var LocalStreamController = class extends Destroyable {
4731
4861
  audio: Boolean(this.options.inputAudioDeviceConstraints)
4732
4862
  });
4733
4863
  logger$19.debug("[LocalStreamController] Screen share media obtained:", stream);
4864
+ this.tagTracks(stream.getTracks(), "display");
4734
4865
  } else {
4735
4866
  const constraints = {
4736
4867
  audio: this.options.inputAudioDeviceConstraints,
@@ -4739,6 +4870,7 @@ var LocalStreamController = class extends Destroyable {
4739
4870
  logger$19.debug("[LocalStreamController] Requesting user media with constraints:", constraints);
4740
4871
  stream = await this.options.getUserMedia(constraints);
4741
4872
  logger$19.debug("[LocalStreamController] User media obtained:", stream);
4873
+ this.tagTracks(stream.getTracks(), "device");
4742
4874
  }
4743
4875
  this._localStream$.next(stream);
4744
4876
  this._localAudioTracks$.next(stream.getAudioTracks());
@@ -4748,10 +4880,13 @@ var LocalStreamController = class extends Destroyable {
4748
4880
  /**
4749
4881
  * Add a local media track to the local stream.
4750
4882
  * @param track - The MediaStreamTrack to add
4883
+ * @param origin - Defaults to `'device'`; every internal caller passes a
4884
+ * fresh `getUserMedia` capture.
4751
4885
  * @returns The MediaStream (either existing or newly created)
4752
4886
  */
4753
- addTrack(track) {
4887
+ addTrack(track, origin = "device") {
4754
4888
  const localStream = this._localStream$.value ?? new MediaStream();
4889
+ this._trackOrigins.set(track, origin);
4755
4890
  track.addEventListener("ended", this.mediaTrackEndedHandler);
4756
4891
  localStream.addTrack(track);
4757
4892
  this._localStream$.next(localStream);
@@ -5021,15 +5156,27 @@ var TransceiverController = class extends Destroyable {
5021
5156
  for (let i = 0; i < Number(msStreamsNumber); i++) this.peerConnection.addTransceiver("video", { direction: "recvonly" });
5022
5157
  }
5023
5158
  }
5159
+ /**
5160
+ * @returns whether every live sender of the kind took the constraints. A
5161
+ * skipped non-device sender, an exhausted fallback, and having no live sender
5162
+ * at all all report `false` — `mediaParamsUpdated.applied` is built from this,
5163
+ * and an application told `true` cannot tell a working push from a no-op.
5164
+ */
5024
5165
  async updateSendersConstraints(kind, constraints) {
5025
5166
  if (!constraints) {
5026
5167
  this.stopTrackSender(kind);
5027
- return Promise.resolve();
5168
+ return false;
5028
5169
  }
5029
5170
  const senders = this.peerConnection.getSenders().filter((sender) => sender.track?.kind === kind && sender.track.readyState === "live");
5171
+ let applied = senders.length > 0;
5030
5172
  for (const sender of senders) {
5031
5173
  const { track } = sender;
5032
5174
  if (track) {
5175
+ if (!this.options.localStreamController.isDeviceCapture(track)) {
5176
+ logger$18.debug(`[TransceiverController] Skipping ${kind} constraints for a non-device track (origin: ${this.options.localStreamController.getTrackOrigin(track) ?? "unrecorded"}), track ${track.id}`);
5177
+ applied = false;
5178
+ continue;
5179
+ }
5033
5180
  const constraintsToApply = {
5034
5181
  ...track.getConstraints(),
5035
5182
  ...constraints
@@ -5045,33 +5192,39 @@ var TransceiverController = class extends Destroyable {
5045
5192
  } catch (fallbackError) {
5046
5193
  logger$18.warn(`[TransceiverController] Track replacement fallback also failed for ${kind} track:`, fallbackError);
5047
5194
  this.options.onError?.(new require_operators.MediaTrackError("updateSendersConstraints", kind, fallbackError));
5195
+ applied = false;
5048
5196
  }
5049
5197
  }
5050
5198
  }
5051
5199
  }
5200
+ return applied;
5052
5201
  }
5053
5202
  /**
5054
- * Fallback when applyConstraints fails: stop the current track, acquire a new
5055
- * one via getUserMedia with the merged constraints (preserving the current
5056
- * deviceId), replace the sender track, and update the localStream.
5203
+ * Fallback when applyConstraints fails, which on iOS Safari it silently does.
5057
5204
  *
5058
- * This is critical for iOS Safari where applyConstraints on audio tracks
5059
- * silently fails or throws.
5205
+ * Order matters: acquiring before stopping means a failed acquisition leaves
5206
+ * the existing media playing. The deviceId goes through the fallback ladder
5207
+ * rather than pinned `{ exact }`, so a stale id degrades instead of failing.
5060
5208
  */
5061
5209
  async replaceTrackFallback(sender, oldTrack, kind, mergedConstraints) {
5062
5210
  const { deviceId } = oldTrack.getSettings();
5063
- const constraintsWithDevice = {
5064
- ...mergedConstraints,
5065
- ...deviceId ? { deviceId: { exact: deviceId } } : {}
5066
- };
5067
- const trackId = oldTrack.id;
5211
+ const { stream, fallbackLevel } = await getUserMediaWithFallback({ getUserMedia: this.options.getUserMedia }, { [kind]: mergedConstraints }, kind, deviceId);
5212
+ const newTrack = stream.getTracks().find((t) => t.kind === kind);
5213
+ if (!newTrack) {
5214
+ stream.getTracks().forEach((t) => t.stop());
5215
+ throw new require_operators.MediaTrackError("replaceTrackFallback", kind, /* @__PURE__ */ new Error("getUserMedia returned no track of the requested kind"));
5216
+ }
5217
+ try {
5218
+ await sender.replaceTrack(newTrack);
5219
+ } catch (error) {
5220
+ stream.getTracks().forEach((t) => t.stop());
5221
+ throw error;
5222
+ }
5223
+ const oldTrackId = oldTrack.id;
5224
+ this.options.localStreamController.removeTrack(oldTrackId);
5068
5225
  oldTrack.stop();
5069
- this.options.localStreamController.removeTrack(trackId);
5070
- const newTrack = (await this.options.getUserMedia({ [kind]: constraintsWithDevice })).getTracks().find((t) => t.kind === kind);
5071
- if (!newTrack) throw new require_operators.MediaTrackError("replaceTrackFallback", kind, /* @__PURE__ */ new Error("getUserMedia returned no track of the requested kind"));
5072
- await sender.replaceTrack(newTrack);
5073
5226
  this.options.localStreamController.addTrack(newTrack);
5074
- logger$18.debug(`[TransceiverController] Track replacement fallback succeeded for ${kind}. New track: ${newTrack.id}`);
5227
+ logger$18.debug(`[TransceiverController] Track replacement fallback succeeded for ${kind} (deviceId fallback level: ${fallbackLevel}). New track: ${newTrack.id}`);
5075
5228
  }
5076
5229
  getMediaDirections() {
5077
5230
  if (this.peerConnection.connectionState === "connected") return this.peerConnection.getTransceivers().reduce((acc, transceiver) => {
@@ -5182,6 +5335,7 @@ var RTCPeerConnectionController = class extends Destroyable {
5182
5335
  this._iceGatheringState$ = this.createReplaySubject(1);
5183
5336
  this._errors$ = this.createReplaySubject(1);
5184
5337
  this._iceCandidates$ = this.createReplaySubject(1);
5338
+ this._localMediaSettled$ = this.createReplaySubject(1);
5185
5339
  this._initialized$ = this.createReplaySubject(1);
5186
5340
  this._remoteDescription$ = this.createReplaySubject(1);
5187
5341
  this._remoteStream$ = this.createBehaviorSubject(null);
@@ -5285,6 +5439,10 @@ var RTCPeerConnectionController = class extends Destroyable {
5285
5439
  get remoteDescription$() {
5286
5440
  return this.cachedObservable("remoteDescription$", () => this._remoteDescription$.asObservable().pipe((0, rxjs.takeUntil)(this.destroyed$)));
5287
5441
  }
5442
+ /** Emits once local media is settled — acquired, or knowingly receive-only. */
5443
+ get localMediaSettled$() {
5444
+ return this.cachedObservable("localMediaSettled$", () => this._localMediaSettled$.asObservable().pipe((0, rxjs.takeUntil)(this.destroyed$)));
5445
+ }
5288
5446
  get localStream$() {
5289
5447
  return this.cachedObservable("localStream$", () => this.localStreamController.localStream$.pipe((0, rxjs.takeUntil)(this.destroyed$)));
5290
5448
  }
@@ -5440,7 +5598,7 @@ var RTCPeerConnectionController = class extends Destroyable {
5440
5598
  this._isNegotiating$.next(true);
5441
5599
  await this._setRemoteDescription(this.sdpInit);
5442
5600
  } else {
5443
- await this.setupTrackHandling();
5601
+ if (!await this.setupTrackHandling()) return;
5444
5602
  this._initialized$.next(true);
5445
5603
  }
5446
5604
  } catch (error) {
@@ -5564,7 +5722,10 @@ var RTCPeerConnectionController = class extends Destroyable {
5564
5722
  inputVideoDeviceConstraints: this.inputVideoDeviceConstraints
5565
5723
  });
5566
5724
  }
5567
- await this.setupLocalTracks();
5725
+ if (!await this.setupLocalTracks()) {
5726
+ logger$17.debug("[RTCPeerConnectionController] Inbound answer abandoned; the connection went away.");
5727
+ return;
5728
+ }
5568
5729
  const { answerOptions } = this;
5569
5730
  logger$17.debug("[RTCPeerConnectionController] Creating inbound answer with options:", answerOptions);
5570
5731
  await this.createAnswer(answerOptions);
@@ -5702,35 +5863,52 @@ var RTCPeerConnectionController = class extends Destroyable {
5702
5863
  }
5703
5864
  /**
5704
5865
  * Setup track handling for remote tracks.
5866
+ *
5867
+ * @returns `false` when the connection went away while local media was being
5868
+ * acquired — see {@link setupLocalTracks}.
5705
5869
  */
5706
5870
  async setupTrackHandling() {
5707
5871
  if (!this.peerConnection) throw new require_operators.DependencyError("RTCPeerConnection is not initialized");
5708
- await this.setupLocalTracks();
5872
+ if (!await this.setupLocalTracks()) return false;
5709
5873
  await this.setupRemoteTracks();
5874
+ return true;
5710
5875
  }
5876
+ /**
5877
+ * @returns `false` when the connection was torn down while getUserMedia was
5878
+ * in flight. The acquisition is not cancellable, so the caller must stop
5879
+ * rather than go on to touch a peer connection that is closed or gone.
5880
+ */
5711
5881
  async setupLocalTracks() {
5712
5882
  logger$17.debug("[RTCPeerConnectionController] Setting up local tracks/transceivers.");
5713
5883
  if (this.hasNoLocalMediaToSend()) {
5714
5884
  if (!this.receiveAudio && !this.receiveVideo) throw new require_operators.InvalidParams("Call requests no media: enable audio/video or receiveAudio/receiveVideo");
5715
5885
  logger$17.debug("[RTCPeerConnectionController] No local media requested; negotiating receive-only.");
5716
5886
  this.setupReceiveOnlyTransceivers();
5717
- return;
5887
+ this._localMediaSettled$.next();
5888
+ return true;
5718
5889
  }
5719
5890
  let localStream;
5720
5891
  try {
5721
5892
  localStream = this.localStream ?? await this.localStreamController.buildLocalStream();
5722
5893
  } catch (error) {
5723
5894
  this.handleLocalMediaFailure(error);
5724
- return;
5895
+ this._localMediaSettled$.next();
5896
+ return true;
5725
5897
  }
5898
+ if (!this.peerConnection || this.peerConnection.signalingState === "closed") {
5899
+ logger$17.debug("[RTCPeerConnectionController] Local media arrived after teardown; releasing it.");
5900
+ localStream.getTracks().forEach((track) => track.stop());
5901
+ return false;
5902
+ }
5903
+ this._localMediaSettled$.next();
5726
5904
  if (this.transceiverController?.useAddStream ?? false) {
5727
5905
  logger$17.warn("[RTCPeerConnectionController] Using deprecated addStream API to add local stream.");
5728
- this.peerConnection?.addStream(localStream);
5906
+ this.peerConnection.addStream(localStream);
5729
5907
  if (!this.isNegotiating) {
5730
5908
  logger$17.debug("[RTCPeerConnectionController] Forcing negotiationneeded after local tracks setup.");
5731
5909
  this.negotiationNeeded$.next();
5732
5910
  }
5733
- return;
5911
+ return true;
5734
5912
  }
5735
5913
  for (const kind of ["audio", "video"]) {
5736
5914
  const tracks = (kind === "audio" ? localStream.getAudioTracks() : localStream.getVideoTracks()).map((track, index) => ({
@@ -5744,10 +5922,11 @@ var RTCPeerConnectionController = class extends Destroyable {
5744
5922
  await this.transceiverController?.setupTransceiverSender(track, localStream, transceivers[index]);
5745
5923
  } else {
5746
5924
  logger$17.debug(`[RTCPeerConnectionController] Using addTrack for local ${kind} track:`, track.id);
5747
- this.peerConnection?.addTrack(track, localStream);
5925
+ this.peerConnection.addTrack(track, localStream);
5748
5926
  }
5749
5927
  }
5750
5928
  }
5929
+ return true;
5751
5930
  }
5752
5931
  /** True for a main connection with no local media to send. */
5753
5932
  hasNoLocalMediaToSend() {
@@ -5868,6 +6047,7 @@ var RTCPeerConnectionController = class extends Destroyable {
5868
6047
  const sender = (this.transceiverController?.audioTransceivers.at(0))?.sender ?? this.peerConnection.getSenders().find((s) => s.track?.kind === "audio");
5869
6048
  if (!sender || !raw) return;
5870
6049
  try {
6050
+ this.localStreamController.setTrackOrigin(this._localAudioPipeline.outputTrack, "processed");
5871
6051
  await sender.replaceTrack(this._localAudioPipeline.outputTrack);
5872
6052
  } catch (error) {
5873
6053
  logger$17.warn("[RTCPeerConnectionController] Failed to route audio sender through pipeline:", error);
@@ -5928,37 +6108,62 @@ var RTCPeerConnectionController = class extends Destroyable {
5928
6108
  for (const existingTrack of existingTracks) this.removeLocalTrack(existingTrack.id);
5929
6109
  this.addLocalTrack(track);
5930
6110
  }
6111
+ /**
6112
+ * @returns whether the constraints reached the media the leg is sending.
6113
+ *
6114
+ * With the pipeline engaged the audio sender carries the processed
6115
+ * destination track, so the sender scan would find nothing it may touch and
6116
+ * every audio constraint API would silently no-op. The constraints belong to
6117
+ * the pipeline's device source, which is the capture that sender ultimately
6118
+ * carries.
6119
+ */
5931
6120
  async updateSendersConstraints(kind, constraints) {
5932
- await this.transceiverController?.updateSendersConstraints(kind, constraints);
6121
+ if (kind === "audio" && constraints && this._localAudioPipeline) return this.applyPipelineSourceConstraints(constraints);
6122
+ return await this.transceiverController?.updateSendersConstraints(kind, constraints) ?? false;
5933
6123
  }
5934
6124
  /**
5935
- * Replace the current audio track with a new one using the given constraints.
5936
- * Used for server-pushed audio constraint changes where applyConstraints
5937
- * fails on iOS Safari. Stops the current track, acquires a new one via
5938
- * getUserMedia, and replaces the sender track.
6125
+ * Mirror of the sender path for a piped audio leg: same merge, same fallback
6126
+ * ladder, same device-capture invariant — but the swap target is the pipeline
6127
+ * input, so the sender keeps emitting the pipeline's output track and its
6128
+ * identity survives the change.
5939
6129
  */
5940
- async replaceAudioTrackWithConstraints(constraints) {
5941
- const senders = this.peerConnection?.getSenders().filter((s) => s.track?.kind === "audio" && s.track.readyState === "live");
5942
- if (!senders || senders.length === 0) {
5943
- logger$17.warn("[RTCPeerConnectionController] No live audio sender to replace");
5944
- return;
6130
+ async applyPipelineSourceConstraints(constraints) {
6131
+ const pipeline = this._localAudioPipeline;
6132
+ const source = this.localStreamController.localAudioTracks.at(0);
6133
+ if (!pipeline || !source) {
6134
+ logger$17.debug("[RTCPeerConnectionController] No pipeline input to constrain.");
6135
+ return false;
5945
6136
  }
5946
- for (const sender of senders) {
5947
- const oldTrack = sender.track;
5948
- if (!oldTrack) continue;
5949
- const { deviceId } = oldTrack.getSettings();
5950
- const mergedConstraints = {
5951
- ...oldTrack.getConstraints(),
5952
- ...constraints,
5953
- ...deviceId ? { deviceId: { exact: deviceId } } : {}
5954
- };
5955
- const trackId = oldTrack.id;
5956
- oldTrack.stop();
5957
- this.localStreamController.removeTrack(trackId);
5958
- const newTrack = (await this.getUserMedia({ audio: mergedConstraints })).getAudioTracks()[0];
5959
- await sender.replaceTrack(newTrack);
6137
+ if (!this.localStreamController.isDeviceCapture(source)) {
6138
+ logger$17.debug(`[RTCPeerConnectionController] Skipping audio constraints for a non-device pipeline input (origin: ${this.localStreamController.getTrackOrigin(source) ?? "unrecorded"}).`);
6139
+ return false;
6140
+ }
6141
+ const merged = {
6142
+ ...source.getConstraints(),
6143
+ ...constraints
6144
+ };
6145
+ try {
6146
+ await source.applyConstraints(merged);
6147
+ logger$17.debug("[RTCPeerConnectionController] Pipeline input constraints updated:", merged);
6148
+ return true;
6149
+ } catch (error) {
6150
+ logger$17.warn("[RTCPeerConnectionController] applyConstraints failed on the pipeline input, re-acquiring:", error);
6151
+ }
6152
+ try {
6153
+ const { stream } = await getUserMediaWithFallback({ getUserMedia: async (c) => this.getUserMedia(c) }, { audio: merged }, "audio", source.getSettings().deviceId);
6154
+ const newTrack = stream.getAudioTracks().at(0);
6155
+ if (!newTrack) {
6156
+ stream.getTracks().forEach((track) => track.stop());
6157
+ throw new Error("getUserMedia returned no audio track");
6158
+ }
6159
+ this.localStreamController.removeTrack(source.id);
5960
6160
  this.localStreamController.addTrack(newTrack);
5961
- logger$17.debug(`[RTCPeerConnectionController] Audio track replaced for server-pushed params. New track: ${newTrack.id}`);
6161
+ pipeline.setInputTrack(newTrack);
6162
+ return true;
6163
+ } catch (error) {
6164
+ logger$17.warn("[RTCPeerConnectionController] Failed to re-acquire the pipeline input for constraints:", error);
6165
+ this._errors$.next(new require_operators.MediaTrackError("updateSendersConstraints", "audio", error));
6166
+ return false;
5962
6167
  }
5963
6168
  }
5964
6169
  /**
@@ -6141,7 +6346,7 @@ var WebRTCVertoManager = class extends VertoManager {
6141
6346
  this._signalingStatus$ = this.createReplaySubject(1);
6142
6347
  this._screenShareStatus$ = this.createBehaviorSubject("none");
6143
6348
  this._rtcPeerConnectionsMap = /* @__PURE__ */ new Map();
6144
- this._screenShareTimeoutMs = 5e4;
6349
+ this._legErrors$ = this.createSubject();
6145
6350
  this._nodeId$ = this.createBehaviorSubject(options.nodeId ?? null);
6146
6351
  this.onError = options.onError;
6147
6352
  this.onModifyFailed = options.onModifyFailed;
@@ -6189,6 +6394,10 @@ var WebRTCVertoManager = class extends VertoManager {
6189
6394
  get selfId$() {
6190
6395
  return this._selfId$.asObservable();
6191
6396
  }
6397
+ /** Separates the media phase of call creation from the signalling phase. */
6398
+ get localMediaSettled$() {
6399
+ return this.mainPeerConnection.localMediaSettled$;
6400
+ }
6192
6401
  get localStream() {
6193
6402
  return this._rtcPeerConnectionsMap.get(this.webRtcCallSession.id)?.localStream ?? null;
6194
6403
  }
@@ -6245,20 +6454,11 @@ var WebRTCVertoManager = class extends VertoManager {
6245
6454
  const { mediaParams, callID } = event;
6246
6455
  const rtcPeerConnController = this._rtcPeerConnectionsMap.get(callID);
6247
6456
  const { audio, video } = mediaParams;
6248
- (async () => {
6249
- try {
6250
- if (audio && rtcPeerConnController) await rtcPeerConnController.replaceAudioTrackWithConstraints(audio);
6251
- if (video) await rtcPeerConnController?.updateSendersConstraints("video", video);
6252
- this.webRtcCallSession.emitMediaParamsUpdated({
6253
- audio,
6254
- video,
6255
- timestamp: Date.now()
6256
- });
6257
- } catch (error) {
6258
- logger$16.warn("[WebRTCManager] Error applying server-pushed media params:", error);
6259
- this.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
6260
- }
6261
- })();
6457
+ if (!rtcPeerConnController) {
6458
+ logger$16.warn(`[WebRTCManager] Ignoring server-pushed media params for unknown leg ${callID}`);
6459
+ return;
6460
+ }
6461
+ this.applyServerMediaParams(rtcPeerConnController, audio, video);
6262
6462
  });
6263
6463
  this.subscribeTo(this.vertoPing$, (vertoPing) => {
6264
6464
  this.attachManager.attach(this.buildAttachableCall());
@@ -6266,6 +6466,75 @@ var WebRTCVertoManager = class extends VertoManager {
6266
6466
  });
6267
6467
  }
6268
6468
  /**
6469
+ * An auxiliary-leg failure must never destroy the call; main-leg and
6470
+ * call-level errors keep `CallFactory.isFatalError`'s classification.
6471
+ *
6472
+ * Every site holding a peer connection reports through here, so the invariant
6473
+ * is structural rather than per-call-site — which is how cloud-product#20523
6474
+ * happened, with only one of fourteen sites passing `{ fatal: false }`.
6475
+ *
6476
+ * `override` composes rather than replaces: a caller may force non-fatal for a
6477
+ * reason of its own (a `verto.info` frame is best-effort whichever leg carries
6478
+ * it), and an auxiliary leg stays non-fatal regardless.
6479
+ */
6480
+ reportLegError(error, rtcPeerConnController, override) {
6481
+ const leg = rtcPeerConnController?.propose;
6482
+ const legId = rtcPeerConnController?.id;
6483
+ const auxiliary = Boolean(rtcPeerConnController) && !rtcPeerConnController?.isMainDevice;
6484
+ this.onError?.(error, {
6485
+ ...override?.fatal === false || auxiliary ? { fatal: false } : {},
6486
+ ...leg ? { leg } : {},
6487
+ ...legId ? { legId } : {}
6488
+ });
6489
+ if (legId) this._legErrors$.next({
6490
+ legId,
6491
+ error
6492
+ });
6493
+ }
6494
+ /**
6495
+ * Errors reported for one leg, as a stream that fails with them.
6496
+ *
6497
+ * Signaling failures are reported, never thrown — so nothing that waits on a
6498
+ * leg's progress would otherwise learn of a rejected invite. Merging this in
6499
+ * lets the wait end with the reason the server gave.
6500
+ */
6501
+ legError$(legId) {
6502
+ return this._legErrors$.pipe((0, rxjs.filter)((report) => report.legId === legId), (0, rxjs.map)((report) => {
6503
+ throw report.error;
6504
+ }));
6505
+ }
6506
+ /**
6507
+ * Audio and video are applied independently so a failure in one cannot
6508
+ * suppress the other, and `mediaParamsUpdated` is emitted whatever happens —
6509
+ * an application should not be starved of the params by a constraint failure.
6510
+ */
6511
+ async applyServerMediaParams(rtcPeerConnController, audio, video) {
6512
+ const failures = [];
6513
+ let applied = true;
6514
+ if (audio) try {
6515
+ applied = await rtcPeerConnController.updateSendersConstraints("audio", audio) && applied;
6516
+ } catch (error) {
6517
+ applied = false;
6518
+ failures.push(toError(error));
6519
+ }
6520
+ if (video) try {
6521
+ applied = await rtcPeerConnController.updateSendersConstraints("video", video) && applied;
6522
+ } catch (error) {
6523
+ applied = false;
6524
+ failures.push(toError(error));
6525
+ }
6526
+ this.webRtcCallSession.emitMediaParamsUpdated({
6527
+ audio,
6528
+ video,
6529
+ timestamp: Date.now(),
6530
+ applied
6531
+ });
6532
+ for (const failure of failures) {
6533
+ logger$16.warn("[WebRTCManager] Error applying server-pushed media params:", failure);
6534
+ this.reportLegError(failure, rtcPeerConnController, { fatal: false });
6535
+ }
6536
+ }
6537
+ /**
6269
6538
  * Set node_id/selfId only when the current value is null.
6270
6539
  *
6271
6540
  * During reattach, `call.joined` and `verto.answer` events can deliver
@@ -6303,7 +6572,7 @@ var WebRTCVertoManager = class extends VertoManager {
6303
6572
  if (video) await this.mainPeerConnection.updateSendersConstraints("video", video);
6304
6573
  } catch (error) {
6305
6574
  logger$16.warn("[WebRTCManager] Error updating media constraints:", error);
6306
- this.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
6575
+ this.reportLegError(toError(error), this.mainPeerConnection);
6307
6576
  throw error;
6308
6577
  }
6309
6578
  }
@@ -6471,23 +6740,24 @@ var WebRTCVertoManager = class extends VertoManager {
6471
6740
  return response;
6472
6741
  }
6473
6742
  async executeVerto(message, optionals = {}) {
6474
- const webrtcVertoMessage = WebrtcVerto({
6743
+ const params = {
6475
6744
  callID: optionals.callID ?? this.webRtcCallSession.id,
6476
6745
  node_id: optionals.node_id ?? this._nodeId$.value ?? "",
6477
6746
  message,
6478
6747
  subscribe: optionals.subscribe
6479
- });
6748
+ };
6749
+ const webrtcVertoMessage = WebrtcVerto(params);
6480
6750
  const response = await this.webRtcCallSession.execute(webrtcVertoMessage);
6481
6751
  const nonFatal = message.method === "verto.info" ? { fatal: false } : void 0;
6482
6752
  if (response.error) {
6483
6753
  const error = new require_operators.JSONRPCError(response.error.code, response.error.message, response.error.data);
6484
- this.onError?.(error, nonFatal);
6754
+ this.reportLegError(error, this._rtcPeerConnectionsMap.get(params.callID), nonFatal);
6485
6755
  return response;
6486
6756
  }
6487
6757
  const innerResult = require_operators.getValueFrom(response, "result.result");
6488
6758
  if (innerResult?.error) {
6489
6759
  const error = new require_operators.JSONRPCError(innerResult.error.code, innerResult.error.message, innerResult.error.data);
6490
- this.onError?.(error, nonFatal);
6760
+ this.reportLegError(error, this._rtcPeerConnectionsMap.get(params.callID), nonFatal);
6491
6761
  return response;
6492
6762
  }
6493
6763
  return response;
@@ -6508,7 +6778,7 @@ var WebRTCVertoManager = class extends VertoManager {
6508
6778
  }
6509
6779
  } catch (error) {
6510
6780
  logger$16.error(`[WebRTCManager] Error sending Verto ${vertoMethod}:`, error);
6511
- this.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
6781
+ this.reportLegError(toError(error), rtcPeerConnController);
6512
6782
  if (vertoMethod === "verto.modify") this.onModifyFailed?.();
6513
6783
  }
6514
6784
  }
@@ -6524,7 +6794,7 @@ var WebRTCVertoManager = class extends VertoManager {
6524
6794
  } catch (error) {
6525
6795
  logger$16.warn("[WebRTCManager] Error processing modify response:", error);
6526
6796
  const modifyError = error instanceof Error ? error : new Error(String(error), { cause: error });
6527
- this.onError?.(modifyError);
6797
+ this.reportLegError(modifyError, rtcPeerConnController);
6528
6798
  }
6529
6799
  }
6530
6800
  }
@@ -6537,7 +6807,7 @@ var WebRTCVertoManager = class extends VertoManager {
6537
6807
  status,
6538
6808
  signalingError
6539
6809
  });
6540
- this.onError?.(signalingError);
6810
+ this.reportLegError(signalingError, null, { fatal: false });
6541
6811
  return;
6542
6812
  }
6543
6813
  if (rtcPeerConnController.isMainDevice) this._signalingStatus$.next(status);
@@ -6567,7 +6837,7 @@ var WebRTCVertoManager = class extends VertoManager {
6567
6837
  } else {
6568
6838
  logger$16.error("[WebRTCManager] Verto invite failed:", response);
6569
6839
  const inviteError = response.error ? new require_operators.JSONRPCError(response.error.code, response.error.message, response.error.data) : /* @__PURE__ */ new Error("Verto invite failed: unexpected response");
6570
- this.onError?.(inviteError);
6840
+ this.reportLegError(inviteError, rtcPeerConnController);
6571
6841
  }
6572
6842
  }
6573
6843
  get RTCPeerConnectionConfig() {
@@ -6606,7 +6876,7 @@ var WebRTCVertoManager = class extends VertoManager {
6606
6876
  this._rtcPeerConnectionsMap.set(rtcPeerConnController.id, rtcPeerConnController);
6607
6877
  this._rtcPeerConnections$.next(Array.from(this._rtcPeerConnectionsMap.values()));
6608
6878
  this.subscribeTo(rtcPeerConnController.errors$, (error) => {
6609
- this.onError?.(error);
6879
+ this.reportLegError(error, rtcPeerConnController);
6610
6880
  });
6611
6881
  if (options.initOffer) this.handleInboundAnswer(rtcPeerConnController);
6612
6882
  }
@@ -6635,7 +6905,7 @@ var WebRTCVertoManager = class extends VertoManager {
6635
6905
  await rtcPeerConnController.acceptInbound(answerOptions);
6636
6906
  } catch (error) {
6637
6907
  logger$16.error("[WebRTCManager] Error creating inbound answer:", error);
6638
- this.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
6908
+ this.reportLegError(toError(error), rtcPeerConnController);
6639
6909
  }
6640
6910
  }
6641
6911
  }
@@ -6739,7 +7009,7 @@ var WebRTCVertoManager = class extends VertoManager {
6739
7009
  await this.attachManager.attach(this.buildAttachableCall());
6740
7010
  } catch (error) {
6741
7011
  logger$16.error("[WebRTCManager] Error sending Verto answer:", error);
6742
- this.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
7012
+ this.reportLegError(toError(error), rtcPeerConnectionController);
6743
7013
  await rtcPeerConnectionController.updateAnswerStatus({ status: "failed" });
6744
7014
  }
6745
7015
  }
@@ -6811,7 +7081,7 @@ var WebRTCVertoManager = class extends VertoManager {
6811
7081
  await this.mainPeerConnection.restoreTrackSender(deviceKind);
6812
7082
  } else {
6813
7083
  const error = new require_operators.InvalidParams("No valid device to be added");
6814
- this.onError?.(error);
7084
+ this.reportLegError(error, this.mainPeerConnection);
6815
7085
  throw error;
6816
7086
  }
6817
7087
  }
@@ -6836,21 +7106,30 @@ var WebRTCVertoManager = class extends VertoManager {
6836
7106
  this._rtcPeerConnections$.next(Array.from(this._rtcPeerConnectionsMap.values()));
6837
7107
  this.subscribeTo(rtcPeerConnController.errors$, (error) => {
6838
7108
  firstPeerConnectionError ??= error;
6839
- this.onError?.(error, { fatal: false });
7109
+ this.reportLegError(error, rtcPeerConnController);
6840
7110
  });
6841
- await (0, rxjs.firstValueFrom)(rtcPeerConnController.connectionState$.pipe((0, rxjs.filter)((state) => state === "connected"), (0, rxjs.take)(1), (0, rxjs.timeout)(this._screenShareTimeoutMs), (0, rxjs.takeUntil)(this.destroyed$)));
7111
+ const pc = rtcPeerConnController;
7112
+ await (0, rxjs.firstValueFrom)((0, rxjs.merge)(pc.localMediaSettled$.pipe((0, rxjs.take)(1), (0, rxjs.switchMap)(() => pc.connectionState$.pipe((0, rxjs.filter)((state) => state === "connected"), (0, rxjs.take)(1), (0, rxjs.timeout)(require_operators.DEFAULT_AUX_LEG_CONNECT_TIMEOUT_MS)))), this.legError$(pc.id)).pipe((0, rxjs.takeUntil)(this.destroyed$)));
6842
7113
  if (isScreenShare) this._screenShareStatus$.next("started");
6843
7114
  logger$16.info(`[WebRTCManager] Additional peer connection connected (${propose}).`);
6844
7115
  return rtcPeerConnController.id;
6845
7116
  } catch (error) {
6846
7117
  logger$16.warn("[WebRTCManager] Error initializing additional peer connection:", error);
6847
- if (rtcPeerConnController) rtcPeerConnController.destroy();
6848
- if (isScreenShare) this._screenShareStatus$.next("none");
7118
+ if (rtcPeerConnController) {
7119
+ rtcPeerConnController.destroy();
7120
+ this._rtcPeerConnectionsMap.delete(rtcPeerConnController.id);
7121
+ this._rtcPeerConnections$.next(Array.from(this._rtcPeerConnectionsMap.values()));
7122
+ }
7123
+ if (isScreenShare) {
7124
+ this._screenShareStatus$.next("none");
7125
+ this._screenShareId = void 0;
7126
+ }
6849
7127
  if (firstPeerConnectionError) throw firstPeerConnectionError instanceof require_operators.MediaAccessError && firstPeerConnectionError.originalError instanceof Error ? firstPeerConnectionError.originalError : firstPeerConnectionError;
6850
7128
  if (error instanceof rxjs.EmptyError) {
6851
7129
  logger$16.debug("[WebRTCManager] Additional peer connection aborted before connecting.");
6852
7130
  return;
6853
7131
  }
7132
+ if (error instanceof rxjs.TimeoutError) throw new require_operators.AuxiliaryLegTimeoutError(propose, error);
6854
7133
  throw error instanceof Error ? error : new Error(String(error), { cause: error });
6855
7134
  }
6856
7135
  }
@@ -6896,7 +7175,7 @@ var WebRTCVertoManager = class extends VertoManager {
6896
7175
  await this.executeVerto(VertoBye({
6897
7176
  ...causeParams,
6898
7177
  dialogParams: this.dialogParams(rtcPeerConnController)
6899
- }));
7178
+ }), { callID: rtcPeerConnController.id });
6900
7179
  } catch (error) {
6901
7180
  logger$16.warn("[WebRTCManager] Call might already be disconnected, error sending Verto bye:", error);
6902
7181
  throw error;
@@ -8290,13 +8569,13 @@ var WebRTCCall = class extends Destroyable {
8290
8569
  get bandwidthConstrained$() {
8291
8570
  return this.deferEmission(this._bandwidthConstrained$.asObservable());
8292
8571
  }
8293
- /** Observable that emits when server-pushed media params are applied. */
8572
+ /** Observable that emits when the server pushes media params. */
8294
8573
  get mediaParamsUpdated$() {
8295
8574
  return this.deferEmission(this._mediaParamsUpdated$.asObservable());
8296
8575
  }
8297
8576
  /**
8298
8577
  * @internal Emit a media params update event.
8299
- * Called by the VertoManager when server-pushed media params are applied.
8578
+ * Called by the VertoManager when the server pushes media params.
8300
8579
  */
8301
8580
  emitMediaParamsUpdated(event) {
8302
8581
  this._mediaParamsUpdated$.next(event);
@@ -8504,6 +8783,10 @@ var WebRTCCall = class extends Destroyable {
8504
8783
  get selfId$() {
8505
8784
  return this.vertoManager.selfId$;
8506
8785
  }
8786
+ /** @internal Lets call creation bound the media and signalling phases apart. */
8787
+ get localMediaSettled$() {
8788
+ return this.vertoManager.localMediaSettled$;
8789
+ }
8507
8790
  /** Local participant's member ID, or `null` if not joined. */
8508
8791
  get selfId() {
8509
8792
  return this.vertoManager.selfId;
@@ -8899,6 +9182,9 @@ var WebRTCCall = class extends Destroyable {
8899
9182
  /**
8900
9183
  * Infers the semantic error category from a raw Error thrown by VertoManager
8901
9184
  * or an RTCPeerConnection layer.
9185
+ *
9186
+ * Pure function — exported for unit testing.
9187
+ * @internal
8902
9188
  */
8903
9189
  function inferCallErrorKind(error) {
8904
9190
  if (error instanceof require_operators.RPCTimeoutError) return "timeout";
@@ -8917,7 +9203,14 @@ const RECOVERABLE_RPC_CODES = new Set([
8917
9203
  require_operators.RPC_ERROR_AUTHENTICATION_FAILED,
8918
9204
  require_operators.RPC_ERROR_INVALID_PARAMS
8919
9205
  ]);
8920
- /** Determines whether an error should be fatal (destroy the call). */
9206
+ /**
9207
+ * A *fallback*: callers knowing which leg failed pass an explicit `fatal` and
9208
+ * never reach here, so the default-fatal branch only sees call- and main-leg
9209
+ * errors. Auxiliary legs go through `WebRTCVertoManager.reportLegError`.
9210
+ *
9211
+ * Pure function — exported for unit testing.
9212
+ * @internal
9213
+ */
8921
9214
  function isFatalError(error) {
8922
9215
  if (error instanceof require_operators.VertoPongError) return false;
8923
9216
  if (error instanceof require_operators.MediaTrackError) return false;
@@ -8952,7 +9245,9 @@ var CallFactory = class {
8952
9245
  kind: inferCallErrorKind(error),
8953
9246
  fatal: options$1?.fatal ?? isFatalError(error),
8954
9247
  error,
8955
- callId: callInstance.id
9248
+ callId: callInstance.id,
9249
+ ...options$1?.leg ? { leg: options$1.leg } : {},
9250
+ ...options$1?.legId ? { legId: options$1.legId } : {}
8956
9251
  };
8957
9252
  callInstance.emitError(callError);
8958
9253
  },
@@ -9420,6 +9715,30 @@ const logger$9 = require_operators.getLogger();
9420
9715
  function shouldAbortDial(callError) {
9421
9716
  return callError.fatal || !(callError.error instanceof require_operators.MediaAccessError);
9422
9717
  }
9718
+ /**
9719
+ * Wait for a dialed call to be ready, or for the failure that stops it.
9720
+ *
9721
+ * Local media acquisition is deliberately unbounded: a permission prompt or a
9722
+ * device picker is human time, and `getUserMedia` cannot be cancelled anyway.
9723
+ * The clock starts only once acquisition settles, so a slow human never spends
9724
+ * the server's budget.
9725
+ *
9726
+ * `merge` rather than `race`, because the two legs settle asymmetrically. A
9727
+ * fatal acquisition failure reports the error and then destroys the call in the
9728
+ * same synchronous step; `errors$` defers delivery by a microtask while
9729
+ * `localMediaSettled$` completes immediately. Under `race` that bare completion
9730
+ * ended the wait first and `dial()` rejected with an RxJS `EmptyError`, burying
9731
+ * the `NotAllowedError` applications are told to inspect. Under `merge` the
9732
+ * completed leg is simply spent, and the queued error — enqueued before the
9733
+ * completion, so delivered before it — arrives to reject the wait. A dial
9734
+ * abandoned with no error at all still ends both legs, and the resulting
9735
+ * `EmptyError` remains the benign-cancel signal.
9736
+ *
9737
+ * Exported for unit testing.
9738
+ */
9739
+ async function awaitDialReady(session, signalingTimeoutMs) {
9740
+ return (0, rxjs.firstValueFrom)((0, rxjs.merge)(session.localMediaSettled$.pipe((0, rxjs.take)(1), (0, rxjs.switchMap)(() => session.selfId$.pipe((0, rxjs.filter)((id) => Boolean(id)), (0, rxjs.take)(1), (0, rxjs.timeout)(signalingTimeoutMs)))), session.errors$.pipe((0, rxjs.filter)(shouldAbortDial), (0, rxjs.take)(1), (0, rxjs.switchMap)((callError) => (0, rxjs.throwError)(() => callError.error)))));
9741
+ }
9423
9742
  const getAddressSearchURI = (options) => {
9424
9743
  const to = options.to?.split("?")[0];
9425
9744
  const from$8 = options.from?.startsWith("subscriber://") ? options.from.replace("subscriber://", "") : options.from;
@@ -9436,7 +9755,6 @@ var ClientSessionManager = class extends Destroyable {
9436
9755
  this.authorizationStateKey = authorizationStateKey;
9437
9756
  this.attachManager = attachManager;
9438
9757
  this.dpopManager = dpopManager;
9439
- this.callCreateTimeout = 6e3;
9440
9758
  this.agent = `signalwire-js/4.0.0`;
9441
9759
  this.eventAcks = true;
9442
9760
  this.authorizationState$ = this.createReplaySubject(1);
@@ -9864,7 +10182,7 @@ var ClientSessionManager = class extends Destroyable {
9864
10182
  to: destinationURI,
9865
10183
  ...options
9866
10184
  });
9867
- await (0, rxjs.firstValueFrom)((0, rxjs.race)(callSession.selfId$.pipe((0, rxjs.filter)((id) => Boolean(id)), (0, rxjs.take)(1), (0, rxjs.timeout)(this.callCreateTimeout)), callSession.errors$.pipe((0, rxjs.filter)(shouldAbortDial), (0, rxjs.take)(1), (0, rxjs.switchMap)((callError) => (0, rxjs.throwError)(() => callError.error)))));
10185
+ await awaitDialReady(callSession, require_operators.DEFAULT_CALL_SIGNALING_TIMEOUT_MS);
9868
10186
  this._calls$.next({
9869
10187
  [`${callSession.id}`]: callSession,
9870
10188
  ...this._calls$.value
@@ -12036,6 +12354,11 @@ var SignalWire = class extends Destroyable {
12036
12354
  * Returns a {@link Call} in `'ringing'` state. Subscribe to {@link Call.status$}
12037
12355
  * to track progression through `'connected'` → `'disconnected'`.
12038
12356
  *
12357
+ * Local media acquisition is deliberately unbounded: an unanswered permission
12358
+ * prompt leaves this promise pending indefinitely, so apply your own bound if
12359
+ * your UI needs one. The 12 s signaling budget starts only once acquisition
12360
+ * settles.
12361
+ *
12039
12362
  * @param destination - Address URI string (e.g. `'/public/my-room'`) or {@link Address} instance.
12040
12363
  * @param options - Media and dial options (audio/video, device constraints). Overrides defaults.
12041
12364
  * @returns The created {@link Call} instance.
@@ -12498,6 +12821,7 @@ emitReadyEvent();
12498
12821
 
12499
12822
  //#endregion
12500
12823
  exports.Address = Address;
12824
+ exports.AuxiliaryLegTimeoutError = require_operators.AuxiliaryLegTimeoutError;
12501
12825
  exports.CallCreateError = require_operators.CallCreateError;
12502
12826
  exports.CallNotReadyError = require_operators.CallNotReadyError;
12503
12827
  exports.ClientPreferences = ClientPreferences;