@signalwire/js 4.0.0-rc.2 → 4.0.0-rc.4
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/browser.mjs +1733 -440
- package/dist/browser.mjs.map +1 -1
- package/dist/browser.umd.js +1737 -439
- package/dist/browser.umd.js.map +1 -1
- package/dist/index.cjs +1627 -430
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2327 -1835
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +2327 -1835
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1623 -431
- package/dist/index.mjs.map +1 -1
- package/dist/operators/index.cjs +1 -1
- package/dist/operators/index.mjs +1 -1
- package/dist/{operators-BlUtq-t0.mjs → operators-CTNnvDcG.mjs} +103 -2
- package/dist/operators-CTNnvDcG.mjs.map +1 -0
- package/dist/{operators-B8ipd4Xl.cjs → operators-IoitK-As.cjs} +168 -1
- package/dist/operators-IoitK-As.cjs.map +1 -0
- package/package.json +1 -1
- package/dist/operators-B8ipd4Xl.cjs.map +0 -1
- package/dist/operators-BlUtq-t0.mjs.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const require_operators = require('./operators-
|
|
1
|
+
const require_operators = require('./operators-IoitK-As.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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
270
|
-
} else logger$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
1879
|
+
logger$29.debug("[DPoP] Key pair restored from IndexedDB, fingerprint:", this._fingerprint);
|
|
1880
1880
|
return this._fingerprint;
|
|
1881
1881
|
} catch (error) {
|
|
1882
|
-
logger$
|
|
1882
|
+
logger$29.warn("[DPoP] Stored key pair unusable, generating new one:", error);
|
|
1883
1883
|
await deleteKeyPairFromDB();
|
|
1884
1884
|
}
|
|
1885
|
-
logger$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
2065
|
+
logger$28.debug("NetworkMonitor: event listeners removed");
|
|
2066
2066
|
}
|
|
2067
2067
|
handleOnline() {
|
|
2068
|
-
logger$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
2436
|
-
} else logger$
|
|
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$
|
|
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$
|
|
2479
|
+
logger$26.debug("VisibilityController: visibility changed", {
|
|
2480
2480
|
from: previousState,
|
|
2481
2481
|
to: newState
|
|
2482
2482
|
});
|
|
@@ -2589,6 +2589,12 @@ const RPCReauthenticate = (params) => {
|
|
|
2589
2589
|
|
|
2590
2590
|
//#endregion
|
|
2591
2591
|
//#region src/core/RPCMessages/RPCPing.ts
|
|
2592
|
+
const RPCPing = () => {
|
|
2593
|
+
return buildRPCRequest({
|
|
2594
|
+
method: "signalwire.ping",
|
|
2595
|
+
params: { timestamp: Date.now() / 1e3 }
|
|
2596
|
+
});
|
|
2597
|
+
};
|
|
2592
2598
|
const RPCPingResponse = (id, timestamp) => {
|
|
2593
2599
|
return makeRPCResponse({
|
|
2594
2600
|
id,
|
|
@@ -2677,15 +2683,57 @@ const RPCEventAckResponse = (id) => makeRPCResponse({
|
|
|
2677
2683
|
result: {}
|
|
2678
2684
|
});
|
|
2679
2685
|
|
|
2686
|
+
//#endregion
|
|
2687
|
+
//#region src/utils/authRecovery.ts
|
|
2688
|
+
/**
|
|
2689
|
+
* Walk an error's `error`/`cause` chain looking for a {@link JSONRPCError}.
|
|
2690
|
+
* Errors thrown by call creation are wrapped (e.g. `CallCreateError`), so the
|
|
2691
|
+
* underlying signaling error is nested. Bounded by a visited set to guard
|
|
2692
|
+
* against cyclic causes.
|
|
2693
|
+
*/
|
|
2694
|
+
function findJSONRPCError(error) {
|
|
2695
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2696
|
+
let current = error;
|
|
2697
|
+
while (current instanceof Error && !seen.has(current)) {
|
|
2698
|
+
seen.add(current);
|
|
2699
|
+
if (current instanceof require_operators.JSONRPCError) return current;
|
|
2700
|
+
current = current.error ?? current.cause;
|
|
2701
|
+
}
|
|
2702
|
+
}
|
|
2703
|
+
/**
|
|
2704
|
+
* Whether an error is a session-recoverable authentication failure
|
|
2705
|
+
* (`-32002` authentication failed or `-32003` requester validation failed)
|
|
2706
|
+
* that a credential re-mint + retry can heal.
|
|
2707
|
+
*/
|
|
2708
|
+
function isRecoverableAuthError(error) {
|
|
2709
|
+
const rpcError = findJSONRPCError(error);
|
|
2710
|
+
return rpcError !== void 0 && (rpcError.code === require_operators.RPC_ERROR_REQUESTER_VALIDATION_FAILED || rpcError.code === require_operators.RPC_ERROR_AUTHENTICATION_FAILED);
|
|
2711
|
+
}
|
|
2712
|
+
/**
|
|
2713
|
+
* Whether an error is specifically a requester-validation rejection
|
|
2714
|
+
* (`-32003`) — the server refusing the session's credential.
|
|
2715
|
+
*
|
|
2716
|
+
* Narrower than {@link isRecoverableAuthError} on purpose. `-32002` is
|
|
2717
|
+
* overloaded server-side: a rejected reattach arrives as `-32002` with
|
|
2718
|
+
* `cause: INVALID_MSG_UNSPECIFIED` and message `CALL ERROR`, which is a
|
|
2719
|
+
* call-level rejection and says nothing about the credential. Use this where
|
|
2720
|
+
* the decision must not be fooled by that, such as deciding whether retrying
|
|
2721
|
+
* an operation could possibly succeed.
|
|
2722
|
+
*/
|
|
2723
|
+
function isRequesterValidationError(error) {
|
|
2724
|
+
return findJSONRPCError(error)?.code === require_operators.RPC_ERROR_REQUESTER_VALIDATION_FAILED;
|
|
2725
|
+
}
|
|
2726
|
+
|
|
2680
2727
|
//#endregion
|
|
2681
2728
|
//#region src/managers/AttachManager.ts
|
|
2682
|
-
const logger$
|
|
2729
|
+
const logger$25 = require_operators.getLogger();
|
|
2683
2730
|
var AttachManager = class {
|
|
2684
|
-
constructor(storage, deviceController, reconnectCallsTimeout, attachKey) {
|
|
2731
|
+
constructor(storage, deviceController, reconnectCallsTimeout, attachKey, credentialRecovered) {
|
|
2685
2732
|
this.storage = storage;
|
|
2686
2733
|
this.deviceController = deviceController;
|
|
2687
2734
|
this.reconnectCallsTimeout = reconnectCallsTimeout;
|
|
2688
2735
|
this.attachKey = attachKey;
|
|
2736
|
+
this.credentialRecovered = credentialRecovered;
|
|
2689
2737
|
this.writeQueue = Promise.resolve();
|
|
2690
2738
|
}
|
|
2691
2739
|
async detachAll() {
|
|
@@ -2700,7 +2748,7 @@ var AttachManager = class {
|
|
|
2700
2748
|
try {
|
|
2701
2749
|
return await this.storage.getItem(this.attachKey) ?? {};
|
|
2702
2750
|
} catch (error) {
|
|
2703
|
-
logger$
|
|
2751
|
+
logger$25.warn("[AttachManager] Failed to retrieve attached calls from storage", error);
|
|
2704
2752
|
return {};
|
|
2705
2753
|
}
|
|
2706
2754
|
}
|
|
@@ -2708,7 +2756,7 @@ var AttachManager = class {
|
|
|
2708
2756
|
try {
|
|
2709
2757
|
await this.storage.setItem(this.attachKey, attached);
|
|
2710
2758
|
} catch (error) {
|
|
2711
|
-
logger$
|
|
2759
|
+
logger$25.warn("[AttachManager] Failed to write attached calls to storage", error);
|
|
2712
2760
|
}
|
|
2713
2761
|
}
|
|
2714
2762
|
/**
|
|
@@ -2727,11 +2775,39 @@ var AttachManager = class {
|
|
|
2727
2775
|
}
|
|
2728
2776
|
async attach(call) {
|
|
2729
2777
|
if (!call.to) {
|
|
2730
|
-
logger$
|
|
2778
|
+
logger$25.warn("[AttachManager] Skip attach for calls with no destination");
|
|
2731
2779
|
return;
|
|
2732
2780
|
}
|
|
2781
|
+
const attachment = this.buildAttachment(call, call.to);
|
|
2782
|
+
await this.mutate((attached) => ({
|
|
2783
|
+
...attached,
|
|
2784
|
+
[call.id]: attachment
|
|
2785
|
+
}));
|
|
2786
|
+
}
|
|
2787
|
+
/**
|
|
2788
|
+
* Keep an already-stored call's reference alive and current — the periodic
|
|
2789
|
+
* refresh the `verto.ping` keepalive drives.
|
|
2790
|
+
*
|
|
2791
|
+
* Only ever updates: a call with no record is one nothing wants reattached,
|
|
2792
|
+
* and re-creating it here would undo a `detach`. That matters because a ping
|
|
2793
|
+
* can land in the window between `bye()` detaching and the call being torn
|
|
2794
|
+
* down, and a record revived there survives the hangup — so the next page
|
|
2795
|
+
* load dials a call nobody is on. The existence check and the write share
|
|
2796
|
+
* one {@link mutate} turn, so a concurrent detach cannot slip between them.
|
|
2797
|
+
*/
|
|
2798
|
+
async refresh(call) {
|
|
2799
|
+
if (!call.to) return;
|
|
2733
2800
|
const destination = call.to;
|
|
2734
|
-
|
|
2801
|
+
await this.mutate((attached) => {
|
|
2802
|
+
if (!Object.hasOwn(attached, call.id)) return attached;
|
|
2803
|
+
return {
|
|
2804
|
+
...attached,
|
|
2805
|
+
[call.id]: this.buildAttachment(call, destination)
|
|
2806
|
+
};
|
|
2807
|
+
});
|
|
2808
|
+
}
|
|
2809
|
+
buildAttachment(call, destination) {
|
|
2810
|
+
return {
|
|
2735
2811
|
nodeId: call.nodeId,
|
|
2736
2812
|
destination,
|
|
2737
2813
|
mediaDirections: call.mediaDirections,
|
|
@@ -2739,10 +2815,6 @@ var AttachManager = class {
|
|
|
2739
2815
|
videoInputDevice: call.mediaDirections.video !== "inactive" ? this.deviceController.selectedVideoInputDevice : null,
|
|
2740
2816
|
attachedAt: Date.now()
|
|
2741
2817
|
};
|
|
2742
|
-
await this.mutate((attached) => ({
|
|
2743
|
-
...attached,
|
|
2744
|
-
[call.id]: attachment
|
|
2745
|
-
}));
|
|
2746
2818
|
}
|
|
2747
2819
|
async detach(call) {
|
|
2748
2820
|
await this.mutate((attached) => {
|
|
@@ -2765,8 +2837,14 @@ var AttachManager = class {
|
|
|
2765
2837
|
* rejecting. Once that fix is deployed, this will work for both
|
|
2766
2838
|
* page reloads and WebSocket reconnects.
|
|
2767
2839
|
*
|
|
2768
|
-
*
|
|
2769
|
-
*
|
|
2840
|
+
* A failed reattach does NOT generally cost the stored reference. It is
|
|
2841
|
+
* discarded only when the server denied the reattach on a session whose
|
|
2842
|
+
* credential it had already accepted — a verified reauthentication followed
|
|
2843
|
+
* by a refusal is the server saying the call is gone, and that is the one
|
|
2844
|
+
* refusal worth acting on. Until then the credential may be what is being
|
|
2845
|
+
* refused, and the record is the only way a later reload can try again;
|
|
2846
|
+
* keeping it costs nothing, since `detachExpired` reaps it once it is older
|
|
2847
|
+
* than `reconnectCallsTimeout`.
|
|
2770
2848
|
*/
|
|
2771
2849
|
async reattachCalls() {
|
|
2772
2850
|
const attached = await this.readAttached();
|
|
@@ -2775,25 +2853,31 @@ var AttachManager = class {
|
|
|
2775
2853
|
const { destination } = attachment;
|
|
2776
2854
|
const options = this.buildCallOptions(attachment);
|
|
2777
2855
|
let succeeded = false;
|
|
2856
|
+
let refusedOnCredentials = false;
|
|
2778
2857
|
for (let attempt = 1; attempt <= 3; attempt++) try {
|
|
2779
2858
|
await this.session.createOutboundCall(destination, {
|
|
2780
2859
|
callId,
|
|
2781
2860
|
...options
|
|
2782
2861
|
});
|
|
2783
|
-
logger$
|
|
2862
|
+
logger$25.info(`[AttachManager] Reattached call ${callId} (attempt ${attempt})`);
|
|
2784
2863
|
succeeded = true;
|
|
2785
2864
|
break;
|
|
2786
2865
|
} catch (error) {
|
|
2787
|
-
logger$
|
|
2866
|
+
logger$25.warn(`[AttachManager] Reattach attempt ${attempt}/3 failed for call ${callId}:`, error);
|
|
2867
|
+
if (isRequesterValidationError(error)) {
|
|
2868
|
+
refusedOnCredentials = true;
|
|
2869
|
+
logger$25.warn(`[AttachManager] Reattach of ${callId} was refused on credentials; not retrying.`);
|
|
2870
|
+
break;
|
|
2871
|
+
}
|
|
2788
2872
|
if (attempt < 3) await new Promise((r) => setTimeout(r, (attempt + 1) * 1e3));
|
|
2789
2873
|
}
|
|
2790
|
-
if (!succeeded) {
|
|
2791
|
-
logger$
|
|
2874
|
+
if (!succeeded) if (this.credentialRecovered() && !refusedOnCredentials) {
|
|
2875
|
+
logger$25.warn(`[AttachManager] Reattach of ${callId} was denied after a verified reauthentication, removing reference`);
|
|
2792
2876
|
await this.detach({
|
|
2793
2877
|
id: callId,
|
|
2794
2878
|
mediaDirections: attachment.mediaDirections
|
|
2795
2879
|
});
|
|
2796
|
-
}
|
|
2880
|
+
} else logger$25.warn(`[AttachManager] Reattach failed for call ${callId}; keeping the reference (credential refused or never proven good)`);
|
|
2797
2881
|
}
|
|
2798
2882
|
}
|
|
2799
2883
|
/**
|
|
@@ -3106,7 +3190,7 @@ function toggleHandraiseMethod(is) {
|
|
|
3106
3190
|
|
|
3107
3191
|
//#endregion
|
|
3108
3192
|
//#region src/core/entities/Participant.ts
|
|
3109
|
-
const logger$
|
|
3193
|
+
const logger$24 = require_operators.getLogger();
|
|
3110
3194
|
const initialState = {};
|
|
3111
3195
|
/**
|
|
3112
3196
|
* Represents a participant in a call.
|
|
@@ -3116,9 +3200,9 @@ const initialState = {};
|
|
|
3116
3200
|
* the local participant with additional device control.
|
|
3117
3201
|
*/
|
|
3118
3202
|
var Participant = class extends Destroyable {
|
|
3119
|
-
constructor(id,
|
|
3203
|
+
constructor(id, callExecuteMethod, deviceController) {
|
|
3120
3204
|
super();
|
|
3121
|
-
this.
|
|
3205
|
+
this.callExecuteMethod = callExecuteMethod;
|
|
3122
3206
|
this.deviceController = deviceController;
|
|
3123
3207
|
this._state$ = this.createBehaviorSubject(initialState);
|
|
3124
3208
|
this.id = id;
|
|
@@ -3340,22 +3424,55 @@ var Participant = class extends Destroyable {
|
|
|
3340
3424
|
get value() {
|
|
3341
3425
|
return this._state$.value;
|
|
3342
3426
|
}
|
|
3427
|
+
/**
|
|
3428
|
+
* Target triple for member RPCs, built from the participant's own state.
|
|
3429
|
+
* The backend locates the member's session by the target `call_id`/`node_id`,
|
|
3430
|
+
* so this must always be the participant's own call context — never the
|
|
3431
|
+
* local call's id (issue #19400).
|
|
3432
|
+
*
|
|
3433
|
+
* Reading it doubles as a readiness probe: it throws until the first full
|
|
3434
|
+
* member event (`member.joined`/`member.updated` or the `call.joined`
|
|
3435
|
+
* roster) arrives, and never regresses afterwards.
|
|
3436
|
+
*
|
|
3437
|
+
* @throws {ParticipantNotReadyError} If the member state has not been
|
|
3438
|
+
* received yet (e.g. a participant first seen via `member.talking`) — an
|
|
3439
|
+
* empty call context can never address the member, so fail fast instead of
|
|
3440
|
+
* sending a doomed RPC.
|
|
3441
|
+
*/
|
|
3442
|
+
get target() {
|
|
3443
|
+
const { call_id, node_id } = this._state$.value;
|
|
3444
|
+
if (!call_id || !node_id) throw new require_operators.ParticipantNotReadyError(this.id);
|
|
3445
|
+
return {
|
|
3446
|
+
member_id: this.id,
|
|
3447
|
+
call_id,
|
|
3448
|
+
node_id
|
|
3449
|
+
};
|
|
3450
|
+
}
|
|
3451
|
+
/**
|
|
3452
|
+
* Executes a member RPC against this participant, injecting its own
|
|
3453
|
+
* {@link target} as the target.
|
|
3454
|
+
*
|
|
3455
|
+
* @throws {ParticipantNotReadyError} Via {@link target}, when the
|
|
3456
|
+
* member state has not been received yet.
|
|
3457
|
+
*/
|
|
3458
|
+
async executeMethod(method, args) {
|
|
3459
|
+
return this.callExecuteMethod(this.target, method, args);
|
|
3460
|
+
}
|
|
3343
3461
|
/** Toggles the deafened state (mutes/unmutes incoming audio). */
|
|
3344
3462
|
async toggleDeaf() {
|
|
3345
|
-
|
|
3346
|
-
await this.executeMethod(this.id, method, {});
|
|
3463
|
+
await this.executeMethod(toggleDeafMethod(this.deaf), {});
|
|
3347
3464
|
}
|
|
3348
3465
|
/** Toggles the hand-raised state. */
|
|
3349
3466
|
async toggleHandraise() {
|
|
3350
|
-
await this.executeMethod(
|
|
3467
|
+
await this.executeMethod(toggleHandraiseMethod(this.handraised), {});
|
|
3351
3468
|
}
|
|
3352
3469
|
/** Mutes the participant's audio. */
|
|
3353
3470
|
async mute() {
|
|
3354
|
-
await this.executeMethod(
|
|
3471
|
+
await this.executeMethod("call.mute", { channels: ["audio"] });
|
|
3355
3472
|
}
|
|
3356
3473
|
/** Unmutes the participant's audio. */
|
|
3357
3474
|
async unmute() {
|
|
3358
|
-
await this.executeMethod(
|
|
3475
|
+
await this.executeMethod("call.unmute", { channels: ["audio"] });
|
|
3359
3476
|
}
|
|
3360
3477
|
/** Toggles the participant's audio mute state. */
|
|
3361
3478
|
async toggleMute() {
|
|
@@ -3363,11 +3480,11 @@ var Participant = class extends Destroyable {
|
|
|
3363
3480
|
}
|
|
3364
3481
|
/** Mutes the participant's video. */
|
|
3365
3482
|
async muteVideo() {
|
|
3366
|
-
await this.executeMethod(
|
|
3483
|
+
await this.executeMethod("call.mute", { channels: ["video"] });
|
|
3367
3484
|
}
|
|
3368
3485
|
/** Unmutes the participant's video. */
|
|
3369
3486
|
async unmuteVideo() {
|
|
3370
|
-
await this.executeMethod(
|
|
3487
|
+
await this.executeMethod("call.unmute", { channels: ["video"] });
|
|
3371
3488
|
}
|
|
3372
3489
|
/** Toggles the participant's video mute state. */
|
|
3373
3490
|
async toggleMuteVideo() {
|
|
@@ -3375,7 +3492,7 @@ var Participant = class extends Destroyable {
|
|
|
3375
3492
|
}
|
|
3376
3493
|
/** Toggles echo cancellation on the audio input. */
|
|
3377
3494
|
async toggleEchoCancellation() {
|
|
3378
|
-
await this.executeMethod(
|
|
3495
|
+
await this.executeMethod("call.audioflags.set", {
|
|
3379
3496
|
echo_cancellation: !this.echoCancellation,
|
|
3380
3497
|
auto_gain: this.autoGain,
|
|
3381
3498
|
noise_suppression: this.noiseSuppression
|
|
@@ -3383,7 +3500,7 @@ var Participant = class extends Destroyable {
|
|
|
3383
3500
|
}
|
|
3384
3501
|
/** Toggles automatic gain control on the audio input. */
|
|
3385
3502
|
async toggleAudioInputAutoGain() {
|
|
3386
|
-
await this.executeMethod(
|
|
3503
|
+
await this.executeMethod("call.audioflags.set", {
|
|
3387
3504
|
echo_cancellation: this.echoCancellation,
|
|
3388
3505
|
auto_gain: !this.autoGain,
|
|
3389
3506
|
noise_suppression: this.noiseSuppression
|
|
@@ -3391,7 +3508,7 @@ var Participant = class extends Destroyable {
|
|
|
3391
3508
|
}
|
|
3392
3509
|
/** Toggles noise suppression on the audio input. */
|
|
3393
3510
|
async toggleNoiseSuppression() {
|
|
3394
|
-
await this.executeMethod(
|
|
3511
|
+
await this.executeMethod("call.audioflags.set", {
|
|
3395
3512
|
echo_cancellation: this.echoCancellation,
|
|
3396
3513
|
auto_gain: this.autoGain,
|
|
3397
3514
|
noise_suppression: !this.noiseSuppression
|
|
@@ -3399,7 +3516,7 @@ var Participant = class extends Destroyable {
|
|
|
3399
3516
|
}
|
|
3400
3517
|
/** Toggles low-bitrate mode for this participant's media. */
|
|
3401
3518
|
async toggleLowbitrate() {
|
|
3402
|
-
await this.executeMethod(
|
|
3519
|
+
await this.executeMethod("call.lowbitrate.set", { lowbitrate: !this.lowbitrate });
|
|
3403
3520
|
}
|
|
3404
3521
|
/**
|
|
3405
3522
|
* Adjusts the **conference-only** microphone energy gate / sensitivity level
|
|
@@ -3415,7 +3532,7 @@ var Participant = class extends Destroyable {
|
|
|
3415
3532
|
* (integer, larger values are more sensitive).
|
|
3416
3533
|
*/
|
|
3417
3534
|
async setAudioInputSensitivity(value) {
|
|
3418
|
-
await this.executeMethod(
|
|
3535
|
+
await this.executeMethod("call.microphone.sensitivity.set", { sensitivity: value });
|
|
3419
3536
|
}
|
|
3420
3537
|
/**
|
|
3421
3538
|
* Sets the **server-side** microphone volume on this participant's bridged
|
|
@@ -3428,7 +3545,7 @@ var Participant = class extends Destroyable {
|
|
|
3428
3545
|
* @param value - Volume level (0-100).
|
|
3429
3546
|
*/
|
|
3430
3547
|
async setAudioInputVolume(value) {
|
|
3431
|
-
await this.executeMethod(
|
|
3548
|
+
await this.executeMethod("call.microphone.volume.set", { volume: value });
|
|
3432
3549
|
}
|
|
3433
3550
|
/**
|
|
3434
3551
|
* Sets the **server-side** speaker volume on this participant's bridged call
|
|
@@ -3442,45 +3559,31 @@ var Participant = class extends Destroyable {
|
|
|
3442
3559
|
* @param value - Volume level (0-100).
|
|
3443
3560
|
*/
|
|
3444
3561
|
async setAudioOutputVolume(value) {
|
|
3445
|
-
await this.executeMethod(
|
|
3562
|
+
await this.executeMethod("call.speaker.volume.set", { volume: value });
|
|
3446
3563
|
}
|
|
3447
3564
|
/**
|
|
3448
3565
|
* Sets the participant's position in the video layout.
|
|
3449
3566
|
*
|
|
3450
|
-
* Requires the `member.position` capability. The gateway
|
|
3451
|
-
*
|
|
3452
|
-
*
|
|
3453
|
-
*
|
|
3454
|
-
* guarantee a visible change: the backend silently returns `200` (no-op) for
|
|
3455
|
-
* non-conference targets.
|
|
3567
|
+
* Requires the `member.position` capability. The gateway requires a
|
|
3568
|
+
* `targets` array of `{ target, position }` entries (issue #19400). A
|
|
3569
|
+
* resolved promise does not guarantee a visible change: the backend silently
|
|
3570
|
+
* returns `200` (no-op) for non-conference targets.
|
|
3456
3571
|
*
|
|
3457
3572
|
* @param value - The {@link VideoPosition} to assign (e.g. `'auto'`, `'reserved-0'`).
|
|
3458
3573
|
*/
|
|
3459
3574
|
async setPosition(value) {
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
member_id: this.id,
|
|
3463
|
-
call_id: state.call_id ?? "",
|
|
3464
|
-
node_id: state.node_id ?? ""
|
|
3465
|
-
};
|
|
3466
|
-
await this.executeMethod(target, "call.member.position.set", { targets: [{
|
|
3467
|
-
target,
|
|
3575
|
+
await this.executeMethod("call.member.position.set", { targets: [{
|
|
3576
|
+
target: this.target,
|
|
3468
3577
|
position: value
|
|
3469
3578
|
}] });
|
|
3470
3579
|
}
|
|
3471
3580
|
/** Removes this participant from the call. */
|
|
3472
3581
|
async remove() {
|
|
3473
|
-
|
|
3474
|
-
const target = {
|
|
3475
|
-
member_id: this.id,
|
|
3476
|
-
call_id: state.call_id ?? "",
|
|
3477
|
-
node_id: state.node_id ?? ""
|
|
3478
|
-
};
|
|
3479
|
-
await this.executeMethod(target, "call.member.remove", {});
|
|
3582
|
+
await this.executeMethod("call.member.remove", { targets: [this.target] });
|
|
3480
3583
|
}
|
|
3481
3584
|
/** Ends the call for this participant. */
|
|
3482
3585
|
async end() {
|
|
3483
|
-
await this.executeMethod(
|
|
3586
|
+
await this.executeMethod("call.end", {});
|
|
3484
3587
|
}
|
|
3485
3588
|
/**
|
|
3486
3589
|
* Replaces custom metadata for this participant.
|
|
@@ -3500,7 +3603,7 @@ var Participant = class extends Destroyable {
|
|
|
3500
3603
|
}
|
|
3501
3604
|
/** Destroys the participant, releasing all subscriptions and references. */
|
|
3502
3605
|
destroy() {
|
|
3503
|
-
this.
|
|
3606
|
+
this.callExecuteMethod = void 0;
|
|
3504
3607
|
super.destroy();
|
|
3505
3608
|
}
|
|
3506
3609
|
};
|
|
@@ -3512,8 +3615,8 @@ var Participant = class extends Destroyable {
|
|
|
3512
3615
|
*/
|
|
3513
3616
|
var SelfParticipant = class extends Participant {
|
|
3514
3617
|
/** @internal */
|
|
3515
|
-
constructor(id,
|
|
3516
|
-
super(id,
|
|
3618
|
+
constructor(id, callExecuteMethod, vertoManager, deviceController) {
|
|
3619
|
+
super(id, callExecuteMethod, deviceController);
|
|
3517
3620
|
this.vertoManager = vertoManager;
|
|
3518
3621
|
this._studioAudio$ = this.createBehaviorSubject(false);
|
|
3519
3622
|
this.capabilities = new SelfCapabilities();
|
|
@@ -3537,7 +3640,7 @@ var SelfParticipant = class extends Participant {
|
|
|
3537
3640
|
async enableStudioAudio() {
|
|
3538
3641
|
if (this._studioAudio$.value) return;
|
|
3539
3642
|
this._studioAudio$.next(true);
|
|
3540
|
-
await this.executeMethod(
|
|
3643
|
+
await this.executeMethod("call.audioflags.set", {
|
|
3541
3644
|
echo_cancellation: false,
|
|
3542
3645
|
auto_gain: false,
|
|
3543
3646
|
noise_suppression: false
|
|
@@ -3550,7 +3653,7 @@ var SelfParticipant = class extends Participant {
|
|
|
3550
3653
|
async disableStudioAudio() {
|
|
3551
3654
|
if (!this._studioAudio$.value) return;
|
|
3552
3655
|
this._studioAudio$.next(false);
|
|
3553
|
-
await this.executeMethod(
|
|
3656
|
+
await this.executeMethod("call.audioflags.set", {
|
|
3554
3657
|
echo_cancellation: true,
|
|
3555
3658
|
auto_gain: true,
|
|
3556
3659
|
noise_suppression: true
|
|
@@ -3559,17 +3662,27 @@ var SelfParticipant = class extends Participant {
|
|
|
3559
3662
|
/**
|
|
3560
3663
|
* Starts sharing the local screen.
|
|
3561
3664
|
*
|
|
3665
|
+
* A call carries at most one screen share. Read `screenShareStatus` before
|
|
3666
|
+
* calling and treat `'starting'`/`'stopping'` as busy.
|
|
3667
|
+
*
|
|
3562
3668
|
* The call is unaffected when acquisition fails.
|
|
3563
3669
|
*
|
|
3670
|
+
* @param options - Pass `{ audio: true }` to also request the shared
|
|
3671
|
+
* surface's audio. Defaults to video only.
|
|
3672
|
+
* @throws {ScreenShareAlreadyActiveError} When this call is already
|
|
3673
|
+
* sharing a screen. Call {@link stopScreenShare} before starting another.
|
|
3674
|
+
* @throws {AuxiliaryLegCancelledError} When {@link stopScreenShare} removes
|
|
3675
|
+
* the share before its leg finishes connecting.
|
|
3564
3676
|
* @throws The raw `getDisplayMedia` error. A dismissed picker or a
|
|
3565
3677
|
* permission denial rejects with a `NotAllowedError` `DOMException` —
|
|
3566
3678
|
* inspect `error.name` to tell benign cancels apart from real failures.
|
|
3567
3679
|
*/
|
|
3568
|
-
async startScreenShare() {
|
|
3680
|
+
async startScreenShare(options) {
|
|
3569
3681
|
try {
|
|
3570
|
-
await this.vertoManager.addScreenMedia();
|
|
3682
|
+
await this.vertoManager.addScreenMedia(options);
|
|
3571
3683
|
} catch (error) {
|
|
3572
|
-
logger$
|
|
3684
|
+
if (error instanceof require_operators.AuxiliaryLegCancelledError) logger$24.debug("[Participant.startScreenShare] Screen share cancelled before connecting.");
|
|
3685
|
+
else logger$24.error("[Participant.startScreenShare] Screen share error:", error);
|
|
3573
3686
|
throw error;
|
|
3574
3687
|
}
|
|
3575
3688
|
}
|
|
@@ -3590,14 +3703,18 @@ var SelfParticipant = class extends Participant {
|
|
|
3590
3703
|
*
|
|
3591
3704
|
* The call is unaffected when acquisition fails.
|
|
3592
3705
|
*
|
|
3706
|
+
* @throws {AuxiliaryLegCancelledError} When {@link removeAdditionalDevice}
|
|
3707
|
+
* removes the device before its leg finishes connecting.
|
|
3593
3708
|
* @throws The raw `getUserMedia` error (e.g. `NotAllowedError` on
|
|
3594
|
-
* permission denial) — inspect `error.name` to decide how to react
|
|
3709
|
+
* permission denial) — inspect `error.name` to decide how to react — or
|
|
3710
|
+
* `AuxiliaryLegTimeoutError` if the leg does not connect in time.
|
|
3595
3711
|
*/
|
|
3596
3712
|
async addAdditionalDevice(options) {
|
|
3597
3713
|
try {
|
|
3598
3714
|
await this.vertoManager.addInputDevice(options);
|
|
3599
3715
|
} catch (error) {
|
|
3600
|
-
logger$
|
|
3716
|
+
if (error instanceof require_operators.AuxiliaryLegCancelledError) logger$24.debug("[Participant.addAdditionalDevice] Device removed before connecting.");
|
|
3717
|
+
else logger$24.error("[Participant.addAdditionalDevice] Additional device error:", error);
|
|
3601
3718
|
throw error;
|
|
3602
3719
|
}
|
|
3603
3720
|
}
|
|
@@ -3632,22 +3749,31 @@ var SelfParticipant = class extends Participant {
|
|
|
3632
3749
|
this.deviceController.selectAudioInputDevice(device);
|
|
3633
3750
|
if (options.savePreference) PreferencesContainer.instance.preferredAudioInput = device;
|
|
3634
3751
|
}
|
|
3635
|
-
/**
|
|
3752
|
+
/**
|
|
3753
|
+
* Updates the audio input track constraints for the active call.
|
|
3754
|
+
* @returns whether the constraints reached the media the call is sending.
|
|
3755
|
+
*/
|
|
3636
3756
|
async setAudioInputDeviceConstraints(constraints) {
|
|
3637
|
-
|
|
3757
|
+
return this.vertoManager.updateMediaConstraints({ audio: constraints });
|
|
3638
3758
|
}
|
|
3639
|
-
/**
|
|
3759
|
+
/**
|
|
3760
|
+
* Updates both audio and video input track constraints for the active call.
|
|
3761
|
+
* @returns whether both kinds took the constraints.
|
|
3762
|
+
*/
|
|
3640
3763
|
async setInputDevicesConstraints(constraints) {
|
|
3641
|
-
|
|
3764
|
+
return this.vertoManager.updateMediaConstraints(constraints);
|
|
3642
3765
|
}
|
|
3643
3766
|
/** Selects the video input device for future calls. Optionally saves as a preference. */
|
|
3644
3767
|
selectVideoInputDevice(device, options = {}) {
|
|
3645
3768
|
this.deviceController.selectVideoInputDevice(device);
|
|
3646
3769
|
if (options.savePreference) PreferencesContainer.instance.preferredVideoInput = device;
|
|
3647
3770
|
}
|
|
3648
|
-
/**
|
|
3771
|
+
/**
|
|
3772
|
+
* Updates the video input track constraints for the active call.
|
|
3773
|
+
* @returns whether the constraints reached the media the call is sending.
|
|
3774
|
+
*/
|
|
3649
3775
|
async setVideoInputDeviceConstraints(constraints) {
|
|
3650
|
-
|
|
3776
|
+
return this.vertoManager.updateMediaConstraints({ video: constraints });
|
|
3651
3777
|
}
|
|
3652
3778
|
/** Selects the audio output device. Optionally saves as a preference. */
|
|
3653
3779
|
selectAudioOutputDevice(device, options = {}) {
|
|
@@ -3660,7 +3786,7 @@ var SelfParticipant = class extends Participant {
|
|
|
3660
3786
|
*/
|
|
3661
3787
|
exitStudioModeIfActive() {
|
|
3662
3788
|
if (this._studioAudio$.value) {
|
|
3663
|
-
logger$
|
|
3789
|
+
logger$24.debug("[SelfParticipant] Exiting studio audio mode due to individual flag toggle");
|
|
3664
3790
|
this._studioAudio$.next(false);
|
|
3665
3791
|
}
|
|
3666
3792
|
}
|
|
@@ -3684,7 +3810,7 @@ var SelfParticipant = class extends Participant {
|
|
|
3684
3810
|
try {
|
|
3685
3811
|
await super.mute();
|
|
3686
3812
|
} catch (error) {
|
|
3687
|
-
logger$
|
|
3813
|
+
logger$24.warn("[Participant.toggleAudioInput] Server Error while muting audio input, proceeding with local toggle anyway", error);
|
|
3688
3814
|
} finally {
|
|
3689
3815
|
this.vertoManager.muteMainAudioInputDevice();
|
|
3690
3816
|
}
|
|
@@ -3694,7 +3820,7 @@ var SelfParticipant = class extends Participant {
|
|
|
3694
3820
|
try {
|
|
3695
3821
|
await super.unmute();
|
|
3696
3822
|
} catch (error) {
|
|
3697
|
-
logger$
|
|
3823
|
+
logger$24.warn("[Participant.toggleAudioInput] Server Error while unmuting audio input, proceeding with local toggle anyway", error);
|
|
3698
3824
|
} finally {
|
|
3699
3825
|
await this.vertoManager.unmuteMainAudioInputDevice();
|
|
3700
3826
|
}
|
|
@@ -3704,7 +3830,7 @@ var SelfParticipant = class extends Participant {
|
|
|
3704
3830
|
try {
|
|
3705
3831
|
await super.muteVideo();
|
|
3706
3832
|
} catch (error) {
|
|
3707
|
-
logger$
|
|
3833
|
+
logger$24.warn("[Participant.toggleVideoInput] Server Error while muting video input, proceeding with local toggle anyway", error);
|
|
3708
3834
|
} finally {
|
|
3709
3835
|
this.vertoManager.muteMainVideoInputDevice();
|
|
3710
3836
|
}
|
|
@@ -3714,7 +3840,7 @@ var SelfParticipant = class extends Participant {
|
|
|
3714
3840
|
try {
|
|
3715
3841
|
await super.unmuteVideo();
|
|
3716
3842
|
} catch (error) {
|
|
3717
|
-
logger$
|
|
3843
|
+
logger$24.warn("[Participant.toggleVideoInput] Server Error while unmuting video input, proceeding with local toggle anyway", error);
|
|
3718
3844
|
} finally {
|
|
3719
3845
|
await this.vertoManager.unmuteMainVideoInputDevice();
|
|
3720
3846
|
}
|
|
@@ -3808,7 +3934,7 @@ function isLayoutChangedPayload(value) {
|
|
|
3808
3934
|
|
|
3809
3935
|
//#endregion
|
|
3810
3936
|
//#region src/managers/CallEventsManager.ts
|
|
3811
|
-
const logger$
|
|
3937
|
+
const logger$23 = require_operators.getLogger();
|
|
3812
3938
|
const initialSessionState = {};
|
|
3813
3939
|
/** @internal */
|
|
3814
3940
|
var CallEventsManager = class extends Destroyable {
|
|
@@ -3912,7 +4038,7 @@ var CallEventsManager = class extends Destroyable {
|
|
|
3912
4038
|
}
|
|
3913
4039
|
initSubscriptions() {
|
|
3914
4040
|
this.subscribeTo(this.callJoinedEvent$, (callJoinedEvent) => {
|
|
3915
|
-
logger$
|
|
4041
|
+
logger$23.debug("[CallEventsManager] Handling call.joined event for call/session IDs:", {
|
|
3916
4042
|
callId: callJoinedEvent.call_id,
|
|
3917
4043
|
roomSessionId: callJoinedEvent.room_session_id
|
|
3918
4044
|
});
|
|
@@ -3939,19 +4065,19 @@ var CallEventsManager = class extends Destroyable {
|
|
|
3939
4065
|
if (this._self$.value?.capabilities.setLayout) this.updateLayouts();
|
|
3940
4066
|
});
|
|
3941
4067
|
this.subscribeTo(this.memberUpdates$, (member) => {
|
|
3942
|
-
logger$
|
|
4068
|
+
logger$23.debug("[CallEventsManager] Handling member update event for member ID:", member);
|
|
3943
4069
|
this.upsertParticipant(member);
|
|
3944
4070
|
});
|
|
3945
4071
|
this.subscribeTo(this.webRtcCallSession.memberLeft$, (memberLeftEvent) => {
|
|
3946
|
-
logger$
|
|
4072
|
+
logger$23.debug("[CallEventsManager] Handling member.left event for member ID:", memberLeftEvent.member.member_id);
|
|
3947
4073
|
const participants = { ...this._participants$.value };
|
|
3948
4074
|
if (memberLeftEvent.member.member_id in participants) {
|
|
3949
4075
|
delete participants[memberLeftEvent.member.member_id];
|
|
3950
4076
|
this._participants$.next(participants);
|
|
3951
|
-
} else logger$
|
|
4077
|
+
} else logger$23.warn(`[CallEventsManager] Received member.left event for unknown member ID: ${memberLeftEvent.member.member_id}`);
|
|
3952
4078
|
});
|
|
3953
4079
|
this.subscribeTo(this.webRtcCallSession.callUpdated$, (callUpdatedEvent) => {
|
|
3954
|
-
logger$
|
|
4080
|
+
logger$23.debug("[CallEventsManager] Handling call.updated event:", callUpdatedEvent);
|
|
3955
4081
|
const roomSession = callUpdatedEvent.room_session;
|
|
3956
4082
|
this._sessionState$.next({
|
|
3957
4083
|
...this._sessionState$.value,
|
|
@@ -3966,7 +4092,7 @@ var CallEventsManager = class extends Destroyable {
|
|
|
3966
4092
|
});
|
|
3967
4093
|
});
|
|
3968
4094
|
this.subscribeTo(this.layoutChangedEvent$, (layoutChangedEvent) => {
|
|
3969
|
-
logger$
|
|
4095
|
+
logger$23.debug("[CallEventsManager] Handling layout.changed event:", layoutChangedEvent);
|
|
3970
4096
|
this._sessionState$.next({
|
|
3971
4097
|
...this._sessionState$.value,
|
|
3972
4098
|
layout_name: layoutChangedEvent.id,
|
|
@@ -3976,10 +4102,10 @@ var CallEventsManager = class extends Destroyable {
|
|
|
3976
4102
|
});
|
|
3977
4103
|
}
|
|
3978
4104
|
updateParticipantPositions(layoutChangedEvent) {
|
|
3979
|
-
if (Object.keys(this._participants$.value).length > 0 && !layoutChangedEvent.layers.some((layer) => !!layer.member_id)) logger$
|
|
4105
|
+
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.");
|
|
3980
4106
|
layoutChangedEvent.layers.filter((layer) => !!layer.member_id).filter((layer) => {
|
|
3981
4107
|
if (!(layer.member_id in this._participants$.value)) {
|
|
3982
|
-
logger$
|
|
4108
|
+
logger$23.warn(`[CallEventsManager] Skipping layout layer for unknown member_id: ${layer.member_id}`);
|
|
3983
4109
|
return false;
|
|
3984
4110
|
}
|
|
3985
4111
|
return true;
|
|
@@ -3997,12 +4123,17 @@ var CallEventsManager = class extends Destroyable {
|
|
|
3997
4123
|
updateLayouts() {
|
|
3998
4124
|
if (!this.selfId) return;
|
|
3999
4125
|
this.webRtcCallSession.executeMethod(this.selfId, "call.layout.list", {}).then((response) => {
|
|
4126
|
+
const layouts = response.result?.layouts;
|
|
4127
|
+
if (!layouts) {
|
|
4128
|
+
logger$23.warn("[CallEventsManager] Layout list response carried no layouts; keeping current layouts");
|
|
4129
|
+
return;
|
|
4130
|
+
}
|
|
4000
4131
|
this._sessionState$.next({
|
|
4001
4132
|
...this._sessionState$.value,
|
|
4002
|
-
layouts
|
|
4133
|
+
layouts
|
|
4003
4134
|
});
|
|
4004
4135
|
}).catch((error) => {
|
|
4005
|
-
logger$
|
|
4136
|
+
logger$23.error("[CallEventsManager] Error fetching layouts:", error);
|
|
4006
4137
|
});
|
|
4007
4138
|
}
|
|
4008
4139
|
updateParticipants(members) {
|
|
@@ -4018,7 +4149,7 @@ var CallEventsManager = class extends Destroyable {
|
|
|
4018
4149
|
}
|
|
4019
4150
|
const participant = this._participants$.value[member.member_id];
|
|
4020
4151
|
const oldValue = participant.value;
|
|
4021
|
-
logger$
|
|
4152
|
+
logger$23.debug("[CallEventsManager] Updating participant:", member.member_id, {
|
|
4022
4153
|
oldValue,
|
|
4023
4154
|
newValue: member
|
|
4024
4155
|
});
|
|
@@ -4031,17 +4162,17 @@ var CallEventsManager = class extends Destroyable {
|
|
|
4031
4162
|
}
|
|
4032
4163
|
get callJoinedEvent$() {
|
|
4033
4164
|
return this.cachedObservable("callJoinedEvent$", () => this.webRtcCallSession.callEvent$.pipe((0, rxjs.filter)(isCallJoinedPayload), (0, rxjs.tap)((event) => {
|
|
4034
|
-
logger$
|
|
4165
|
+
logger$23.debug("[CallEventsManager] Call joined event:", event);
|
|
4035
4166
|
})));
|
|
4036
4167
|
}
|
|
4037
4168
|
get layoutChangedEvent$() {
|
|
4038
4169
|
return this.cachedObservable("layoutChangedEvent$", () => this.webRtcCallSession.callEvent$.pipe(require_operators.filterAs(isLayoutChangedPayload, "layout"), (0, rxjs.tap)((event) => {
|
|
4039
|
-
logger$
|
|
4170
|
+
logger$23.debug("[CallEventsManager] Layout changed event:", event);
|
|
4040
4171
|
})));
|
|
4041
4172
|
}
|
|
4042
4173
|
get memberUpdates$() {
|
|
4043
4174
|
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) => {
|
|
4044
|
-
logger$
|
|
4175
|
+
logger$23.debug("[CallEventsManager] Member update event:", event);
|
|
4045
4176
|
})));
|
|
4046
4177
|
}
|
|
4047
4178
|
destroy() {
|
|
@@ -4060,6 +4191,111 @@ var CallEventsManager = class extends Destroyable {
|
|
|
4060
4191
|
}
|
|
4061
4192
|
};
|
|
4062
4193
|
|
|
4194
|
+
//#endregion
|
|
4195
|
+
//#region src/controllers/ConstraintFallbackHelper.ts
|
|
4196
|
+
/**
|
|
4197
|
+
* ConstraintFallbackHelper - Provides getUserMedia with automatic constraint
|
|
4198
|
+
* fallback on OverconstrainedError.
|
|
4199
|
+
*
|
|
4200
|
+
* When a specific device ID is requested, the helper tries progressively
|
|
4201
|
+
* looser constraints:
|
|
4202
|
+
* 1. `{ deviceId: { exact: deviceId } }` -- exact match
|
|
4203
|
+
* 2. `{ deviceId: deviceId }` -- preferred (browser may pick another)
|
|
4204
|
+
* 3. `{}` -- no constraint, browser default
|
|
4205
|
+
*
|
|
4206
|
+
* This prevents stale device IDs from blocking call setup.
|
|
4207
|
+
*
|
|
4208
|
+
* @see Section 5.8 and Section 11 of the Implementation Guide
|
|
4209
|
+
*/
|
|
4210
|
+
const logger$22 = require_operators.getLogger();
|
|
4211
|
+
/**
|
|
4212
|
+
* Attempts getUserMedia with progressively looser constraints.
|
|
4213
|
+
*
|
|
4214
|
+
* The function tries three levels of constraint specificity for the given
|
|
4215
|
+
* device kind. Each level is only attempted if the previous one fails with
|
|
4216
|
+
* an OverconstrainedError. Non-OverconstrainedError failures (e.g.,
|
|
4217
|
+
* NotAllowedError) are thrown immediately without fallback.
|
|
4218
|
+
*
|
|
4219
|
+
* @param mediaDevices - Anything exposing `getUserMedia` (a full
|
|
4220
|
+
* `WebRTCMediaDevices`, or a shim wrapping one)
|
|
4221
|
+
* @param constraints - The full MediaStreamConstraints to use as a base
|
|
4222
|
+
* @param kind - Which track kind to apply fallback to ('audio' | 'video')
|
|
4223
|
+
* @param deviceId - The device ID to try (if undefined, calls getUserMedia as-is)
|
|
4224
|
+
* @returns The stream and the fallback level that succeeded
|
|
4225
|
+
* @throws When all fallback levels fail, or when a non-OverconstrainedError occurs
|
|
4226
|
+
*/
|
|
4227
|
+
async function getUserMediaWithFallback(mediaDevices, constraints, kind, deviceId) {
|
|
4228
|
+
if (!deviceId) return {
|
|
4229
|
+
stream: await mediaDevices.getUserMedia(constraints),
|
|
4230
|
+
fallbackLevel: "default"
|
|
4231
|
+
};
|
|
4232
|
+
const baseConstraints = typeof constraints[kind] === "object" ? constraints[kind] : {};
|
|
4233
|
+
try {
|
|
4234
|
+
const exactConstraints = {
|
|
4235
|
+
...constraints,
|
|
4236
|
+
[kind]: {
|
|
4237
|
+
...baseConstraints,
|
|
4238
|
+
deviceId: { exact: deviceId }
|
|
4239
|
+
}
|
|
4240
|
+
};
|
|
4241
|
+
return {
|
|
4242
|
+
stream: await mediaDevices.getUserMedia(exactConstraints),
|
|
4243
|
+
fallbackLevel: "exact"
|
|
4244
|
+
};
|
|
4245
|
+
} catch (error) {
|
|
4246
|
+
if (!isOverconstrainedError(error)) throw error;
|
|
4247
|
+
logger$22.debug(`[ConstraintFallbackHelper] Exact constraint failed for ${kind}, trying preferred`, { deviceId });
|
|
4248
|
+
}
|
|
4249
|
+
try {
|
|
4250
|
+
const preferredConstraints = {
|
|
4251
|
+
...constraints,
|
|
4252
|
+
[kind]: {
|
|
4253
|
+
...baseConstraints,
|
|
4254
|
+
deviceId
|
|
4255
|
+
}
|
|
4256
|
+
};
|
|
4257
|
+
return {
|
|
4258
|
+
stream: await mediaDevices.getUserMedia(preferredConstraints),
|
|
4259
|
+
fallbackLevel: "preferred"
|
|
4260
|
+
};
|
|
4261
|
+
} catch (error) {
|
|
4262
|
+
if (!isOverconstrainedError(error)) throw error;
|
|
4263
|
+
logger$22.debug(`[ConstraintFallbackHelper] Preferred constraint failed for ${kind}, trying default`, { deviceId });
|
|
4264
|
+
}
|
|
4265
|
+
try {
|
|
4266
|
+
const defaultConstraints = {
|
|
4267
|
+
...constraints,
|
|
4268
|
+
[kind]: { ...baseConstraints }
|
|
4269
|
+
};
|
|
4270
|
+
if (typeof defaultConstraints[kind] === "object") {
|
|
4271
|
+
const { deviceId: _removed, ...rest } = defaultConstraints[kind];
|
|
4272
|
+
defaultConstraints[kind] = rest;
|
|
4273
|
+
}
|
|
4274
|
+
const stream = await mediaDevices.getUserMedia(defaultConstraints);
|
|
4275
|
+
logger$22.warn(`[ConstraintFallbackHelper] Fell back to browser default for ${kind}`, { requestedDeviceId: deviceId });
|
|
4276
|
+
return {
|
|
4277
|
+
stream,
|
|
4278
|
+
fallbackLevel: "default"
|
|
4279
|
+
};
|
|
4280
|
+
} catch (error) {
|
|
4281
|
+
logger$22.error(`[ConstraintFallbackHelper] All fallback levels exhausted for ${kind}`, {
|
|
4282
|
+
deviceId,
|
|
4283
|
+
error
|
|
4284
|
+
});
|
|
4285
|
+
throw error;
|
|
4286
|
+
}
|
|
4287
|
+
}
|
|
4288
|
+
/**
|
|
4289
|
+
* Checks whether an error is an OverconstrainedError.
|
|
4290
|
+
*
|
|
4291
|
+
* Browsers may throw either a native OverconstrainedError or a DOMException
|
|
4292
|
+
* with a specific name.
|
|
4293
|
+
*/
|
|
4294
|
+
function isOverconstrainedError(error) {
|
|
4295
|
+
if (error instanceof Error) return error.name === "OverconstrainedError" || error.name === "ConstraintNotSatisfiedError";
|
|
4296
|
+
return false;
|
|
4297
|
+
}
|
|
4298
|
+
|
|
4063
4299
|
//#endregion
|
|
4064
4300
|
//#region src/helpers/SDPHelper.ts
|
|
4065
4301
|
/**
|
|
@@ -4615,6 +4851,7 @@ var LocalStreamController = class extends Destroyable {
|
|
|
4615
4851
|
this._localAudioTracks$ = this.createBehaviorSubject([]);
|
|
4616
4852
|
this._localVideoTracks$ = this.createBehaviorSubject([]);
|
|
4617
4853
|
this._mediaTrackEnded$ = this.createSubject();
|
|
4854
|
+
this._trackOrigins = /* @__PURE__ */ new WeakMap();
|
|
4618
4855
|
}
|
|
4619
4856
|
get localStream$() {
|
|
4620
4857
|
return this._localStream$.asObservable().pipe((0, rxjs.takeUntil)(this.destroyed$));
|
|
@@ -4637,6 +4874,22 @@ var LocalStreamController = class extends Destroyable {
|
|
|
4637
4874
|
get localVideoTracks() {
|
|
4638
4875
|
return this._localVideoTracks$.value;
|
|
4639
4876
|
}
|
|
4877
|
+
tagTracks(tracks, origin) {
|
|
4878
|
+
for (const track of tracks) this._trackOrigins.set(track, origin);
|
|
4879
|
+
}
|
|
4880
|
+
setTrackOrigin(track, origin) {
|
|
4881
|
+
this._trackOrigins.set(track, origin);
|
|
4882
|
+
}
|
|
4883
|
+
getTrackOrigin(track) {
|
|
4884
|
+
return this._trackOrigins.get(track);
|
|
4885
|
+
}
|
|
4886
|
+
/**
|
|
4887
|
+
* Fail-safe: an unrecorded track reads as not-a-device-capture, so a missed
|
|
4888
|
+
* tagging site leaves media alone rather than destroying it.
|
|
4889
|
+
*/
|
|
4890
|
+
isDeviceCapture(track) {
|
|
4891
|
+
return this._trackOrigins.get(track) === "device";
|
|
4892
|
+
}
|
|
4640
4893
|
/**
|
|
4641
4894
|
* Build the local media stream based on the provided options.
|
|
4642
4895
|
*/
|
|
@@ -4646,13 +4899,16 @@ var LocalStreamController = class extends Destroyable {
|
|
|
4646
4899
|
if (this.options.inputAudioStream ?? this.options.inputVideoStream) {
|
|
4647
4900
|
const tracks = [...this.options.inputAudioStream?.getTracks() ?? [], ...this.options.inputVideoStream?.getTracks() ?? []];
|
|
4648
4901
|
stream = new MediaStream(tracks);
|
|
4902
|
+
this.tagTracks(tracks, "application");
|
|
4649
4903
|
} else if (this.options.propose === "screenshare") {
|
|
4650
|
-
|
|
4904
|
+
const audio = this.options.screenShareAudio ?? false;
|
|
4905
|
+
logger$19.debug("[LocalStreamController] Requesting display media for screen sharing with audio:", audio);
|
|
4651
4906
|
stream = await this.options.getDisplayMedia({
|
|
4652
4907
|
video: true,
|
|
4653
|
-
audio
|
|
4908
|
+
audio
|
|
4654
4909
|
});
|
|
4655
4910
|
logger$19.debug("[LocalStreamController] Screen share media obtained:", stream);
|
|
4911
|
+
this.tagTracks(stream.getTracks(), "display");
|
|
4656
4912
|
} else {
|
|
4657
4913
|
const constraints = {
|
|
4658
4914
|
audio: this.options.inputAudioDeviceConstraints,
|
|
@@ -4661,6 +4917,7 @@ var LocalStreamController = class extends Destroyable {
|
|
|
4661
4917
|
logger$19.debug("[LocalStreamController] Requesting user media with constraints:", constraints);
|
|
4662
4918
|
stream = await this.options.getUserMedia(constraints);
|
|
4663
4919
|
logger$19.debug("[LocalStreamController] User media obtained:", stream);
|
|
4920
|
+
this.tagTracks(stream.getTracks(), "device");
|
|
4664
4921
|
}
|
|
4665
4922
|
this._localStream$.next(stream);
|
|
4666
4923
|
this._localAudioTracks$.next(stream.getAudioTracks());
|
|
@@ -4670,10 +4927,13 @@ var LocalStreamController = class extends Destroyable {
|
|
|
4670
4927
|
/**
|
|
4671
4928
|
* Add a local media track to the local stream.
|
|
4672
4929
|
* @param track - The MediaStreamTrack to add
|
|
4930
|
+
* @param origin - Defaults to `'device'`; every internal caller passes a
|
|
4931
|
+
* fresh `getUserMedia` capture.
|
|
4673
4932
|
* @returns The MediaStream (either existing or newly created)
|
|
4674
4933
|
*/
|
|
4675
|
-
addTrack(track) {
|
|
4934
|
+
addTrack(track, origin = "device") {
|
|
4676
4935
|
const localStream = this._localStream$.value ?? new MediaStream();
|
|
4936
|
+
this._trackOrigins.set(track, origin);
|
|
4677
4937
|
track.addEventListener("ended", this.mediaTrackEndedHandler);
|
|
4678
4938
|
localStream.addTrack(track);
|
|
4679
4939
|
this._localStream$.next(localStream);
|
|
@@ -4943,15 +5203,27 @@ var TransceiverController = class extends Destroyable {
|
|
|
4943
5203
|
for (let i = 0; i < Number(msStreamsNumber); i++) this.peerConnection.addTransceiver("video", { direction: "recvonly" });
|
|
4944
5204
|
}
|
|
4945
5205
|
}
|
|
5206
|
+
/**
|
|
5207
|
+
* @returns whether every live sender of the kind took the constraints. A
|
|
5208
|
+
* skipped non-device sender, an exhausted fallback, and having no live sender
|
|
5209
|
+
* at all all report `false` — `mediaParamsUpdated.applied` is built from this,
|
|
5210
|
+
* and an application told `true` cannot tell a working push from a no-op.
|
|
5211
|
+
*/
|
|
4946
5212
|
async updateSendersConstraints(kind, constraints) {
|
|
4947
5213
|
if (!constraints) {
|
|
4948
5214
|
this.stopTrackSender(kind);
|
|
4949
|
-
return
|
|
5215
|
+
return false;
|
|
4950
5216
|
}
|
|
4951
5217
|
const senders = this.peerConnection.getSenders().filter((sender) => sender.track?.kind === kind && sender.track.readyState === "live");
|
|
5218
|
+
let applied = senders.length > 0;
|
|
4952
5219
|
for (const sender of senders) {
|
|
4953
5220
|
const { track } = sender;
|
|
4954
5221
|
if (track) {
|
|
5222
|
+
if (!this.options.localStreamController.isDeviceCapture(track)) {
|
|
5223
|
+
logger$18.debug(`[TransceiverController] Skipping ${kind} constraints for a non-device track (origin: ${this.options.localStreamController.getTrackOrigin(track) ?? "unrecorded"}), track ${track.id}`);
|
|
5224
|
+
applied = false;
|
|
5225
|
+
continue;
|
|
5226
|
+
}
|
|
4955
5227
|
const constraintsToApply = {
|
|
4956
5228
|
...track.getConstraints(),
|
|
4957
5229
|
...constraints
|
|
@@ -4967,33 +5239,39 @@ var TransceiverController = class extends Destroyable {
|
|
|
4967
5239
|
} catch (fallbackError) {
|
|
4968
5240
|
logger$18.warn(`[TransceiverController] Track replacement fallback also failed for ${kind} track:`, fallbackError);
|
|
4969
5241
|
this.options.onError?.(new require_operators.MediaTrackError("updateSendersConstraints", kind, fallbackError));
|
|
5242
|
+
applied = false;
|
|
4970
5243
|
}
|
|
4971
5244
|
}
|
|
4972
5245
|
}
|
|
4973
5246
|
}
|
|
5247
|
+
return applied;
|
|
4974
5248
|
}
|
|
4975
5249
|
/**
|
|
4976
|
-
* Fallback when applyConstraints fails
|
|
4977
|
-
* one via getUserMedia with the merged constraints (preserving the current
|
|
4978
|
-
* deviceId), replace the sender track, and update the localStream.
|
|
5250
|
+
* Fallback when applyConstraints fails, which on iOS Safari it silently does.
|
|
4979
5251
|
*
|
|
4980
|
-
*
|
|
4981
|
-
*
|
|
5252
|
+
* Order matters: acquiring before stopping means a failed acquisition leaves
|
|
5253
|
+
* the existing media playing. The deviceId goes through the fallback ladder
|
|
5254
|
+
* rather than pinned `{ exact }`, so a stale id degrades instead of failing.
|
|
4982
5255
|
*/
|
|
4983
5256
|
async replaceTrackFallback(sender, oldTrack, kind, mergedConstraints) {
|
|
4984
5257
|
const { deviceId } = oldTrack.getSettings();
|
|
4985
|
-
const
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
|
|
5258
|
+
const { stream, fallbackLevel } = await getUserMediaWithFallback({ getUserMedia: this.options.getUserMedia }, { [kind]: mergedConstraints }, kind, deviceId);
|
|
5259
|
+
const newTrack = stream.getTracks().find((t) => t.kind === kind);
|
|
5260
|
+
if (!newTrack) {
|
|
5261
|
+
stream.getTracks().forEach((t) => t.stop());
|
|
5262
|
+
throw new require_operators.MediaTrackError("replaceTrackFallback", kind, /* @__PURE__ */ new Error("getUserMedia returned no track of the requested kind"));
|
|
5263
|
+
}
|
|
5264
|
+
try {
|
|
5265
|
+
await sender.replaceTrack(newTrack);
|
|
5266
|
+
} catch (error) {
|
|
5267
|
+
stream.getTracks().forEach((t) => t.stop());
|
|
5268
|
+
throw error;
|
|
5269
|
+
}
|
|
5270
|
+
const oldTrackId = oldTrack.id;
|
|
5271
|
+
this.options.localStreamController.removeTrack(oldTrackId);
|
|
4990
5272
|
oldTrack.stop();
|
|
4991
|
-
this.options.localStreamController.removeTrack(trackId);
|
|
4992
|
-
const newTrack = (await this.options.getUserMedia({ [kind]: constraintsWithDevice })).getTracks().find((t) => t.kind === kind);
|
|
4993
|
-
if (!newTrack) throw new require_operators.MediaTrackError("replaceTrackFallback", kind, /* @__PURE__ */ new Error("getUserMedia returned no track of the requested kind"));
|
|
4994
|
-
await sender.replaceTrack(newTrack);
|
|
4995
5273
|
this.options.localStreamController.addTrack(newTrack);
|
|
4996
|
-
logger$18.debug(`[TransceiverController] Track replacement fallback succeeded for ${kind}. New track: ${newTrack.id}`);
|
|
5274
|
+
logger$18.debug(`[TransceiverController] Track replacement fallback succeeded for ${kind} (deviceId fallback level: ${fallbackLevel}). New track: ${newTrack.id}`);
|
|
4997
5275
|
}
|
|
4998
5276
|
getMediaDirections() {
|
|
4999
5277
|
if (this.peerConnection.connectionState === "connected") return this.peerConnection.getTransceivers().reduce((acc, transceiver) => {
|
|
@@ -5062,48 +5340,44 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5062
5340
|
this.negotiationNeeded$.next();
|
|
5063
5341
|
};
|
|
5064
5342
|
this.updateSelectedInputDevice = async (kind, deviceInfo) => {
|
|
5343
|
+
const { localStream } = this;
|
|
5344
|
+
if (!localStream) {
|
|
5345
|
+
logger$17.warn("[RTCPeerConnectionController] No local stream available to update input device.");
|
|
5346
|
+
return;
|
|
5347
|
+
}
|
|
5348
|
+
const currentTrack = localStream.getTracks().find((track) => track.kind === kind);
|
|
5349
|
+
if (!currentTrack) {
|
|
5350
|
+
logger$17.debug(`[RTCPeerConnectionController] No ${kind} track to switch.`);
|
|
5351
|
+
return;
|
|
5352
|
+
}
|
|
5353
|
+
if (!deviceInfo) {
|
|
5354
|
+
logger$17.debug(`[RTCPeerConnectionController] ${kind} input device selected: none`);
|
|
5355
|
+
this.stopTrackSender(kind);
|
|
5356
|
+
return;
|
|
5357
|
+
}
|
|
5358
|
+
const constraints = {
|
|
5359
|
+
...currentTrack.getConstraints(),
|
|
5360
|
+
...this.deviceController.deviceInfoToConstraints(deviceInfo)
|
|
5361
|
+
};
|
|
5065
5362
|
try {
|
|
5066
|
-
const
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
return;
|
|
5070
|
-
}
|
|
5071
|
-
logger$17.debug(`[RTCPeerConnectionController] Updating selected ${kind} input device:`, localStream.getTracks());
|
|
5072
|
-
const track = localStream.getTracks().find((track$1) => track$1.kind === kind);
|
|
5073
|
-
if (track) {
|
|
5074
|
-
this.transceiverController?.stopTrackSender(kind);
|
|
5075
|
-
this.localStreamController.removeTrack(track.id);
|
|
5076
|
-
logger$17.debug(`[RTCPeerConnectionController] Stopped existing ${kind} track: ${track.id}`, localStream.getTracks());
|
|
5077
|
-
if (!deviceInfo) {
|
|
5078
|
-
logger$17.debug(`[RTCPeerConnectionController] ${kind} input device selected: none`);
|
|
5079
|
-
return;
|
|
5080
|
-
}
|
|
5081
|
-
const streamTrack = (await this.getUserMedia({ [kind]: {
|
|
5082
|
-
...track.getConstraints(),
|
|
5083
|
-
...this.deviceController.deviceInfoToConstraints(deviceInfo)
|
|
5084
|
-
} })).getTracks().find((t) => t.kind === kind);
|
|
5085
|
-
if (streamTrack) {
|
|
5086
|
-
logger$17.debug(`[RTCPeerConnectionController] Adding new ${kind} track: ${streamTrack.id}`);
|
|
5087
|
-
this.localStreamController.addTrack(streamTrack);
|
|
5088
|
-
await this.transceiverController?.replaceSenderTrack(kind, streamTrack);
|
|
5089
|
-
logger$17.debug(`[RTCPeerConnectionController] Added new ${kind} track: ${streamTrack.id}`, this.localStream?.getTracks());
|
|
5090
|
-
}
|
|
5091
|
-
}
|
|
5092
|
-
logger$17.debug(`[RTCPeerConnectionController] ${kind} input device selected:`, deviceInfo?.label);
|
|
5363
|
+
const newTrack = await this.acquireInputTrack(kind, constraints, deviceInfo, currentTrack);
|
|
5364
|
+
await this.attachInputTrack(kind, newTrack, currentTrack);
|
|
5365
|
+
logger$17.debug(`[RTCPeerConnectionController] ${kind} input device selected:`, deviceInfo.label, newTrack.id);
|
|
5093
5366
|
} catch (error) {
|
|
5094
5367
|
logger$17.error(`[RTCPeerConnectionController] Failed to select ${kind} input device:`, error);
|
|
5095
5368
|
this._errors$.next(new require_operators.MediaTrackError("updateSelectedInputDevice", kind, error));
|
|
5096
|
-
throw error;
|
|
5097
5369
|
}
|
|
5098
5370
|
};
|
|
5099
5371
|
this._isNegotiating$ = this.createBehaviorSubject(false);
|
|
5100
5372
|
this._memberId = null;
|
|
5373
|
+
this._nodeId = null;
|
|
5101
5374
|
this._iceConnectionState$ = this.createReplaySubject(1);
|
|
5102
5375
|
this._connectionState$ = this.createReplaySubject(1);
|
|
5103
5376
|
this._signalingState$ = this.createReplaySubject(1);
|
|
5104
5377
|
this._iceGatheringState$ = this.createReplaySubject(1);
|
|
5105
5378
|
this._errors$ = this.createReplaySubject(1);
|
|
5106
5379
|
this._iceCandidates$ = this.createReplaySubject(1);
|
|
5380
|
+
this._localMediaSettled$ = this.createReplaySubject(1);
|
|
5107
5381
|
this._initialized$ = this.createReplaySubject(1);
|
|
5108
5382
|
this._remoteDescription$ = this.createReplaySubject(1);
|
|
5109
5383
|
this._remoteStream$ = this.createBehaviorSubject(null);
|
|
@@ -5136,6 +5410,7 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5136
5410
|
inputVideoStream: this.options.inputVideoStream,
|
|
5137
5411
|
inputAudioDeviceConstraints: this.inputAudioDeviceConstraints,
|
|
5138
5412
|
inputVideoDeviceConstraints: this.inputVideoDeviceConstraints,
|
|
5413
|
+
screenShareAudio: this.options.screenShareAudio,
|
|
5139
5414
|
getUserMedia: async (constraints) => this.getUserMedia(constraints),
|
|
5140
5415
|
getDisplayMedia: async (options$1) => this.getDisplayMedia(options$1)
|
|
5141
5416
|
});
|
|
@@ -5162,6 +5437,13 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5162
5437
|
get memberId() {
|
|
5163
5438
|
return this._memberId;
|
|
5164
5439
|
}
|
|
5440
|
+
/** The node this leg's invite landed on — auxiliary legs are placed independently. */
|
|
5441
|
+
setNodeId(nodeId) {
|
|
5442
|
+
this._nodeId = nodeId;
|
|
5443
|
+
}
|
|
5444
|
+
get nodeId() {
|
|
5445
|
+
return this._nodeId;
|
|
5446
|
+
}
|
|
5165
5447
|
stopTrackSender(kind, options = { updateTransceiverDirection: false }) {
|
|
5166
5448
|
const audioCovered = kind === "audio" || kind === "both";
|
|
5167
5449
|
if (audioCovered && this._localAudioPipeline) this.stopRawAudioInputForPipeline();
|
|
@@ -5171,10 +5453,7 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5171
5453
|
}
|
|
5172
5454
|
stopRawAudioInputForPipeline() {
|
|
5173
5455
|
const rawTracks = this.localStreamController.localAudioTracks;
|
|
5174
|
-
for (const track of rawTracks) if (track.readyState === "live")
|
|
5175
|
-
track.stop();
|
|
5176
|
-
this.localStreamController.removeTrack(track.id);
|
|
5177
|
-
}
|
|
5456
|
+
for (const track of rawTracks) if (track.readyState === "live") this.localStreamController.removeTrack(track.id);
|
|
5178
5457
|
this._localAudioPipeline?.setInputTrack(null);
|
|
5179
5458
|
}
|
|
5180
5459
|
get isNegotiating$() {
|
|
@@ -5207,6 +5486,10 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5207
5486
|
get remoteDescription$() {
|
|
5208
5487
|
return this.cachedObservable("remoteDescription$", () => this._remoteDescription$.asObservable().pipe((0, rxjs.takeUntil)(this.destroyed$)));
|
|
5209
5488
|
}
|
|
5489
|
+
/** Emits once local media is settled — acquired, or knowingly receive-only. */
|
|
5490
|
+
get localMediaSettled$() {
|
|
5491
|
+
return this.cachedObservable("localMediaSettled$", () => this._localMediaSettled$.asObservable().pipe((0, rxjs.takeUntil)(this.destroyed$)));
|
|
5492
|
+
}
|
|
5210
5493
|
get localStream$() {
|
|
5211
5494
|
return this.cachedObservable("localStream$", () => this.localStreamController.localStream$.pipe((0, rxjs.takeUntil)(this.destroyed$)));
|
|
5212
5495
|
}
|
|
@@ -5234,6 +5517,9 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5234
5517
|
get propose() {
|
|
5235
5518
|
return this.options.propose ?? "main";
|
|
5236
5519
|
}
|
|
5520
|
+
get connectionState() {
|
|
5521
|
+
return this.peerConnection?.connectionState;
|
|
5522
|
+
}
|
|
5237
5523
|
get isAdditionalDevice() {
|
|
5238
5524
|
return this.propose === "additional-device";
|
|
5239
5525
|
}
|
|
@@ -5362,7 +5648,7 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5362
5648
|
this._isNegotiating$.next(true);
|
|
5363
5649
|
await this._setRemoteDescription(this.sdpInit);
|
|
5364
5650
|
} else {
|
|
5365
|
-
await this.setupTrackHandling();
|
|
5651
|
+
if (!await this.setupTrackHandling()) return;
|
|
5366
5652
|
this._initialized$.next(true);
|
|
5367
5653
|
}
|
|
5368
5654
|
} catch (error) {
|
|
@@ -5486,7 +5772,10 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5486
5772
|
inputVideoDeviceConstraints: this.inputVideoDeviceConstraints
|
|
5487
5773
|
});
|
|
5488
5774
|
}
|
|
5489
|
-
await this.setupLocalTracks()
|
|
5775
|
+
if (!await this.setupLocalTracks()) {
|
|
5776
|
+
logger$17.debug("[RTCPeerConnectionController] Inbound answer abandoned; the connection went away.");
|
|
5777
|
+
return;
|
|
5778
|
+
}
|
|
5490
5779
|
const { answerOptions } = this;
|
|
5491
5780
|
logger$17.debug("[RTCPeerConnectionController] Creating inbound answer with options:", answerOptions);
|
|
5492
5781
|
await this.createAnswer(answerOptions);
|
|
@@ -5624,35 +5913,52 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5624
5913
|
}
|
|
5625
5914
|
/**
|
|
5626
5915
|
* Setup track handling for remote tracks.
|
|
5916
|
+
*
|
|
5917
|
+
* @returns `false` when the connection went away while local media was being
|
|
5918
|
+
* acquired — see {@link setupLocalTracks}.
|
|
5627
5919
|
*/
|
|
5628
5920
|
async setupTrackHandling() {
|
|
5629
5921
|
if (!this.peerConnection) throw new require_operators.DependencyError("RTCPeerConnection is not initialized");
|
|
5630
|
-
await this.setupLocalTracks();
|
|
5922
|
+
if (!await this.setupLocalTracks()) return false;
|
|
5631
5923
|
await this.setupRemoteTracks();
|
|
5924
|
+
return true;
|
|
5632
5925
|
}
|
|
5926
|
+
/**
|
|
5927
|
+
* @returns `false` when the connection was torn down while getUserMedia was
|
|
5928
|
+
* in flight. The acquisition is not cancellable, so the caller must stop
|
|
5929
|
+
* rather than go on to touch a peer connection that is closed or gone.
|
|
5930
|
+
*/
|
|
5633
5931
|
async setupLocalTracks() {
|
|
5634
5932
|
logger$17.debug("[RTCPeerConnectionController] Setting up local tracks/transceivers.");
|
|
5635
5933
|
if (this.hasNoLocalMediaToSend()) {
|
|
5636
5934
|
if (!this.receiveAudio && !this.receiveVideo) throw new require_operators.InvalidParams("Call requests no media: enable audio/video or receiveAudio/receiveVideo");
|
|
5637
5935
|
logger$17.debug("[RTCPeerConnectionController] No local media requested; negotiating receive-only.");
|
|
5638
5936
|
this.setupReceiveOnlyTransceivers();
|
|
5639
|
-
|
|
5937
|
+
this._localMediaSettled$.next();
|
|
5938
|
+
return true;
|
|
5640
5939
|
}
|
|
5641
5940
|
let localStream;
|
|
5642
5941
|
try {
|
|
5643
5942
|
localStream = this.localStream ?? await this.localStreamController.buildLocalStream();
|
|
5644
5943
|
} catch (error) {
|
|
5645
5944
|
this.handleLocalMediaFailure(error);
|
|
5646
|
-
|
|
5945
|
+
this._localMediaSettled$.next();
|
|
5946
|
+
return true;
|
|
5947
|
+
}
|
|
5948
|
+
if (!this.peerConnection || this.peerConnection.signalingState === "closed") {
|
|
5949
|
+
logger$17.debug("[RTCPeerConnectionController] Local media arrived after teardown; releasing it.");
|
|
5950
|
+
localStream.getTracks().forEach((track) => track.stop());
|
|
5951
|
+
return false;
|
|
5647
5952
|
}
|
|
5953
|
+
this._localMediaSettled$.next();
|
|
5648
5954
|
if (this.transceiverController?.useAddStream ?? false) {
|
|
5649
5955
|
logger$17.warn("[RTCPeerConnectionController] Using deprecated addStream API to add local stream.");
|
|
5650
|
-
this.peerConnection
|
|
5956
|
+
this.peerConnection.addStream(localStream);
|
|
5651
5957
|
if (!this.isNegotiating) {
|
|
5652
5958
|
logger$17.debug("[RTCPeerConnectionController] Forcing negotiationneeded after local tracks setup.");
|
|
5653
5959
|
this.negotiationNeeded$.next();
|
|
5654
5960
|
}
|
|
5655
|
-
return;
|
|
5961
|
+
return true;
|
|
5656
5962
|
}
|
|
5657
5963
|
for (const kind of ["audio", "video"]) {
|
|
5658
5964
|
const tracks = (kind === "audio" ? localStream.getAudioTracks() : localStream.getVideoTracks()).map((track, index) => ({
|
|
@@ -5666,10 +5972,11 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5666
5972
|
await this.transceiverController?.setupTransceiverSender(track, localStream, transceivers[index]);
|
|
5667
5973
|
} else {
|
|
5668
5974
|
logger$17.debug(`[RTCPeerConnectionController] Using addTrack for local ${kind} track:`, track.id);
|
|
5669
|
-
this.peerConnection
|
|
5975
|
+
this.peerConnection.addTrack(track, localStream);
|
|
5670
5976
|
}
|
|
5671
5977
|
}
|
|
5672
5978
|
}
|
|
5979
|
+
return true;
|
|
5673
5980
|
}
|
|
5674
5981
|
/** True for a main connection with no local media to send. */
|
|
5675
5982
|
hasNoLocalMediaToSend() {
|
|
@@ -5758,6 +6065,61 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5758
6065
|
this._localAudioPipeline.setInputTrack(newTrack);
|
|
5759
6066
|
}
|
|
5760
6067
|
/**
|
|
6068
|
+
* Capture the newly selected device, leaving the current capture running.
|
|
6069
|
+
*
|
|
6070
|
+
* A rejection must leave the current track sending, so nothing is released
|
|
6071
|
+
* until the replacement is in hand. The one exception is hardware that admits
|
|
6072
|
+
* a single opener — a phone's front and back cameras, typically — which
|
|
6073
|
+
* rejects the second capture until the first is closed.
|
|
6074
|
+
*/
|
|
6075
|
+
async acquireInputTrack(kind, constraints, deviceInfo, currentTrack) {
|
|
6076
|
+
try {
|
|
6077
|
+
return await this.captureTrack(kind, constraints, deviceInfo.deviceId);
|
|
6078
|
+
} catch (error) {
|
|
6079
|
+
if (!require_operators.isMediaDeviceInUse(error)) throw error;
|
|
6080
|
+
logger$17.warn(`[RTCPeerConnectionController] ${kind} device is held exclusively; releasing the current capture to retry:`, error);
|
|
6081
|
+
const previousDeviceId = currentTrack.getSettings().deviceId;
|
|
6082
|
+
this.stopTrackSender(kind);
|
|
6083
|
+
try {
|
|
6084
|
+
return await this.captureTrack(kind, constraints, deviceInfo.deviceId);
|
|
6085
|
+
} catch (retryError) {
|
|
6086
|
+
await this.restorePreviousInputTrack(kind, constraints, previousDeviceId, currentTrack);
|
|
6087
|
+
throw retryError;
|
|
6088
|
+
}
|
|
6089
|
+
}
|
|
6090
|
+
}
|
|
6091
|
+
async captureTrack(kind, constraints, deviceId) {
|
|
6092
|
+
const { stream, fallbackLevel } = await getUserMediaWithFallback({ getUserMedia: async (c) => this.getUserMedia(c) }, { [kind]: constraints }, kind, deviceId);
|
|
6093
|
+
const track = stream.getTracks().find((t) => t.kind === kind);
|
|
6094
|
+
if (!track) {
|
|
6095
|
+
stream.getTracks().forEach((t) => t.stop());
|
|
6096
|
+
throw new require_operators.MediaTrackError("updateSelectedInputDevice", kind, /* @__PURE__ */ new Error("getUserMedia returned no track of the requested kind"));
|
|
6097
|
+
}
|
|
6098
|
+
if (fallbackLevel !== "exact") logger$17.warn(`[RTCPeerConnectionController] ${kind} device acquired at fallback level '${fallbackLevel}'; the capture may not be the requested device.`);
|
|
6099
|
+
return track;
|
|
6100
|
+
}
|
|
6101
|
+
/** Best-effort return to the device that was released for an exclusive retry. */
|
|
6102
|
+
async restorePreviousInputTrack(kind, constraints, previousDeviceId, releasedTrack) {
|
|
6103
|
+
try {
|
|
6104
|
+
const restored = await this.captureTrack(kind, constraints, previousDeviceId);
|
|
6105
|
+
await this.attachInputTrack(kind, restored, releasedTrack);
|
|
6106
|
+
} catch (error) {
|
|
6107
|
+
logger$17.error(`[RTCPeerConnectionController] Failed to restore the previous ${kind} device:`, error);
|
|
6108
|
+
}
|
|
6109
|
+
}
|
|
6110
|
+
async attachInputTrack(kind, newTrack, oldTrack) {
|
|
6111
|
+
const pipelineOwnsAudio = kind === "audio" && this._localAudioPipeline;
|
|
6112
|
+
if (!pipelineOwnsAudio) try {
|
|
6113
|
+
await this.transceiverController?.replaceSenderTrack(kind, newTrack);
|
|
6114
|
+
} catch (error) {
|
|
6115
|
+
newTrack.stop();
|
|
6116
|
+
throw error;
|
|
6117
|
+
}
|
|
6118
|
+
this.localStreamController.removeTrack(oldTrack.id);
|
|
6119
|
+
this.localStreamController.addTrack(newTrack);
|
|
6120
|
+
if (pipelineOwnsAudio) this._localAudioPipeline?.setInputTrack(newTrack);
|
|
6121
|
+
}
|
|
6122
|
+
/**
|
|
5761
6123
|
* Return the lazily-created {@link LocalAudioPipeline}, constructing it on
|
|
5762
6124
|
* first access. On creation the current audio sender's track is routed
|
|
5763
6125
|
* through the pipeline (input → gain → analyser → destination) and the
|
|
@@ -5790,6 +6152,7 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5790
6152
|
const sender = (this.transceiverController?.audioTransceivers.at(0))?.sender ?? this.peerConnection.getSenders().find((s) => s.track?.kind === "audio");
|
|
5791
6153
|
if (!sender || !raw) return;
|
|
5792
6154
|
try {
|
|
6155
|
+
this.localStreamController.setTrackOrigin(this._localAudioPipeline.outputTrack, "processed");
|
|
5793
6156
|
await sender.replaceTrack(this._localAudioPipeline.outputTrack);
|
|
5794
6157
|
} catch (error) {
|
|
5795
6158
|
logger$17.warn("[RTCPeerConnectionController] Failed to route audio sender through pipeline:", error);
|
|
@@ -5850,37 +6213,68 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5850
6213
|
for (const existingTrack of existingTracks) this.removeLocalTrack(existingTrack.id);
|
|
5851
6214
|
this.addLocalTrack(track);
|
|
5852
6215
|
}
|
|
6216
|
+
/**
|
|
6217
|
+
* @returns whether the constraints reached the media the leg is sending.
|
|
6218
|
+
*
|
|
6219
|
+
* With the pipeline engaged the audio sender carries the processed
|
|
6220
|
+
* destination track, so the sender scan would find nothing it may touch and
|
|
6221
|
+
* every audio constraint API would silently no-op. The constraints belong to
|
|
6222
|
+
* the pipeline's device source, which is the capture that sender ultimately
|
|
6223
|
+
* carries.
|
|
6224
|
+
*/
|
|
5853
6225
|
async updateSendersConstraints(kind, constraints) {
|
|
5854
|
-
|
|
6226
|
+
if (kind === "audio" && this._localAudioPipeline) {
|
|
6227
|
+
if (!constraints) {
|
|
6228
|
+
this.stopTrackSender("audio");
|
|
6229
|
+
return false;
|
|
6230
|
+
}
|
|
6231
|
+
return this.applyPipelineSourceConstraints(constraints);
|
|
6232
|
+
}
|
|
6233
|
+
return await this.transceiverController?.updateSendersConstraints(kind, constraints) ?? false;
|
|
5855
6234
|
}
|
|
5856
6235
|
/**
|
|
5857
|
-
*
|
|
5858
|
-
*
|
|
5859
|
-
*
|
|
5860
|
-
*
|
|
6236
|
+
* Mirror of the sender path for a piped audio leg: same merge, same fallback
|
|
6237
|
+
* ladder, same device-capture invariant — but the swap target is the pipeline
|
|
6238
|
+
* input, so the sender keeps emitting the pipeline's output track and its
|
|
6239
|
+
* identity survives the change.
|
|
5861
6240
|
*/
|
|
5862
|
-
async
|
|
5863
|
-
const
|
|
5864
|
-
|
|
5865
|
-
|
|
5866
|
-
|
|
6241
|
+
async applyPipelineSourceConstraints(constraints) {
|
|
6242
|
+
const pipeline = this._localAudioPipeline;
|
|
6243
|
+
const source = this.localStreamController.localAudioTracks.at(0);
|
|
6244
|
+
if (!pipeline || !source) {
|
|
6245
|
+
logger$17.debug("[RTCPeerConnectionController] No pipeline input to constrain.");
|
|
6246
|
+
return false;
|
|
5867
6247
|
}
|
|
5868
|
-
|
|
5869
|
-
|
|
5870
|
-
|
|
5871
|
-
|
|
5872
|
-
|
|
5873
|
-
|
|
5874
|
-
|
|
5875
|
-
|
|
5876
|
-
|
|
5877
|
-
|
|
5878
|
-
|
|
5879
|
-
|
|
5880
|
-
|
|
5881
|
-
|
|
6248
|
+
if (!this.localStreamController.isDeviceCapture(source)) {
|
|
6249
|
+
logger$17.debug(`[RTCPeerConnectionController] Skipping audio constraints for a non-device pipeline input (origin: ${this.localStreamController.getTrackOrigin(source) ?? "unrecorded"}).`);
|
|
6250
|
+
return false;
|
|
6251
|
+
}
|
|
6252
|
+
const merged = {
|
|
6253
|
+
...source.getConstraints(),
|
|
6254
|
+
...constraints
|
|
6255
|
+
};
|
|
6256
|
+
try {
|
|
6257
|
+
await source.applyConstraints(merged);
|
|
6258
|
+
logger$17.debug("[RTCPeerConnectionController] Pipeline input constraints updated:", merged);
|
|
6259
|
+
return true;
|
|
6260
|
+
} catch (error) {
|
|
6261
|
+
logger$17.warn("[RTCPeerConnectionController] applyConstraints failed on the pipeline input, re-acquiring:", error);
|
|
6262
|
+
}
|
|
6263
|
+
try {
|
|
6264
|
+
const { stream } = await getUserMediaWithFallback({ getUserMedia: async (c) => this.getUserMedia(c) }, { audio: merged }, "audio", source.getSettings().deviceId);
|
|
6265
|
+
const newTrack = stream.getAudioTracks().at(0);
|
|
6266
|
+
if (!newTrack) {
|
|
6267
|
+
stream.getTracks().forEach((track) => track.stop());
|
|
6268
|
+
throw new Error("getUserMedia returned no audio track");
|
|
6269
|
+
}
|
|
6270
|
+
this.localStreamController.removeTrack(source.id);
|
|
5882
6271
|
this.localStreamController.addTrack(newTrack);
|
|
5883
|
-
|
|
6272
|
+
pipeline.setInputTrack(newTrack);
|
|
6273
|
+
return true;
|
|
6274
|
+
} catch (error) {
|
|
6275
|
+
logger$17.warn("[RTCPeerConnectionController] Failed to re-acquire the pipeline input for constraints:", error);
|
|
6276
|
+
this._errors$.next(new require_operators.MediaTrackError("updateSendersConstraints", "audio", error));
|
|
6277
|
+
return false;
|
|
5884
6278
|
}
|
|
5885
6279
|
}
|
|
5886
6280
|
/**
|
|
@@ -5999,6 +6393,48 @@ const logger$16 = require_operators.getLogger();
|
|
|
5999
6393
|
function resolveInviteNodeId(args) {
|
|
6000
6394
|
return args.isInvite && !args.reattach && !args.explicitNodeId ? "" : args.currentNodeId ?? "";
|
|
6001
6395
|
}
|
|
6396
|
+
/**
|
|
6397
|
+
* Surface the real outcome of a `webrtc.verto` reply.
|
|
6398
|
+
*
|
|
6399
|
+
* A webrtc.verto response nests several envelopes, each keyed by a verto-style
|
|
6400
|
+
* string `code` ("200" ok, "400"/etc. fail) rather than a JSON-RPC `error`. An outer
|
|
6401
|
+
* layer reports only whether the frame was delivered; an inner layer carries the op's
|
|
6402
|
+
* own outcome:
|
|
6403
|
+
*
|
|
6404
|
+
* response.result = { code:"200", result:{…} } ← delivery acknowledgement
|
|
6405
|
+
* .result = { jsonrpc, id, result:{…} } ← the reply payload
|
|
6406
|
+
* .result = { code:"400", message:"Bad request" } ← the actual op outcome
|
|
6407
|
+
*
|
|
6408
|
+
* A failure can appear at any layer (delivery refused, or the op itself rejected
|
|
6409
|
+
* deeper down), so walk every nested `.result` object and return the FIRST non-2xx
|
|
6410
|
+
* `code` with its message. Returns null when every `code` seen is 2xx or absent —
|
|
6411
|
+
* i.e. the op succeeded. This is the only way to detect that e.g. a mute/kick was
|
|
6412
|
+
* rejected, since the outer delivery `code` is "200" (delivered) even then.
|
|
6413
|
+
*
|
|
6414
|
+
* Pure function — exported for unit testing.
|
|
6415
|
+
*/
|
|
6416
|
+
function findNestedVertoFailure(response) {
|
|
6417
|
+
let node = response;
|
|
6418
|
+
while (node !== null && typeof node === "object") {
|
|
6419
|
+
const obj = node;
|
|
6420
|
+
const err = obj.error;
|
|
6421
|
+
if (err !== null && typeof err === "object") {
|
|
6422
|
+
const e = err;
|
|
6423
|
+
const errCode = typeof e.code === "string" || typeof e.code === "number" ? String(e.code) : void 0;
|
|
6424
|
+
if (errCode !== void 0 && !/^2\d\d$/.test(errCode)) return {
|
|
6425
|
+
code: errCode,
|
|
6426
|
+
message: typeof e.message === "string" ? e.message : void 0
|
|
6427
|
+
};
|
|
6428
|
+
}
|
|
6429
|
+
const code = typeof obj.code === "string" || typeof obj.code === "number" ? String(obj.code) : void 0;
|
|
6430
|
+
if (code !== void 0 && !/^2\d\d$/.test(code)) return {
|
|
6431
|
+
code,
|
|
6432
|
+
message: typeof obj.message === "string" ? obj.message : void 0
|
|
6433
|
+
};
|
|
6434
|
+
node = obj.result !== null && typeof obj.result === "object" ? obj.result : null;
|
|
6435
|
+
}
|
|
6436
|
+
return null;
|
|
6437
|
+
}
|
|
6002
6438
|
var VertoManager = class extends Destroyable {
|
|
6003
6439
|
constructor(callSession) {
|
|
6004
6440
|
super();
|
|
@@ -6021,7 +6457,8 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6021
6457
|
this._signalingStatus$ = this.createReplaySubject(1);
|
|
6022
6458
|
this._screenShareStatus$ = this.createBehaviorSubject("none");
|
|
6023
6459
|
this._rtcPeerConnectionsMap = /* @__PURE__ */ new Map();
|
|
6024
|
-
this.
|
|
6460
|
+
this._lastVertoPings = /* @__PURE__ */ new Map();
|
|
6461
|
+
this._legErrors$ = this.createSubject();
|
|
6025
6462
|
this._nodeId$ = this.createBehaviorSubject(options.nodeId ?? null);
|
|
6026
6463
|
this.onError = options.onError;
|
|
6027
6464
|
this.onModifyFailed = options.onModifyFailed;
|
|
@@ -6069,6 +6506,10 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6069
6506
|
get selfId$() {
|
|
6070
6507
|
return this._selfId$.asObservable();
|
|
6071
6508
|
}
|
|
6509
|
+
/** Separates the media phase of call creation from the signalling phase. */
|
|
6510
|
+
get localMediaSettled$() {
|
|
6511
|
+
return this.mainPeerConnection.localMediaSettled$;
|
|
6512
|
+
}
|
|
6072
6513
|
get localStream() {
|
|
6073
6514
|
return this._rtcPeerConnectionsMap.get(this.webRtcCallSession.id)?.localStream ?? null;
|
|
6074
6515
|
}
|
|
@@ -6125,39 +6566,103 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6125
6566
|
const { mediaParams, callID } = event;
|
|
6126
6567
|
const rtcPeerConnController = this._rtcPeerConnectionsMap.get(callID);
|
|
6127
6568
|
const { audio, video } = mediaParams;
|
|
6128
|
-
|
|
6129
|
-
|
|
6130
|
-
|
|
6131
|
-
|
|
6132
|
-
|
|
6133
|
-
audio,
|
|
6134
|
-
video,
|
|
6135
|
-
timestamp: Date.now()
|
|
6136
|
-
});
|
|
6137
|
-
} catch (error) {
|
|
6138
|
-
logger$16.warn("[WebRTCManager] Error applying server-pushed media params:", error);
|
|
6139
|
-
this.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
6140
|
-
}
|
|
6141
|
-
})();
|
|
6569
|
+
if (!rtcPeerConnController) {
|
|
6570
|
+
logger$16.warn(`[WebRTCManager] Ignoring server-pushed media params for unknown leg ${callID}`);
|
|
6571
|
+
return;
|
|
6572
|
+
}
|
|
6573
|
+
this.applyServerMediaParams(rtcPeerConnController, audio, video);
|
|
6142
6574
|
});
|
|
6143
6575
|
this.subscribeTo(this.vertoPing$, (vertoPing) => {
|
|
6144
|
-
this.
|
|
6576
|
+
this._lastVertoPings.set(vertoPing.callID, vertoPing);
|
|
6577
|
+
this.attachManager.refresh(this.buildAttachableCall());
|
|
6145
6578
|
this.sendVertoPong(vertoPing);
|
|
6146
6579
|
});
|
|
6580
|
+
this.subscribeTo(this.webRtcCallSession.clientSession.authenticated$.pipe((0, rxjs.skip)(1), (0, rxjs.filter)(Boolean)), () => {
|
|
6581
|
+
this._lastVertoPings.forEach((vertoPing) => void this.sendVertoPong(vertoPing));
|
|
6582
|
+
});
|
|
6147
6583
|
}
|
|
6148
6584
|
/**
|
|
6149
|
-
*
|
|
6585
|
+
* An auxiliary-leg failure must never destroy the call; main-leg and
|
|
6586
|
+
* call-level errors keep `CallFactory.isFatalError`'s classification.
|
|
6150
6587
|
*
|
|
6151
|
-
*
|
|
6152
|
-
*
|
|
6153
|
-
*
|
|
6154
|
-
*
|
|
6155
|
-
*
|
|
6156
|
-
*
|
|
6588
|
+
* Every site holding a peer connection reports through here, so the invariant
|
|
6589
|
+
* is structural rather than per-call-site — which is how cloud-product#20523
|
|
6590
|
+
* happened, with only one of fourteen sites passing `{ fatal: false }`.
|
|
6591
|
+
*
|
|
6592
|
+
* `override` composes rather than replaces: a caller may force non-fatal for a
|
|
6593
|
+
* reason of its own (a `verto.info` frame is best-effort whichever leg carries
|
|
6594
|
+
* it), and an auxiliary leg stays non-fatal regardless.
|
|
6157
6595
|
*/
|
|
6158
|
-
|
|
6159
|
-
|
|
6160
|
-
|
|
6596
|
+
reportLegError(error, rtcPeerConnController, override) {
|
|
6597
|
+
const leg = rtcPeerConnController?.propose;
|
|
6598
|
+
const legId = rtcPeerConnController?.id;
|
|
6599
|
+
const auxiliary = Boolean(rtcPeerConnController) && !rtcPeerConnController?.isMainDevice;
|
|
6600
|
+
this.onError?.(error, {
|
|
6601
|
+
...override?.fatal === false || auxiliary ? { fatal: false } : {},
|
|
6602
|
+
...leg ? { leg } : {},
|
|
6603
|
+
...legId ? { legId } : {}
|
|
6604
|
+
});
|
|
6605
|
+
if (legId) this._legErrors$.next({
|
|
6606
|
+
legId,
|
|
6607
|
+
error
|
|
6608
|
+
});
|
|
6609
|
+
}
|
|
6610
|
+
/**
|
|
6611
|
+
* Errors reported for one leg, as a stream that fails with them.
|
|
6612
|
+
*
|
|
6613
|
+
* Signaling failures are reported, never thrown — so nothing that waits on a
|
|
6614
|
+
* leg's progress would otherwise learn of a rejected invite. Merging this in
|
|
6615
|
+
* lets the wait end with the reason the server gave.
|
|
6616
|
+
*/
|
|
6617
|
+
legError$(legId) {
|
|
6618
|
+
return this._legErrors$.pipe((0, rxjs.filter)((report) => report.legId === legId), (0, rxjs.map)((report) => {
|
|
6619
|
+
throw report.error;
|
|
6620
|
+
}));
|
|
6621
|
+
}
|
|
6622
|
+
/**
|
|
6623
|
+
* Audio and video are applied independently so a failure in one cannot
|
|
6624
|
+
* suppress the other, and `mediaParamsUpdated` is emitted whatever happens —
|
|
6625
|
+
* an application should not be starved of the params by a constraint failure.
|
|
6626
|
+
*/
|
|
6627
|
+
async applyServerMediaParams(rtcPeerConnController, audio, video) {
|
|
6628
|
+
const failures = [];
|
|
6629
|
+
let applied = true;
|
|
6630
|
+
if (audio) try {
|
|
6631
|
+
applied = await rtcPeerConnController.updateSendersConstraints("audio", audio) && applied;
|
|
6632
|
+
} catch (error) {
|
|
6633
|
+
applied = false;
|
|
6634
|
+
failures.push(toError(error));
|
|
6635
|
+
}
|
|
6636
|
+
if (video) try {
|
|
6637
|
+
applied = await rtcPeerConnController.updateSendersConstraints("video", video) && applied;
|
|
6638
|
+
} catch (error) {
|
|
6639
|
+
applied = false;
|
|
6640
|
+
failures.push(toError(error));
|
|
6641
|
+
}
|
|
6642
|
+
this.webRtcCallSession.emitMediaParamsUpdated({
|
|
6643
|
+
audio,
|
|
6644
|
+
video,
|
|
6645
|
+
timestamp: Date.now(),
|
|
6646
|
+
applied
|
|
6647
|
+
});
|
|
6648
|
+
for (const failure of failures) {
|
|
6649
|
+
logger$16.warn("[WebRTCManager] Error applying server-pushed media params:", failure);
|
|
6650
|
+
this.reportLegError(failure, rtcPeerConnController, { fatal: false });
|
|
6651
|
+
}
|
|
6652
|
+
}
|
|
6653
|
+
/**
|
|
6654
|
+
* Set node_id/selfId only when the current value is null.
|
|
6655
|
+
*
|
|
6656
|
+
* During reattach, `call.joined` and `verto.answer` events can deliver
|
|
6657
|
+
* these identifiers before the `verto.invite` RPC response (`CALL CREATED`)
|
|
6658
|
+
* arrives. These methods let early events populate them eagerly so that
|
|
6659
|
+
* downstream RPC calls (e.g. `call.layout.list`) don't fail with empty
|
|
6660
|
+
* identifiers. `processInviteResponse()` remains the authoritative source and
|
|
6661
|
+
* overwrites unconditionally — for selfId, on the main leg only.
|
|
6662
|
+
*/
|
|
6663
|
+
setNodeIdIfNull(nodeId) {
|
|
6664
|
+
if (!this._nodeId$.value && nodeId) {
|
|
6665
|
+
logger$16.debug(`[WebRTCManager] Early node_id set: ${nodeId}`);
|
|
6161
6666
|
this._nodeId$.next(nodeId);
|
|
6162
6667
|
}
|
|
6163
6668
|
}
|
|
@@ -6176,24 +6681,33 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6176
6681
|
this.onError?.(new require_operators.VertoPongError(error));
|
|
6177
6682
|
}
|
|
6178
6683
|
}
|
|
6684
|
+
/**
|
|
6685
|
+
* @returns whether the constraints reached the media the call is sending.
|
|
6686
|
+
* `false` is an outcome, not a failure: the leg may have no live sender of
|
|
6687
|
+
* the kind, or carry media the SDK did not capture and may not replace.
|
|
6688
|
+
* A failure behind it still reaches the call's `errors$`, so a caller that
|
|
6689
|
+
* ignores this value learns of it there.
|
|
6690
|
+
*/
|
|
6179
6691
|
async updateMediaConstraints(options = {}) {
|
|
6180
6692
|
const { audio, video } = options;
|
|
6693
|
+
let applied = true;
|
|
6181
6694
|
try {
|
|
6182
|
-
if (audio) await this.mainPeerConnection.updateSendersConstraints("audio", audio);
|
|
6183
|
-
if (video) await this.mainPeerConnection.updateSendersConstraints("video", video);
|
|
6695
|
+
if (audio) applied = await this.mainPeerConnection.updateSendersConstraints("audio", audio) && applied;
|
|
6696
|
+
if (video) applied = await this.mainPeerConnection.updateSendersConstraints("video", video) && applied;
|
|
6184
6697
|
} catch (error) {
|
|
6185
6698
|
logger$16.warn("[WebRTCManager] Error updating media constraints:", error);
|
|
6186
|
-
this.
|
|
6699
|
+
this.reportLegError(toError(error), this.mainPeerConnection);
|
|
6187
6700
|
throw error;
|
|
6188
6701
|
}
|
|
6702
|
+
return applied;
|
|
6189
6703
|
}
|
|
6190
6704
|
get selfId() {
|
|
6191
6705
|
return this._selfId$.value;
|
|
6192
6706
|
}
|
|
6193
6707
|
/** Build an AttachableCall from the current call state. */
|
|
6194
|
-
buildAttachableCall(idOverride) {
|
|
6708
|
+
buildAttachableCall(idOverride, nodeIdOverride) {
|
|
6195
6709
|
return {
|
|
6196
|
-
nodeId: this.nodeId ?? void 0,
|
|
6710
|
+
nodeId: nodeIdOverride ?? this.nodeId ?? void 0,
|
|
6197
6711
|
id: idOverride ?? this.webRtcCallSession.id,
|
|
6198
6712
|
to: this.webRtcCallSession.to,
|
|
6199
6713
|
mediaDirections: this.webRtcCallSession.mediaDirections
|
|
@@ -6323,24 +6837,49 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6323
6837
|
get vertoPing$() {
|
|
6324
6838
|
return this.cachedObservable("vertoPing$", () => this.webRtcCallSession.webrtcMessages$.pipe(require_operators.filterAs(isVertoPingInnerParams, "params"), (0, rxjs.takeUntil)(this.destroyed$)));
|
|
6325
6839
|
}
|
|
6840
|
+
/**
|
|
6841
|
+
* Send a member-control op in-dialog via verto.info.
|
|
6842
|
+
*
|
|
6843
|
+
* The control payload rides in `params.command` — a sibling of `dialogParams`,
|
|
6844
|
+
* at the same level as `dtmf` in {@link sendDigits} — and the inner verto.info
|
|
6845
|
+
* is matched to this call's channel by `dialogParams.callID`, the in-dialog
|
|
6846
|
+
* convention for member-scoped frames. Because it is delivered on the dialog
|
|
6847
|
+
* itself, control lands on the call's own channel with no {node_id,call_id,member_id}
|
|
6848
|
+
* "self" tuple to get wrong. The outer webrtc.verto envelope (added by executeVerto)
|
|
6849
|
+
* still carries the own-leg callID + node_id for session routing.
|
|
6850
|
+
*
|
|
6851
|
+
* Keep `command` OUT of `dialogParams`: it is read at the params level, and
|
|
6852
|
+
* filterVertoParams rewrites/filters dialogParams keys but passes params-level
|
|
6853
|
+
* keys through verbatim.
|
|
6854
|
+
*/
|
|
6855
|
+
async sendCallControl(method, params) {
|
|
6856
|
+
const response = await this.executeVerto(VertoInfo({
|
|
6857
|
+
dialogParams: { callID: this.webRtcCallSession.id },
|
|
6858
|
+
command: {
|
|
6859
|
+
method,
|
|
6860
|
+
params
|
|
6861
|
+
}
|
|
6862
|
+
}));
|
|
6863
|
+
const failure = findNestedVertoFailure(response);
|
|
6864
|
+
if (failure) throw new require_operators.JSONRPCError(Number.parseInt(failure.code, 10) || 0, `Call control "${method}" failed (code ${failure.code})${failure.message ? `: ${failure.message}` : ""}`, void 0);
|
|
6865
|
+
return response;
|
|
6866
|
+
}
|
|
6326
6867
|
async executeVerto(message, optionals = {}) {
|
|
6327
|
-
const
|
|
6868
|
+
const params = {
|
|
6328
6869
|
callID: optionals.callID ?? this.webRtcCallSession.id,
|
|
6329
6870
|
node_id: optionals.node_id ?? this._nodeId$.value ?? "",
|
|
6330
6871
|
message,
|
|
6331
6872
|
subscribe: optionals.subscribe
|
|
6332
|
-
}
|
|
6873
|
+
};
|
|
6874
|
+
const webrtcVertoMessage = WebrtcVerto(params);
|
|
6333
6875
|
const response = await this.webRtcCallSession.execute(webrtcVertoMessage);
|
|
6334
|
-
|
|
6335
|
-
const error = new require_operators.JSONRPCError(response.error.code, response.error.message, response.error.data);
|
|
6336
|
-
this.onError?.(error);
|
|
6337
|
-
return response;
|
|
6338
|
-
}
|
|
6876
|
+
const nonFatal = message.method === "verto.info" ? { fatal: false } : void 0;
|
|
6339
6877
|
const innerResult = require_operators.getValueFrom(response, "result.result");
|
|
6340
|
-
|
|
6341
|
-
|
|
6342
|
-
|
|
6343
|
-
|
|
6878
|
+
const failure = response.error ?? innerResult?.error;
|
|
6879
|
+
if (failure) {
|
|
6880
|
+
const error = new require_operators.JSONRPCError(failure.code, failure.message, failure.data);
|
|
6881
|
+
if (message.method === "verto.invite" || message.method === "verto.answer") throw error;
|
|
6882
|
+
this.reportLegError(error, this._rtcPeerConnectionsMap.get(params.callID), nonFatal);
|
|
6344
6883
|
}
|
|
6345
6884
|
return response;
|
|
6346
6885
|
}
|
|
@@ -6359,8 +6898,9 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6359
6898
|
default:
|
|
6360
6899
|
}
|
|
6361
6900
|
} catch (error) {
|
|
6901
|
+
if (vertoMethod === "verto.answer") throw error;
|
|
6362
6902
|
logger$16.error(`[WebRTCManager] Error sending Verto ${vertoMethod}:`, error);
|
|
6363
|
-
this.
|
|
6903
|
+
this.reportLegError(toError(error), rtcPeerConnController);
|
|
6364
6904
|
if (vertoMethod === "verto.modify") this.onModifyFailed?.();
|
|
6365
6905
|
}
|
|
6366
6906
|
}
|
|
@@ -6376,7 +6916,7 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6376
6916
|
} catch (error) {
|
|
6377
6917
|
logger$16.warn("[WebRTCManager] Error processing modify response:", error);
|
|
6378
6918
|
const modifyError = error instanceof Error ? error : new Error(String(error), { cause: error });
|
|
6379
|
-
this.
|
|
6919
|
+
this.reportLegError(modifyError, rtcPeerConnController);
|
|
6380
6920
|
}
|
|
6381
6921
|
}
|
|
6382
6922
|
}
|
|
@@ -6389,37 +6929,35 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6389
6929
|
status,
|
|
6390
6930
|
signalingError
|
|
6391
6931
|
});
|
|
6392
|
-
this.
|
|
6932
|
+
this.reportLegError(signalingError, null, { fatal: false });
|
|
6393
6933
|
return;
|
|
6394
6934
|
}
|
|
6395
6935
|
if (rtcPeerConnController.isMainDevice) this._signalingStatus$.next(status);
|
|
6396
6936
|
}
|
|
6397
6937
|
processInviteResponse(response, rtcPeerConnController) {
|
|
6398
|
-
if (
|
|
6938
|
+
if (require_operators.getValueFrom(response, "result.result.result.message") === "CALL CREATED") {
|
|
6399
6939
|
this.emitMainSignalingStatus(rtcPeerConnController.id, "trying");
|
|
6400
|
-
|
|
6940
|
+
const nodeId = require_operators.getValueFrom(response, "result.node_id") ?? null;
|
|
6401
6941
|
const memberId = require_operators.getValueFrom(response, "result.result.result.memberID") ?? null;
|
|
6402
|
-
const callId = require_operators.getValueFrom(response, "result.result.result.callID")
|
|
6942
|
+
const callId = require_operators.getValueFrom(response, "result.result.result.callID");
|
|
6403
6943
|
logger$16.debug("[WebRTCManager] Verto invite response:", {
|
|
6404
6944
|
callId,
|
|
6405
6945
|
memberId,
|
|
6406
6946
|
response
|
|
6407
6947
|
});
|
|
6408
|
-
this._selfId$.next(memberId);
|
|
6409
6948
|
rtcPeerConnController.setMemberId(memberId);
|
|
6410
|
-
|
|
6411
|
-
|
|
6412
|
-
this.
|
|
6413
|
-
|
|
6414
|
-
|
|
6415
|
-
|
|
6416
|
-
|
|
6949
|
+
rtcPeerConnController.setNodeId(nodeId);
|
|
6950
|
+
if (rtcPeerConnController.isMainDevice) {
|
|
6951
|
+
this._selfId$.next(memberId);
|
|
6952
|
+
this._nodeId$.next(nodeId);
|
|
6953
|
+
this.attachManager.attach(this.buildAttachableCall(callId, nodeId ?? void 0));
|
|
6954
|
+
}
|
|
6955
|
+
if (callId) this.webRtcCallSession.addCallId(callId);
|
|
6417
6956
|
logger$16.info("[WebRTCManager] Verto invite successful");
|
|
6418
6957
|
logger$16.debug(`[WebRTCManager] nodeid: ${this._nodeId$.value}, selfId: ${this._selfId$.value}`);
|
|
6419
6958
|
} else {
|
|
6420
6959
|
logger$16.error("[WebRTCManager] Verto invite failed:", response);
|
|
6421
|
-
|
|
6422
|
-
this.onError?.(inviteError);
|
|
6960
|
+
this.reportLegError(/* @__PURE__ */ new Error("Verto invite failed: unexpected response"), rtcPeerConnController);
|
|
6423
6961
|
}
|
|
6424
6962
|
}
|
|
6425
6963
|
get RTCPeerConnectionConfig() {
|
|
@@ -6458,7 +6996,7 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6458
6996
|
this._rtcPeerConnectionsMap.set(rtcPeerConnController.id, rtcPeerConnController);
|
|
6459
6997
|
this._rtcPeerConnections$.next(Array.from(this._rtcPeerConnectionsMap.values()));
|
|
6460
6998
|
this.subscribeTo(rtcPeerConnController.errors$, (error) => {
|
|
6461
|
-
this.
|
|
6999
|
+
this.reportLegError(error, rtcPeerConnController);
|
|
6462
7000
|
});
|
|
6463
7001
|
if (options.initOffer) this.handleInboundAnswer(rtcPeerConnController);
|
|
6464
7002
|
}
|
|
@@ -6487,7 +7025,7 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6487
7025
|
await rtcPeerConnController.acceptInbound(answerOptions);
|
|
6488
7026
|
} catch (error) {
|
|
6489
7027
|
logger$16.error("[WebRTCManager] Error creating inbound answer:", error);
|
|
6490
|
-
this.
|
|
7028
|
+
this.reportLegError(toError(error), rtcPeerConnController);
|
|
6491
7029
|
}
|
|
6492
7030
|
}
|
|
6493
7031
|
}
|
|
@@ -6559,7 +7097,7 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6559
7097
|
isInvite: isVertoInviteMessage(vertoMessage),
|
|
6560
7098
|
reattach: this.webRtcCallSession.options.reattach === true,
|
|
6561
7099
|
explicitNodeId: this.webRtcCallSession.options.nodeId,
|
|
6562
|
-
currentNodeId: this._nodeId$.value
|
|
7100
|
+
currentNodeId: rtcPeerConnController.nodeId ?? this._nodeId$.value
|
|
6563
7101
|
}),
|
|
6564
7102
|
subscribe
|
|
6565
7103
|
};
|
|
@@ -6591,7 +7129,7 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6591
7129
|
await this.attachManager.attach(this.buildAttachableCall());
|
|
6592
7130
|
} catch (error) {
|
|
6593
7131
|
logger$16.error("[WebRTCManager] Error sending Verto answer:", error);
|
|
6594
|
-
this.
|
|
7132
|
+
this.reportLegError(toError(error), rtcPeerConnectionController);
|
|
6595
7133
|
await rtcPeerConnectionController.updateAnswerStatus({ status: "failed" });
|
|
6596
7134
|
}
|
|
6597
7135
|
}
|
|
@@ -6663,15 +7201,19 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6663
7201
|
await this.mainPeerConnection.restoreTrackSender(deviceKind);
|
|
6664
7202
|
} else {
|
|
6665
7203
|
const error = new require_operators.InvalidParams("No valid device to be added");
|
|
6666
|
-
this.
|
|
7204
|
+
this.reportLegError(error, this.mainPeerConnection);
|
|
6667
7205
|
throw error;
|
|
6668
7206
|
}
|
|
6669
7207
|
}
|
|
6670
|
-
async addScreenMedia(options = {
|
|
6671
|
-
await this.initAdditionalPeerConnection("screenshare",
|
|
7208
|
+
async addScreenMedia(options = {}) {
|
|
7209
|
+
await this.initAdditionalPeerConnection("screenshare", {
|
|
7210
|
+
audio: false,
|
|
7211
|
+
screenShareAudio: options.audio ?? false
|
|
7212
|
+
});
|
|
6672
7213
|
}
|
|
6673
7214
|
async initAdditionalPeerConnection(propose, options) {
|
|
6674
7215
|
const isScreenShare = propose === "screenshare";
|
|
7216
|
+
if (isScreenShare && this._screenShareId && this._rtcPeerConnectionsMap.has(this._screenShareId)) throw new require_operators.ScreenShareAlreadyActiveError(this._screenShareId);
|
|
6675
7217
|
let firstPeerConnectionError;
|
|
6676
7218
|
let rtcPeerConnController = null;
|
|
6677
7219
|
try {
|
|
@@ -6688,21 +7230,36 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6688
7230
|
this._rtcPeerConnections$.next(Array.from(this._rtcPeerConnectionsMap.values()));
|
|
6689
7231
|
this.subscribeTo(rtcPeerConnController.errors$, (error) => {
|
|
6690
7232
|
firstPeerConnectionError ??= error;
|
|
6691
|
-
this.
|
|
7233
|
+
this.reportLegError(error, rtcPeerConnController);
|
|
6692
7234
|
});
|
|
6693
|
-
|
|
7235
|
+
const pc = rtcPeerConnController;
|
|
7236
|
+
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)((0, rxjs.merge)(this.destroyed$, pc.destroyed$))));
|
|
6694
7237
|
if (isScreenShare) this._screenShareStatus$.next("started");
|
|
6695
7238
|
logger$16.info(`[WebRTCManager] Additional peer connection connected (${propose}).`);
|
|
6696
7239
|
return rtcPeerConnController.id;
|
|
6697
7240
|
} catch (error) {
|
|
6698
|
-
|
|
6699
|
-
|
|
6700
|
-
if (
|
|
7241
|
+
const cancelled = error instanceof require_operators.AuxiliaryLegCancelledError;
|
|
7242
|
+
const aborted = error instanceof rxjs.EmptyError && !firstPeerConnectionError;
|
|
7243
|
+
if (!cancelled && !aborted) logger$16.warn("[WebRTCManager] Error initializing additional peer connection:", error);
|
|
7244
|
+
if (rtcPeerConnController && this._rtcPeerConnectionsMap.has(rtcPeerConnController.id)) {
|
|
7245
|
+
rtcPeerConnController.destroy();
|
|
7246
|
+
this._rtcPeerConnectionsMap.delete(rtcPeerConnController.id);
|
|
7247
|
+
this._rtcPeerConnections$.next(Array.from(this._rtcPeerConnectionsMap.values()));
|
|
7248
|
+
}
|
|
7249
|
+
if (isScreenShare) {
|
|
7250
|
+
this._screenShareId = void 0;
|
|
7251
|
+
this._screenShareStatus$.next("none");
|
|
7252
|
+
}
|
|
7253
|
+
if (cancelled) {
|
|
7254
|
+
logger$16.debug("[WebRTCManager] Additional peer connection removed before connecting.");
|
|
7255
|
+
throw error;
|
|
7256
|
+
}
|
|
6701
7257
|
if (firstPeerConnectionError) throw firstPeerConnectionError instanceof require_operators.MediaAccessError && firstPeerConnectionError.originalError instanceof Error ? firstPeerConnectionError.originalError : firstPeerConnectionError;
|
|
6702
7258
|
if (error instanceof rxjs.EmptyError) {
|
|
6703
7259
|
logger$16.debug("[WebRTCManager] Additional peer connection aborted before connecting.");
|
|
6704
7260
|
return;
|
|
6705
7261
|
}
|
|
7262
|
+
if (error instanceof rxjs.TimeoutError) throw new require_operators.AuxiliaryLegTimeoutError(propose, error);
|
|
6706
7263
|
throw error instanceof Error ? error : new Error(String(error), { cause: error });
|
|
6707
7264
|
}
|
|
6708
7265
|
}
|
|
@@ -6719,7 +7276,10 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6719
7276
|
if (removeTrack) return this.mainPeerConnection.stopTrackSender(removeTrack, { updateTransceiverDirection: true });
|
|
6720
7277
|
}
|
|
6721
7278
|
async removeScreenMedia() {
|
|
6722
|
-
if (!["starting", "started"].includes(this._screenShareStatus$.value))
|
|
7279
|
+
if (!["starting", "started"].includes(this._screenShareStatus$.value)) {
|
|
7280
|
+
logger$16.warn("[WebRTCManager] No active screen share to stop.");
|
|
7281
|
+
return;
|
|
7282
|
+
}
|
|
6723
7283
|
if (!this._screenShareId) {
|
|
6724
7284
|
logger$16.debug("[WebRTCManager] No screen share peer connection found.");
|
|
6725
7285
|
return;
|
|
@@ -6734,6 +7294,10 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6734
7294
|
try {
|
|
6735
7295
|
if (rtcPeerConnController) await this.executeVertoBye(rtcPeerConnController);
|
|
6736
7296
|
} finally {
|
|
7297
|
+
if (rtcPeerConnController && rtcPeerConnController.connectionState !== "connected") this._legErrors$.next({
|
|
7298
|
+
legId: id,
|
|
7299
|
+
error: new require_operators.AuxiliaryLegCancelledError(rtcPeerConnController.propose)
|
|
7300
|
+
});
|
|
6737
7301
|
rtcPeerConnController?.destroy();
|
|
6738
7302
|
this._rtcPeerConnectionsMap.delete(id);
|
|
6739
7303
|
this._rtcPeerConnections$.next(Array.from(this._rtcPeerConnectionsMap.values()));
|
|
@@ -6748,7 +7312,10 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6748
7312
|
await this.executeVerto(VertoBye({
|
|
6749
7313
|
...causeParams,
|
|
6750
7314
|
dialogParams: this.dialogParams(rtcPeerConnController)
|
|
6751
|
-
})
|
|
7315
|
+
}), {
|
|
7316
|
+
callID: rtcPeerConnController.id,
|
|
7317
|
+
node_id: rtcPeerConnController.nodeId ?? void 0
|
|
7318
|
+
});
|
|
6752
7319
|
} catch (error) {
|
|
6753
7320
|
logger$16.warn("[WebRTCManager] Call might already be disconnected, error sending Verto bye:", error);
|
|
6754
7321
|
throw error;
|
|
@@ -7631,14 +8198,35 @@ function mosToQualityLevel(mos) {
|
|
|
7631
8198
|
}
|
|
7632
8199
|
|
|
7633
8200
|
//#endregion
|
|
7634
|
-
//#region src/
|
|
7635
|
-
const logger$12 = require_operators.getLogger();
|
|
8201
|
+
//#region src/utils/unwrapVertoReply.ts
|
|
7636
8202
|
/**
|
|
7637
|
-
*
|
|
7638
|
-
*
|
|
7639
|
-
*
|
|
8203
|
+
* Unwrap a method's own reply from a `webrtc.verto` envelope.
|
|
8204
|
+
*
|
|
8205
|
+
* A control verb sent in-dialog comes back nested two levels deep:
|
|
8206
|
+
*
|
|
8207
|
+
* ```
|
|
8208
|
+
* { result: { node_id, code, result: { jsonrpc, id, result: <method payload> } } }
|
|
8209
|
+
* ```
|
|
8210
|
+
*
|
|
8211
|
+
* whereas the routed transport resolves the method payload directly under
|
|
8212
|
+
* `.result`. Readers want the latter shape, and the difference is silent when it
|
|
8213
|
+
* is wrong — `response.result.layouts` simply evaluates to `undefined` against the
|
|
8214
|
+
* envelope, so the data arrives, nothing throws, and the caller sees an empty
|
|
8215
|
+
* value. That exact failure produced an empty layout dropdown with a successful
|
|
8216
|
+
* request behind it.
|
|
8217
|
+
*
|
|
8218
|
+
* Accepting both shapes here keeps every reader indifferent to which transport
|
|
8219
|
+
* produced the response. Anything that is not a nested envelope (a plain ack, or
|
|
8220
|
+
* an already-unwrapped reply) passes through untouched.
|
|
7640
8221
|
*/
|
|
7641
|
-
|
|
8222
|
+
function unwrapVertoReply(response) {
|
|
8223
|
+
const inner = require_operators.getValueFrom(response, "result.result");
|
|
8224
|
+
return inner && typeof inner === "object" && "result" in inner ? inner : response;
|
|
8225
|
+
}
|
|
8226
|
+
|
|
8227
|
+
//#endregion
|
|
8228
|
+
//#region src/core/entities/Call.ts
|
|
8229
|
+
const logger$12 = require_operators.getLogger();
|
|
7642
8230
|
/**
|
|
7643
8231
|
* Ratio between the critical and warning RTT spike multipliers.
|
|
7644
8232
|
* Warning threshold = baseline * warningMultiplier (default 3x)
|
|
@@ -7734,8 +8322,11 @@ var WebRTCCall = class extends Destroyable {
|
|
|
7734
8322
|
emitError(callError) {
|
|
7735
8323
|
if (this._status$.value === "destroyed" || this._status$.value === "failed") return;
|
|
7736
8324
|
this._errors$.next(callError);
|
|
7737
|
-
if (callError.fatal) {
|
|
8325
|
+
if (callError.fatal && this._status$.value !== "disconnecting") {
|
|
7738
8326
|
this._status$.next("failed");
|
|
8327
|
+
this.vertoManager.bye().catch((error) => {
|
|
8328
|
+
logger$12.debug("[Call] fatal-teardown bye failed (signaling likely already dead):", error);
|
|
8329
|
+
});
|
|
7739
8330
|
this.destroy();
|
|
7740
8331
|
}
|
|
7741
8332
|
}
|
|
@@ -7790,7 +8381,7 @@ var WebRTCCall = class extends Destroyable {
|
|
|
7790
8381
|
/** Toggles the call lock state, preventing or allowing new participants from joining. */
|
|
7791
8382
|
async toggleLock() {
|
|
7792
8383
|
const method = this.locked ? "call.unlock" : "call.lock";
|
|
7793
|
-
await this.executeMethod(this.
|
|
8384
|
+
await this.executeMethod(this.callSelf, method, {});
|
|
7794
8385
|
}
|
|
7795
8386
|
/**
|
|
7796
8387
|
* Toggles the hold state of the call (pauses/resumes local media transmission).
|
|
@@ -7839,14 +8430,25 @@ var WebRTCCall = class extends Destroyable {
|
|
|
7839
8430
|
*
|
|
7840
8431
|
* Constructs call context (node_id, call_id, member_id) and sends the RPC request.
|
|
7841
8432
|
*
|
|
7842
|
-
* @param target - Target
|
|
8433
|
+
* @param target - Target {@link MemberTarget} triple, or the local member's
|
|
8434
|
+
* ID string for self-operations (any other string is rejected — a bare
|
|
8435
|
+
* member id cannot carry the remote member's own call context).
|
|
7843
8436
|
* @param method - Verto method name (e.g. `'call.mute'`, `'call.member.remove'`).
|
|
7844
8437
|
* @param args - Parameters for the RPC method.
|
|
7845
8438
|
* @returns The RPC response.
|
|
8439
|
+
* @throws {CallNotReadyError} If the call has no self member context yet.
|
|
8440
|
+
* @throws {InvalidParams} If a string target is not the local member's ID.
|
|
7846
8441
|
* @throws {JSONRPCError} If the RPC call returns an error.
|
|
7847
8442
|
*/
|
|
7848
8443
|
async executeMethod(target, method, args) {
|
|
7849
|
-
const
|
|
8444
|
+
const self = this.callSelf;
|
|
8445
|
+
if (typeof target === "string" && target !== self.member_id) throw new require_operators.InvalidParams(`Target member ID ${target} does not match call's self member ID ${self.member_id}`);
|
|
8446
|
+
if (this.clientSession.callControl === "in-dialog") return this.executeMethodInDialog(target, method, args);
|
|
8447
|
+
const params = {
|
|
8448
|
+
...args,
|
|
8449
|
+
self,
|
|
8450
|
+
target: typeof target === "string" ? self : target
|
|
8451
|
+
};
|
|
7850
8452
|
const request = buildRPCRequest({
|
|
7851
8453
|
method,
|
|
7852
8454
|
params
|
|
@@ -7860,29 +8462,98 @@ var WebRTCCall = class extends Destroyable {
|
|
|
7860
8462
|
throw error;
|
|
7861
8463
|
}
|
|
7862
8464
|
}
|
|
7863
|
-
|
|
7864
|
-
|
|
7865
|
-
|
|
7866
|
-
|
|
7867
|
-
|
|
7868
|
-
|
|
7869
|
-
|
|
7870
|
-
|
|
7871
|
-
|
|
7872
|
-
|
|
7873
|
-
|
|
7874
|
-
|
|
7875
|
-
|
|
7876
|
-
|
|
7877
|
-
};
|
|
7878
|
-
|
|
7879
|
-
|
|
7880
|
-
|
|
7881
|
-
|
|
7882
|
-
|
|
8465
|
+
/**
|
|
8466
|
+
* `executeMethod` for a call opened with `callControl: 'in-dialog'`.
|
|
8467
|
+
*
|
|
8468
|
+
* Translates the routed transport's calling convention into the in-dialog one. No
|
|
8469
|
+
* `self` tuple is sent, but a `target` is — the same {call_id, member_id} the routed
|
|
8470
|
+
* transport puts in `target` (minus node_id), for self-ops and cross-member ops alike.
|
|
8471
|
+
*
|
|
8472
|
+
* Target shapes are per-verb and irregular, so they are centralised here rather
|
|
8473
|
+
* than left to callers: most verbs take a singular `target`, `call.member.remove`
|
|
8474
|
+
* takes a plural `targets` array, and `call.member.position.set` takes a flat
|
|
8475
|
+
* `targets` of `{call_id, position}` — the one verb keyed on call_id rather than
|
|
8476
|
+
* member_id, so the member triple `Participant.setPosition` built is unwrapped.
|
|
8477
|
+
*/
|
|
8478
|
+
async executeMethodInDialog(target, method, args) {
|
|
8479
|
+
const control = { ...args };
|
|
8480
|
+
if (method === "call.member.position.set") control.targets = (args.targets ?? []).map((entry) => ({
|
|
8481
|
+
call_id: entry.target?.call_id ?? entry.call_id,
|
|
8482
|
+
position: entry.position
|
|
8483
|
+
}));
|
|
8484
|
+
else {
|
|
8485
|
+
const member = typeof target === "object" ? {
|
|
8486
|
+
call_id: target.call_id,
|
|
8487
|
+
member_id: target.member_id
|
|
8488
|
+
} : {
|
|
7883
8489
|
call_id: this.id,
|
|
7884
8490
|
member_id: target
|
|
7885
|
-
}
|
|
8491
|
+
};
|
|
8492
|
+
if (method === "call.member.remove") control.targets = [member];
|
|
8493
|
+
else control.target = member;
|
|
8494
|
+
}
|
|
8495
|
+
return this.sendCommand(method, control);
|
|
8496
|
+
}
|
|
8497
|
+
/**
|
|
8498
|
+
* Sends a `call.*` control verb **in-dialog** via `verto.info`, as an alternative
|
|
8499
|
+
* to the routed {@link executeMethod} transport.
|
|
8500
|
+
*
|
|
8501
|
+
* Why both exist: `executeMethod` addresses the member with an explicit
|
|
8502
|
+
* `{node_id, call_id, member_id}` tuple, which does not resolve for every conference,
|
|
8503
|
+
* so the op can fail. An in-dialog frame carries the verb on the member's own
|
|
8504
|
+
* signaling channel instead, so control works without the client needing to know how
|
|
8505
|
+
* the conference is hosted.
|
|
8506
|
+
*
|
|
8507
|
+
* The trade-off is reach: the in-dialog transport is only accepted for calls that
|
|
8508
|
+
* join a conference over SWML (e.g. an SWML `join_conference`); use the routed
|
|
8509
|
+
* default otherwise.
|
|
8510
|
+
*
|
|
8511
|
+
* `params` are sent verbatim — nothing is built for you, which includes the target.
|
|
8512
|
+
* **A self-directed op still needs one**, or it is refused; name yourself explicitly:
|
|
8513
|
+
*
|
|
8514
|
+
* ```ts
|
|
8515
|
+
* const { call_id, member_id } = call.self.target;
|
|
8516
|
+
* await call.sendCommand('call.mute', { channels: ['audio'], target: { call_id, member_id } });
|
|
8517
|
+
* ```
|
|
8518
|
+
*
|
|
8519
|
+
* Never include `node_id` — only the two ids. The shapes are per-verb: most take a
|
|
8520
|
+
* singular `target`, `call.member.remove` takes a plural `targets` array, and
|
|
8521
|
+
* `call.member.position.set` takes a flat `targets: [{call_id, position}]` (the one
|
|
8522
|
+
* verb keyed on `call_id` rather than `member_id`). Verbs that act on the call as a
|
|
8523
|
+
* whole, or that the SDK does not wrap at all, take no target.
|
|
8524
|
+
*
|
|
8525
|
+
* For the typed alternative that handles all of this, create the client with
|
|
8526
|
+
* `callControl: 'in-dialog'` and use the ordinary `Call`/`Participant` methods.
|
|
8527
|
+
*
|
|
8528
|
+
* @internal Not part of the supported surface while the in-dialog transport is still
|
|
8529
|
+
* rolling out. `WebRTCCall` is exported from the package entry, so without this tag
|
|
8530
|
+
* TypeDoc publishes the method — and the example above — as public API.
|
|
8531
|
+
*
|
|
8532
|
+
* @param method - A `call.*` method name (e.g. `'call.mute'`).
|
|
8533
|
+
* @param params - Method parameters, sent verbatim.
|
|
8534
|
+
* @returns The method's own reply, unwrapped from the `verto.info` envelope.
|
|
8535
|
+
* @throws {JSONRPCError} If the control op fails.
|
|
8536
|
+
*/
|
|
8537
|
+
async sendCommand(method, params = {}) {
|
|
8538
|
+
return unwrapVertoReply(await this.vertoManager.sendCallControl(method, params));
|
|
8539
|
+
}
|
|
8540
|
+
/**
|
|
8541
|
+
* The local leg's member triple — sent as `self` in every member RPC
|
|
8542
|
+
* envelope, and as the `target` of call-scoped self-operations (e.g. lock,
|
|
8543
|
+
* layout).
|
|
8544
|
+
*
|
|
8545
|
+
* @throws {CallNotReadyError} Before `call.joined` delivers the self member
|
|
8546
|
+
* context (`selfId`/`nodeId`) — an RPC without it cannot be routed, so fail
|
|
8547
|
+
* fast instead of sending a doomed request.
|
|
8548
|
+
*/
|
|
8549
|
+
get callSelf() {
|
|
8550
|
+
const node_id = this.nodeId;
|
|
8551
|
+
const member_id = this.vertoManager.selfId;
|
|
8552
|
+
if (!node_id || !member_id) throw new require_operators.CallNotReadyError(this.id);
|
|
8553
|
+
return {
|
|
8554
|
+
node_id,
|
|
8555
|
+
call_id: this.id,
|
|
8556
|
+
member_id
|
|
7886
8557
|
};
|
|
7887
8558
|
}
|
|
7888
8559
|
/** Observable of the current call status (e.g. `'ringing'`, `'connected'`). */
|
|
@@ -8038,13 +8709,13 @@ var WebRTCCall = class extends Destroyable {
|
|
|
8038
8709
|
get bandwidthConstrained$() {
|
|
8039
8710
|
return this.deferEmission(this._bandwidthConstrained$.asObservable());
|
|
8040
8711
|
}
|
|
8041
|
-
/** Observable that emits when server
|
|
8712
|
+
/** Observable that emits when the server pushes media params. */
|
|
8042
8713
|
get mediaParamsUpdated$() {
|
|
8043
8714
|
return this.deferEmission(this._mediaParamsUpdated$.asObservable());
|
|
8044
8715
|
}
|
|
8045
8716
|
/**
|
|
8046
8717
|
* @internal Emit a media params update event.
|
|
8047
|
-
* Called by the VertoManager when server
|
|
8718
|
+
* Called by the VertoManager when the server pushes media params.
|
|
8048
8719
|
*/
|
|
8049
8720
|
emitMediaParamsUpdated(event) {
|
|
8050
8721
|
this._mediaParamsUpdated$.next(event);
|
|
@@ -8252,6 +8923,10 @@ var WebRTCCall = class extends Destroyable {
|
|
|
8252
8923
|
get selfId$() {
|
|
8253
8924
|
return this.vertoManager.selfId$;
|
|
8254
8925
|
}
|
|
8926
|
+
/** @internal Lets call creation bound the media and signalling phases apart. */
|
|
8927
|
+
get localMediaSettled$() {
|
|
8928
|
+
return this.vertoManager.localMediaSettled$;
|
|
8929
|
+
}
|
|
8255
8930
|
/** Local participant's member ID, or `null` if not joined. */
|
|
8256
8931
|
get selfId() {
|
|
8257
8932
|
return this.vertoManager.selfId;
|
|
@@ -8445,12 +9120,17 @@ var WebRTCCall = class extends Destroyable {
|
|
|
8445
9120
|
*
|
|
8446
9121
|
* **These operations are NOT atomic.** The layout is applied first, then each
|
|
8447
9122
|
* member position sequentially, so members may briefly flash into their
|
|
8448
|
-
* default slots before being moved to the requested positions.
|
|
9123
|
+
* default slots before being moved to the requested positions. Targeted
|
|
9124
|
+
* members are validated upfront, though: when any of them has no
|
|
9125
|
+
* {@link Participant.target | member call context} yet, the whole call
|
|
9126
|
+
* rejects before any request is sent and the layout is left unchanged.
|
|
8449
9127
|
*
|
|
8450
9128
|
* @param layout - Layout name (must be one of {@link layouts}).
|
|
8451
9129
|
* @param positions - Optional map of member IDs to {@link VideoPosition} values.
|
|
8452
9130
|
* When omitted or empty, only the layout is changed.
|
|
8453
9131
|
* @throws {InvalidParams} If the layout is not in the available {@link layouts}.
|
|
9132
|
+
* @throws {ParticipantNotReadyError} If a targeted member's call context has
|
|
9133
|
+
* not been received yet — thrown before any request is sent.
|
|
8454
9134
|
*
|
|
8455
9135
|
* @example
|
|
8456
9136
|
* ```ts
|
|
@@ -8461,18 +9141,19 @@ var WebRTCCall = class extends Destroyable {
|
|
|
8461
9141
|
*/
|
|
8462
9142
|
async setLayout(layout, positions) {
|
|
8463
9143
|
if (!this.layouts.includes(layout)) throw new require_operators.InvalidParams(`Layout ${layout} is not available in the current call layouts: ${this.layouts.join(", ")}`);
|
|
8464
|
-
const
|
|
8465
|
-
|
|
8466
|
-
const positionEntries = Object.entries(positions ?? {});
|
|
8467
|
-
if (positionEntries.length === 0) return;
|
|
8468
|
-
for (const [memberId, position] of positionEntries) {
|
|
9144
|
+
const targets = [];
|
|
9145
|
+
for (const [memberId, position] of Object.entries(positions ?? {})) {
|
|
8469
9146
|
const participant = this.participants.find((p) => p.id === memberId);
|
|
8470
9147
|
if (!participant) {
|
|
8471
9148
|
logger$12.warn(`[Call] setLayout: member ${memberId} not found in participants; skipping position ${position}`);
|
|
8472
9149
|
continue;
|
|
8473
9150
|
}
|
|
8474
|
-
|
|
9151
|
+
participant.target;
|
|
9152
|
+
targets.push([participant, position]);
|
|
8475
9153
|
}
|
|
9154
|
+
const selfId = await (0, rxjs.firstValueFrom)(this.selfId$.pipe((0, rxjs.filter)((id) => id !== null)));
|
|
9155
|
+
await this.executeMethod(selfId, "call.layout.set", { layout });
|
|
9156
|
+
for (const [participant, position] of targets) await participant.setPosition(position);
|
|
8476
9157
|
}
|
|
8477
9158
|
/**
|
|
8478
9159
|
* Transfers the call to another destination.
|
|
@@ -8574,17 +9255,28 @@ var WebRTCCall = class extends Destroyable {
|
|
|
8574
9255
|
* (notably iOS Safari) fall back to re-acquiring the track with the new
|
|
8575
9256
|
* constraint set and plumbing the replacement through the local audio
|
|
8576
9257
|
* pipeline if one is active.
|
|
9258
|
+
*
|
|
9259
|
+
* @returns whether the constraint reached the microphone. `false` is an
|
|
9260
|
+
* outcome rather than an error — a leg sending media the SDK did not capture
|
|
9261
|
+
* is left alone — so a UI that reflects the toggle must read it. Any failure
|
|
9262
|
+
* behind a `false` is also reported on {@link errors$}.
|
|
8577
9263
|
*/
|
|
8578
9264
|
async setEchoCancellation(enabled) {
|
|
8579
|
-
|
|
9265
|
+
return this.vertoManager.updateMediaConstraints({ audio: { echoCancellation: enabled } });
|
|
8580
9266
|
}
|
|
8581
|
-
/**
|
|
9267
|
+
/**
|
|
9268
|
+
* Toggle browser noise suppression on the local mic at runtime.
|
|
9269
|
+
* @returns whether the constraint reached the microphone.
|
|
9270
|
+
*/
|
|
8582
9271
|
async setNoiseSuppression(enabled) {
|
|
8583
|
-
|
|
9272
|
+
return this.vertoManager.updateMediaConstraints({ audio: { noiseSuppression: enabled } });
|
|
8584
9273
|
}
|
|
8585
|
-
/**
|
|
9274
|
+
/**
|
|
9275
|
+
* Toggle browser automatic gain control on the local mic at runtime.
|
|
9276
|
+
* @returns whether the constraint reached the microphone.
|
|
9277
|
+
*/
|
|
8586
9278
|
async setAutoGainControl(enabled) {
|
|
8587
|
-
|
|
9279
|
+
return this.vertoManager.updateMediaConstraints({ audio: { autoGainControl: enabled } });
|
|
8588
9280
|
}
|
|
8589
9281
|
/**
|
|
8590
9282
|
* Observable of the aggregate remote audio level, 0..1 RMS. The server
|
|
@@ -8641,6 +9333,9 @@ var WebRTCCall = class extends Destroyable {
|
|
|
8641
9333
|
/**
|
|
8642
9334
|
* Infers the semantic error category from a raw Error thrown by VertoManager
|
|
8643
9335
|
* or an RTCPeerConnection layer.
|
|
9336
|
+
*
|
|
9337
|
+
* Pure function — exported for unit testing.
|
|
9338
|
+
* @internal
|
|
8644
9339
|
*/
|
|
8645
9340
|
function inferCallErrorKind(error) {
|
|
8646
9341
|
if (error instanceof require_operators.RPCTimeoutError) return "timeout";
|
|
@@ -8659,7 +9354,14 @@ const RECOVERABLE_RPC_CODES = new Set([
|
|
|
8659
9354
|
require_operators.RPC_ERROR_AUTHENTICATION_FAILED,
|
|
8660
9355
|
require_operators.RPC_ERROR_INVALID_PARAMS
|
|
8661
9356
|
]);
|
|
8662
|
-
/**
|
|
9357
|
+
/**
|
|
9358
|
+
* A *fallback*: callers knowing which leg failed pass an explicit `fatal` and
|
|
9359
|
+
* never reach here, so the default-fatal branch only sees call- and main-leg
|
|
9360
|
+
* errors. Auxiliary legs go through `WebRTCVertoManager.reportLegError`.
|
|
9361
|
+
*
|
|
9362
|
+
* Pure function — exported for unit testing.
|
|
9363
|
+
* @internal
|
|
9364
|
+
*/
|
|
8663
9365
|
function isFatalError(error) {
|
|
8664
9366
|
if (error instanceof require_operators.VertoPongError) return false;
|
|
8665
9367
|
if (error instanceof require_operators.MediaTrackError) return false;
|
|
@@ -8694,7 +9396,9 @@ var CallFactory = class {
|
|
|
8694
9396
|
kind: inferCallErrorKind(error),
|
|
8695
9397
|
fatal: options$1?.fatal ?? isFatalError(error),
|
|
8696
9398
|
error,
|
|
8697
|
-
callId: callInstance.id
|
|
9399
|
+
callId: callInstance.id,
|
|
9400
|
+
...options$1?.leg ? { leg: options$1.leg } : {},
|
|
9401
|
+
...options$1?.legId ? { legId: options$1.legId } : {}
|
|
8698
9402
|
};
|
|
8699
9403
|
callInstance.emitError(callError);
|
|
8700
9404
|
},
|
|
@@ -9162,6 +9866,30 @@ const logger$9 = require_operators.getLogger();
|
|
|
9162
9866
|
function shouldAbortDial(callError) {
|
|
9163
9867
|
return callError.fatal || !(callError.error instanceof require_operators.MediaAccessError);
|
|
9164
9868
|
}
|
|
9869
|
+
/**
|
|
9870
|
+
* Wait for a dialed call to be ready, or for the failure that stops it.
|
|
9871
|
+
*
|
|
9872
|
+
* Local media acquisition is deliberately unbounded: a permission prompt or a
|
|
9873
|
+
* device picker is human time, and `getUserMedia` cannot be cancelled anyway.
|
|
9874
|
+
* The clock starts only once acquisition settles, so a slow human never spends
|
|
9875
|
+
* the server's budget.
|
|
9876
|
+
*
|
|
9877
|
+
* `merge` rather than `race`, because the two legs settle asymmetrically. A
|
|
9878
|
+
* fatal acquisition failure reports the error and then destroys the call in the
|
|
9879
|
+
* same synchronous step; `errors$` defers delivery by a microtask while
|
|
9880
|
+
* `localMediaSettled$` completes immediately. Under `race` that bare completion
|
|
9881
|
+
* ended the wait first and `dial()` rejected with an RxJS `EmptyError`, burying
|
|
9882
|
+
* the `NotAllowedError` applications are told to inspect. Under `merge` the
|
|
9883
|
+
* completed leg is simply spent, and the queued error — enqueued before the
|
|
9884
|
+
* completion, so delivered before it — arrives to reject the wait. A dial
|
|
9885
|
+
* abandoned with no error at all still ends both legs, and the resulting
|
|
9886
|
+
* `EmptyError` remains the benign-cancel signal.
|
|
9887
|
+
*
|
|
9888
|
+
* Exported for unit testing.
|
|
9889
|
+
*/
|
|
9890
|
+
async function awaitDialReady(session, signalingTimeoutMs) {
|
|
9891
|
+
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)))));
|
|
9892
|
+
}
|
|
9165
9893
|
const getAddressSearchURI = (options) => {
|
|
9166
9894
|
const to = options.to?.split("?")[0];
|
|
9167
9895
|
const from$8 = options.from?.startsWith("subscriber://") ? options.from.replace("subscriber://", "") : options.from;
|
|
@@ -9178,7 +9906,6 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
9178
9906
|
this.authorizationStateKey = authorizationStateKey;
|
|
9179
9907
|
this.attachManager = attachManager;
|
|
9180
9908
|
this.dpopManager = dpopManager;
|
|
9181
|
-
this.callCreateTimeout = 6e3;
|
|
9182
9909
|
this.agent = `signalwire-js/4.0.0`;
|
|
9183
9910
|
this.eventAcks = true;
|
|
9184
9911
|
this.authorizationState$ = this.createReplaySubject(1);
|
|
@@ -9187,6 +9914,7 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
9187
9914
|
minor: 0,
|
|
9188
9915
|
revision: 0
|
|
9189
9916
|
};
|
|
9917
|
+
this.callControl = "routed";
|
|
9190
9918
|
this._authorization$ = this.createBehaviorSubject(void 0);
|
|
9191
9919
|
this._errors$ = this.createReplaySubject(1);
|
|
9192
9920
|
this._authState$ = this.createBehaviorSubject({ kind: "unauthenticated" });
|
|
@@ -9386,22 +10114,23 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
9386
10114
|
await (0, rxjs.firstValueFrom)(this.authenticated$.pipe((0, rxjs.takeUntil)(this.destroyed$), (0, rxjs.filter)(Boolean), (0, rxjs.take)(1), (0, rxjs.timeout)({ first: 15e3 })));
|
|
9387
10115
|
}
|
|
9388
10116
|
async handleAuthenticationError(error) {
|
|
10117
|
+
if (error instanceof require_operators.TransportConnectionError) {
|
|
10118
|
+
logger$9.debug("[Session] Socket closed during authentication, waiting for reconnect");
|
|
10119
|
+
return;
|
|
10120
|
+
}
|
|
9389
10121
|
logger$9.error("Authentication error:", error);
|
|
9390
|
-
const isRecoverableAuthError = error instanceof require_operators.JSONRPCError && (error.code === require_operators.RPC_ERROR_REQUESTER_VALIDATION_FAILED || error.code === require_operators.RPC_ERROR_INVALID_PARAMS || error.code === require_operators.RPC_ERROR_AUTHENTICATION_FAILED);
|
|
10122
|
+
const isRecoverableAuthError$1 = error instanceof require_operators.JSONRPCError && (error.code === require_operators.RPC_ERROR_REQUESTER_VALIDATION_FAILED || error.code === require_operators.RPC_ERROR_INVALID_PARAMS || error.code === require_operators.RPC_ERROR_AUTHENTICATION_FAILED);
|
|
9391
10123
|
const hasStoredState = await (0, rxjs.firstValueFrom)(this.authorizationState$.pipe((0, rxjs.take)(1))) !== void 0;
|
|
9392
|
-
if (isRecoverableAuthError && hasStoredState) {
|
|
10124
|
+
if (isRecoverableAuthError$1 && hasStoredState) {
|
|
9393
10125
|
logger$9.debug("[Session] Recoverable auth error — cleaning up stored state and reconnecting fresh");
|
|
9394
|
-
|
|
9395
|
-
await this.cleanupStoredConnectionParams();
|
|
9396
|
-
} catch (cleanupError) {
|
|
9397
|
-
logger$9.error("Failed to cleanup stored connection params:", cleanupError);
|
|
9398
|
-
} finally {
|
|
9399
|
-
this.transport.reconnect();
|
|
9400
|
-
}
|
|
10126
|
+
await this.discardResumeStateAndReconnect();
|
|
9401
10127
|
} else this._errors$.next(error);
|
|
9402
10128
|
}
|
|
9403
10129
|
/**
|
|
9404
|
-
* Clear the resume state (authorization_state + protocol)
|
|
10130
|
+
* Clear the resume state (authorization_state + protocol) and ask the
|
|
10131
|
+
* transport to reconnect. The `connected` event re-triggers
|
|
10132
|
+
* `authenticate()`, which now has no stored state and so performs a fresh
|
|
10133
|
+
* connect.
|
|
9405
10134
|
*
|
|
9406
10135
|
* This is the stale-auth-state recovery helper used by handleAuthError:
|
|
9407
10136
|
* the server rejected a reconnect, so the resume state is discarded and a
|
|
@@ -9409,9 +10138,24 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
9409
10138
|
* session lives on through the reconnect and reattachCalls() needs the
|
|
9410
10139
|
* stored call references afterwards. Do NOT add detachAll() here.
|
|
9411
10140
|
*
|
|
10141
|
+
* Connect-time recovery only. A *request* refused on an already
|
|
10142
|
+
* authenticated session is never healed here: dropping the resume state
|
|
10143
|
+
* destroys the association between the socket and the previous session,
|
|
10144
|
+
* which is what reattach depends on. That path mints a fresh credential and
|
|
10145
|
+
* reauthenticates instead (see `SignalWire.recoverAndRetry`).
|
|
10146
|
+
*
|
|
9412
10147
|
* For public teardown (disconnect/destroy), use {@link teardownSessionState}
|
|
9413
10148
|
* instead, which clears the attach records as well.
|
|
9414
10149
|
*/
|
|
10150
|
+
async discardResumeStateAndReconnect() {
|
|
10151
|
+
try {
|
|
10152
|
+
await this.cleanupStoredConnectionParams();
|
|
10153
|
+
} catch (cleanupError) {
|
|
10154
|
+
logger$9.error("Failed to cleanup stored connection params:", cleanupError);
|
|
10155
|
+
} finally {
|
|
10156
|
+
this.transport.reconnect();
|
|
10157
|
+
}
|
|
10158
|
+
}
|
|
9415
10159
|
async cleanupStoredConnectionParams() {
|
|
9416
10160
|
await this.transport.setProtocol(void 0);
|
|
9417
10161
|
await this.updateAuthorizationStateInStorage(void 0);
|
|
@@ -9485,11 +10229,15 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
9485
10229
|
const isReconnect = hasReconnectState && storedToken;
|
|
9486
10230
|
let dpopToken;
|
|
9487
10231
|
if (isReconnect) logger$9.debug("[Session] Reconnecting with stored jwt_token + authorization_state");
|
|
9488
|
-
else
|
|
9489
|
-
|
|
9490
|
-
|
|
10232
|
+
else {
|
|
10233
|
+
const credential = this.getCredential();
|
|
10234
|
+
const credentialExpired = credential.expiry_at !== void 0 && credential.expiry_at <= Date.now() + require_operators.CREDENTIAL_EXPIRY_SKEW_MS;
|
|
10235
|
+
if (this.onBeforeReconnect && (this.clientBound || credentialExpired)) {
|
|
10236
|
+
logger$9.debug("[Session] Refreshing credentials before fresh connect");
|
|
10237
|
+
await this.onBeforeReconnect();
|
|
10238
|
+
}
|
|
9491
10239
|
}
|
|
9492
|
-
if (
|
|
10240
|
+
if (this.dpopManager?.initialized) try {
|
|
9493
10241
|
dpopToken = await this.dpopManager.createRpcProof({ method: "signalwire.connect" });
|
|
9494
10242
|
} catch (error) {
|
|
9495
10243
|
if (this.clientBound) throw error;
|
|
@@ -9509,7 +10257,9 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
9509
10257
|
protocol: persistedParams.protocol
|
|
9510
10258
|
} : {}
|
|
9511
10259
|
});
|
|
9512
|
-
|
|
10260
|
+
if (this.transport.connectionStatus !== "connected") throw new require_operators.TransportConnectionError("WebSocket closed during authentication");
|
|
10261
|
+
const socketClosed$ = this.transport.connectionStatus$.pipe((0, rxjs.skip)(1), (0, rxjs.filter)((status) => status === "reconnecting" || status === "disconnected"), (0, rxjs.take)(1), (0, rxjs.switchMap)(() => (0, rxjs.throwError)(() => new require_operators.TransportConnectionError("WebSocket closed during authentication"))));
|
|
10262
|
+
const response = await (0, rxjs.lastValueFrom)((0, rxjs.race)((0, rxjs.from)(this.transport.execute(rpcConnectRequest)), socketClosed$).pipe(require_operators.throwOnRPCError(), (0, rxjs.map)((res) => res.result), (0, rxjs.filter)(isRPCConnectResult), (0, rxjs.tap)(() => {
|
|
9513
10263
|
logger$9.debug("[Session] Response passed filter, processing authentication result");
|
|
9514
10264
|
}), (0, rxjs.take)(1), (0, rxjs.catchError)((err) => {
|
|
9515
10265
|
logger$9.error("[Session] Authentication RPC failed:", err);
|
|
@@ -9522,7 +10272,9 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
9522
10272
|
});
|
|
9523
10273
|
if (response.protocol) await this.transport.setProtocol(response.protocol);
|
|
9524
10274
|
this._authorization$.next(response.authorization);
|
|
10275
|
+
if (response.authorization.cnf?.jkt) this._wasClientBound = true;
|
|
9525
10276
|
this._iceServers$.next(response.ice_servers ?? []);
|
|
10277
|
+
this.transport.setAuthenticated();
|
|
9526
10278
|
this._authState$.next({ kind: "authenticated" });
|
|
9527
10279
|
logger$9.debug("[Session] Authentication completed successfully");
|
|
9528
10280
|
}
|
|
@@ -9588,7 +10340,7 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
9588
10340
|
to: destinationURI,
|
|
9589
10341
|
...options
|
|
9590
10342
|
});
|
|
9591
|
-
await (
|
|
10343
|
+
await awaitDialReady(callSession, require_operators.DEFAULT_CALL_SIGNALING_TIMEOUT_MS);
|
|
9592
10344
|
this._calls$.next({
|
|
9593
10345
|
[`${callSession.id}`]: callSession,
|
|
9594
10346
|
...this._calls$.value
|
|
@@ -9641,12 +10393,23 @@ var ClientSessionWrapper = class {
|
|
|
9641
10393
|
get authenticated() {
|
|
9642
10394
|
return this.clientSessionManager.authenticated;
|
|
9643
10395
|
}
|
|
10396
|
+
/**
|
|
10397
|
+
* Whether the session is using a Client Bound SAT (DPoP). Sticky — set
|
|
10398
|
+
* when the binding is established or restored from a resumed session's
|
|
10399
|
+
* server authorization.
|
|
10400
|
+
*/
|
|
10401
|
+
get clientBound() {
|
|
10402
|
+
return this.clientSessionManager.clientBound;
|
|
10403
|
+
}
|
|
9644
10404
|
get signalingEvent$() {
|
|
9645
10405
|
return this.clientSessionManager.signalingEvent$;
|
|
9646
10406
|
}
|
|
9647
10407
|
get iceServers() {
|
|
9648
10408
|
return this.clientSessionManager.iceServers;
|
|
9649
10409
|
}
|
|
10410
|
+
get callControl() {
|
|
10411
|
+
return this.clientSessionManager.callControl;
|
|
10412
|
+
}
|
|
9650
10413
|
async execute(request, options) {
|
|
9651
10414
|
return this.clientSessionManager.execute(request, options);
|
|
9652
10415
|
}
|
|
@@ -9841,7 +10604,7 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
9841
10604
|
await session.reauthenticate(tokenData.token, rpcProof, { clientBound: true });
|
|
9842
10605
|
updateCredential({ token: tokenData.token });
|
|
9843
10606
|
logger$7.info("[DeviceToken] Client Bound SAT activated successfully");
|
|
9844
|
-
this.
|
|
10607
|
+
this.emitCurrentToken(tokenData);
|
|
9845
10608
|
return { activated: true };
|
|
9846
10609
|
} catch (error) {
|
|
9847
10610
|
logger$7.error("[DeviceToken] Failed to activate Client Bound SAT:", error);
|
|
@@ -9853,6 +10616,21 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
9853
10616
|
}
|
|
9854
10617
|
}
|
|
9855
10618
|
/**
|
|
10619
|
+
* Emit a freshly received token to the reactive pipeline, stamping an
|
|
10620
|
+
* absolute `expires_at` when the response carried only `expires_in`.
|
|
10621
|
+
* Resolving the expiry at RECEIVE time (not at read time) is what lets
|
|
10622
|
+
* {@link refreshNowIfDue} detect due-ness on resume: a bare `expires_in`
|
|
10623
|
+
* re-resolved later would always compute a full TTL from "now" and never
|
|
10624
|
+
* cross the refresh buffer.
|
|
10625
|
+
*/
|
|
10626
|
+
emitCurrentToken(token) {
|
|
10627
|
+
const stamped = token.expires_at ? token : {
|
|
10628
|
+
...token,
|
|
10629
|
+
expires_at: resolveExpiresAt(token)
|
|
10630
|
+
};
|
|
10631
|
+
this._currentToken$.next(stamped);
|
|
10632
|
+
}
|
|
10633
|
+
/**
|
|
9856
10634
|
* Returns true when the cached token has enough headroom before expiry to
|
|
9857
10635
|
* be safely reused on reactivation. The headroom matches the refresh
|
|
9858
10636
|
* buffer, so a token within the refresh window is treated as stale (the
|
|
@@ -9870,7 +10648,7 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
9870
10648
|
method: "POST",
|
|
9871
10649
|
uri: require_operators.DEVICE_TOKEN_ENDPOINT
|
|
9872
10650
|
});
|
|
9873
|
-
const response = await this.http.request({
|
|
10651
|
+
const response = await this.http().request({
|
|
9874
10652
|
url: require_operators.DEVICE_TOKEN_ENDPOINT,
|
|
9875
10653
|
...POST_PARAMS,
|
|
9876
10654
|
body: JSON.stringify({
|
|
@@ -9897,7 +10675,7 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
9897
10675
|
uri: require_operators.DEVICE_REFRESH_ENDPOINT,
|
|
9898
10676
|
accessToken: currentToken
|
|
9899
10677
|
});
|
|
9900
|
-
const response = await this.http.request({
|
|
10678
|
+
const response = await this.http().request({
|
|
9901
10679
|
url: require_operators.DEVICE_REFRESH_ENDPOINT,
|
|
9902
10680
|
...POST_PARAMS,
|
|
9903
10681
|
body: JSON.stringify({
|
|
@@ -9945,7 +10723,7 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
9945
10723
|
const currentToken = this.getCredential().token;
|
|
9946
10724
|
if (!currentToken) throw new require_operators.TokenRefreshError("No current token available for refresh");
|
|
9947
10725
|
const newTokenData = await this.retryRefresh(session, currentToken, updateCredential);
|
|
9948
|
-
this.
|
|
10726
|
+
this.emitCurrentToken(newTokenData);
|
|
9949
10727
|
} catch (error) {
|
|
9950
10728
|
logger$7.error("[DeviceToken] Automatic Client Bound SAT refresh failed:", error);
|
|
9951
10729
|
this.errorHandler(error instanceof require_operators.TokenRefreshError ? error : new require_operators.TokenRefreshError("Automatic token refresh failed", error));
|
|
@@ -9972,6 +10750,22 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
9972
10750
|
throw lastError instanceof Error ? lastError : new require_operators.TokenRefreshError("All refresh retries exhausted", lastError);
|
|
9973
10751
|
}
|
|
9974
10752
|
/**
|
|
10753
|
+
* Force an immediate refresh when the cached Client Bound SAT is already
|
|
10754
|
+
* past its refresh window. Called on resume from suspension where
|
|
10755
|
+
* background-tab throttling can delay the reactive timer past the buffer.
|
|
10756
|
+
* A no-op when no token is cached or it still has headroom; the normal
|
|
10757
|
+
* {@link executeRefresh} guards (paused / in-progress / unauthenticated)
|
|
10758
|
+
* still apply.
|
|
10759
|
+
*/
|
|
10760
|
+
refreshNowIfDue() {
|
|
10761
|
+
const token = this._currentToken$.value;
|
|
10762
|
+
if (!token) return;
|
|
10763
|
+
if (resolveExpiresAt(token) * 1e3 - Date.now() <= require_operators.DEVICE_TOKEN_REFRESH_BUFFER_MS) {
|
|
10764
|
+
logger$7.debug("[DeviceToken] Resume: cached SAT past refresh window; refreshing now");
|
|
10765
|
+
this.executeRefresh();
|
|
10766
|
+
}
|
|
10767
|
+
}
|
|
10768
|
+
/**
|
|
9975
10769
|
* Stops the reactive refresh pipeline from firing. Use when the underlying
|
|
9976
10770
|
* session is being torn down (e.g., during {@link SignalWire.disconnect})
|
|
9977
10771
|
* so a scheduled refresh cannot fire against a destroyed session.
|
|
@@ -10022,6 +10816,7 @@ var CredentialRefreshCoordinator = class extends Destroyable {
|
|
|
10022
10816
|
this.deps = deps;
|
|
10023
10817
|
this._activating = false;
|
|
10024
10818
|
this._activationGeneration = 0;
|
|
10819
|
+
this._developerRefreshInProgress = false;
|
|
10025
10820
|
if (dpopManager?.initialized) this._deviceTokenManager = (deps.deviceTokenManagerFactory ?? defaultDeviceTokenManagerFactory)(dpopManager, deps.http, (error) => deps.notifier.onError(error), () => deps.store.read());
|
|
10026
10821
|
}
|
|
10027
10822
|
/** True when the Client Bound SAT path is available (DPoP initialized). */
|
|
@@ -10040,28 +10835,120 @@ var CredentialRefreshCoordinator = class extends Destroyable {
|
|
|
10040
10835
|
* invokes `deps.onRefreshExhausted` so the orchestrator can disconnect.
|
|
10041
10836
|
*/
|
|
10042
10837
|
scheduleDeveloperRefresh(provider, expiresAt, attempt = 0) {
|
|
10838
|
+
this._activeProvider = provider;
|
|
10043
10839
|
if (this._developerTimerId !== void 0) clearTimeout(this._developerTimerId);
|
|
10044
10840
|
const refreshInterval = attempt === 0 ? Math.max(expiresAt - Date.now() - require_operators.CREDENTIAL_REFRESH_BUFFER_MS, 1e3) : Math.min(require_operators.CREDENTIAL_REFRESH_RETRY_BASE_MS * Math.pow(2, attempt) * (.5 + Math.random() * .5), require_operators.CREDENTIAL_REFRESH_MAX_DELAY_MS);
|
|
10045
|
-
this._developerTimerId = setTimeout(
|
|
10841
|
+
this._developerTimerId = setTimeout(() => {
|
|
10842
|
+
this._developerTimerId = void 0;
|
|
10843
|
+
this.executeDeveloperRefresh(provider, expiresAt, attempt);
|
|
10844
|
+
}, refreshInterval);
|
|
10845
|
+
}
|
|
10846
|
+
/**
|
|
10847
|
+
* Runs the developer-provided refresh once: mints a new credential, stores
|
|
10848
|
+
* and persists it, reauthenticates the live session (via the notifier), and
|
|
10849
|
+
* reschedules against the new expiry. On failure retries with backoff up to
|
|
10850
|
+
* {@link CREDENTIAL_REFRESH_MAX_RETRIES}, then signals exhaustion.
|
|
10851
|
+
*
|
|
10852
|
+
* Shared by the scheduled timer tick and {@link forceRefreshIfDue}. The
|
|
10853
|
+
* `_developerRefreshInProgress` guard prevents the two from overlapping.
|
|
10854
|
+
*/
|
|
10855
|
+
async executeDeveloperRefresh(provider, expiresAt, attempt) {
|
|
10856
|
+
if (this._developerRefreshInProgress) {
|
|
10857
|
+
logger$6.debug("[Coordinator] Developer refresh already in progress; skipping");
|
|
10858
|
+
return;
|
|
10859
|
+
}
|
|
10860
|
+
this._developerRefreshInProgress = true;
|
|
10861
|
+
try {
|
|
10862
|
+
const newCredentials = await this.refreshCredential(provider);
|
|
10863
|
+
this.deps.store.write(newCredentials);
|
|
10864
|
+
this.deps.store.persist(newCredentials);
|
|
10046
10865
|
try {
|
|
10047
|
-
|
|
10048
|
-
|
|
10049
|
-
|
|
10050
|
-
this.deps.store.persist(newCredentials);
|
|
10051
|
-
logger$6.info("[Coordinator] Credentials refreshed successfully.");
|
|
10052
|
-
if (newCredentials.expiry_at) this.scheduleDeveloperRefresh(provider, newCredentials.expiry_at, 0);
|
|
10053
|
-
} catch (error) {
|
|
10054
|
-
const nextAttempt = attempt + 1;
|
|
10055
|
-
logger$6.error(`[Coordinator] Credential refresh failed (attempt ${nextAttempt}/${require_operators.CREDENTIAL_REFRESH_MAX_RETRIES}):`, error);
|
|
10056
|
-
this.deps.notifier.onError(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
10057
|
-
if (nextAttempt < require_operators.CREDENTIAL_REFRESH_MAX_RETRIES) this.scheduleDeveloperRefresh(provider, expiresAt, nextAttempt);
|
|
10058
|
-
else {
|
|
10059
|
-
logger$6.error("[Coordinator] Credential refresh exhausted all retries. Disconnecting.");
|
|
10060
|
-
this.deps.notifier.onError(new require_operators.TokenRefreshError("Credential refresh failed after max retries"));
|
|
10061
|
-
this.deps.notifier.onRefreshExhausted();
|
|
10062
|
-
}
|
|
10866
|
+
await this.deps.notifier.onCredentialRefreshed(newCredentials);
|
|
10867
|
+
} catch (reauthError) {
|
|
10868
|
+
logger$6.warn("[Coordinator] onCredentialRefreshed rejected (non-fatal):", reauthError);
|
|
10063
10869
|
}
|
|
10064
|
-
|
|
10870
|
+
logger$6.info("[Coordinator] Credentials refreshed successfully.");
|
|
10871
|
+
if (newCredentials.expiry_at) this.scheduleDeveloperRefresh(provider, newCredentials.expiry_at, 0);
|
|
10872
|
+
} catch (error) {
|
|
10873
|
+
const nextAttempt = attempt + 1;
|
|
10874
|
+
logger$6.error(`[Coordinator] Credential refresh failed (attempt ${nextAttempt}/${require_operators.CREDENTIAL_REFRESH_MAX_RETRIES}):`, error);
|
|
10875
|
+
this.deps.notifier.onError(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
10876
|
+
if (nextAttempt < require_operators.CREDENTIAL_REFRESH_MAX_RETRIES) this.scheduleDeveloperRefresh(provider, expiresAt, nextAttempt);
|
|
10877
|
+
else {
|
|
10878
|
+
logger$6.error("[Coordinator] Credential refresh exhausted all retries. Disconnecting.");
|
|
10879
|
+
this.deps.notifier.onError(new require_operators.TokenRefreshError("Credential refresh failed after max retries"));
|
|
10880
|
+
this.deps.notifier.onRefreshExhausted();
|
|
10881
|
+
}
|
|
10882
|
+
} finally {
|
|
10883
|
+
this._developerRefreshInProgress = false;
|
|
10884
|
+
}
|
|
10885
|
+
}
|
|
10886
|
+
/**
|
|
10887
|
+
* Force an immediate refresh when the current credential is already past its
|
|
10888
|
+
* scheduled refresh window. Called on resume from suspension, where
|
|
10889
|
+
* background-tab timer throttling can delay the armed refresh well past
|
|
10890
|
+
* expiry, leaving the live session stale.
|
|
10891
|
+
*
|
|
10892
|
+
* Routes to whichever mechanism is armed: the developer timer if armed,
|
|
10893
|
+
* otherwise the Client Bound SAT pipeline. A no-op when nothing is due.
|
|
10894
|
+
*/
|
|
10895
|
+
forceRefreshIfDue() {
|
|
10896
|
+
if (this._developerTimerId !== void 0 && this._activeProvider) {
|
|
10897
|
+
const expiry = this.deps.store.read().expiry_at;
|
|
10898
|
+
if (expiry !== void 0 && Date.now() >= expiry - require_operators.CREDENTIAL_REFRESH_BUFFER_MS) {
|
|
10899
|
+
logger$6.debug("[Coordinator] Resume: credential past refresh window; forcing refresh");
|
|
10900
|
+
clearTimeout(this._developerTimerId);
|
|
10901
|
+
this._developerTimerId = void 0;
|
|
10902
|
+
this.executeDeveloperRefresh(this._activeProvider, expiry, 0);
|
|
10903
|
+
}
|
|
10904
|
+
return;
|
|
10905
|
+
}
|
|
10906
|
+
this._deviceTokenManager?.refreshNowIfDue();
|
|
10907
|
+
}
|
|
10908
|
+
/**
|
|
10909
|
+
* Sync the credential's expiry from the server-provided authorization (the
|
|
10910
|
+
* `signalwire.connect` result). SATs are opaque JWE, so
|
|
10911
|
+
* `fabric_subscriber.expires_at` is the authoritative expiry of the token
|
|
10912
|
+
* the session actually connected with — the provider-reported `expiry_at`
|
|
10913
|
+
* is only a hint (and may be wrong or absent). Corrects the stored
|
|
10914
|
+
* credential and re-arms the developer refresh timer against the real
|
|
10915
|
+
* deadline when the provider supports `refresh()`.
|
|
10916
|
+
*/
|
|
10917
|
+
syncExpiryFromAuthorization(authorization, provider) {
|
|
10918
|
+
const expiresAtSec = authorization?.fabric_subscriber?.expires_at;
|
|
10919
|
+
if (!expiresAtSec) return;
|
|
10920
|
+
const expiryAt = expiresAtSec * 1e3;
|
|
10921
|
+
const credential = this.deps.store.read();
|
|
10922
|
+
if (credential.expiry_at === expiryAt) return;
|
|
10923
|
+
logger$6.debug(`[Coordinator] Correcting credential expiry from server authorization: ${new Date(expiryAt).toISOString()}`);
|
|
10924
|
+
const updated = {
|
|
10925
|
+
...credential,
|
|
10926
|
+
expiry_at: expiryAt
|
|
10927
|
+
};
|
|
10928
|
+
this.deps.store.write(updated);
|
|
10929
|
+
this.deps.store.persist(updated);
|
|
10930
|
+
if (provider?.refresh) this.scheduleDeveloperRefresh(provider, expiryAt);
|
|
10931
|
+
}
|
|
10932
|
+
/**
|
|
10933
|
+
* Invoke `provider.refresh()` deduped against any concurrent developer
|
|
10934
|
+
* refresh. Concurrent callers — the scheduled tick, a resume-forced refresh,
|
|
10935
|
+
* and the orchestrator's -32003 recovery / reconnect re-mint — share one
|
|
10936
|
+
* in-flight promise, so a provider backed by one-time-use rotating refresh
|
|
10937
|
+
* tokens is never invoked twice in parallel.
|
|
10938
|
+
*
|
|
10939
|
+
* The caller owns applying the returned credential (store write, session
|
|
10940
|
+
* reauth, rescheduling); this method only serializes the network call.
|
|
10941
|
+
*/
|
|
10942
|
+
async refreshCredential(provider) {
|
|
10943
|
+
if (this._refreshInFlight) return this._refreshInFlight;
|
|
10944
|
+
if (!provider.refresh) throw new require_operators.InvalidCredentialsError("Credential provider does not support refresh");
|
|
10945
|
+
const run = provider.refresh();
|
|
10946
|
+
this._refreshInFlight = run;
|
|
10947
|
+
const clear = () => {
|
|
10948
|
+
if (this._refreshInFlight === run) this._refreshInFlight = void 0;
|
|
10949
|
+
};
|
|
10950
|
+
run.then(clear, clear);
|
|
10951
|
+
return run;
|
|
10065
10952
|
}
|
|
10066
10953
|
/**
|
|
10067
10954
|
* Cancels any scheduled developer-provided refresh. Idempotent.
|
|
@@ -10580,6 +11467,7 @@ var WebSocketController = class WebSocketController extends Destroyable {
|
|
|
10580
11467
|
}
|
|
10581
11468
|
handleClose(_event) {
|
|
10582
11469
|
this.clearConnectionTimeout();
|
|
11470
|
+
this.messageQueue = [];
|
|
10583
11471
|
if (this.shouldReconnect) {
|
|
10584
11472
|
this._status$.next("reconnecting");
|
|
10585
11473
|
this.scheduleReconnection();
|
|
@@ -10649,6 +11537,9 @@ var WebSocketController = class WebSocketController extends Destroyable {
|
|
|
10649
11537
|
function isSignalwirePingRequest(value) {
|
|
10650
11538
|
return isJSONRPCRequest(value) && value.method === "signalwire.ping";
|
|
10651
11539
|
}
|
|
11540
|
+
function isSignalwireConnectRequest(value) {
|
|
11541
|
+
return isJSONRPCRequest(value) && value.method === "signalwire.connect";
|
|
11542
|
+
}
|
|
10652
11543
|
|
|
10653
11544
|
//#endregion
|
|
10654
11545
|
//#region src/managers/TransportManager.ts
|
|
@@ -10662,6 +11553,10 @@ var TransportManager = class extends Destroyable {
|
|
|
10662
11553
|
this.protocol$ = this.createReplaySubject(1);
|
|
10663
11554
|
this.isConnecting = false;
|
|
10664
11555
|
this.isConnected = false;
|
|
11556
|
+
this.socketStatus = "disconnected";
|
|
11557
|
+
this.sessionAuthenticated = false;
|
|
11558
|
+
this.heldRequests = [];
|
|
11559
|
+
this.serverPingWatch = 0;
|
|
10665
11560
|
this.ackEvent = () => {
|
|
10666
11561
|
return (0, rxjs.tap)((message) => {
|
|
10667
11562
|
if (isSignalwireRequest(message)) try {
|
|
@@ -10677,6 +11572,7 @@ var TransportManager = class extends Destroyable {
|
|
|
10677
11572
|
if (isSignalwirePingRequest(message)) {
|
|
10678
11573
|
try {
|
|
10679
11574
|
logger$2.debug("[Transport] Received ping, sending pong", { pingId: message.id });
|
|
11575
|
+
if (this.sessionAuthenticated) this.armServerPingWatchdog();
|
|
10680
11576
|
this.send(RPCPingResponse(message.id));
|
|
10681
11577
|
} catch (error) {
|
|
10682
11578
|
logger$2.error("[Transport] Failed to send ping response:", error);
|
|
@@ -10714,6 +11610,14 @@ var TransportManager = class extends Destroyable {
|
|
|
10714
11610
|
this.subscribeTo(this._webSocketConnections.errors$, (error) => {
|
|
10715
11611
|
this.onError?.(error);
|
|
10716
11612
|
});
|
|
11613
|
+
this.subscribeTo(this._webSocketConnections.status$, (status) => {
|
|
11614
|
+
this.socketStatus = status;
|
|
11615
|
+
if (status !== "connected") {
|
|
11616
|
+
this.sessionAuthenticated = false;
|
|
11617
|
+
this.clearServerPingWatchdog();
|
|
11618
|
+
}
|
|
11619
|
+
if (status === "disconnected") this.heldRequests = [];
|
|
11620
|
+
});
|
|
10717
11621
|
this.initialized$ = (0, rxjs.defer)(() => (0, rxjs.from)(this._init())).pipe((0, rxjs.shareReplay)(1), (0, rxjs.takeUntil)(this.destroyed$));
|
|
10718
11622
|
this._jsonRPCMessage$ = this._webSocketConnections.incomingMessages$.pipe((0, rxjs.map)((event) => {
|
|
10719
11623
|
try {
|
|
@@ -10742,6 +11646,9 @@ var TransportManager = class extends Destroyable {
|
|
|
10742
11646
|
get connectionStatus$() {
|
|
10743
11647
|
return this._webSocketConnections.status$;
|
|
10744
11648
|
}
|
|
11649
|
+
get connectionStatus() {
|
|
11650
|
+
return this.socketStatus;
|
|
11651
|
+
}
|
|
10745
11652
|
async connect() {
|
|
10746
11653
|
if (this.isConnecting || this.isConnected) {
|
|
10747
11654
|
logger$2.warn("[Transport] Already connecting or connected");
|
|
@@ -10786,16 +11693,45 @@ var TransportManager = class extends Destroyable {
|
|
|
10786
11693
|
}
|
|
10787
11694
|
async execute(request, options) {
|
|
10788
11695
|
this.send(request);
|
|
10789
|
-
|
|
11696
|
+
const pending = new PendingRPC(request, this._jsonRPCResponse$, options).promise;
|
|
11697
|
+
pending.catch(() => {
|
|
11698
|
+
this.heldRequests = this.heldRequests.filter((held) => held.id !== request.id);
|
|
11699
|
+
});
|
|
11700
|
+
return pending;
|
|
10790
11701
|
}
|
|
10791
11702
|
send(message) {
|
|
10792
11703
|
const payload = JSON.stringify(message);
|
|
11704
|
+
if (isJSONRPCResponse(message)) {
|
|
11705
|
+
if (this.socketStatus !== "connected") {
|
|
11706
|
+
logger$2.debug("[Transport] Dropping response while disconnected", { id: message.id });
|
|
11707
|
+
return;
|
|
11708
|
+
}
|
|
11709
|
+
} else if (!this.sessionAuthenticated && !isSignalwireConnectRequest(message)) {
|
|
11710
|
+
this.heldRequests.push({
|
|
11711
|
+
id: message.id,
|
|
11712
|
+
payload
|
|
11713
|
+
});
|
|
11714
|
+
return;
|
|
11715
|
+
}
|
|
10793
11716
|
this._outgoingMessages$.next(payload);
|
|
10794
11717
|
}
|
|
11718
|
+
/**
|
|
11719
|
+
* Called by the session when signalwire.connect succeeds on the current
|
|
11720
|
+
* socket. Sends the requests held since the socket opened, in order.
|
|
11721
|
+
*/
|
|
11722
|
+
setAuthenticated() {
|
|
11723
|
+
if (this.socketStatus !== "connected") return;
|
|
11724
|
+
this.sessionAuthenticated = true;
|
|
11725
|
+
this.armServerPingWatchdog();
|
|
11726
|
+
const held = this.heldRequests;
|
|
11727
|
+
this.heldRequests = [];
|
|
11728
|
+
held.forEach(({ payload }) => this._outgoingMessages$.next(payload));
|
|
11729
|
+
}
|
|
10795
11730
|
disconnect() {
|
|
10796
11731
|
logger$2.debug("[Transport] Disconnecting");
|
|
10797
11732
|
this.isConnected = false;
|
|
10798
11733
|
this.isConnecting = false;
|
|
11734
|
+
this.clearServerPingWatchdog();
|
|
10799
11735
|
this._webSocketConnections.disconnect();
|
|
10800
11736
|
}
|
|
10801
11737
|
destroy() {
|
|
@@ -10804,6 +11740,28 @@ var TransportManager = class extends Destroyable {
|
|
|
10804
11740
|
super.destroy();
|
|
10805
11741
|
this._webSocketConnections.destroy();
|
|
10806
11742
|
}
|
|
11743
|
+
armServerPingWatchdog() {
|
|
11744
|
+
this.clearServerPingWatchdog();
|
|
11745
|
+
const watch = this.serverPingWatch;
|
|
11746
|
+
this.serverPingTimer = setTimeout(() => void this.probeServer(watch), require_operators.SERVER_PING_TIMEOUT_MS);
|
|
11747
|
+
}
|
|
11748
|
+
/** Bumps the watch so a probe still in flight no longer acts on its result. */
|
|
11749
|
+
clearServerPingWatchdog() {
|
|
11750
|
+
clearTimeout(this.serverPingTimer);
|
|
11751
|
+
this.serverPingTimer = void 0;
|
|
11752
|
+
this.serverPingWatch += 1;
|
|
11753
|
+
}
|
|
11754
|
+
async probeServer(watch) {
|
|
11755
|
+
logger$2.debug(`[Transport] No signalwire.ping for ${require_operators.SERVER_PING_TIMEOUT_MS}ms, probing the server`);
|
|
11756
|
+
try {
|
|
11757
|
+
await this.execute(RPCPing(), { timeoutMs: require_operators.SERVER_PING_PROBE_TIMEOUT_MS });
|
|
11758
|
+
if (watch === this.serverPingWatch) this.armServerPingWatchdog();
|
|
11759
|
+
} catch (error) {
|
|
11760
|
+
if (watch !== this.serverPingWatch) return;
|
|
11761
|
+
logger$2.warn("[Transport] Server did not answer the ping probe, replacing the socket", error);
|
|
11762
|
+
this.reconnect();
|
|
11763
|
+
}
|
|
11764
|
+
}
|
|
10807
11765
|
async _loadProtocolFromStorage() {
|
|
10808
11766
|
try {
|
|
10809
11767
|
const storedProtocol = await this.storage.getItem(this.protocolKey);
|
|
@@ -10841,6 +11799,13 @@ var TransportManager = class extends Destroyable {
|
|
|
10841
11799
|
//#endregion
|
|
10842
11800
|
//#region src/clients/SignalWire.ts
|
|
10843
11801
|
const logger$1 = require_operators.getLogger();
|
|
11802
|
+
/**
|
|
11803
|
+
* Storage key for the client-bound marker. The SAT and authorization_state are
|
|
11804
|
+
* both opaque to the SDK, so on a page reload the preflight recovery — which
|
|
11805
|
+
* runs before any session exists — has no other way to know the session was
|
|
11806
|
+
* client-bound. See {@link SignalWire.persistClientBoundMarker}.
|
|
11807
|
+
*/
|
|
11808
|
+
const CLIENT_BOUND_STORAGE_KEY = "sw:client_bound";
|
|
10844
11809
|
const buildOptionsFromDestination = (destination) => {
|
|
10845
11810
|
if (typeof destination === "string") {
|
|
10846
11811
|
const queryStartIndex = destination.indexOf("?");
|
|
@@ -10884,6 +11849,7 @@ var SignalWire = class extends Destroyable {
|
|
|
10884
11849
|
this.preferences = new ClientPreferences();
|
|
10885
11850
|
this._user$ = this.createBehaviorSubject(void 0);
|
|
10886
11851
|
this._directory$ = this.createBehaviorSubject(void 0);
|
|
11852
|
+
this._credentialRecovered = false;
|
|
10887
11853
|
this._isConnected$ = this.createBehaviorSubject(false);
|
|
10888
11854
|
this._isRegistered$ = this.createBehaviorSubject(false);
|
|
10889
11855
|
this._errors$ = this.createReplaySubject(1);
|
|
@@ -10922,6 +11888,16 @@ var SignalWire = class extends Destroyable {
|
|
|
10922
11888
|
});
|
|
10923
11889
|
}
|
|
10924
11890
|
/**
|
|
11891
|
+
* Build the refresh path's own HTTP controller, against whatever host is current.
|
|
11892
|
+
*
|
|
11893
|
+
* Called on first use rather than up front, so `apiHost` already reflects the
|
|
11894
|
+
* token's `ch` claim. Same credential source as the container's controller — only
|
|
11895
|
+
* the instance, and therefore its observable streams, is separate.
|
|
11896
|
+
*/
|
|
11897
|
+
createRefreshHttpController() {
|
|
11898
|
+
return new HTTPRequestController(this._deps.apiHost, () => this._deps.credential);
|
|
11899
|
+
}
|
|
11900
|
+
/**
|
|
10925
11901
|
* Initializes DPoP if not already set up. Returns the fingerprint on success.
|
|
10926
11902
|
*/
|
|
10927
11903
|
async initDPoP() {
|
|
@@ -10948,11 +11924,19 @@ var SignalWire = class extends Destroyable {
|
|
|
10948
11924
|
async resolveCredentials() {
|
|
10949
11925
|
const fingerprint = await this.initDPoP();
|
|
10950
11926
|
this._refreshCoordinator = new CredentialRefreshCoordinator(this._dpopManager, {
|
|
10951
|
-
http:
|
|
11927
|
+
http: () => {
|
|
11928
|
+
if (!this._refreshHttp || this._refreshHttpHost !== this._deps.apiHost) {
|
|
11929
|
+
this._refreshHttp?.destroy();
|
|
11930
|
+
this._refreshHttp = this.createRefreshHttpController();
|
|
11931
|
+
this._refreshHttpHost = this._deps.apiHost;
|
|
11932
|
+
}
|
|
11933
|
+
return this._refreshHttp;
|
|
11934
|
+
},
|
|
10952
11935
|
notifier: {
|
|
10953
11936
|
onError: (error) => this._errors$.next(error),
|
|
10954
11937
|
onWarning: (warning) => this._warnings$.next(warning),
|
|
10955
|
-
onRefreshExhausted: () => void this.disconnect()
|
|
11938
|
+
onRefreshExhausted: () => void this.disconnect(),
|
|
11939
|
+
onCredentialRefreshed: async (credential) => this.reauthenticateLiveSession(credential)
|
|
10956
11940
|
},
|
|
10957
11941
|
store: {
|
|
10958
11942
|
read: () => this._deps.credential,
|
|
@@ -10964,6 +11948,7 @@ var SignalWire = class extends Destroyable {
|
|
|
10964
11948
|
...this._deps.credential,
|
|
10965
11949
|
...partial
|
|
10966
11950
|
};
|
|
11951
|
+
this.persistCredential(this._deps.credential);
|
|
10967
11952
|
},
|
|
10968
11953
|
persist: (credential) => this.persistCredential(credential)
|
|
10969
11954
|
}
|
|
@@ -11009,20 +11994,181 @@ var SignalWire = class extends Destroyable {
|
|
|
11009
11994
|
}
|
|
11010
11995
|
this._deps.credential = _credentials;
|
|
11011
11996
|
this.persistCredential(_credentials);
|
|
11012
|
-
|
|
11013
|
-
|
|
11997
|
+
await this.reauthenticateLiveSession(_credentials);
|
|
11998
|
+
}
|
|
11999
|
+
/**
|
|
12000
|
+
* Reauthenticate the currently-open session with a freshly obtained
|
|
12001
|
+
* credential so the new token takes effect on the live socket immediately —
|
|
12002
|
+
* not just on the next reconnect. No-op when the session is not
|
|
12003
|
+
* connected/authenticated or the credential carries no token (e.g. an
|
|
12004
|
+
* authorization-state-only refresh). Non-fatal: reauth failures surface on
|
|
12005
|
+
* `errors$` without aborting the refresh that triggered this.
|
|
12006
|
+
*/
|
|
12007
|
+
async reauthenticateLiveSession(credential) {
|
|
12008
|
+
if (!this.isConnected || !this._clientSession.authenticated || !credential.token) return;
|
|
12009
|
+
try {
|
|
12010
|
+
await this._clientSession.reauthenticate(credential.token);
|
|
11014
12011
|
logger$1.info("[SignalWire] Session refreshed with new credentials.");
|
|
11015
12012
|
} catch (error) {
|
|
11016
12013
|
logger$1.error("[SignalWire] Failed to refresh session with new credentials:", error);
|
|
11017
12014
|
this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
11018
12015
|
}
|
|
11019
12016
|
}
|
|
12017
|
+
/**
|
|
12018
|
+
* Recover a session the server is refusing: mint a fresh credential,
|
|
12019
|
+
* reauthenticate the live session with it, and retry the operation.
|
|
12020
|
+
*
|
|
12021
|
+
* The connection is deliberately kept. A reload authenticates the new socket
|
|
12022
|
+
* against the persisted `authorization_state`, and that handshake is what
|
|
12023
|
+
* associates the socket with the previous session — the association reattach
|
|
12024
|
+
* depends on. `signalwire.reauthenticate` swaps the credential *on that same
|
|
12025
|
+
* session*, so recovery never touches the resume state. Discarding it would
|
|
12026
|
+
* heal the credential by destroying the very thing the caller is trying to
|
|
12027
|
+
* get back to.
|
|
12028
|
+
*
|
|
12029
|
+
* The operation is still the verdict, never the RPC. Reauthenticating with
|
|
12030
|
+
* the in-memory token is accepted by a resume even while requests stay
|
|
12031
|
+
* refused, because the persisted `authorization_state` short-circuits token
|
|
12032
|
+
* validation — and `signalwire.reauthenticate` with a *freshly minted* token
|
|
12033
|
+
* has also been observed accepted while `subscriber.online` keeps being
|
|
12034
|
+
* refused (staging run 33826974634). Both look like success and are not.
|
|
12035
|
+
*
|
|
12036
|
+
* @returns the operation's value, or the reason recovery could not deliver
|
|
12037
|
+
* one. `error` is undefined when there was no way to mint at all.
|
|
12038
|
+
*/
|
|
12039
|
+
async recoverAndRetry(operation) {
|
|
12040
|
+
if (!await this.remintAndReauthenticate()) return { ok: false };
|
|
12041
|
+
try {
|
|
12042
|
+
const value = await operation();
|
|
12043
|
+
this._credentialRecovered = true;
|
|
12044
|
+
return {
|
|
12045
|
+
ok: true,
|
|
12046
|
+
value
|
|
12047
|
+
};
|
|
12048
|
+
} catch (error) {
|
|
12049
|
+
logger$1.warn("[SignalWire] Reauthentication was accepted but the operation is still refused:", error);
|
|
12050
|
+
return {
|
|
12051
|
+
ok: false,
|
|
12052
|
+
error
|
|
12053
|
+
};
|
|
12054
|
+
}
|
|
12055
|
+
}
|
|
12056
|
+
/**
|
|
12057
|
+
* Re-mint a credential and adopt it only if the live session accepts it.
|
|
12058
|
+
*
|
|
12059
|
+
* The mechanism follows the binding: a client-bound session re-mints a bound
|
|
12060
|
+
* base SAT through `authenticate()` with the DPoP fingerprint, because the
|
|
12061
|
+
* developer refresh handler would hand back an unbound token and silently
|
|
12062
|
+
* degrade the session. An unbound session uses the refresh handler. Rotation
|
|
12063
|
+
* cost is not a reason to skip this — the only reason is having no mechanism.
|
|
12064
|
+
*
|
|
12065
|
+
* @returns whether the session is now running on a freshly accepted credential.
|
|
12066
|
+
*/
|
|
12067
|
+
async remintAndReauthenticate() {
|
|
12068
|
+
const provider = this._credentialProvider;
|
|
12069
|
+
if (!provider) return false;
|
|
12070
|
+
const { clientBound } = this._clientSession;
|
|
12071
|
+
if (!clientBound && !provider.refresh) {
|
|
12072
|
+
logger$1.debug("[SignalWire] [SW-NO-REFRESH-HANDLER] Unbound session with no refresh handler; cannot re-mint.");
|
|
12073
|
+
return false;
|
|
12074
|
+
}
|
|
12075
|
+
try {
|
|
12076
|
+
const newCredentials = clientBound ? await provider.authenticate(this._dpopManager?.initialized ? { fingerprint: this._dpopManager.fingerprint } : void 0) : await this.remintCredential(provider);
|
|
12077
|
+
if (!newCredentials.token) {
|
|
12078
|
+
logger$1.warn("[SignalWire] Re-minted credential has no token; keeping the current one.");
|
|
12079
|
+
return false;
|
|
12080
|
+
}
|
|
12081
|
+
await this._clientSession.reauthenticate(newCredentials.token);
|
|
12082
|
+
this._deps.credential = newCredentials;
|
|
12083
|
+
this.persistCredential(newCredentials);
|
|
12084
|
+
if (newCredentials.expiry_at && provider.refresh) this._refreshCoordinator?.scheduleDeveloperRefresh(provider, newCredentials.expiry_at);
|
|
12085
|
+
return true;
|
|
12086
|
+
} catch (error) {
|
|
12087
|
+
logger$1.warn("[SignalWire] Re-mint recovery failed:", error);
|
|
12088
|
+
return false;
|
|
12089
|
+
}
|
|
12090
|
+
}
|
|
12091
|
+
/**
|
|
12092
|
+
* Re-mint a credential via `provider.refresh()`, routed through the
|
|
12093
|
+
* coordinator's shared in-flight guard so concurrent re-mint paths (a
|
|
12094
|
+
* scheduled/resume refresh, -32003 recovery, and reconnect) never fire a
|
|
12095
|
+
* second `provider.refresh()` in parallel — which rotating one-time-use
|
|
12096
|
+
* refresh tokens reject. Falls back to a direct call only if the coordinator
|
|
12097
|
+
* has not been constructed yet.
|
|
12098
|
+
*/
|
|
12099
|
+
async remintCredential(provider) {
|
|
12100
|
+
if (this._refreshCoordinator) return this._refreshCoordinator.refreshCredential(provider);
|
|
12101
|
+
if (!provider.refresh) throw new require_operators.InvalidCredentialsError("Credential provider does not support refresh");
|
|
12102
|
+
return provider.refresh();
|
|
12103
|
+
}
|
|
12104
|
+
/**
|
|
12105
|
+
* Re-mint credentials before a fresh (re)connect (`onBeforeReconnect` hook).
|
|
12106
|
+
* The session invokes this only when it is client-bound OR the in-memory
|
|
12107
|
+
* token is expired. The re-mint mechanism depends on the binding:
|
|
12108
|
+
* - Client-bound: `authenticate()` with the DPoP fingerprint to obtain a
|
|
12109
|
+
* fresh base SAT the upcoming reconnect can re-bind (the
|
|
12110
|
+
* DeviceTokenManager re-activates afterwards).
|
|
12111
|
+
* - Unbound: the developer's non-interactive `refresh()` handler.
|
|
12112
|
+
* `authenticate()` is deliberately NOT used here — it may be interactive
|
|
12113
|
+
* (a login prompt) and must not fire on a background reconnect.
|
|
12114
|
+
*
|
|
12115
|
+
* Rejects on failure so the session aborts the reconnect rather than
|
|
12116
|
+
* replaying a stale token.
|
|
12117
|
+
*/
|
|
12118
|
+
async refreshCredentialForReconnect() {
|
|
12119
|
+
if (!this._credentialProvider) return;
|
|
12120
|
+
try {
|
|
12121
|
+
let newCredentials;
|
|
12122
|
+
if (this._clientSession?.clientBound ?? await this.wasClientBound()) {
|
|
12123
|
+
logger$1.debug("[SignalWire] Re-minting client-bound base SAT before reconnect");
|
|
12124
|
+
newCredentials = await this._credentialProvider.authenticate(this._dpopManager?.initialized ? { fingerprint: this._dpopManager.fingerprint } : void 0);
|
|
12125
|
+
} else if (this._credentialProvider.refresh) {
|
|
12126
|
+
logger$1.debug("[SignalWire] Refreshing unbound credential before reconnect");
|
|
12127
|
+
newCredentials = await this.remintCredential(this._credentialProvider);
|
|
12128
|
+
} else {
|
|
12129
|
+
logger$1.warn("[SignalWire] [SW-NO-REFRESH-HANDLER] Token expired on reconnect but no refresh handler; reconnecting with the existing token.");
|
|
12130
|
+
return;
|
|
12131
|
+
}
|
|
12132
|
+
if (!newCredentials.token) {
|
|
12133
|
+
logger$1.warn("[SignalWire] Re-minted credential has no token; keeping the existing credential for reconnect.");
|
|
12134
|
+
return;
|
|
12135
|
+
}
|
|
12136
|
+
this._deps.credential = newCredentials;
|
|
12137
|
+
this.persistCredential(newCredentials);
|
|
12138
|
+
if (newCredentials.expiry_at && this._credentialProvider.refresh) this._refreshCoordinator?.scheduleDeveloperRefresh(this._credentialProvider, newCredentials.expiry_at);
|
|
12139
|
+
logger$1.debug("[SignalWire] Credential refreshed successfully for reconnect");
|
|
12140
|
+
} catch (error) {
|
|
12141
|
+
logger$1.error("[SignalWire] Failed to refresh credentials for reconnect:", error);
|
|
12142
|
+
this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
12143
|
+
throw error;
|
|
12144
|
+
}
|
|
12145
|
+
}
|
|
11020
12146
|
/** Persist credential to localStorage when persistSession is enabled. */
|
|
11021
12147
|
persistCredential(credential) {
|
|
11022
12148
|
if (!credential.token) return;
|
|
11023
12149
|
this._deps.storage.setItem("sw:cached_credential", credential);
|
|
11024
12150
|
if (this._deps.persistSession) this._deps.storage.setItem("sw:cached_credential", credential, "local");
|
|
11025
12151
|
}
|
|
12152
|
+
/**
|
|
12153
|
+
* Persist whether the session is client-bound, mirroring the credential's
|
|
12154
|
+
* storage scopes so it survives a reload. The preflight recovery reads it
|
|
12155
|
+
* before any session exists to decide whether to re-bind via `authenticate()`
|
|
12156
|
+
* or refresh an unbound token; the marker tracks the latest binding, so an
|
|
12157
|
+
* unbound reconnect clears a stale marker from an earlier client-bound login.
|
|
12158
|
+
*/
|
|
12159
|
+
persistClientBoundMarker(bound) {
|
|
12160
|
+
const scopes = this._deps.persistSession ? ["session", "local"] : ["session"];
|
|
12161
|
+
for (const scope of scopes) if (bound) this._deps.storage.setItem(CLIENT_BOUND_STORAGE_KEY, true, scope);
|
|
12162
|
+
else this._deps.storage.removeItem(CLIENT_BOUND_STORAGE_KEY, scope);
|
|
12163
|
+
}
|
|
12164
|
+
/** Read the persisted client-bound marker (see {@link persistClientBoundMarker}). */
|
|
12165
|
+
async wasClientBound() {
|
|
12166
|
+
const scopes = this._deps.persistSession ? ["local", "session"] : ["session"];
|
|
12167
|
+
for (const scope of scopes) try {
|
|
12168
|
+
if (await this._deps.storage.getItem(CLIENT_BOUND_STORAGE_KEY, scope)) return true;
|
|
12169
|
+
} catch {}
|
|
12170
|
+
return false;
|
|
12171
|
+
}
|
|
11026
12172
|
async init() {
|
|
11027
12173
|
this._user$.next(new User(this._deps.http));
|
|
11028
12174
|
if (!this._options.skipConnection) await this.connect();
|
|
@@ -11046,6 +12192,42 @@ var SignalWire = class extends Destroyable {
|
|
|
11046
12192
|
} catch (error) {
|
|
11047
12193
|
logger$1.error("[SignalWire] Failed to reattach calls:", error);
|
|
11048
12194
|
this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
12195
|
+
} finally {
|
|
12196
|
+
this._credentialRecovered = false;
|
|
12197
|
+
}
|
|
12198
|
+
}
|
|
12199
|
+
/**
|
|
12200
|
+
* Fetch the authenticated user profile, recovering a stale credential.
|
|
12201
|
+
*
|
|
12202
|
+
* On a reload the persisted credential can be expired. Unlike the WS resume —
|
|
12203
|
+
* which the server accepts against the persisted `authorization_state` even
|
|
12204
|
+
* with an expired token — this REST preflight has no such short-circuit and is
|
|
12205
|
+
* refused (401). There is no session yet to reauthenticate, so recovery
|
|
12206
|
+
* re-mints the credential through the provider ({@link refreshCredentialForReconnect})
|
|
12207
|
+
* and retries with a FRESH {@link User}: Fetchable memoizes its result
|
|
12208
|
+
* (shareReplay), so reusing the instance would replay the 401 instead of
|
|
12209
|
+
* re-fetching with the new token. Without the user id the transport/session —
|
|
12210
|
+
* and the reattach a reload is trying to preserve — cannot even be addressed.
|
|
12211
|
+
*/
|
|
12212
|
+
async fetchUserOrRecover() {
|
|
12213
|
+
const fetchUser = async (user$1) => {
|
|
12214
|
+
if (!await (0, rxjs.firstValueFrom)(user$1.fetched$)) throw new require_operators.UnexpectedError("Failed to fetch user information - fetched$ emitted false");
|
|
12215
|
+
this._deps.user = user$1;
|
|
12216
|
+
};
|
|
12217
|
+
const user = this._user$.value;
|
|
12218
|
+
if (!user) throw new require_operators.UnexpectedError("User not initialized before connect");
|
|
12219
|
+
try {
|
|
12220
|
+
await fetchUser(user);
|
|
12221
|
+
} catch (firstError) {
|
|
12222
|
+
logger$1.error(`[SignalWire] Failed to fetch user information: ${firstError instanceof Error ? firstError.message : "Unknown error"}. This usually means the user token is invalid or expired. Re-minting the credential and retrying.`);
|
|
12223
|
+
try {
|
|
12224
|
+
await this.refreshCredentialForReconnect();
|
|
12225
|
+
const refetched = new User(this._deps.http);
|
|
12226
|
+
await fetchUser(refetched);
|
|
12227
|
+
this._user$.next(refetched);
|
|
12228
|
+
} catch (retryError) {
|
|
12229
|
+
throw new require_operators.UnexpectedError("Error fetching user information", { cause: retryError });
|
|
12230
|
+
}
|
|
11049
12231
|
}
|
|
11050
12232
|
}
|
|
11051
12233
|
/**
|
|
@@ -11087,40 +12269,23 @@ var SignalWire = class extends Destroyable {
|
|
|
11087
12269
|
*/
|
|
11088
12270
|
async connect() {
|
|
11089
12271
|
await this.teardownTransportAndSession();
|
|
11090
|
-
|
|
11091
|
-
const user = this._user$.value;
|
|
11092
|
-
if (!user) throw new require_operators.UnexpectedError("User not initialized before connect");
|
|
11093
|
-
if (!await (0, rxjs.firstValueFrom)(user.fetched$)) throw new require_operators.UnexpectedError("Failed to fetch user information - fetched$ emitted false");
|
|
11094
|
-
this._deps.user = user;
|
|
11095
|
-
} catch (error) {
|
|
11096
|
-
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.`);
|
|
11097
|
-
throw new require_operators.UnexpectedError("Error fetching user information", { cause: error });
|
|
11098
|
-
}
|
|
12272
|
+
await this.fetchUserOrRecover();
|
|
11099
12273
|
const errorHandler = (error) => {
|
|
11100
12274
|
this._errors$.next(error);
|
|
11101
12275
|
};
|
|
11102
12276
|
this._transport = new TransportManager(this._deps.storage, this._deps.protocolKey, this._deps.WebSocket, PreferencesContainer.instance.relayHost ?? this._deps.relayHost, errorHandler);
|
|
11103
|
-
this._attachManager = new AttachManager(this._deps.storage, this._deps.deviceController, PreferencesContainer.instance.reconnectCallsTimeout, this._deps.attachedCallsKey);
|
|
12277
|
+
this._attachManager = new AttachManager(this._deps.storage, this._deps.deviceController, PreferencesContainer.instance.reconnectCallsTimeout, this._deps.attachedCallsKey, () => this._credentialRecovered);
|
|
11104
12278
|
this._clientSession = new ClientSessionManager(() => this._deps.credential, this._transport, this._deps.storage, this._deps.authorizationStateKey, this._deps.deviceController, this._attachManager, this._deps.webRTCApiProvider, this._dpopManager, this._networkMonitor?.networkChange$);
|
|
11105
12279
|
this._publicSession = new ClientSessionWrapper(this._clientSession);
|
|
11106
|
-
this._clientSession.
|
|
11107
|
-
|
|
11108
|
-
try {
|
|
11109
|
-
const fingerprint = this._dpopManager?.initialized ? this._dpopManager.fingerprint : void 0;
|
|
11110
|
-
logger$1.debug("[SignalWire] Credential expired, refreshing before reconnect");
|
|
11111
|
-
const newCredentials = await this._credentialProvider.authenticate(fingerprint ? { fingerprint } : void 0);
|
|
11112
|
-
this._deps.credential = newCredentials;
|
|
11113
|
-
if (newCredentials.expiry_at && this._credentialProvider.refresh) this._refreshCoordinator?.scheduleDeveloperRefresh(this._credentialProvider, newCredentials.expiry_at);
|
|
11114
|
-
logger$1.debug("[SignalWire] Credential refreshed successfully for reconnect");
|
|
11115
|
-
} catch (error) {
|
|
11116
|
-
logger$1.error("[SignalWire] Failed to refresh credentials for reconnect:", error);
|
|
11117
|
-
this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
11118
|
-
throw error;
|
|
11119
|
-
}
|
|
11120
|
-
};
|
|
12280
|
+
this._clientSession.callControl = this._options.callControl ?? "routed";
|
|
12281
|
+
this._clientSession.onBeforeReconnect = async () => this.refreshCredentialForReconnect();
|
|
11121
12282
|
this.subscribeTo(this._clientSession.errors$, (error) => {
|
|
11122
12283
|
this._errors$.next(error);
|
|
11123
12284
|
});
|
|
12285
|
+
this.subscribeTo(this._clientSession.authorization$, (authorization) => {
|
|
12286
|
+
this._refreshCoordinator?.syncExpiryFromAuthorization(authorization, this._credentialProvider);
|
|
12287
|
+
this.persistClientBoundMarker(Boolean(authorization?.cnf?.jkt));
|
|
12288
|
+
});
|
|
11124
12289
|
await this._clientSession.connect();
|
|
11125
12290
|
await this._refreshCoordinator?.activate(this._deps.user, this._clientSession);
|
|
11126
12291
|
this.subscribeTo(this._clientSession.authenticated$.pipe((0, rxjs.skip)(1), (0, rxjs.filter)(Boolean)), async () => {
|
|
@@ -11283,6 +12448,13 @@ var SignalWire = class extends Destroyable {
|
|
|
11283
12448
|
}
|
|
11284
12449
|
try {
|
|
11285
12450
|
this._visibilityController = new VisibilityController();
|
|
12451
|
+
this.subscribeTo(this._visibilityController.visibilityChange$.pipe((0, rxjs.filter)((event) => event.to === "visible")), () => {
|
|
12452
|
+
try {
|
|
12453
|
+
this._refreshCoordinator?.forceRefreshIfDue();
|
|
12454
|
+
} catch (error) {
|
|
12455
|
+
logger$1.warn("[SignalWire] Resume credential revalidation failed (non-fatal):", error);
|
|
12456
|
+
}
|
|
12457
|
+
});
|
|
11286
12458
|
this.subscribeTo(this._visibilityController.visibilityChange$.pipe((0, rxjs.filter)((event) => event.to === "visible" && PreferencesContainer.instance.refreshDevicesOnVisible)), () => {
|
|
11287
12459
|
logger$1.debug("[SignalWire] Page visible, re-enumerating devices");
|
|
11288
12460
|
try {
|
|
@@ -11294,7 +12466,7 @@ var SignalWire = class extends Destroyable {
|
|
|
11294
12466
|
logger$1.warn("[SignalWire] Failed to initialize VisibilityController:", error);
|
|
11295
12467
|
}
|
|
11296
12468
|
try {
|
|
11297
|
-
this._diagnosticsCollector = new DiagnosticsCollector({ sdkVersion: "4.0.0-rc.
|
|
12469
|
+
this._diagnosticsCollector = new DiagnosticsCollector({ sdkVersion: "4.0.0-rc.4" });
|
|
11298
12470
|
} catch (error) {
|
|
11299
12471
|
logger$1.warn("[SignalWire] Failed to initialize DiagnosticsCollector:", error);
|
|
11300
12472
|
}
|
|
@@ -11365,21 +12537,22 @@ var SignalWire = class extends Destroyable {
|
|
|
11365
12537
|
this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
11366
12538
|
throw error;
|
|
11367
12539
|
}
|
|
11368
|
-
logger$1.debug("[SignalWire] Failed to register user,
|
|
11369
|
-
|
|
11370
|
-
await this._clientSession.reauthenticate(this._deps.credential.token);
|
|
11371
|
-
logger$1.debug("[SignalWire] Reauthentication successful, retrying register()");
|
|
12540
|
+
logger$1.debug("[SignalWire] Failed to register user, attempting credential recovery...");
|
|
12541
|
+
const outcome = await this.recoverAndRetry(async () => {
|
|
11372
12542
|
await this._transport.execute(RPCExecute({
|
|
11373
12543
|
method: "subscriber.online",
|
|
11374
12544
|
params: {}
|
|
11375
12545
|
}));
|
|
12546
|
+
});
|
|
12547
|
+
if (outcome.ok) {
|
|
12548
|
+
logger$1.debug("[SignalWire] Recovery restored registration");
|
|
11376
12549
|
this._isRegistered$.next(true);
|
|
11377
|
-
|
|
11378
|
-
logger$1.error("[SignalWire] Reauthentication failed during register():", reauthError);
|
|
11379
|
-
const registerError = new require_operators.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 }) });
|
|
11380
|
-
this._errors$.next(registerError);
|
|
11381
|
-
throw registerError;
|
|
12550
|
+
return;
|
|
11382
12551
|
}
|
|
12552
|
+
const failureCause = outcome.error ?? error;
|
|
12553
|
+
const registerError = new require_operators.InvalidCredentialsError("Failed to register user, and credential recovery also failed. Please check your credentials.", { cause: failureCause instanceof Error ? failureCause : new Error(String(failureCause), { cause: failureCause }) });
|
|
12554
|
+
this._errors$.next(registerError);
|
|
12555
|
+
throw registerError;
|
|
11383
12556
|
}
|
|
11384
12557
|
}
|
|
11385
12558
|
/**
|
|
@@ -11410,6 +12583,11 @@ var SignalWire = class extends Destroyable {
|
|
|
11410
12583
|
* Returns a {@link Call} in `'ringing'` state. Subscribe to {@link Call.status$}
|
|
11411
12584
|
* to track progression through `'connected'` → `'disconnected'`.
|
|
11412
12585
|
*
|
|
12586
|
+
* Local media acquisition is deliberately unbounded: an unanswered permission
|
|
12587
|
+
* prompt leaves this promise pending indefinitely, so apply your own bound if
|
|
12588
|
+
* your UI needs one. The 12 s signaling budget starts only once acquisition
|
|
12589
|
+
* settles.
|
|
12590
|
+
*
|
|
11413
12591
|
* @param destination - Address URI string (e.g. `'/public/my-room'`) or {@link Address} instance.
|
|
11414
12592
|
* @param options - Media and dial options (audio/video, device constraints). Overrides defaults.
|
|
11415
12593
|
* @returns The created {@link Call} instance.
|
|
@@ -11432,7 +12610,18 @@ var SignalWire = class extends Destroyable {
|
|
|
11432
12610
|
};
|
|
11433
12611
|
await this.waitAuthentication();
|
|
11434
12612
|
logger$1.debug("[SignalWire] Dialing with options:", computed_options);
|
|
11435
|
-
|
|
12613
|
+
try {
|
|
12614
|
+
return await this._clientSession.createOutboundCall(destination, computed_options);
|
|
12615
|
+
} catch (error) {
|
|
12616
|
+
if (!isRecoverableAuthError(error)) throw error;
|
|
12617
|
+
logger$1.debug("[SignalWire] Dial hit a recoverable auth error; recovering the session and retrying");
|
|
12618
|
+
const outcome = await this.recoverAndRetry(async () => {
|
|
12619
|
+
await this.waitAuthentication();
|
|
12620
|
+
return this._clientSession.createOutboundCall(destination, computed_options);
|
|
12621
|
+
});
|
|
12622
|
+
if (outcome.ok) return outcome.value;
|
|
12623
|
+
throw outcome.error ?? error;
|
|
12624
|
+
}
|
|
11436
12625
|
}
|
|
11437
12626
|
/**
|
|
11438
12627
|
* Runs a multi-phase connectivity test against the given destination.
|
|
@@ -11713,10 +12902,13 @@ var SignalWire = class extends Destroyable {
|
|
|
11713
12902
|
destroy() {
|
|
11714
12903
|
this._refreshCoordinator?.destroy();
|
|
11715
12904
|
this._refreshCoordinator = void 0;
|
|
12905
|
+
this._refreshHttp?.destroy();
|
|
12906
|
+
this._refreshHttp = void 0;
|
|
11716
12907
|
this._dpopManager?.destroy();
|
|
11717
|
-
this._clientSession
|
|
11718
|
-
|
|
11719
|
-
this.
|
|
12908
|
+
const session = this._clientSession;
|
|
12909
|
+
session?.teardownSessionState();
|
|
12910
|
+
this._transport?.destroy();
|
|
12911
|
+
session?.destroy();
|
|
11720
12912
|
try {
|
|
11721
12913
|
this._networkMonitor?.destroy();
|
|
11722
12914
|
} catch {}
|
|
@@ -11828,7 +13020,7 @@ var StaticCredentialProvider = class {
|
|
|
11828
13020
|
/**
|
|
11829
13021
|
* Library version from package.json, injected at build time.
|
|
11830
13022
|
*/
|
|
11831
|
-
const version = "4.0.0-rc.
|
|
13023
|
+
const version = "4.0.0-rc.4";
|
|
11832
13024
|
/**
|
|
11833
13025
|
* Flag indicating the library has been loaded and is ready to use.
|
|
11834
13026
|
* For UMD builds: `window.SignalWire.ready`
|
|
@@ -11850,7 +13042,7 @@ const ready = true;
|
|
|
11850
13042
|
*/
|
|
11851
13043
|
const emitReadyEvent = () => {
|
|
11852
13044
|
if (typeof window !== "undefined") {
|
|
11853
|
-
const event = new CustomEvent("signalwire:js:ready", { detail: { version: "4.0.0-rc.
|
|
13045
|
+
const event = new CustomEvent("signalwire:js:ready", { detail: { version: "4.0.0-rc.4" } });
|
|
11854
13046
|
window.dispatchEvent(event);
|
|
11855
13047
|
}
|
|
11856
13048
|
};
|
|
@@ -11858,7 +13050,10 @@ emitReadyEvent();
|
|
|
11858
13050
|
|
|
11859
13051
|
//#endregion
|
|
11860
13052
|
exports.Address = Address;
|
|
13053
|
+
exports.AuxiliaryLegCancelledError = require_operators.AuxiliaryLegCancelledError;
|
|
13054
|
+
exports.AuxiliaryLegTimeoutError = require_operators.AuxiliaryLegTimeoutError;
|
|
11861
13055
|
exports.CallCreateError = require_operators.CallCreateError;
|
|
13056
|
+
exports.CallNotReadyError = require_operators.CallNotReadyError;
|
|
11862
13057
|
exports.ClientPreferences = ClientPreferences;
|
|
11863
13058
|
exports.CollectionFetchError = require_operators.CollectionFetchError;
|
|
11864
13059
|
exports.DPoPInitError = require_operators.DPoPInitError;
|
|
@@ -11870,8 +13065,10 @@ exports.MediaTrackError = require_operators.MediaTrackError;
|
|
|
11870
13065
|
exports.MessageParseError = require_operators.MessageParseError;
|
|
11871
13066
|
exports.OverconstrainedFallbackError = require_operators.OverconstrainedFallbackError;
|
|
11872
13067
|
exports.Participant = Participant;
|
|
13068
|
+
exports.ParticipantNotReadyError = require_operators.ParticipantNotReadyError;
|
|
11873
13069
|
exports.PreflightError = require_operators.PreflightError;
|
|
11874
13070
|
exports.RecoveryError = require_operators.RecoveryError;
|
|
13071
|
+
exports.ScreenShareAlreadyActiveError = require_operators.ScreenShareAlreadyActiveError;
|
|
11875
13072
|
exports.SelfCapabilities = SelfCapabilities;
|
|
11876
13073
|
exports.SelfParticipant = SelfParticipant;
|
|
11877
13074
|
exports.SignalWire = SignalWire;
|