@signalwire/js 4.0.0-rc.2 → 4.0.0-rc.3
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 +1638 -446
- package/dist/browser.mjs.map +1 -1
- package/dist/browser.umd.js +1642 -445
- package/dist/browser.umd.js.map +1 -1
- package/dist/index.cjs +1538 -430
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2312 -1835
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +2312 -1835
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1534 -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-B8ipd4Xl.cjs → operators-BJ6QW1VP.cjs} +144 -1
- package/dist/operators-BJ6QW1VP.cjs.map +1 -0
- package/dist/{operators-BlUtq-t0.mjs → operators-BVxi3KpN.mjs} +91 -2
- package/dist/operators-BVxi3KpN.mjs.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.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as
|
|
1
|
+
import { $ as CREDENTIAL_REFRESH_BUFFER_MS, $t as ICE_GATHERING_COMPLETE_TIMEOUT_MS, A as RPCTimeoutError, At as DEFAULT_REFRESH_DEVICES_ON_VISIBLE, B as TransportConnectionError, Bt as DEFAULT_STATS_RTT_SPIKE_MULTIPLIER, C as JSONRPCError, Ct as DEFAULT_MAX_RECOVERY_ATTEMPTS, D as OverconstrainedFallbackError, Dt as DEFAULT_RECONNECT_DELAY_MIN_MS, E as MessageParseError, Et as DEFAULT_RECONNECT_DELAY_MAX_MS, F as SerializationError, Ft as DEFAULT_STATS_HISTORY_SIZE, G as VertoInviteHandlerError, Gt as DEVICE_STORAGE_KEY_AUDIO_INPUT, H as UnimplementedError, Ht as DEFAULT_STEREO_MAX_AVERAGE_BITRATE, I as StorageNotAvailableError, It as DEFAULT_STATS_JITTER_SPIKE_MULTIPLIER, J as WebSocketTimeoutError, Jt as DEVICE_TOKEN_DEFAULT_EXPIRE_IN, K as VertoPongError, Kt as DEVICE_STORAGE_KEY_AUDIO_OUTPUT, L as StorageReadError, Lt as DEFAULT_STATS_NO_PACKET_THRESHOLD_MS, M as RequestError, Mt as DEFAULT_REINVITE_MAX_ATTEMPTS, N as RequestTimeoutError, Nt as DEFAULT_REINVITE_TIMEOUT_MS, O as ParticipantNotReadyError, Ot as DEFAULT_RECOVERY_COOLDOWN_MS, P as ScreenShareAlreadyActiveError, Pt as DEFAULT_STATS_BASELINE_SAMPLES, Q as CREDENTIAL_EXPIRY_SKEW_MS, Qt as DEVICE_TOKEN_REFRESH_RETRY_BASE_MS, R as StorageWriteError, Rt as DEFAULT_STATS_PACKET_LOSS_THRESHOLD, S as InvalidParams, St as DEFAULT_KEYFRAME_MAX_BURST$1, T as MediaTrackError, Tt as DEFAULT_RECONNECT_CALLS_TIMEOUT_MS, U as ValidationError, Ut as DEFAULT_SYNC_DEVICES_TO_ACTIVE_CALLS, V as UnexpectedError, Vt as DEFAULT_STEREO_AUDIO, W as VertoAttachHandlerError, Wt as DEVICE_REFRESH_ENDPOINT, X as AUDIO_LEVEL_POLL_INTERVAL_MS, Xt as DEVICE_TOKEN_REFRESH_BUFFER_MS, Y as isMediaDeviceInUse, Yt as DEVICE_TOKEN_ENDPOINT, Z as CREDENTIAL_ACTIVATE_TIMEOUT_MS, Zt as DEVICE_TOKEN_REFRESH_MAX_RETRIES, _ as DPoPInitError, _t as DEFAULT_ICE_DISCONNECTED_GRACE_PERIOD_MS, a as filterNull, an as RPC_ERROR_INVALID_PARAMS, at as DEFAULT_CALL_SIGNALING_TIMEOUT_MS, b as DeviceTokenError, bt as DEFAULT_KEYFRAME_BURST_WINDOW_MS$1, c as setLogLevel, cn as VAD_HOLD_MS, ct as DEFAULT_DEGRADATION_BITRATE_THRESHOLD_KBPS, d as AuxiliaryLegCancelledError, dt as DEFAULT_DEVICE_POLLING_INTERVAL_MS, en as INVITE_VERSION, et as CREDENTIAL_REFRESH_MAX_DELAY_MS, f as AuxiliaryLegTimeoutError, ft as DEFAULT_ENABLE_AUTO_DEGRADATION, g as ConversationError, gt as DEFAULT_ICE_CANDIDATE_TIMEOUT_MS, h as CollectionFetchError, ht as DEFAULT_ENABLE_SERVER_HANGUP_INTERCEPTION, i as getValueFrom, in as RPC_ERROR_AUTHENTICATION_FAILED, it as DEFAULT_AUX_LEG_CONNECT_TIMEOUT_MS, j as RecoveryError, jt as DEFAULT_REINVITE_DEBOUNCE_TIME_MS, k as PreflightError, kt as DEFAULT_RECOVERY_DEBOUNCE_TIME_MS, l as setLogger, ln as VAD_THRESHOLD, lt as DEFAULT_DEGRADATION_RECOVERY_THRESHOLD_KBPS, m as CallNotReadyError, mt as DEFAULT_ENABLE_RELAY_FALLBACK, n as filterAs, nn as PEER_CONNECTION_RECOVERY_WAIT_MS, nt as CREDENTIAL_REFRESH_RETRY_BASE_MS, o as getLogger, on as RPC_ERROR_REQUESTER_VALIDATION_FAILED, ot as DEFAULT_CHECK_CONNECTION_ON_VISIBLE, p as CallCreateError, pt as DEFAULT_ENABLE_NETWORK_CHANGE_DETECTION, q as WebSocketConnectionError, qt as DEVICE_STORAGE_KEY_VIDEO_INPUT, rn as PREFERENCES_STORAGE_KEY, rt as DEFAULT_AUTO_MUTE_VIDEO_ON_HIDDEN, s as setDebugOptions, sn as SAT_REFRESH_SCOPE, st as DEFAULT_CONNECTION_TIMEOUT_MS, t as throwOnRPCError, tn as PEER_CONNECTION_RECOVERY_POLL_MS, tt as CREDENTIAL_REFRESH_MAX_RETRIES, u as AuthStateHandlerError, ut as DEFAULT_DEVICE_DEBOUNCE_TIME_MS, v as DependencyError, vt as DEFAULT_ICE_GATHERING_TIMEOUT_MS, w as MediaAccessError, wt as DEFAULT_PERSIST_DEVICE_SELECTION, x as InvalidCredentialsError, xt as DEFAULT_KEYFRAME_COOLDOWN_MS$1, y as DeserializationError, yt as DEFAULT_ICE_RESTART_TIMEOUT_MS$1, z as TokenRefreshError, zt as DEFAULT_STATS_POLLING_INTERVAL_MS } from "./operators-BVxi3KpN.mjs";
|
|
2
2
|
import { jwtDecode } from "jwt-decode";
|
|
3
3
|
import { BehaviorSubject, EMPTY, EmptyError, NEVER, Observable, ReplaySubject, Subject, TimeoutError, animationFrameScheduler, asapScheduler, auditTime, catchError, combineLatest, debounceTime, defer, distinctUntilChanged, exhaustMap, filter, first, firstValueFrom, from, interval, lastValueFrom, map, merge, mergeMap, observeOn, of, pipe, race, scan, share, shareReplay, skip, skipWhile, startWith, switchMap, take, takeUntil, tap, throwError, timeout, timer, toArray, withLatestFrom } from "rxjs";
|
|
4
4
|
import { v4 } from "uuid";
|
|
@@ -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 = 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$1 = 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 RequestTimeoutError(`Request timeout after ${timeout$1}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 = 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(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(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 = getLogger();
|
|
1111
1111
|
/** Maps a device kind to its storage key. */
|
|
1112
1112
|
const DEVICE_STORAGE_KEYS = {
|
|
1113
1113
|
audioinput: 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(map((state) => state.videoinput), distinctUntilChanged(), takeUntil(this.destroyed$)));
|
|
1195
1195
|
}
|
|
1196
1196
|
get selectedAudioInputDevice$() {
|
|
1197
|
-
return this.cachedObservable("selectedAudioInputDevice$", () => this._selectedDevicesState$.asObservable().pipe(map((state) => state.audioinput), distinctUntilChanged(), takeUntil(this.destroyed$), tap((info) => logger$
|
|
1197
|
+
return this.cachedObservable("selectedAudioInputDevice$", () => this._selectedDevicesState$.asObservable().pipe(map((state) => state.audioinput), distinctUntilChanged(), takeUntil(this.destroyed$), 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(map((state) => state.audiooutput), distinctUntilChanged(), takeUntil(this.destroyed$), tap((info) => logger$
|
|
1200
|
+
return this.cachedObservable("selectedAudioOutputDevice$", () => this._selectedDevicesState$.asObservable().pipe(map((state) => state.audiooutput), distinctUntilChanged(), takeUntil(this.destroyed$), 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(map((state) => state.videoinput), distinctUntilChanged(), takeUntil(this.destroyed$), tap((info) => logger$
|
|
1203
|
+
return this.cachedObservable("selectedVideoInputDevice$", () => this._selectedDevicesState$.asObservable().pipe(map((state) => state.videoinput), distinctUntilChanged(), takeUntil(this.destroyed$), 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 = 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 = 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 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 = 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 = 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 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 = 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
|
});
|
|
@@ -2677,15 +2677,57 @@ const RPCEventAckResponse = (id) => makeRPCResponse({
|
|
|
2677
2677
|
result: {}
|
|
2678
2678
|
});
|
|
2679
2679
|
|
|
2680
|
+
//#endregion
|
|
2681
|
+
//#region src/utils/authRecovery.ts
|
|
2682
|
+
/**
|
|
2683
|
+
* Walk an error's `error`/`cause` chain looking for a {@link JSONRPCError}.
|
|
2684
|
+
* Errors thrown by call creation are wrapped (e.g. `CallCreateError`), so the
|
|
2685
|
+
* underlying signaling error is nested. Bounded by a visited set to guard
|
|
2686
|
+
* against cyclic causes.
|
|
2687
|
+
*/
|
|
2688
|
+
function findJSONRPCError(error) {
|
|
2689
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2690
|
+
let current = error;
|
|
2691
|
+
while (current instanceof Error && !seen.has(current)) {
|
|
2692
|
+
seen.add(current);
|
|
2693
|
+
if (current instanceof JSONRPCError) return current;
|
|
2694
|
+
current = current.error ?? current.cause;
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2697
|
+
/**
|
|
2698
|
+
* Whether an error is a session-recoverable authentication failure
|
|
2699
|
+
* (`-32002` authentication failed or `-32003` requester validation failed)
|
|
2700
|
+
* that a credential re-mint + retry can heal.
|
|
2701
|
+
*/
|
|
2702
|
+
function isRecoverableAuthError(error) {
|
|
2703
|
+
const rpcError = findJSONRPCError(error);
|
|
2704
|
+
return rpcError !== void 0 && (rpcError.code === RPC_ERROR_REQUESTER_VALIDATION_FAILED || rpcError.code === RPC_ERROR_AUTHENTICATION_FAILED);
|
|
2705
|
+
}
|
|
2706
|
+
/**
|
|
2707
|
+
* Whether an error is specifically a requester-validation rejection
|
|
2708
|
+
* (`-32003`) — the server refusing the session's credential.
|
|
2709
|
+
*
|
|
2710
|
+
* Narrower than {@link isRecoverableAuthError} on purpose. `-32002` is
|
|
2711
|
+
* overloaded server-side: a rejected reattach arrives as `-32002` with
|
|
2712
|
+
* `cause: INVALID_MSG_UNSPECIFIED` and message `CALL ERROR`, which is a
|
|
2713
|
+
* call-level rejection and says nothing about the credential. Use this where
|
|
2714
|
+
* the decision must not be fooled by that, such as deciding whether retrying
|
|
2715
|
+
* an operation could possibly succeed.
|
|
2716
|
+
*/
|
|
2717
|
+
function isRequesterValidationError(error) {
|
|
2718
|
+
return findJSONRPCError(error)?.code === RPC_ERROR_REQUESTER_VALIDATION_FAILED;
|
|
2719
|
+
}
|
|
2720
|
+
|
|
2680
2721
|
//#endregion
|
|
2681
2722
|
//#region src/managers/AttachManager.ts
|
|
2682
|
-
const logger$
|
|
2723
|
+
const logger$25 = getLogger();
|
|
2683
2724
|
var AttachManager = class {
|
|
2684
|
-
constructor(storage, deviceController, reconnectCallsTimeout, attachKey) {
|
|
2725
|
+
constructor(storage, deviceController, reconnectCallsTimeout, attachKey, credentialRecovered) {
|
|
2685
2726
|
this.storage = storage;
|
|
2686
2727
|
this.deviceController = deviceController;
|
|
2687
2728
|
this.reconnectCallsTimeout = reconnectCallsTimeout;
|
|
2688
2729
|
this.attachKey = attachKey;
|
|
2730
|
+
this.credentialRecovered = credentialRecovered;
|
|
2689
2731
|
this.writeQueue = Promise.resolve();
|
|
2690
2732
|
}
|
|
2691
2733
|
async detachAll() {
|
|
@@ -2700,7 +2742,7 @@ var AttachManager = class {
|
|
|
2700
2742
|
try {
|
|
2701
2743
|
return await this.storage.getItem(this.attachKey) ?? {};
|
|
2702
2744
|
} catch (error) {
|
|
2703
|
-
logger$
|
|
2745
|
+
logger$25.warn("[AttachManager] Failed to retrieve attached calls from storage", error);
|
|
2704
2746
|
return {};
|
|
2705
2747
|
}
|
|
2706
2748
|
}
|
|
@@ -2708,7 +2750,7 @@ var AttachManager = class {
|
|
|
2708
2750
|
try {
|
|
2709
2751
|
await this.storage.setItem(this.attachKey, attached);
|
|
2710
2752
|
} catch (error) {
|
|
2711
|
-
logger$
|
|
2753
|
+
logger$25.warn("[AttachManager] Failed to write attached calls to storage", error);
|
|
2712
2754
|
}
|
|
2713
2755
|
}
|
|
2714
2756
|
/**
|
|
@@ -2727,11 +2769,39 @@ var AttachManager = class {
|
|
|
2727
2769
|
}
|
|
2728
2770
|
async attach(call) {
|
|
2729
2771
|
if (!call.to) {
|
|
2730
|
-
logger$
|
|
2772
|
+
logger$25.warn("[AttachManager] Skip attach for calls with no destination");
|
|
2731
2773
|
return;
|
|
2732
2774
|
}
|
|
2775
|
+
const attachment = this.buildAttachment(call, call.to);
|
|
2776
|
+
await this.mutate((attached) => ({
|
|
2777
|
+
...attached,
|
|
2778
|
+
[call.id]: attachment
|
|
2779
|
+
}));
|
|
2780
|
+
}
|
|
2781
|
+
/**
|
|
2782
|
+
* Keep an already-stored call's reference alive and current — the periodic
|
|
2783
|
+
* refresh the `verto.ping` keepalive drives.
|
|
2784
|
+
*
|
|
2785
|
+
* Only ever updates: a call with no record is one nothing wants reattached,
|
|
2786
|
+
* and re-creating it here would undo a `detach`. That matters because a ping
|
|
2787
|
+
* can land in the window between `bye()` detaching and the call being torn
|
|
2788
|
+
* down, and a record revived there survives the hangup — so the next page
|
|
2789
|
+
* load dials a call nobody is on. The existence check and the write share
|
|
2790
|
+
* one {@link mutate} turn, so a concurrent detach cannot slip between them.
|
|
2791
|
+
*/
|
|
2792
|
+
async refresh(call) {
|
|
2793
|
+
if (!call.to) return;
|
|
2733
2794
|
const destination = call.to;
|
|
2734
|
-
|
|
2795
|
+
await this.mutate((attached) => {
|
|
2796
|
+
if (!Object.hasOwn(attached, call.id)) return attached;
|
|
2797
|
+
return {
|
|
2798
|
+
...attached,
|
|
2799
|
+
[call.id]: this.buildAttachment(call, destination)
|
|
2800
|
+
};
|
|
2801
|
+
});
|
|
2802
|
+
}
|
|
2803
|
+
buildAttachment(call, destination) {
|
|
2804
|
+
return {
|
|
2735
2805
|
nodeId: call.nodeId,
|
|
2736
2806
|
destination,
|
|
2737
2807
|
mediaDirections: call.mediaDirections,
|
|
@@ -2739,10 +2809,6 @@ var AttachManager = class {
|
|
|
2739
2809
|
videoInputDevice: call.mediaDirections.video !== "inactive" ? this.deviceController.selectedVideoInputDevice : null,
|
|
2740
2810
|
attachedAt: Date.now()
|
|
2741
2811
|
};
|
|
2742
|
-
await this.mutate((attached) => ({
|
|
2743
|
-
...attached,
|
|
2744
|
-
[call.id]: attachment
|
|
2745
|
-
}));
|
|
2746
2812
|
}
|
|
2747
2813
|
async detach(call) {
|
|
2748
2814
|
await this.mutate((attached) => {
|
|
@@ -2765,8 +2831,14 @@ var AttachManager = class {
|
|
|
2765
2831
|
* rejecting. Once that fix is deployed, this will work for both
|
|
2766
2832
|
* page reloads and WebSocket reconnects.
|
|
2767
2833
|
*
|
|
2768
|
-
*
|
|
2769
|
-
*
|
|
2834
|
+
* A failed reattach does NOT generally cost the stored reference. It is
|
|
2835
|
+
* discarded only when the server denied the reattach on a session whose
|
|
2836
|
+
* credential it had already accepted — a verified reauthentication followed
|
|
2837
|
+
* by a refusal is the server saying the call is gone, and that is the one
|
|
2838
|
+
* refusal worth acting on. Until then the credential may be what is being
|
|
2839
|
+
* refused, and the record is the only way a later reload can try again;
|
|
2840
|
+
* keeping it costs nothing, since `detachExpired` reaps it once it is older
|
|
2841
|
+
* than `reconnectCallsTimeout`.
|
|
2770
2842
|
*/
|
|
2771
2843
|
async reattachCalls() {
|
|
2772
2844
|
const attached = await this.readAttached();
|
|
@@ -2775,25 +2847,31 @@ var AttachManager = class {
|
|
|
2775
2847
|
const { destination } = attachment;
|
|
2776
2848
|
const options = this.buildCallOptions(attachment);
|
|
2777
2849
|
let succeeded = false;
|
|
2850
|
+
let refusedOnCredentials = false;
|
|
2778
2851
|
for (let attempt = 1; attempt <= 3; attempt++) try {
|
|
2779
2852
|
await this.session.createOutboundCall(destination, {
|
|
2780
2853
|
callId,
|
|
2781
2854
|
...options
|
|
2782
2855
|
});
|
|
2783
|
-
logger$
|
|
2856
|
+
logger$25.info(`[AttachManager] Reattached call ${callId} (attempt ${attempt})`);
|
|
2784
2857
|
succeeded = true;
|
|
2785
2858
|
break;
|
|
2786
2859
|
} catch (error) {
|
|
2787
|
-
logger$
|
|
2860
|
+
logger$25.warn(`[AttachManager] Reattach attempt ${attempt}/3 failed for call ${callId}:`, error);
|
|
2861
|
+
if (isRequesterValidationError(error)) {
|
|
2862
|
+
refusedOnCredentials = true;
|
|
2863
|
+
logger$25.warn(`[AttachManager] Reattach of ${callId} was refused on credentials; not retrying.`);
|
|
2864
|
+
break;
|
|
2865
|
+
}
|
|
2788
2866
|
if (attempt < 3) await new Promise((r) => setTimeout(r, (attempt + 1) * 1e3));
|
|
2789
2867
|
}
|
|
2790
|
-
if (!succeeded) {
|
|
2791
|
-
logger$
|
|
2868
|
+
if (!succeeded) if (this.credentialRecovered() && !refusedOnCredentials) {
|
|
2869
|
+
logger$25.warn(`[AttachManager] Reattach of ${callId} was denied after a verified reauthentication, removing reference`);
|
|
2792
2870
|
await this.detach({
|
|
2793
2871
|
id: callId,
|
|
2794
2872
|
mediaDirections: attachment.mediaDirections
|
|
2795
2873
|
});
|
|
2796
|
-
}
|
|
2874
|
+
} else logger$25.warn(`[AttachManager] Reattach failed for call ${callId}; keeping the reference (credential refused or never proven good)`);
|
|
2797
2875
|
}
|
|
2798
2876
|
}
|
|
2799
2877
|
/**
|
|
@@ -3106,7 +3184,7 @@ function toggleHandraiseMethod(is) {
|
|
|
3106
3184
|
|
|
3107
3185
|
//#endregion
|
|
3108
3186
|
//#region src/core/entities/Participant.ts
|
|
3109
|
-
const logger$
|
|
3187
|
+
const logger$24 = getLogger();
|
|
3110
3188
|
const initialState = {};
|
|
3111
3189
|
/**
|
|
3112
3190
|
* Represents a participant in a call.
|
|
@@ -3116,9 +3194,9 @@ const initialState = {};
|
|
|
3116
3194
|
* the local participant with additional device control.
|
|
3117
3195
|
*/
|
|
3118
3196
|
var Participant = class extends Destroyable {
|
|
3119
|
-
constructor(id,
|
|
3197
|
+
constructor(id, callExecuteMethod, deviceController) {
|
|
3120
3198
|
super();
|
|
3121
|
-
this.
|
|
3199
|
+
this.callExecuteMethod = callExecuteMethod;
|
|
3122
3200
|
this.deviceController = deviceController;
|
|
3123
3201
|
this._state$ = this.createBehaviorSubject(initialState);
|
|
3124
3202
|
this.id = id;
|
|
@@ -3340,22 +3418,55 @@ var Participant = class extends Destroyable {
|
|
|
3340
3418
|
get value() {
|
|
3341
3419
|
return this._state$.value;
|
|
3342
3420
|
}
|
|
3421
|
+
/**
|
|
3422
|
+
* Target triple for member RPCs, built from the participant's own state.
|
|
3423
|
+
* The backend locates the member's session by the target `call_id`/`node_id`,
|
|
3424
|
+
* so this must always be the participant's own call context — never the
|
|
3425
|
+
* local call's id (issue #19400).
|
|
3426
|
+
*
|
|
3427
|
+
* Reading it doubles as a readiness probe: it throws until the first full
|
|
3428
|
+
* member event (`member.joined`/`member.updated` or the `call.joined`
|
|
3429
|
+
* roster) arrives, and never regresses afterwards.
|
|
3430
|
+
*
|
|
3431
|
+
* @throws {ParticipantNotReadyError} If the member state has not been
|
|
3432
|
+
* received yet (e.g. a participant first seen via `member.talking`) — an
|
|
3433
|
+
* empty call context can never address the member, so fail fast instead of
|
|
3434
|
+
* sending a doomed RPC.
|
|
3435
|
+
*/
|
|
3436
|
+
get target() {
|
|
3437
|
+
const { call_id, node_id } = this._state$.value;
|
|
3438
|
+
if (!call_id || !node_id) throw new ParticipantNotReadyError(this.id);
|
|
3439
|
+
return {
|
|
3440
|
+
member_id: this.id,
|
|
3441
|
+
call_id,
|
|
3442
|
+
node_id
|
|
3443
|
+
};
|
|
3444
|
+
}
|
|
3445
|
+
/**
|
|
3446
|
+
* Executes a member RPC against this participant, injecting its own
|
|
3447
|
+
* {@link target} as the target.
|
|
3448
|
+
*
|
|
3449
|
+
* @throws {ParticipantNotReadyError} Via {@link target}, when the
|
|
3450
|
+
* member state has not been received yet.
|
|
3451
|
+
*/
|
|
3452
|
+
async executeMethod(method, args) {
|
|
3453
|
+
return this.callExecuteMethod(this.target, method, args);
|
|
3454
|
+
}
|
|
3343
3455
|
/** Toggles the deafened state (mutes/unmutes incoming audio). */
|
|
3344
3456
|
async toggleDeaf() {
|
|
3345
|
-
|
|
3346
|
-
await this.executeMethod(this.id, method, {});
|
|
3457
|
+
await this.executeMethod(toggleDeafMethod(this.deaf), {});
|
|
3347
3458
|
}
|
|
3348
3459
|
/** Toggles the hand-raised state. */
|
|
3349
3460
|
async toggleHandraise() {
|
|
3350
|
-
await this.executeMethod(
|
|
3461
|
+
await this.executeMethod(toggleHandraiseMethod(this.handraised), {});
|
|
3351
3462
|
}
|
|
3352
3463
|
/** Mutes the participant's audio. */
|
|
3353
3464
|
async mute() {
|
|
3354
|
-
await this.executeMethod(
|
|
3465
|
+
await this.executeMethod("call.mute", { channels: ["audio"] });
|
|
3355
3466
|
}
|
|
3356
3467
|
/** Unmutes the participant's audio. */
|
|
3357
3468
|
async unmute() {
|
|
3358
|
-
await this.executeMethod(
|
|
3469
|
+
await this.executeMethod("call.unmute", { channels: ["audio"] });
|
|
3359
3470
|
}
|
|
3360
3471
|
/** Toggles the participant's audio mute state. */
|
|
3361
3472
|
async toggleMute() {
|
|
@@ -3363,11 +3474,11 @@ var Participant = class extends Destroyable {
|
|
|
3363
3474
|
}
|
|
3364
3475
|
/** Mutes the participant's video. */
|
|
3365
3476
|
async muteVideo() {
|
|
3366
|
-
await this.executeMethod(
|
|
3477
|
+
await this.executeMethod("call.mute", { channels: ["video"] });
|
|
3367
3478
|
}
|
|
3368
3479
|
/** Unmutes the participant's video. */
|
|
3369
3480
|
async unmuteVideo() {
|
|
3370
|
-
await this.executeMethod(
|
|
3481
|
+
await this.executeMethod("call.unmute", { channels: ["video"] });
|
|
3371
3482
|
}
|
|
3372
3483
|
/** Toggles the participant's video mute state. */
|
|
3373
3484
|
async toggleMuteVideo() {
|
|
@@ -3375,7 +3486,7 @@ var Participant = class extends Destroyable {
|
|
|
3375
3486
|
}
|
|
3376
3487
|
/** Toggles echo cancellation on the audio input. */
|
|
3377
3488
|
async toggleEchoCancellation() {
|
|
3378
|
-
await this.executeMethod(
|
|
3489
|
+
await this.executeMethod("call.audioflags.set", {
|
|
3379
3490
|
echo_cancellation: !this.echoCancellation,
|
|
3380
3491
|
auto_gain: this.autoGain,
|
|
3381
3492
|
noise_suppression: this.noiseSuppression
|
|
@@ -3383,7 +3494,7 @@ var Participant = class extends Destroyable {
|
|
|
3383
3494
|
}
|
|
3384
3495
|
/** Toggles automatic gain control on the audio input. */
|
|
3385
3496
|
async toggleAudioInputAutoGain() {
|
|
3386
|
-
await this.executeMethod(
|
|
3497
|
+
await this.executeMethod("call.audioflags.set", {
|
|
3387
3498
|
echo_cancellation: this.echoCancellation,
|
|
3388
3499
|
auto_gain: !this.autoGain,
|
|
3389
3500
|
noise_suppression: this.noiseSuppression
|
|
@@ -3391,7 +3502,7 @@ var Participant = class extends Destroyable {
|
|
|
3391
3502
|
}
|
|
3392
3503
|
/** Toggles noise suppression on the audio input. */
|
|
3393
3504
|
async toggleNoiseSuppression() {
|
|
3394
|
-
await this.executeMethod(
|
|
3505
|
+
await this.executeMethod("call.audioflags.set", {
|
|
3395
3506
|
echo_cancellation: this.echoCancellation,
|
|
3396
3507
|
auto_gain: this.autoGain,
|
|
3397
3508
|
noise_suppression: !this.noiseSuppression
|
|
@@ -3399,7 +3510,7 @@ var Participant = class extends Destroyable {
|
|
|
3399
3510
|
}
|
|
3400
3511
|
/** Toggles low-bitrate mode for this participant's media. */
|
|
3401
3512
|
async toggleLowbitrate() {
|
|
3402
|
-
await this.executeMethod(
|
|
3513
|
+
await this.executeMethod("call.lowbitrate.set", { lowbitrate: !this.lowbitrate });
|
|
3403
3514
|
}
|
|
3404
3515
|
/**
|
|
3405
3516
|
* Adjusts the **conference-only** microphone energy gate / sensitivity level
|
|
@@ -3415,7 +3526,7 @@ var Participant = class extends Destroyable {
|
|
|
3415
3526
|
* (integer, larger values are more sensitive).
|
|
3416
3527
|
*/
|
|
3417
3528
|
async setAudioInputSensitivity(value) {
|
|
3418
|
-
await this.executeMethod(
|
|
3529
|
+
await this.executeMethod("call.microphone.sensitivity.set", { sensitivity: value });
|
|
3419
3530
|
}
|
|
3420
3531
|
/**
|
|
3421
3532
|
* Sets the **server-side** microphone volume on this participant's bridged
|
|
@@ -3428,7 +3539,7 @@ var Participant = class extends Destroyable {
|
|
|
3428
3539
|
* @param value - Volume level (0-100).
|
|
3429
3540
|
*/
|
|
3430
3541
|
async setAudioInputVolume(value) {
|
|
3431
|
-
await this.executeMethod(
|
|
3542
|
+
await this.executeMethod("call.microphone.volume.set", { volume: value });
|
|
3432
3543
|
}
|
|
3433
3544
|
/**
|
|
3434
3545
|
* Sets the **server-side** speaker volume on this participant's bridged call
|
|
@@ -3442,45 +3553,31 @@ var Participant = class extends Destroyable {
|
|
|
3442
3553
|
* @param value - Volume level (0-100).
|
|
3443
3554
|
*/
|
|
3444
3555
|
async setAudioOutputVolume(value) {
|
|
3445
|
-
await this.executeMethod(
|
|
3556
|
+
await this.executeMethod("call.speaker.volume.set", { volume: value });
|
|
3446
3557
|
}
|
|
3447
3558
|
/**
|
|
3448
3559
|
* Sets the participant's position in the video layout.
|
|
3449
3560
|
*
|
|
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.
|
|
3561
|
+
* Requires the `member.position` capability. The gateway requires a
|
|
3562
|
+
* `targets` array of `{ target, position }` entries (issue #19400). A
|
|
3563
|
+
* resolved promise does not guarantee a visible change: the backend silently
|
|
3564
|
+
* returns `200` (no-op) for non-conference targets.
|
|
3456
3565
|
*
|
|
3457
3566
|
* @param value - The {@link VideoPosition} to assign (e.g. `'auto'`, `'reserved-0'`).
|
|
3458
3567
|
*/
|
|
3459
3568
|
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,
|
|
3569
|
+
await this.executeMethod("call.member.position.set", { targets: [{
|
|
3570
|
+
target: this.target,
|
|
3468
3571
|
position: value
|
|
3469
3572
|
}] });
|
|
3470
3573
|
}
|
|
3471
3574
|
/** Removes this participant from the call. */
|
|
3472
3575
|
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", {});
|
|
3576
|
+
await this.executeMethod("call.member.remove", { targets: [this.target] });
|
|
3480
3577
|
}
|
|
3481
3578
|
/** Ends the call for this participant. */
|
|
3482
3579
|
async end() {
|
|
3483
|
-
await this.executeMethod(
|
|
3580
|
+
await this.executeMethod("call.end", {});
|
|
3484
3581
|
}
|
|
3485
3582
|
/**
|
|
3486
3583
|
* Replaces custom metadata for this participant.
|
|
@@ -3500,7 +3597,7 @@ var Participant = class extends Destroyable {
|
|
|
3500
3597
|
}
|
|
3501
3598
|
/** Destroys the participant, releasing all subscriptions and references. */
|
|
3502
3599
|
destroy() {
|
|
3503
|
-
this.
|
|
3600
|
+
this.callExecuteMethod = void 0;
|
|
3504
3601
|
super.destroy();
|
|
3505
3602
|
}
|
|
3506
3603
|
};
|
|
@@ -3512,8 +3609,8 @@ var Participant = class extends Destroyable {
|
|
|
3512
3609
|
*/
|
|
3513
3610
|
var SelfParticipant = class extends Participant {
|
|
3514
3611
|
/** @internal */
|
|
3515
|
-
constructor(id,
|
|
3516
|
-
super(id,
|
|
3612
|
+
constructor(id, callExecuteMethod, vertoManager, deviceController) {
|
|
3613
|
+
super(id, callExecuteMethod, deviceController);
|
|
3517
3614
|
this.vertoManager = vertoManager;
|
|
3518
3615
|
this._studioAudio$ = this.createBehaviorSubject(false);
|
|
3519
3616
|
this.capabilities = new SelfCapabilities();
|
|
@@ -3537,7 +3634,7 @@ var SelfParticipant = class extends Participant {
|
|
|
3537
3634
|
async enableStudioAudio() {
|
|
3538
3635
|
if (this._studioAudio$.value) return;
|
|
3539
3636
|
this._studioAudio$.next(true);
|
|
3540
|
-
await this.executeMethod(
|
|
3637
|
+
await this.executeMethod("call.audioflags.set", {
|
|
3541
3638
|
echo_cancellation: false,
|
|
3542
3639
|
auto_gain: false,
|
|
3543
3640
|
noise_suppression: false
|
|
@@ -3550,7 +3647,7 @@ var SelfParticipant = class extends Participant {
|
|
|
3550
3647
|
async disableStudioAudio() {
|
|
3551
3648
|
if (!this._studioAudio$.value) return;
|
|
3552
3649
|
this._studioAudio$.next(false);
|
|
3553
|
-
await this.executeMethod(
|
|
3650
|
+
await this.executeMethod("call.audioflags.set", {
|
|
3554
3651
|
echo_cancellation: true,
|
|
3555
3652
|
auto_gain: true,
|
|
3556
3653
|
noise_suppression: true
|
|
@@ -3559,17 +3656,27 @@ var SelfParticipant = class extends Participant {
|
|
|
3559
3656
|
/**
|
|
3560
3657
|
* Starts sharing the local screen.
|
|
3561
3658
|
*
|
|
3659
|
+
* A call carries at most one screen share. Read `screenShareStatus` before
|
|
3660
|
+
* calling and treat `'starting'`/`'stopping'` as busy.
|
|
3661
|
+
*
|
|
3562
3662
|
* The call is unaffected when acquisition fails.
|
|
3563
3663
|
*
|
|
3664
|
+
* @param options - Pass `{ audio: true }` to also request the shared
|
|
3665
|
+
* surface's audio. Defaults to video only.
|
|
3666
|
+
* @throws {ScreenShareAlreadyActiveError} When this call is already
|
|
3667
|
+
* sharing a screen. Call {@link stopScreenShare} before starting another.
|
|
3668
|
+
* @throws {AuxiliaryLegCancelledError} When {@link stopScreenShare} removes
|
|
3669
|
+
* the share before its leg finishes connecting.
|
|
3564
3670
|
* @throws The raw `getDisplayMedia` error. A dismissed picker or a
|
|
3565
3671
|
* permission denial rejects with a `NotAllowedError` `DOMException` —
|
|
3566
3672
|
* inspect `error.name` to tell benign cancels apart from real failures.
|
|
3567
3673
|
*/
|
|
3568
|
-
async startScreenShare() {
|
|
3674
|
+
async startScreenShare(options) {
|
|
3569
3675
|
try {
|
|
3570
|
-
await this.vertoManager.addScreenMedia();
|
|
3676
|
+
await this.vertoManager.addScreenMedia(options);
|
|
3571
3677
|
} catch (error) {
|
|
3572
|
-
logger$
|
|
3678
|
+
if (error instanceof AuxiliaryLegCancelledError) logger$24.debug("[Participant.startScreenShare] Screen share cancelled before connecting.");
|
|
3679
|
+
else logger$24.error("[Participant.startScreenShare] Screen share error:", error);
|
|
3573
3680
|
throw error;
|
|
3574
3681
|
}
|
|
3575
3682
|
}
|
|
@@ -3590,14 +3697,18 @@ var SelfParticipant = class extends Participant {
|
|
|
3590
3697
|
*
|
|
3591
3698
|
* The call is unaffected when acquisition fails.
|
|
3592
3699
|
*
|
|
3700
|
+
* @throws {AuxiliaryLegCancelledError} When {@link removeAdditionalDevice}
|
|
3701
|
+
* removes the device before its leg finishes connecting.
|
|
3593
3702
|
* @throws The raw `getUserMedia` error (e.g. `NotAllowedError` on
|
|
3594
|
-
* permission denial) — inspect `error.name` to decide how to react
|
|
3703
|
+
* permission denial) — inspect `error.name` to decide how to react — or
|
|
3704
|
+
* `AuxiliaryLegTimeoutError` if the leg does not connect in time.
|
|
3595
3705
|
*/
|
|
3596
3706
|
async addAdditionalDevice(options) {
|
|
3597
3707
|
try {
|
|
3598
3708
|
await this.vertoManager.addInputDevice(options);
|
|
3599
3709
|
} catch (error) {
|
|
3600
|
-
logger$
|
|
3710
|
+
if (error instanceof AuxiliaryLegCancelledError) logger$24.debug("[Participant.addAdditionalDevice] Device removed before connecting.");
|
|
3711
|
+
else logger$24.error("[Participant.addAdditionalDevice] Additional device error:", error);
|
|
3601
3712
|
throw error;
|
|
3602
3713
|
}
|
|
3603
3714
|
}
|
|
@@ -3632,22 +3743,31 @@ var SelfParticipant = class extends Participant {
|
|
|
3632
3743
|
this.deviceController.selectAudioInputDevice(device);
|
|
3633
3744
|
if (options.savePreference) PreferencesContainer.instance.preferredAudioInput = device;
|
|
3634
3745
|
}
|
|
3635
|
-
/**
|
|
3746
|
+
/**
|
|
3747
|
+
* Updates the audio input track constraints for the active call.
|
|
3748
|
+
* @returns whether the constraints reached the media the call is sending.
|
|
3749
|
+
*/
|
|
3636
3750
|
async setAudioInputDeviceConstraints(constraints) {
|
|
3637
|
-
|
|
3751
|
+
return this.vertoManager.updateMediaConstraints({ audio: constraints });
|
|
3638
3752
|
}
|
|
3639
|
-
/**
|
|
3753
|
+
/**
|
|
3754
|
+
* Updates both audio and video input track constraints for the active call.
|
|
3755
|
+
* @returns whether both kinds took the constraints.
|
|
3756
|
+
*/
|
|
3640
3757
|
async setInputDevicesConstraints(constraints) {
|
|
3641
|
-
|
|
3758
|
+
return this.vertoManager.updateMediaConstraints(constraints);
|
|
3642
3759
|
}
|
|
3643
3760
|
/** Selects the video input device for future calls. Optionally saves as a preference. */
|
|
3644
3761
|
selectVideoInputDevice(device, options = {}) {
|
|
3645
3762
|
this.deviceController.selectVideoInputDevice(device);
|
|
3646
3763
|
if (options.savePreference) PreferencesContainer.instance.preferredVideoInput = device;
|
|
3647
3764
|
}
|
|
3648
|
-
/**
|
|
3765
|
+
/**
|
|
3766
|
+
* Updates the video input track constraints for the active call.
|
|
3767
|
+
* @returns whether the constraints reached the media the call is sending.
|
|
3768
|
+
*/
|
|
3649
3769
|
async setVideoInputDeviceConstraints(constraints) {
|
|
3650
|
-
|
|
3770
|
+
return this.vertoManager.updateMediaConstraints({ video: constraints });
|
|
3651
3771
|
}
|
|
3652
3772
|
/** Selects the audio output device. Optionally saves as a preference. */
|
|
3653
3773
|
selectAudioOutputDevice(device, options = {}) {
|
|
@@ -3660,7 +3780,7 @@ var SelfParticipant = class extends Participant {
|
|
|
3660
3780
|
*/
|
|
3661
3781
|
exitStudioModeIfActive() {
|
|
3662
3782
|
if (this._studioAudio$.value) {
|
|
3663
|
-
logger$
|
|
3783
|
+
logger$24.debug("[SelfParticipant] Exiting studio audio mode due to individual flag toggle");
|
|
3664
3784
|
this._studioAudio$.next(false);
|
|
3665
3785
|
}
|
|
3666
3786
|
}
|
|
@@ -3684,7 +3804,7 @@ var SelfParticipant = class extends Participant {
|
|
|
3684
3804
|
try {
|
|
3685
3805
|
await super.mute();
|
|
3686
3806
|
} catch (error) {
|
|
3687
|
-
logger$
|
|
3807
|
+
logger$24.warn("[Participant.toggleAudioInput] Server Error while muting audio input, proceeding with local toggle anyway", error);
|
|
3688
3808
|
} finally {
|
|
3689
3809
|
this.vertoManager.muteMainAudioInputDevice();
|
|
3690
3810
|
}
|
|
@@ -3694,7 +3814,7 @@ var SelfParticipant = class extends Participant {
|
|
|
3694
3814
|
try {
|
|
3695
3815
|
await super.unmute();
|
|
3696
3816
|
} catch (error) {
|
|
3697
|
-
logger$
|
|
3817
|
+
logger$24.warn("[Participant.toggleAudioInput] Server Error while unmuting audio input, proceeding with local toggle anyway", error);
|
|
3698
3818
|
} finally {
|
|
3699
3819
|
await this.vertoManager.unmuteMainAudioInputDevice();
|
|
3700
3820
|
}
|
|
@@ -3704,7 +3824,7 @@ var SelfParticipant = class extends Participant {
|
|
|
3704
3824
|
try {
|
|
3705
3825
|
await super.muteVideo();
|
|
3706
3826
|
} catch (error) {
|
|
3707
|
-
logger$
|
|
3827
|
+
logger$24.warn("[Participant.toggleVideoInput] Server Error while muting video input, proceeding with local toggle anyway", error);
|
|
3708
3828
|
} finally {
|
|
3709
3829
|
this.vertoManager.muteMainVideoInputDevice();
|
|
3710
3830
|
}
|
|
@@ -3714,7 +3834,7 @@ var SelfParticipant = class extends Participant {
|
|
|
3714
3834
|
try {
|
|
3715
3835
|
await super.unmuteVideo();
|
|
3716
3836
|
} catch (error) {
|
|
3717
|
-
logger$
|
|
3837
|
+
logger$24.warn("[Participant.toggleVideoInput] Server Error while unmuting video input, proceeding with local toggle anyway", error);
|
|
3718
3838
|
} finally {
|
|
3719
3839
|
await this.vertoManager.unmuteMainVideoInputDevice();
|
|
3720
3840
|
}
|
|
@@ -3808,7 +3928,7 @@ function isLayoutChangedPayload(value) {
|
|
|
3808
3928
|
|
|
3809
3929
|
//#endregion
|
|
3810
3930
|
//#region src/managers/CallEventsManager.ts
|
|
3811
|
-
const logger$
|
|
3931
|
+
const logger$23 = getLogger();
|
|
3812
3932
|
const initialSessionState = {};
|
|
3813
3933
|
/** @internal */
|
|
3814
3934
|
var CallEventsManager = class extends Destroyable {
|
|
@@ -3912,7 +4032,7 @@ var CallEventsManager = class extends Destroyable {
|
|
|
3912
4032
|
}
|
|
3913
4033
|
initSubscriptions() {
|
|
3914
4034
|
this.subscribeTo(this.callJoinedEvent$, (callJoinedEvent) => {
|
|
3915
|
-
logger$
|
|
4035
|
+
logger$23.debug("[CallEventsManager] Handling call.joined event for call/session IDs:", {
|
|
3916
4036
|
callId: callJoinedEvent.call_id,
|
|
3917
4037
|
roomSessionId: callJoinedEvent.room_session_id
|
|
3918
4038
|
});
|
|
@@ -3939,19 +4059,19 @@ var CallEventsManager = class extends Destroyable {
|
|
|
3939
4059
|
if (this._self$.value?.capabilities.setLayout) this.updateLayouts();
|
|
3940
4060
|
});
|
|
3941
4061
|
this.subscribeTo(this.memberUpdates$, (member) => {
|
|
3942
|
-
logger$
|
|
4062
|
+
logger$23.debug("[CallEventsManager] Handling member update event for member ID:", member);
|
|
3943
4063
|
this.upsertParticipant(member);
|
|
3944
4064
|
});
|
|
3945
4065
|
this.subscribeTo(this.webRtcCallSession.memberLeft$, (memberLeftEvent) => {
|
|
3946
|
-
logger$
|
|
4066
|
+
logger$23.debug("[CallEventsManager] Handling member.left event for member ID:", memberLeftEvent.member.member_id);
|
|
3947
4067
|
const participants = { ...this._participants$.value };
|
|
3948
4068
|
if (memberLeftEvent.member.member_id in participants) {
|
|
3949
4069
|
delete participants[memberLeftEvent.member.member_id];
|
|
3950
4070
|
this._participants$.next(participants);
|
|
3951
|
-
} else logger$
|
|
4071
|
+
} else logger$23.warn(`[CallEventsManager] Received member.left event for unknown member ID: ${memberLeftEvent.member.member_id}`);
|
|
3952
4072
|
});
|
|
3953
4073
|
this.subscribeTo(this.webRtcCallSession.callUpdated$, (callUpdatedEvent) => {
|
|
3954
|
-
logger$
|
|
4074
|
+
logger$23.debug("[CallEventsManager] Handling call.updated event:", callUpdatedEvent);
|
|
3955
4075
|
const roomSession = callUpdatedEvent.room_session;
|
|
3956
4076
|
this._sessionState$.next({
|
|
3957
4077
|
...this._sessionState$.value,
|
|
@@ -3966,7 +4086,7 @@ var CallEventsManager = class extends Destroyable {
|
|
|
3966
4086
|
});
|
|
3967
4087
|
});
|
|
3968
4088
|
this.subscribeTo(this.layoutChangedEvent$, (layoutChangedEvent) => {
|
|
3969
|
-
logger$
|
|
4089
|
+
logger$23.debug("[CallEventsManager] Handling layout.changed event:", layoutChangedEvent);
|
|
3970
4090
|
this._sessionState$.next({
|
|
3971
4091
|
...this._sessionState$.value,
|
|
3972
4092
|
layout_name: layoutChangedEvent.id,
|
|
@@ -3976,10 +4096,10 @@ var CallEventsManager = class extends Destroyable {
|
|
|
3976
4096
|
});
|
|
3977
4097
|
}
|
|
3978
4098
|
updateParticipantPositions(layoutChangedEvent) {
|
|
3979
|
-
if (Object.keys(this._participants$.value).length > 0 && !layoutChangedEvent.layers.some((layer) => !!layer.member_id)) logger$
|
|
4099
|
+
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
4100
|
layoutChangedEvent.layers.filter((layer) => !!layer.member_id).filter((layer) => {
|
|
3981
4101
|
if (!(layer.member_id in this._participants$.value)) {
|
|
3982
|
-
logger$
|
|
4102
|
+
logger$23.warn(`[CallEventsManager] Skipping layout layer for unknown member_id: ${layer.member_id}`);
|
|
3983
4103
|
return false;
|
|
3984
4104
|
}
|
|
3985
4105
|
return true;
|
|
@@ -3997,12 +4117,17 @@ var CallEventsManager = class extends Destroyable {
|
|
|
3997
4117
|
updateLayouts() {
|
|
3998
4118
|
if (!this.selfId) return;
|
|
3999
4119
|
this.webRtcCallSession.executeMethod(this.selfId, "call.layout.list", {}).then((response) => {
|
|
4120
|
+
const layouts = response.result?.layouts;
|
|
4121
|
+
if (!layouts) {
|
|
4122
|
+
logger$23.warn("[CallEventsManager] Layout list response carried no layouts; keeping current layouts");
|
|
4123
|
+
return;
|
|
4124
|
+
}
|
|
4000
4125
|
this._sessionState$.next({
|
|
4001
4126
|
...this._sessionState$.value,
|
|
4002
|
-
layouts
|
|
4127
|
+
layouts
|
|
4003
4128
|
});
|
|
4004
4129
|
}).catch((error) => {
|
|
4005
|
-
logger$
|
|
4130
|
+
logger$23.error("[CallEventsManager] Error fetching layouts:", error);
|
|
4006
4131
|
});
|
|
4007
4132
|
}
|
|
4008
4133
|
updateParticipants(members) {
|
|
@@ -4018,7 +4143,7 @@ var CallEventsManager = class extends Destroyable {
|
|
|
4018
4143
|
}
|
|
4019
4144
|
const participant = this._participants$.value[member.member_id];
|
|
4020
4145
|
const oldValue = participant.value;
|
|
4021
|
-
logger$
|
|
4146
|
+
logger$23.debug("[CallEventsManager] Updating participant:", member.member_id, {
|
|
4022
4147
|
oldValue,
|
|
4023
4148
|
newValue: member
|
|
4024
4149
|
});
|
|
@@ -4031,17 +4156,17 @@ var CallEventsManager = class extends Destroyable {
|
|
|
4031
4156
|
}
|
|
4032
4157
|
get callJoinedEvent$() {
|
|
4033
4158
|
return this.cachedObservable("callJoinedEvent$", () => this.webRtcCallSession.callEvent$.pipe(filter(isCallJoinedPayload), tap((event) => {
|
|
4034
|
-
logger$
|
|
4159
|
+
logger$23.debug("[CallEventsManager] Call joined event:", event);
|
|
4035
4160
|
})));
|
|
4036
4161
|
}
|
|
4037
4162
|
get layoutChangedEvent$() {
|
|
4038
4163
|
return this.cachedObservable("layoutChangedEvent$", () => this.webRtcCallSession.callEvent$.pipe(filterAs(isLayoutChangedPayload, "layout"), tap((event) => {
|
|
4039
|
-
logger$
|
|
4164
|
+
logger$23.debug("[CallEventsManager] Layout changed event:", event);
|
|
4040
4165
|
})));
|
|
4041
4166
|
}
|
|
4042
4167
|
get memberUpdates$() {
|
|
4043
4168
|
return this.cachedObservable("memberUpdates$", () => merge(this.webRtcCallSession.memberJoined$, this.webRtcCallSession.memberUpdated$, this.webRtcCallSession.memberTalking$).pipe(map((event) => event.member), tap((event) => {
|
|
4044
|
-
logger$
|
|
4169
|
+
logger$23.debug("[CallEventsManager] Member update event:", event);
|
|
4045
4170
|
})));
|
|
4046
4171
|
}
|
|
4047
4172
|
destroy() {
|
|
@@ -4060,6 +4185,111 @@ var CallEventsManager = class extends Destroyable {
|
|
|
4060
4185
|
}
|
|
4061
4186
|
};
|
|
4062
4187
|
|
|
4188
|
+
//#endregion
|
|
4189
|
+
//#region src/controllers/ConstraintFallbackHelper.ts
|
|
4190
|
+
/**
|
|
4191
|
+
* ConstraintFallbackHelper - Provides getUserMedia with automatic constraint
|
|
4192
|
+
* fallback on OverconstrainedError.
|
|
4193
|
+
*
|
|
4194
|
+
* When a specific device ID is requested, the helper tries progressively
|
|
4195
|
+
* looser constraints:
|
|
4196
|
+
* 1. `{ deviceId: { exact: deviceId } }` -- exact match
|
|
4197
|
+
* 2. `{ deviceId: deviceId }` -- preferred (browser may pick another)
|
|
4198
|
+
* 3. `{}` -- no constraint, browser default
|
|
4199
|
+
*
|
|
4200
|
+
* This prevents stale device IDs from blocking call setup.
|
|
4201
|
+
*
|
|
4202
|
+
* @see Section 5.8 and Section 11 of the Implementation Guide
|
|
4203
|
+
*/
|
|
4204
|
+
const logger$22 = getLogger();
|
|
4205
|
+
/**
|
|
4206
|
+
* Attempts getUserMedia with progressively looser constraints.
|
|
4207
|
+
*
|
|
4208
|
+
* The function tries three levels of constraint specificity for the given
|
|
4209
|
+
* device kind. Each level is only attempted if the previous one fails with
|
|
4210
|
+
* an OverconstrainedError. Non-OverconstrainedError failures (e.g.,
|
|
4211
|
+
* NotAllowedError) are thrown immediately without fallback.
|
|
4212
|
+
*
|
|
4213
|
+
* @param mediaDevices - Anything exposing `getUserMedia` (a full
|
|
4214
|
+
* `WebRTCMediaDevices`, or a shim wrapping one)
|
|
4215
|
+
* @param constraints - The full MediaStreamConstraints to use as a base
|
|
4216
|
+
* @param kind - Which track kind to apply fallback to ('audio' | 'video')
|
|
4217
|
+
* @param deviceId - The device ID to try (if undefined, calls getUserMedia as-is)
|
|
4218
|
+
* @returns The stream and the fallback level that succeeded
|
|
4219
|
+
* @throws When all fallback levels fail, or when a non-OverconstrainedError occurs
|
|
4220
|
+
*/
|
|
4221
|
+
async function getUserMediaWithFallback(mediaDevices, constraints, kind, deviceId) {
|
|
4222
|
+
if (!deviceId) return {
|
|
4223
|
+
stream: await mediaDevices.getUserMedia(constraints),
|
|
4224
|
+
fallbackLevel: "default"
|
|
4225
|
+
};
|
|
4226
|
+
const baseConstraints = typeof constraints[kind] === "object" ? constraints[kind] : {};
|
|
4227
|
+
try {
|
|
4228
|
+
const exactConstraints = {
|
|
4229
|
+
...constraints,
|
|
4230
|
+
[kind]: {
|
|
4231
|
+
...baseConstraints,
|
|
4232
|
+
deviceId: { exact: deviceId }
|
|
4233
|
+
}
|
|
4234
|
+
};
|
|
4235
|
+
return {
|
|
4236
|
+
stream: await mediaDevices.getUserMedia(exactConstraints),
|
|
4237
|
+
fallbackLevel: "exact"
|
|
4238
|
+
};
|
|
4239
|
+
} catch (error) {
|
|
4240
|
+
if (!isOverconstrainedError(error)) throw error;
|
|
4241
|
+
logger$22.debug(`[ConstraintFallbackHelper] Exact constraint failed for ${kind}, trying preferred`, { deviceId });
|
|
4242
|
+
}
|
|
4243
|
+
try {
|
|
4244
|
+
const preferredConstraints = {
|
|
4245
|
+
...constraints,
|
|
4246
|
+
[kind]: {
|
|
4247
|
+
...baseConstraints,
|
|
4248
|
+
deviceId
|
|
4249
|
+
}
|
|
4250
|
+
};
|
|
4251
|
+
return {
|
|
4252
|
+
stream: await mediaDevices.getUserMedia(preferredConstraints),
|
|
4253
|
+
fallbackLevel: "preferred"
|
|
4254
|
+
};
|
|
4255
|
+
} catch (error) {
|
|
4256
|
+
if (!isOverconstrainedError(error)) throw error;
|
|
4257
|
+
logger$22.debug(`[ConstraintFallbackHelper] Preferred constraint failed for ${kind}, trying default`, { deviceId });
|
|
4258
|
+
}
|
|
4259
|
+
try {
|
|
4260
|
+
const defaultConstraints = {
|
|
4261
|
+
...constraints,
|
|
4262
|
+
[kind]: { ...baseConstraints }
|
|
4263
|
+
};
|
|
4264
|
+
if (typeof defaultConstraints[kind] === "object") {
|
|
4265
|
+
const { deviceId: _removed, ...rest } = defaultConstraints[kind];
|
|
4266
|
+
defaultConstraints[kind] = rest;
|
|
4267
|
+
}
|
|
4268
|
+
const stream = await mediaDevices.getUserMedia(defaultConstraints);
|
|
4269
|
+
logger$22.warn(`[ConstraintFallbackHelper] Fell back to browser default for ${kind}`, { requestedDeviceId: deviceId });
|
|
4270
|
+
return {
|
|
4271
|
+
stream,
|
|
4272
|
+
fallbackLevel: "default"
|
|
4273
|
+
};
|
|
4274
|
+
} catch (error) {
|
|
4275
|
+
logger$22.error(`[ConstraintFallbackHelper] All fallback levels exhausted for ${kind}`, {
|
|
4276
|
+
deviceId,
|
|
4277
|
+
error
|
|
4278
|
+
});
|
|
4279
|
+
throw error;
|
|
4280
|
+
}
|
|
4281
|
+
}
|
|
4282
|
+
/**
|
|
4283
|
+
* Checks whether an error is an OverconstrainedError.
|
|
4284
|
+
*
|
|
4285
|
+
* Browsers may throw either a native OverconstrainedError or a DOMException
|
|
4286
|
+
* with a specific name.
|
|
4287
|
+
*/
|
|
4288
|
+
function isOverconstrainedError(error) {
|
|
4289
|
+
if (error instanceof Error) return error.name === "OverconstrainedError" || error.name === "ConstraintNotSatisfiedError";
|
|
4290
|
+
return false;
|
|
4291
|
+
}
|
|
4292
|
+
|
|
4063
4293
|
//#endregion
|
|
4064
4294
|
//#region src/helpers/SDPHelper.ts
|
|
4065
4295
|
/**
|
|
@@ -4615,6 +4845,7 @@ var LocalStreamController = class extends Destroyable {
|
|
|
4615
4845
|
this._localAudioTracks$ = this.createBehaviorSubject([]);
|
|
4616
4846
|
this._localVideoTracks$ = this.createBehaviorSubject([]);
|
|
4617
4847
|
this._mediaTrackEnded$ = this.createSubject();
|
|
4848
|
+
this._trackOrigins = /* @__PURE__ */ new WeakMap();
|
|
4618
4849
|
}
|
|
4619
4850
|
get localStream$() {
|
|
4620
4851
|
return this._localStream$.asObservable().pipe(takeUntil(this.destroyed$));
|
|
@@ -4637,6 +4868,22 @@ var LocalStreamController = class extends Destroyable {
|
|
|
4637
4868
|
get localVideoTracks() {
|
|
4638
4869
|
return this._localVideoTracks$.value;
|
|
4639
4870
|
}
|
|
4871
|
+
tagTracks(tracks, origin) {
|
|
4872
|
+
for (const track of tracks) this._trackOrigins.set(track, origin);
|
|
4873
|
+
}
|
|
4874
|
+
setTrackOrigin(track, origin) {
|
|
4875
|
+
this._trackOrigins.set(track, origin);
|
|
4876
|
+
}
|
|
4877
|
+
getTrackOrigin(track) {
|
|
4878
|
+
return this._trackOrigins.get(track);
|
|
4879
|
+
}
|
|
4880
|
+
/**
|
|
4881
|
+
* Fail-safe: an unrecorded track reads as not-a-device-capture, so a missed
|
|
4882
|
+
* tagging site leaves media alone rather than destroying it.
|
|
4883
|
+
*/
|
|
4884
|
+
isDeviceCapture(track) {
|
|
4885
|
+
return this._trackOrigins.get(track) === "device";
|
|
4886
|
+
}
|
|
4640
4887
|
/**
|
|
4641
4888
|
* Build the local media stream based on the provided options.
|
|
4642
4889
|
*/
|
|
@@ -4646,13 +4893,16 @@ var LocalStreamController = class extends Destroyable {
|
|
|
4646
4893
|
if (this.options.inputAudioStream ?? this.options.inputVideoStream) {
|
|
4647
4894
|
const tracks = [...this.options.inputAudioStream?.getTracks() ?? [], ...this.options.inputVideoStream?.getTracks() ?? []];
|
|
4648
4895
|
stream = new MediaStream(tracks);
|
|
4896
|
+
this.tagTracks(tracks, "application");
|
|
4649
4897
|
} else if (this.options.propose === "screenshare") {
|
|
4650
|
-
|
|
4898
|
+
const audio = this.options.screenShareAudio ?? false;
|
|
4899
|
+
logger$19.debug("[LocalStreamController] Requesting display media for screen sharing with audio:", audio);
|
|
4651
4900
|
stream = await this.options.getDisplayMedia({
|
|
4652
4901
|
video: true,
|
|
4653
|
-
audio
|
|
4902
|
+
audio
|
|
4654
4903
|
});
|
|
4655
4904
|
logger$19.debug("[LocalStreamController] Screen share media obtained:", stream);
|
|
4905
|
+
this.tagTracks(stream.getTracks(), "display");
|
|
4656
4906
|
} else {
|
|
4657
4907
|
const constraints = {
|
|
4658
4908
|
audio: this.options.inputAudioDeviceConstraints,
|
|
@@ -4661,6 +4911,7 @@ var LocalStreamController = class extends Destroyable {
|
|
|
4661
4911
|
logger$19.debug("[LocalStreamController] Requesting user media with constraints:", constraints);
|
|
4662
4912
|
stream = await this.options.getUserMedia(constraints);
|
|
4663
4913
|
logger$19.debug("[LocalStreamController] User media obtained:", stream);
|
|
4914
|
+
this.tagTracks(stream.getTracks(), "device");
|
|
4664
4915
|
}
|
|
4665
4916
|
this._localStream$.next(stream);
|
|
4666
4917
|
this._localAudioTracks$.next(stream.getAudioTracks());
|
|
@@ -4670,10 +4921,13 @@ var LocalStreamController = class extends Destroyable {
|
|
|
4670
4921
|
/**
|
|
4671
4922
|
* Add a local media track to the local stream.
|
|
4672
4923
|
* @param track - The MediaStreamTrack to add
|
|
4924
|
+
* @param origin - Defaults to `'device'`; every internal caller passes a
|
|
4925
|
+
* fresh `getUserMedia` capture.
|
|
4673
4926
|
* @returns The MediaStream (either existing or newly created)
|
|
4674
4927
|
*/
|
|
4675
|
-
addTrack(track) {
|
|
4928
|
+
addTrack(track, origin = "device") {
|
|
4676
4929
|
const localStream = this._localStream$.value ?? new MediaStream();
|
|
4930
|
+
this._trackOrigins.set(track, origin);
|
|
4677
4931
|
track.addEventListener("ended", this.mediaTrackEndedHandler);
|
|
4678
4932
|
localStream.addTrack(track);
|
|
4679
4933
|
this._localStream$.next(localStream);
|
|
@@ -4943,15 +5197,27 @@ var TransceiverController = class extends Destroyable {
|
|
|
4943
5197
|
for (let i = 0; i < Number(msStreamsNumber); i++) this.peerConnection.addTransceiver("video", { direction: "recvonly" });
|
|
4944
5198
|
}
|
|
4945
5199
|
}
|
|
5200
|
+
/**
|
|
5201
|
+
* @returns whether every live sender of the kind took the constraints. A
|
|
5202
|
+
* skipped non-device sender, an exhausted fallback, and having no live sender
|
|
5203
|
+
* at all all report `false` — `mediaParamsUpdated.applied` is built from this,
|
|
5204
|
+
* and an application told `true` cannot tell a working push from a no-op.
|
|
5205
|
+
*/
|
|
4946
5206
|
async updateSendersConstraints(kind, constraints) {
|
|
4947
5207
|
if (!constraints) {
|
|
4948
5208
|
this.stopTrackSender(kind);
|
|
4949
|
-
return
|
|
5209
|
+
return false;
|
|
4950
5210
|
}
|
|
4951
5211
|
const senders = this.peerConnection.getSenders().filter((sender) => sender.track?.kind === kind && sender.track.readyState === "live");
|
|
5212
|
+
let applied = senders.length > 0;
|
|
4952
5213
|
for (const sender of senders) {
|
|
4953
5214
|
const { track } = sender;
|
|
4954
5215
|
if (track) {
|
|
5216
|
+
if (!this.options.localStreamController.isDeviceCapture(track)) {
|
|
5217
|
+
logger$18.debug(`[TransceiverController] Skipping ${kind} constraints for a non-device track (origin: ${this.options.localStreamController.getTrackOrigin(track) ?? "unrecorded"}), track ${track.id}`);
|
|
5218
|
+
applied = false;
|
|
5219
|
+
continue;
|
|
5220
|
+
}
|
|
4955
5221
|
const constraintsToApply = {
|
|
4956
5222
|
...track.getConstraints(),
|
|
4957
5223
|
...constraints
|
|
@@ -4967,33 +5233,39 @@ var TransceiverController = class extends Destroyable {
|
|
|
4967
5233
|
} catch (fallbackError) {
|
|
4968
5234
|
logger$18.warn(`[TransceiverController] Track replacement fallback also failed for ${kind} track:`, fallbackError);
|
|
4969
5235
|
this.options.onError?.(new MediaTrackError("updateSendersConstraints", kind, fallbackError));
|
|
5236
|
+
applied = false;
|
|
4970
5237
|
}
|
|
4971
5238
|
}
|
|
4972
5239
|
}
|
|
4973
5240
|
}
|
|
5241
|
+
return applied;
|
|
4974
5242
|
}
|
|
4975
5243
|
/**
|
|
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.
|
|
5244
|
+
* Fallback when applyConstraints fails, which on iOS Safari it silently does.
|
|
4979
5245
|
*
|
|
4980
|
-
*
|
|
4981
|
-
*
|
|
5246
|
+
* Order matters: acquiring before stopping means a failed acquisition leaves
|
|
5247
|
+
* the existing media playing. The deviceId goes through the fallback ladder
|
|
5248
|
+
* rather than pinned `{ exact }`, so a stale id degrades instead of failing.
|
|
4982
5249
|
*/
|
|
4983
5250
|
async replaceTrackFallback(sender, oldTrack, kind, mergedConstraints) {
|
|
4984
5251
|
const { deviceId } = oldTrack.getSettings();
|
|
4985
|
-
const
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
|
|
5252
|
+
const { stream, fallbackLevel } = await getUserMediaWithFallback({ getUserMedia: this.options.getUserMedia }, { [kind]: mergedConstraints }, kind, deviceId);
|
|
5253
|
+
const newTrack = stream.getTracks().find((t) => t.kind === kind);
|
|
5254
|
+
if (!newTrack) {
|
|
5255
|
+
stream.getTracks().forEach((t) => t.stop());
|
|
5256
|
+
throw new MediaTrackError("replaceTrackFallback", kind, /* @__PURE__ */ new Error("getUserMedia returned no track of the requested kind"));
|
|
5257
|
+
}
|
|
5258
|
+
try {
|
|
5259
|
+
await sender.replaceTrack(newTrack);
|
|
5260
|
+
} catch (error) {
|
|
5261
|
+
stream.getTracks().forEach((t) => t.stop());
|
|
5262
|
+
throw error;
|
|
5263
|
+
}
|
|
5264
|
+
const oldTrackId = oldTrack.id;
|
|
5265
|
+
this.options.localStreamController.removeTrack(oldTrackId);
|
|
4990
5266
|
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 MediaTrackError("replaceTrackFallback", kind, /* @__PURE__ */ new Error("getUserMedia returned no track of the requested kind"));
|
|
4994
|
-
await sender.replaceTrack(newTrack);
|
|
4995
5267
|
this.options.localStreamController.addTrack(newTrack);
|
|
4996
|
-
logger$18.debug(`[TransceiverController] Track replacement fallback succeeded for ${kind}. New track: ${newTrack.id}`);
|
|
5268
|
+
logger$18.debug(`[TransceiverController] Track replacement fallback succeeded for ${kind} (deviceId fallback level: ${fallbackLevel}). New track: ${newTrack.id}`);
|
|
4997
5269
|
}
|
|
4998
5270
|
getMediaDirections() {
|
|
4999
5271
|
if (this.peerConnection.connectionState === "connected") return this.peerConnection.getTransceivers().reduce((acc, transceiver) => {
|
|
@@ -5062,48 +5334,44 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5062
5334
|
this.negotiationNeeded$.next();
|
|
5063
5335
|
};
|
|
5064
5336
|
this.updateSelectedInputDevice = async (kind, deviceInfo) => {
|
|
5337
|
+
const { localStream } = this;
|
|
5338
|
+
if (!localStream) {
|
|
5339
|
+
logger$17.warn("[RTCPeerConnectionController] No local stream available to update input device.");
|
|
5340
|
+
return;
|
|
5341
|
+
}
|
|
5342
|
+
const currentTrack = localStream.getTracks().find((track) => track.kind === kind);
|
|
5343
|
+
if (!currentTrack) {
|
|
5344
|
+
logger$17.debug(`[RTCPeerConnectionController] No ${kind} track to switch.`);
|
|
5345
|
+
return;
|
|
5346
|
+
}
|
|
5347
|
+
if (!deviceInfo) {
|
|
5348
|
+
logger$17.debug(`[RTCPeerConnectionController] ${kind} input device selected: none`);
|
|
5349
|
+
this.stopTrackSender(kind);
|
|
5350
|
+
return;
|
|
5351
|
+
}
|
|
5352
|
+
const constraints = {
|
|
5353
|
+
...currentTrack.getConstraints(),
|
|
5354
|
+
...this.deviceController.deviceInfoToConstraints(deviceInfo)
|
|
5355
|
+
};
|
|
5065
5356
|
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);
|
|
5357
|
+
const newTrack = await this.acquireInputTrack(kind, constraints, deviceInfo, currentTrack);
|
|
5358
|
+
await this.attachInputTrack(kind, newTrack, currentTrack);
|
|
5359
|
+
logger$17.debug(`[RTCPeerConnectionController] ${kind} input device selected:`, deviceInfo.label, newTrack.id);
|
|
5093
5360
|
} catch (error) {
|
|
5094
5361
|
logger$17.error(`[RTCPeerConnectionController] Failed to select ${kind} input device:`, error);
|
|
5095
5362
|
this._errors$.next(new MediaTrackError("updateSelectedInputDevice", kind, error));
|
|
5096
|
-
throw error;
|
|
5097
5363
|
}
|
|
5098
5364
|
};
|
|
5099
5365
|
this._isNegotiating$ = this.createBehaviorSubject(false);
|
|
5100
5366
|
this._memberId = null;
|
|
5367
|
+
this._nodeId = null;
|
|
5101
5368
|
this._iceConnectionState$ = this.createReplaySubject(1);
|
|
5102
5369
|
this._connectionState$ = this.createReplaySubject(1);
|
|
5103
5370
|
this._signalingState$ = this.createReplaySubject(1);
|
|
5104
5371
|
this._iceGatheringState$ = this.createReplaySubject(1);
|
|
5105
5372
|
this._errors$ = this.createReplaySubject(1);
|
|
5106
5373
|
this._iceCandidates$ = this.createReplaySubject(1);
|
|
5374
|
+
this._localMediaSettled$ = this.createReplaySubject(1);
|
|
5107
5375
|
this._initialized$ = this.createReplaySubject(1);
|
|
5108
5376
|
this._remoteDescription$ = this.createReplaySubject(1);
|
|
5109
5377
|
this._remoteStream$ = this.createBehaviorSubject(null);
|
|
@@ -5136,6 +5404,7 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5136
5404
|
inputVideoStream: this.options.inputVideoStream,
|
|
5137
5405
|
inputAudioDeviceConstraints: this.inputAudioDeviceConstraints,
|
|
5138
5406
|
inputVideoDeviceConstraints: this.inputVideoDeviceConstraints,
|
|
5407
|
+
screenShareAudio: this.options.screenShareAudio,
|
|
5139
5408
|
getUserMedia: async (constraints) => this.getUserMedia(constraints),
|
|
5140
5409
|
getDisplayMedia: async (options$1) => this.getDisplayMedia(options$1)
|
|
5141
5410
|
});
|
|
@@ -5162,6 +5431,13 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5162
5431
|
get memberId() {
|
|
5163
5432
|
return this._memberId;
|
|
5164
5433
|
}
|
|
5434
|
+
/** The node this leg's invite landed on — auxiliary legs are placed independently. */
|
|
5435
|
+
setNodeId(nodeId) {
|
|
5436
|
+
this._nodeId = nodeId;
|
|
5437
|
+
}
|
|
5438
|
+
get nodeId() {
|
|
5439
|
+
return this._nodeId;
|
|
5440
|
+
}
|
|
5165
5441
|
stopTrackSender(kind, options = { updateTransceiverDirection: false }) {
|
|
5166
5442
|
const audioCovered = kind === "audio" || kind === "both";
|
|
5167
5443
|
if (audioCovered && this._localAudioPipeline) this.stopRawAudioInputForPipeline();
|
|
@@ -5171,10 +5447,7 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5171
5447
|
}
|
|
5172
5448
|
stopRawAudioInputForPipeline() {
|
|
5173
5449
|
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
|
-
}
|
|
5450
|
+
for (const track of rawTracks) if (track.readyState === "live") this.localStreamController.removeTrack(track.id);
|
|
5178
5451
|
this._localAudioPipeline?.setInputTrack(null);
|
|
5179
5452
|
}
|
|
5180
5453
|
get isNegotiating$() {
|
|
@@ -5207,6 +5480,10 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5207
5480
|
get remoteDescription$() {
|
|
5208
5481
|
return this.cachedObservable("remoteDescription$", () => this._remoteDescription$.asObservable().pipe(takeUntil(this.destroyed$)));
|
|
5209
5482
|
}
|
|
5483
|
+
/** Emits once local media is settled — acquired, or knowingly receive-only. */
|
|
5484
|
+
get localMediaSettled$() {
|
|
5485
|
+
return this.cachedObservable("localMediaSettled$", () => this._localMediaSettled$.asObservable().pipe(takeUntil(this.destroyed$)));
|
|
5486
|
+
}
|
|
5210
5487
|
get localStream$() {
|
|
5211
5488
|
return this.cachedObservable("localStream$", () => this.localStreamController.localStream$.pipe(takeUntil(this.destroyed$)));
|
|
5212
5489
|
}
|
|
@@ -5234,6 +5511,9 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5234
5511
|
get propose() {
|
|
5235
5512
|
return this.options.propose ?? "main";
|
|
5236
5513
|
}
|
|
5514
|
+
get connectionState() {
|
|
5515
|
+
return this.peerConnection?.connectionState;
|
|
5516
|
+
}
|
|
5237
5517
|
get isAdditionalDevice() {
|
|
5238
5518
|
return this.propose === "additional-device";
|
|
5239
5519
|
}
|
|
@@ -5362,7 +5642,7 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5362
5642
|
this._isNegotiating$.next(true);
|
|
5363
5643
|
await this._setRemoteDescription(this.sdpInit);
|
|
5364
5644
|
} else {
|
|
5365
|
-
await this.setupTrackHandling();
|
|
5645
|
+
if (!await this.setupTrackHandling()) return;
|
|
5366
5646
|
this._initialized$.next(true);
|
|
5367
5647
|
}
|
|
5368
5648
|
} catch (error) {
|
|
@@ -5486,7 +5766,10 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5486
5766
|
inputVideoDeviceConstraints: this.inputVideoDeviceConstraints
|
|
5487
5767
|
});
|
|
5488
5768
|
}
|
|
5489
|
-
await this.setupLocalTracks()
|
|
5769
|
+
if (!await this.setupLocalTracks()) {
|
|
5770
|
+
logger$17.debug("[RTCPeerConnectionController] Inbound answer abandoned; the connection went away.");
|
|
5771
|
+
return;
|
|
5772
|
+
}
|
|
5490
5773
|
const { answerOptions } = this;
|
|
5491
5774
|
logger$17.debug("[RTCPeerConnectionController] Creating inbound answer with options:", answerOptions);
|
|
5492
5775
|
await this.createAnswer(answerOptions);
|
|
@@ -5624,35 +5907,52 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5624
5907
|
}
|
|
5625
5908
|
/**
|
|
5626
5909
|
* Setup track handling for remote tracks.
|
|
5910
|
+
*
|
|
5911
|
+
* @returns `false` when the connection went away while local media was being
|
|
5912
|
+
* acquired — see {@link setupLocalTracks}.
|
|
5627
5913
|
*/
|
|
5628
5914
|
async setupTrackHandling() {
|
|
5629
5915
|
if (!this.peerConnection) throw new DependencyError("RTCPeerConnection is not initialized");
|
|
5630
|
-
await this.setupLocalTracks();
|
|
5916
|
+
if (!await this.setupLocalTracks()) return false;
|
|
5631
5917
|
await this.setupRemoteTracks();
|
|
5918
|
+
return true;
|
|
5632
5919
|
}
|
|
5920
|
+
/**
|
|
5921
|
+
* @returns `false` when the connection was torn down while getUserMedia was
|
|
5922
|
+
* in flight. The acquisition is not cancellable, so the caller must stop
|
|
5923
|
+
* rather than go on to touch a peer connection that is closed or gone.
|
|
5924
|
+
*/
|
|
5633
5925
|
async setupLocalTracks() {
|
|
5634
5926
|
logger$17.debug("[RTCPeerConnectionController] Setting up local tracks/transceivers.");
|
|
5635
5927
|
if (this.hasNoLocalMediaToSend()) {
|
|
5636
5928
|
if (!this.receiveAudio && !this.receiveVideo) throw new InvalidParams("Call requests no media: enable audio/video or receiveAudio/receiveVideo");
|
|
5637
5929
|
logger$17.debug("[RTCPeerConnectionController] No local media requested; negotiating receive-only.");
|
|
5638
5930
|
this.setupReceiveOnlyTransceivers();
|
|
5639
|
-
|
|
5931
|
+
this._localMediaSettled$.next();
|
|
5932
|
+
return true;
|
|
5640
5933
|
}
|
|
5641
5934
|
let localStream;
|
|
5642
5935
|
try {
|
|
5643
5936
|
localStream = this.localStream ?? await this.localStreamController.buildLocalStream();
|
|
5644
5937
|
} catch (error) {
|
|
5645
5938
|
this.handleLocalMediaFailure(error);
|
|
5646
|
-
|
|
5939
|
+
this._localMediaSettled$.next();
|
|
5940
|
+
return true;
|
|
5941
|
+
}
|
|
5942
|
+
if (!this.peerConnection || this.peerConnection.signalingState === "closed") {
|
|
5943
|
+
logger$17.debug("[RTCPeerConnectionController] Local media arrived after teardown; releasing it.");
|
|
5944
|
+
localStream.getTracks().forEach((track) => track.stop());
|
|
5945
|
+
return false;
|
|
5647
5946
|
}
|
|
5947
|
+
this._localMediaSettled$.next();
|
|
5648
5948
|
if (this.transceiverController?.useAddStream ?? false) {
|
|
5649
5949
|
logger$17.warn("[RTCPeerConnectionController] Using deprecated addStream API to add local stream.");
|
|
5650
|
-
this.peerConnection
|
|
5950
|
+
this.peerConnection.addStream(localStream);
|
|
5651
5951
|
if (!this.isNegotiating) {
|
|
5652
5952
|
logger$17.debug("[RTCPeerConnectionController] Forcing negotiationneeded after local tracks setup.");
|
|
5653
5953
|
this.negotiationNeeded$.next();
|
|
5654
5954
|
}
|
|
5655
|
-
return;
|
|
5955
|
+
return true;
|
|
5656
5956
|
}
|
|
5657
5957
|
for (const kind of ["audio", "video"]) {
|
|
5658
5958
|
const tracks = (kind === "audio" ? localStream.getAudioTracks() : localStream.getVideoTracks()).map((track, index) => ({
|
|
@@ -5666,10 +5966,11 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5666
5966
|
await this.transceiverController?.setupTransceiverSender(track, localStream, transceivers[index]);
|
|
5667
5967
|
} else {
|
|
5668
5968
|
logger$17.debug(`[RTCPeerConnectionController] Using addTrack for local ${kind} track:`, track.id);
|
|
5669
|
-
this.peerConnection
|
|
5969
|
+
this.peerConnection.addTrack(track, localStream);
|
|
5670
5970
|
}
|
|
5671
5971
|
}
|
|
5672
5972
|
}
|
|
5973
|
+
return true;
|
|
5673
5974
|
}
|
|
5674
5975
|
/** True for a main connection with no local media to send. */
|
|
5675
5976
|
hasNoLocalMediaToSend() {
|
|
@@ -5758,6 +6059,61 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5758
6059
|
this._localAudioPipeline.setInputTrack(newTrack);
|
|
5759
6060
|
}
|
|
5760
6061
|
/**
|
|
6062
|
+
* Capture the newly selected device, leaving the current capture running.
|
|
6063
|
+
*
|
|
6064
|
+
* A rejection must leave the current track sending, so nothing is released
|
|
6065
|
+
* until the replacement is in hand. The one exception is hardware that admits
|
|
6066
|
+
* a single opener — a phone's front and back cameras, typically — which
|
|
6067
|
+
* rejects the second capture until the first is closed.
|
|
6068
|
+
*/
|
|
6069
|
+
async acquireInputTrack(kind, constraints, deviceInfo, currentTrack) {
|
|
6070
|
+
try {
|
|
6071
|
+
return await this.captureTrack(kind, constraints, deviceInfo.deviceId);
|
|
6072
|
+
} catch (error) {
|
|
6073
|
+
if (!isMediaDeviceInUse(error)) throw error;
|
|
6074
|
+
logger$17.warn(`[RTCPeerConnectionController] ${kind} device is held exclusively; releasing the current capture to retry:`, error);
|
|
6075
|
+
const previousDeviceId = currentTrack.getSettings().deviceId;
|
|
6076
|
+
this.stopTrackSender(kind);
|
|
6077
|
+
try {
|
|
6078
|
+
return await this.captureTrack(kind, constraints, deviceInfo.deviceId);
|
|
6079
|
+
} catch (retryError) {
|
|
6080
|
+
await this.restorePreviousInputTrack(kind, constraints, previousDeviceId, currentTrack);
|
|
6081
|
+
throw retryError;
|
|
6082
|
+
}
|
|
6083
|
+
}
|
|
6084
|
+
}
|
|
6085
|
+
async captureTrack(kind, constraints, deviceId) {
|
|
6086
|
+
const { stream, fallbackLevel } = await getUserMediaWithFallback({ getUserMedia: async (c) => this.getUserMedia(c) }, { [kind]: constraints }, kind, deviceId);
|
|
6087
|
+
const track = stream.getTracks().find((t) => t.kind === kind);
|
|
6088
|
+
if (!track) {
|
|
6089
|
+
stream.getTracks().forEach((t) => t.stop());
|
|
6090
|
+
throw new MediaTrackError("updateSelectedInputDevice", kind, /* @__PURE__ */ new Error("getUserMedia returned no track of the requested kind"));
|
|
6091
|
+
}
|
|
6092
|
+
if (fallbackLevel !== "exact") logger$17.warn(`[RTCPeerConnectionController] ${kind} device acquired at fallback level '${fallbackLevel}'; the capture may not be the requested device.`);
|
|
6093
|
+
return track;
|
|
6094
|
+
}
|
|
6095
|
+
/** Best-effort return to the device that was released for an exclusive retry. */
|
|
6096
|
+
async restorePreviousInputTrack(kind, constraints, previousDeviceId, releasedTrack) {
|
|
6097
|
+
try {
|
|
6098
|
+
const restored = await this.captureTrack(kind, constraints, previousDeviceId);
|
|
6099
|
+
await this.attachInputTrack(kind, restored, releasedTrack);
|
|
6100
|
+
} catch (error) {
|
|
6101
|
+
logger$17.error(`[RTCPeerConnectionController] Failed to restore the previous ${kind} device:`, error);
|
|
6102
|
+
}
|
|
6103
|
+
}
|
|
6104
|
+
async attachInputTrack(kind, newTrack, oldTrack) {
|
|
6105
|
+
const pipelineOwnsAudio = kind === "audio" && this._localAudioPipeline;
|
|
6106
|
+
if (!pipelineOwnsAudio) try {
|
|
6107
|
+
await this.transceiverController?.replaceSenderTrack(kind, newTrack);
|
|
6108
|
+
} catch (error) {
|
|
6109
|
+
newTrack.stop();
|
|
6110
|
+
throw error;
|
|
6111
|
+
}
|
|
6112
|
+
this.localStreamController.removeTrack(oldTrack.id);
|
|
6113
|
+
this.localStreamController.addTrack(newTrack);
|
|
6114
|
+
if (pipelineOwnsAudio) this._localAudioPipeline?.setInputTrack(newTrack);
|
|
6115
|
+
}
|
|
6116
|
+
/**
|
|
5761
6117
|
* Return the lazily-created {@link LocalAudioPipeline}, constructing it on
|
|
5762
6118
|
* first access. On creation the current audio sender's track is routed
|
|
5763
6119
|
* through the pipeline (input → gain → analyser → destination) and the
|
|
@@ -5790,6 +6146,7 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5790
6146
|
const sender = (this.transceiverController?.audioTransceivers.at(0))?.sender ?? this.peerConnection.getSenders().find((s) => s.track?.kind === "audio");
|
|
5791
6147
|
if (!sender || !raw) return;
|
|
5792
6148
|
try {
|
|
6149
|
+
this.localStreamController.setTrackOrigin(this._localAudioPipeline.outputTrack, "processed");
|
|
5793
6150
|
await sender.replaceTrack(this._localAudioPipeline.outputTrack);
|
|
5794
6151
|
} catch (error) {
|
|
5795
6152
|
logger$17.warn("[RTCPeerConnectionController] Failed to route audio sender through pipeline:", error);
|
|
@@ -5850,37 +6207,68 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5850
6207
|
for (const existingTrack of existingTracks) this.removeLocalTrack(existingTrack.id);
|
|
5851
6208
|
this.addLocalTrack(track);
|
|
5852
6209
|
}
|
|
6210
|
+
/**
|
|
6211
|
+
* @returns whether the constraints reached the media the leg is sending.
|
|
6212
|
+
*
|
|
6213
|
+
* With the pipeline engaged the audio sender carries the processed
|
|
6214
|
+
* destination track, so the sender scan would find nothing it may touch and
|
|
6215
|
+
* every audio constraint API would silently no-op. The constraints belong to
|
|
6216
|
+
* the pipeline's device source, which is the capture that sender ultimately
|
|
6217
|
+
* carries.
|
|
6218
|
+
*/
|
|
5853
6219
|
async updateSendersConstraints(kind, constraints) {
|
|
5854
|
-
|
|
6220
|
+
if (kind === "audio" && this._localAudioPipeline) {
|
|
6221
|
+
if (!constraints) {
|
|
6222
|
+
this.stopTrackSender("audio");
|
|
6223
|
+
return false;
|
|
6224
|
+
}
|
|
6225
|
+
return this.applyPipelineSourceConstraints(constraints);
|
|
6226
|
+
}
|
|
6227
|
+
return await this.transceiverController?.updateSendersConstraints(kind, constraints) ?? false;
|
|
5855
6228
|
}
|
|
5856
6229
|
/**
|
|
5857
|
-
*
|
|
5858
|
-
*
|
|
5859
|
-
*
|
|
5860
|
-
*
|
|
6230
|
+
* Mirror of the sender path for a piped audio leg: same merge, same fallback
|
|
6231
|
+
* ladder, same device-capture invariant — but the swap target is the pipeline
|
|
6232
|
+
* input, so the sender keeps emitting the pipeline's output track and its
|
|
6233
|
+
* identity survives the change.
|
|
5861
6234
|
*/
|
|
5862
|
-
async
|
|
5863
|
-
const
|
|
5864
|
-
|
|
5865
|
-
|
|
5866
|
-
|
|
6235
|
+
async applyPipelineSourceConstraints(constraints) {
|
|
6236
|
+
const pipeline = this._localAudioPipeline;
|
|
6237
|
+
const source = this.localStreamController.localAudioTracks.at(0);
|
|
6238
|
+
if (!pipeline || !source) {
|
|
6239
|
+
logger$17.debug("[RTCPeerConnectionController] No pipeline input to constrain.");
|
|
6240
|
+
return false;
|
|
5867
6241
|
}
|
|
5868
|
-
|
|
5869
|
-
|
|
5870
|
-
|
|
5871
|
-
|
|
5872
|
-
|
|
5873
|
-
|
|
5874
|
-
|
|
5875
|
-
|
|
5876
|
-
|
|
5877
|
-
|
|
5878
|
-
|
|
5879
|
-
|
|
5880
|
-
|
|
5881
|
-
|
|
6242
|
+
if (!this.localStreamController.isDeviceCapture(source)) {
|
|
6243
|
+
logger$17.debug(`[RTCPeerConnectionController] Skipping audio constraints for a non-device pipeline input (origin: ${this.localStreamController.getTrackOrigin(source) ?? "unrecorded"}).`);
|
|
6244
|
+
return false;
|
|
6245
|
+
}
|
|
6246
|
+
const merged = {
|
|
6247
|
+
...source.getConstraints(),
|
|
6248
|
+
...constraints
|
|
6249
|
+
};
|
|
6250
|
+
try {
|
|
6251
|
+
await source.applyConstraints(merged);
|
|
6252
|
+
logger$17.debug("[RTCPeerConnectionController] Pipeline input constraints updated:", merged);
|
|
6253
|
+
return true;
|
|
6254
|
+
} catch (error) {
|
|
6255
|
+
logger$17.warn("[RTCPeerConnectionController] applyConstraints failed on the pipeline input, re-acquiring:", error);
|
|
6256
|
+
}
|
|
6257
|
+
try {
|
|
6258
|
+
const { stream } = await getUserMediaWithFallback({ getUserMedia: async (c) => this.getUserMedia(c) }, { audio: merged }, "audio", source.getSettings().deviceId);
|
|
6259
|
+
const newTrack = stream.getAudioTracks().at(0);
|
|
6260
|
+
if (!newTrack) {
|
|
6261
|
+
stream.getTracks().forEach((track) => track.stop());
|
|
6262
|
+
throw new Error("getUserMedia returned no audio track");
|
|
6263
|
+
}
|
|
6264
|
+
this.localStreamController.removeTrack(source.id);
|
|
5882
6265
|
this.localStreamController.addTrack(newTrack);
|
|
5883
|
-
|
|
6266
|
+
pipeline.setInputTrack(newTrack);
|
|
6267
|
+
return true;
|
|
6268
|
+
} catch (error) {
|
|
6269
|
+
logger$17.warn("[RTCPeerConnectionController] Failed to re-acquire the pipeline input for constraints:", error);
|
|
6270
|
+
this._errors$.next(new MediaTrackError("updateSendersConstraints", "audio", error));
|
|
6271
|
+
return false;
|
|
5884
6272
|
}
|
|
5885
6273
|
}
|
|
5886
6274
|
/**
|
|
@@ -5999,6 +6387,48 @@ const logger$16 = getLogger();
|
|
|
5999
6387
|
function resolveInviteNodeId(args) {
|
|
6000
6388
|
return args.isInvite && !args.reattach && !args.explicitNodeId ? "" : args.currentNodeId ?? "";
|
|
6001
6389
|
}
|
|
6390
|
+
/**
|
|
6391
|
+
* Surface the real outcome of a `webrtc.verto` reply.
|
|
6392
|
+
*
|
|
6393
|
+
* A webrtc.verto response nests several envelopes, each keyed by a verto-style
|
|
6394
|
+
* string `code` ("200" ok, "400"/etc. fail) rather than a JSON-RPC `error`. An outer
|
|
6395
|
+
* layer reports only whether the frame was delivered; an inner layer carries the op's
|
|
6396
|
+
* own outcome:
|
|
6397
|
+
*
|
|
6398
|
+
* response.result = { code:"200", result:{…} } ← delivery acknowledgement
|
|
6399
|
+
* .result = { jsonrpc, id, result:{…} } ← the reply payload
|
|
6400
|
+
* .result = { code:"400", message:"Bad request" } ← the actual op outcome
|
|
6401
|
+
*
|
|
6402
|
+
* A failure can appear at any layer (delivery refused, or the op itself rejected
|
|
6403
|
+
* deeper down), so walk every nested `.result` object and return the FIRST non-2xx
|
|
6404
|
+
* `code` with its message. Returns null when every `code` seen is 2xx or absent —
|
|
6405
|
+
* i.e. the op succeeded. This is the only way to detect that e.g. a mute/kick was
|
|
6406
|
+
* rejected, since the outer delivery `code` is "200" (delivered) even then.
|
|
6407
|
+
*
|
|
6408
|
+
* Pure function — exported for unit testing.
|
|
6409
|
+
*/
|
|
6410
|
+
function findNestedVertoFailure(response) {
|
|
6411
|
+
let node = response;
|
|
6412
|
+
while (node !== null && typeof node === "object") {
|
|
6413
|
+
const obj = node;
|
|
6414
|
+
const err = obj.error;
|
|
6415
|
+
if (err !== null && typeof err === "object") {
|
|
6416
|
+
const e = err;
|
|
6417
|
+
const errCode = typeof e.code === "string" || typeof e.code === "number" ? String(e.code) : void 0;
|
|
6418
|
+
if (errCode !== void 0 && !/^2\d\d$/.test(errCode)) return {
|
|
6419
|
+
code: errCode,
|
|
6420
|
+
message: typeof e.message === "string" ? e.message : void 0
|
|
6421
|
+
};
|
|
6422
|
+
}
|
|
6423
|
+
const code = typeof obj.code === "string" || typeof obj.code === "number" ? String(obj.code) : void 0;
|
|
6424
|
+
if (code !== void 0 && !/^2\d\d$/.test(code)) return {
|
|
6425
|
+
code,
|
|
6426
|
+
message: typeof obj.message === "string" ? obj.message : void 0
|
|
6427
|
+
};
|
|
6428
|
+
node = obj.result !== null && typeof obj.result === "object" ? obj.result : null;
|
|
6429
|
+
}
|
|
6430
|
+
return null;
|
|
6431
|
+
}
|
|
6002
6432
|
var VertoManager = class extends Destroyable {
|
|
6003
6433
|
constructor(callSession) {
|
|
6004
6434
|
super();
|
|
@@ -6021,7 +6451,7 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6021
6451
|
this._signalingStatus$ = this.createReplaySubject(1);
|
|
6022
6452
|
this._screenShareStatus$ = this.createBehaviorSubject("none");
|
|
6023
6453
|
this._rtcPeerConnectionsMap = /* @__PURE__ */ new Map();
|
|
6024
|
-
this.
|
|
6454
|
+
this._legErrors$ = this.createSubject();
|
|
6025
6455
|
this._nodeId$ = this.createBehaviorSubject(options.nodeId ?? null);
|
|
6026
6456
|
this.onError = options.onError;
|
|
6027
6457
|
this.onModifyFailed = options.onModifyFailed;
|
|
@@ -6069,6 +6499,10 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6069
6499
|
get selfId$() {
|
|
6070
6500
|
return this._selfId$.asObservable();
|
|
6071
6501
|
}
|
|
6502
|
+
/** Separates the media phase of call creation from the signalling phase. */
|
|
6503
|
+
get localMediaSettled$() {
|
|
6504
|
+
return this.mainPeerConnection.localMediaSettled$;
|
|
6505
|
+
}
|
|
6072
6506
|
get localStream() {
|
|
6073
6507
|
return this._rtcPeerConnectionsMap.get(this.webRtcCallSession.id)?.localStream ?? null;
|
|
6074
6508
|
}
|
|
@@ -6125,41 +6559,101 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6125
6559
|
const { mediaParams, callID } = event;
|
|
6126
6560
|
const rtcPeerConnController = this._rtcPeerConnectionsMap.get(callID);
|
|
6127
6561
|
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
|
-
})();
|
|
6562
|
+
if (!rtcPeerConnController) {
|
|
6563
|
+
logger$16.warn(`[WebRTCManager] Ignoring server-pushed media params for unknown leg ${callID}`);
|
|
6564
|
+
return;
|
|
6565
|
+
}
|
|
6566
|
+
this.applyServerMediaParams(rtcPeerConnController, audio, video);
|
|
6142
6567
|
});
|
|
6143
6568
|
this.subscribeTo(this.vertoPing$, (vertoPing) => {
|
|
6144
|
-
this.attachManager.
|
|
6569
|
+
this.attachManager.refresh(this.buildAttachableCall());
|
|
6145
6570
|
this.sendVertoPong(vertoPing);
|
|
6146
6571
|
});
|
|
6147
6572
|
}
|
|
6148
6573
|
/**
|
|
6149
|
-
*
|
|
6574
|
+
* An auxiliary-leg failure must never destroy the call; main-leg and
|
|
6575
|
+
* call-level errors keep `CallFactory.isFatalError`'s classification.
|
|
6150
6576
|
*
|
|
6151
|
-
*
|
|
6152
|
-
*
|
|
6153
|
-
*
|
|
6154
|
-
*
|
|
6155
|
-
*
|
|
6156
|
-
*
|
|
6577
|
+
* Every site holding a peer connection reports through here, so the invariant
|
|
6578
|
+
* is structural rather than per-call-site — which is how cloud-product#20523
|
|
6579
|
+
* happened, with only one of fourteen sites passing `{ fatal: false }`.
|
|
6580
|
+
*
|
|
6581
|
+
* `override` composes rather than replaces: a caller may force non-fatal for a
|
|
6582
|
+
* reason of its own (a `verto.info` frame is best-effort whichever leg carries
|
|
6583
|
+
* it), and an auxiliary leg stays non-fatal regardless.
|
|
6157
6584
|
*/
|
|
6158
|
-
|
|
6159
|
-
|
|
6160
|
-
|
|
6161
|
-
|
|
6162
|
-
|
|
6585
|
+
reportLegError(error, rtcPeerConnController, override) {
|
|
6586
|
+
const leg = rtcPeerConnController?.propose;
|
|
6587
|
+
const legId = rtcPeerConnController?.id;
|
|
6588
|
+
const auxiliary = Boolean(rtcPeerConnController) && !rtcPeerConnController?.isMainDevice;
|
|
6589
|
+
this.onError?.(error, {
|
|
6590
|
+
...override?.fatal === false || auxiliary ? { fatal: false } : {},
|
|
6591
|
+
...leg ? { leg } : {},
|
|
6592
|
+
...legId ? { legId } : {}
|
|
6593
|
+
});
|
|
6594
|
+
if (legId) this._legErrors$.next({
|
|
6595
|
+
legId,
|
|
6596
|
+
error
|
|
6597
|
+
});
|
|
6598
|
+
}
|
|
6599
|
+
/**
|
|
6600
|
+
* Errors reported for one leg, as a stream that fails with them.
|
|
6601
|
+
*
|
|
6602
|
+
* Signaling failures are reported, never thrown — so nothing that waits on a
|
|
6603
|
+
* leg's progress would otherwise learn of a rejected invite. Merging this in
|
|
6604
|
+
* lets the wait end with the reason the server gave.
|
|
6605
|
+
*/
|
|
6606
|
+
legError$(legId) {
|
|
6607
|
+
return this._legErrors$.pipe(filter((report) => report.legId === legId), map((report) => {
|
|
6608
|
+
throw report.error;
|
|
6609
|
+
}));
|
|
6610
|
+
}
|
|
6611
|
+
/**
|
|
6612
|
+
* Audio and video are applied independently so a failure in one cannot
|
|
6613
|
+
* suppress the other, and `mediaParamsUpdated` is emitted whatever happens —
|
|
6614
|
+
* an application should not be starved of the params by a constraint failure.
|
|
6615
|
+
*/
|
|
6616
|
+
async applyServerMediaParams(rtcPeerConnController, audio, video) {
|
|
6617
|
+
const failures = [];
|
|
6618
|
+
let applied = true;
|
|
6619
|
+
if (audio) try {
|
|
6620
|
+
applied = await rtcPeerConnController.updateSendersConstraints("audio", audio) && applied;
|
|
6621
|
+
} catch (error) {
|
|
6622
|
+
applied = false;
|
|
6623
|
+
failures.push(toError(error));
|
|
6624
|
+
}
|
|
6625
|
+
if (video) try {
|
|
6626
|
+
applied = await rtcPeerConnController.updateSendersConstraints("video", video) && applied;
|
|
6627
|
+
} catch (error) {
|
|
6628
|
+
applied = false;
|
|
6629
|
+
failures.push(toError(error));
|
|
6630
|
+
}
|
|
6631
|
+
this.webRtcCallSession.emitMediaParamsUpdated({
|
|
6632
|
+
audio,
|
|
6633
|
+
video,
|
|
6634
|
+
timestamp: Date.now(),
|
|
6635
|
+
applied
|
|
6636
|
+
});
|
|
6637
|
+
for (const failure of failures) {
|
|
6638
|
+
logger$16.warn("[WebRTCManager] Error applying server-pushed media params:", failure);
|
|
6639
|
+
this.reportLegError(failure, rtcPeerConnController, { fatal: false });
|
|
6640
|
+
}
|
|
6641
|
+
}
|
|
6642
|
+
/**
|
|
6643
|
+
* Set node_id/selfId only when the current value is null.
|
|
6644
|
+
*
|
|
6645
|
+
* During reattach, `call.joined` and `verto.answer` events can deliver
|
|
6646
|
+
* these identifiers before the `verto.invite` RPC response (`CALL CREATED`)
|
|
6647
|
+
* arrives. These methods let early events populate them eagerly so that
|
|
6648
|
+
* downstream RPC calls (e.g. `call.layout.list`) don't fail with empty
|
|
6649
|
+
* identifiers. `processInviteResponse()` remains the authoritative source and
|
|
6650
|
+
* overwrites unconditionally — for selfId, on the main leg only.
|
|
6651
|
+
*/
|
|
6652
|
+
setNodeIdIfNull(nodeId) {
|
|
6653
|
+
if (!this._nodeId$.value && nodeId) {
|
|
6654
|
+
logger$16.debug(`[WebRTCManager] Early node_id set: ${nodeId}`);
|
|
6655
|
+
this._nodeId$.next(nodeId);
|
|
6656
|
+
}
|
|
6163
6657
|
}
|
|
6164
6658
|
setSelfIdIfNull(selfId) {
|
|
6165
6659
|
if (!this._selfId$.value && selfId) {
|
|
@@ -6176,24 +6670,33 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6176
6670
|
this.onError?.(new VertoPongError(error));
|
|
6177
6671
|
}
|
|
6178
6672
|
}
|
|
6673
|
+
/**
|
|
6674
|
+
* @returns whether the constraints reached the media the call is sending.
|
|
6675
|
+
* `false` is an outcome, not a failure: the leg may have no live sender of
|
|
6676
|
+
* the kind, or carry media the SDK did not capture and may not replace.
|
|
6677
|
+
* A failure behind it still reaches the call's `errors$`, so a caller that
|
|
6678
|
+
* ignores this value learns of it there.
|
|
6679
|
+
*/
|
|
6179
6680
|
async updateMediaConstraints(options = {}) {
|
|
6180
6681
|
const { audio, video } = options;
|
|
6682
|
+
let applied = true;
|
|
6181
6683
|
try {
|
|
6182
|
-
if (audio) await this.mainPeerConnection.updateSendersConstraints("audio", audio);
|
|
6183
|
-
if (video) await this.mainPeerConnection.updateSendersConstraints("video", video);
|
|
6684
|
+
if (audio) applied = await this.mainPeerConnection.updateSendersConstraints("audio", audio) && applied;
|
|
6685
|
+
if (video) applied = await this.mainPeerConnection.updateSendersConstraints("video", video) && applied;
|
|
6184
6686
|
} catch (error) {
|
|
6185
6687
|
logger$16.warn("[WebRTCManager] Error updating media constraints:", error);
|
|
6186
|
-
this.
|
|
6688
|
+
this.reportLegError(toError(error), this.mainPeerConnection);
|
|
6187
6689
|
throw error;
|
|
6188
6690
|
}
|
|
6691
|
+
return applied;
|
|
6189
6692
|
}
|
|
6190
6693
|
get selfId() {
|
|
6191
6694
|
return this._selfId$.value;
|
|
6192
6695
|
}
|
|
6193
6696
|
/** Build an AttachableCall from the current call state. */
|
|
6194
|
-
buildAttachableCall(idOverride) {
|
|
6697
|
+
buildAttachableCall(idOverride, nodeIdOverride) {
|
|
6195
6698
|
return {
|
|
6196
|
-
nodeId: this.nodeId ?? void 0,
|
|
6699
|
+
nodeId: nodeIdOverride ?? this.nodeId ?? void 0,
|
|
6197
6700
|
id: idOverride ?? this.webRtcCallSession.id,
|
|
6198
6701
|
to: this.webRtcCallSession.to,
|
|
6199
6702
|
mediaDirections: this.webRtcCallSession.mediaDirections
|
|
@@ -6323,24 +6826,49 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6323
6826
|
get vertoPing$() {
|
|
6324
6827
|
return this.cachedObservable("vertoPing$", () => this.webRtcCallSession.webrtcMessages$.pipe(filterAs(isVertoPingInnerParams, "params"), takeUntil(this.destroyed$)));
|
|
6325
6828
|
}
|
|
6829
|
+
/**
|
|
6830
|
+
* Send a member-control op in-dialog via verto.info.
|
|
6831
|
+
*
|
|
6832
|
+
* The control payload rides in `params.command` — a sibling of `dialogParams`,
|
|
6833
|
+
* at the same level as `dtmf` in {@link sendDigits} — and the inner verto.info
|
|
6834
|
+
* is matched to this call's channel by `dialogParams.callID`, the in-dialog
|
|
6835
|
+
* convention for member-scoped frames. Because it is delivered on the dialog
|
|
6836
|
+
* itself, control lands on the call's own channel with no {node_id,call_id,member_id}
|
|
6837
|
+
* "self" tuple to get wrong. The outer webrtc.verto envelope (added by executeVerto)
|
|
6838
|
+
* still carries the own-leg callID + node_id for session routing.
|
|
6839
|
+
*
|
|
6840
|
+
* Keep `command` OUT of `dialogParams`: it is read at the params level, and
|
|
6841
|
+
* filterVertoParams rewrites/filters dialogParams keys but passes params-level
|
|
6842
|
+
* keys through verbatim.
|
|
6843
|
+
*/
|
|
6844
|
+
async sendCallControl(method, params) {
|
|
6845
|
+
const response = await this.executeVerto(VertoInfo({
|
|
6846
|
+
dialogParams: { callID: this.webRtcCallSession.id },
|
|
6847
|
+
command: {
|
|
6848
|
+
method,
|
|
6849
|
+
params
|
|
6850
|
+
}
|
|
6851
|
+
}));
|
|
6852
|
+
const failure = findNestedVertoFailure(response);
|
|
6853
|
+
if (failure) throw new JSONRPCError(Number.parseInt(failure.code, 10) || 0, `Call control "${method}" failed (code ${failure.code})${failure.message ? `: ${failure.message}` : ""}`, void 0);
|
|
6854
|
+
return response;
|
|
6855
|
+
}
|
|
6326
6856
|
async executeVerto(message, optionals = {}) {
|
|
6327
|
-
const
|
|
6857
|
+
const params = {
|
|
6328
6858
|
callID: optionals.callID ?? this.webRtcCallSession.id,
|
|
6329
6859
|
node_id: optionals.node_id ?? this._nodeId$.value ?? "",
|
|
6330
6860
|
message,
|
|
6331
6861
|
subscribe: optionals.subscribe
|
|
6332
|
-
}
|
|
6862
|
+
};
|
|
6863
|
+
const webrtcVertoMessage = WebrtcVerto(params);
|
|
6333
6864
|
const response = await this.webRtcCallSession.execute(webrtcVertoMessage);
|
|
6334
|
-
|
|
6335
|
-
const error = new JSONRPCError(response.error.code, response.error.message, response.error.data);
|
|
6336
|
-
this.onError?.(error);
|
|
6337
|
-
return response;
|
|
6338
|
-
}
|
|
6865
|
+
const nonFatal = message.method === "verto.info" ? { fatal: false } : void 0;
|
|
6339
6866
|
const innerResult = getValueFrom(response, "result.result");
|
|
6340
|
-
|
|
6341
|
-
|
|
6342
|
-
|
|
6343
|
-
|
|
6867
|
+
const failure = response.error ?? innerResult?.error;
|
|
6868
|
+
if (failure) {
|
|
6869
|
+
const error = new JSONRPCError(failure.code, failure.message, failure.data);
|
|
6870
|
+
if (message.method === "verto.invite" || message.method === "verto.answer") throw error;
|
|
6871
|
+
this.reportLegError(error, this._rtcPeerConnectionsMap.get(params.callID), nonFatal);
|
|
6344
6872
|
}
|
|
6345
6873
|
return response;
|
|
6346
6874
|
}
|
|
@@ -6359,8 +6887,9 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6359
6887
|
default:
|
|
6360
6888
|
}
|
|
6361
6889
|
} catch (error) {
|
|
6890
|
+
if (vertoMethod === "verto.answer") throw error;
|
|
6362
6891
|
logger$16.error(`[WebRTCManager] Error sending Verto ${vertoMethod}:`, error);
|
|
6363
|
-
this.
|
|
6892
|
+
this.reportLegError(toError(error), rtcPeerConnController);
|
|
6364
6893
|
if (vertoMethod === "verto.modify") this.onModifyFailed?.();
|
|
6365
6894
|
}
|
|
6366
6895
|
}
|
|
@@ -6376,7 +6905,7 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6376
6905
|
} catch (error) {
|
|
6377
6906
|
logger$16.warn("[WebRTCManager] Error processing modify response:", error);
|
|
6378
6907
|
const modifyError = error instanceof Error ? error : new Error(String(error), { cause: error });
|
|
6379
|
-
this.
|
|
6908
|
+
this.reportLegError(modifyError, rtcPeerConnController);
|
|
6380
6909
|
}
|
|
6381
6910
|
}
|
|
6382
6911
|
}
|
|
@@ -6389,37 +6918,35 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6389
6918
|
status,
|
|
6390
6919
|
signalingError
|
|
6391
6920
|
});
|
|
6392
|
-
this.
|
|
6921
|
+
this.reportLegError(signalingError, null, { fatal: false });
|
|
6393
6922
|
return;
|
|
6394
6923
|
}
|
|
6395
6924
|
if (rtcPeerConnController.isMainDevice) this._signalingStatus$.next(status);
|
|
6396
6925
|
}
|
|
6397
6926
|
processInviteResponse(response, rtcPeerConnController) {
|
|
6398
|
-
if (
|
|
6927
|
+
if (getValueFrom(response, "result.result.result.message") === "CALL CREATED") {
|
|
6399
6928
|
this.emitMainSignalingStatus(rtcPeerConnController.id, "trying");
|
|
6400
|
-
|
|
6929
|
+
const nodeId = getValueFrom(response, "result.node_id") ?? null;
|
|
6401
6930
|
const memberId = getValueFrom(response, "result.result.result.memberID") ?? null;
|
|
6402
|
-
const callId = getValueFrom(response, "result.result.result.callID")
|
|
6931
|
+
const callId = getValueFrom(response, "result.result.result.callID");
|
|
6403
6932
|
logger$16.debug("[WebRTCManager] Verto invite response:", {
|
|
6404
6933
|
callId,
|
|
6405
6934
|
memberId,
|
|
6406
6935
|
response
|
|
6407
6936
|
});
|
|
6408
|
-
this._selfId$.next(memberId);
|
|
6409
6937
|
rtcPeerConnController.setMemberId(memberId);
|
|
6410
|
-
|
|
6411
|
-
|
|
6412
|
-
this.
|
|
6413
|
-
|
|
6414
|
-
|
|
6415
|
-
|
|
6416
|
-
|
|
6938
|
+
rtcPeerConnController.setNodeId(nodeId);
|
|
6939
|
+
if (rtcPeerConnController.isMainDevice) {
|
|
6940
|
+
this._selfId$.next(memberId);
|
|
6941
|
+
this._nodeId$.next(nodeId);
|
|
6942
|
+
this.attachManager.attach(this.buildAttachableCall(callId, nodeId ?? void 0));
|
|
6943
|
+
}
|
|
6944
|
+
if (callId) this.webRtcCallSession.addCallId(callId);
|
|
6417
6945
|
logger$16.info("[WebRTCManager] Verto invite successful");
|
|
6418
6946
|
logger$16.debug(`[WebRTCManager] nodeid: ${this._nodeId$.value}, selfId: ${this._selfId$.value}`);
|
|
6419
6947
|
} else {
|
|
6420
6948
|
logger$16.error("[WebRTCManager] Verto invite failed:", response);
|
|
6421
|
-
|
|
6422
|
-
this.onError?.(inviteError);
|
|
6949
|
+
this.reportLegError(/* @__PURE__ */ new Error("Verto invite failed: unexpected response"), rtcPeerConnController);
|
|
6423
6950
|
}
|
|
6424
6951
|
}
|
|
6425
6952
|
get RTCPeerConnectionConfig() {
|
|
@@ -6458,7 +6985,7 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6458
6985
|
this._rtcPeerConnectionsMap.set(rtcPeerConnController.id, rtcPeerConnController);
|
|
6459
6986
|
this._rtcPeerConnections$.next(Array.from(this._rtcPeerConnectionsMap.values()));
|
|
6460
6987
|
this.subscribeTo(rtcPeerConnController.errors$, (error) => {
|
|
6461
|
-
this.
|
|
6988
|
+
this.reportLegError(error, rtcPeerConnController);
|
|
6462
6989
|
});
|
|
6463
6990
|
if (options.initOffer) this.handleInboundAnswer(rtcPeerConnController);
|
|
6464
6991
|
}
|
|
@@ -6487,7 +7014,7 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6487
7014
|
await rtcPeerConnController.acceptInbound(answerOptions);
|
|
6488
7015
|
} catch (error) {
|
|
6489
7016
|
logger$16.error("[WebRTCManager] Error creating inbound answer:", error);
|
|
6490
|
-
this.
|
|
7017
|
+
this.reportLegError(toError(error), rtcPeerConnController);
|
|
6491
7018
|
}
|
|
6492
7019
|
}
|
|
6493
7020
|
}
|
|
@@ -6559,7 +7086,7 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6559
7086
|
isInvite: isVertoInviteMessage(vertoMessage),
|
|
6560
7087
|
reattach: this.webRtcCallSession.options.reattach === true,
|
|
6561
7088
|
explicitNodeId: this.webRtcCallSession.options.nodeId,
|
|
6562
|
-
currentNodeId: this._nodeId$.value
|
|
7089
|
+
currentNodeId: rtcPeerConnController.nodeId ?? this._nodeId$.value
|
|
6563
7090
|
}),
|
|
6564
7091
|
subscribe
|
|
6565
7092
|
};
|
|
@@ -6591,7 +7118,7 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6591
7118
|
await this.attachManager.attach(this.buildAttachableCall());
|
|
6592
7119
|
} catch (error) {
|
|
6593
7120
|
logger$16.error("[WebRTCManager] Error sending Verto answer:", error);
|
|
6594
|
-
this.
|
|
7121
|
+
this.reportLegError(toError(error), rtcPeerConnectionController);
|
|
6595
7122
|
await rtcPeerConnectionController.updateAnswerStatus({ status: "failed" });
|
|
6596
7123
|
}
|
|
6597
7124
|
}
|
|
@@ -6663,15 +7190,19 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6663
7190
|
await this.mainPeerConnection.restoreTrackSender(deviceKind);
|
|
6664
7191
|
} else {
|
|
6665
7192
|
const error = new InvalidParams("No valid device to be added");
|
|
6666
|
-
this.
|
|
7193
|
+
this.reportLegError(error, this.mainPeerConnection);
|
|
6667
7194
|
throw error;
|
|
6668
7195
|
}
|
|
6669
7196
|
}
|
|
6670
|
-
async addScreenMedia(options = {
|
|
6671
|
-
await this.initAdditionalPeerConnection("screenshare",
|
|
7197
|
+
async addScreenMedia(options = {}) {
|
|
7198
|
+
await this.initAdditionalPeerConnection("screenshare", {
|
|
7199
|
+
audio: false,
|
|
7200
|
+
screenShareAudio: options.audio ?? false
|
|
7201
|
+
});
|
|
6672
7202
|
}
|
|
6673
7203
|
async initAdditionalPeerConnection(propose, options) {
|
|
6674
7204
|
const isScreenShare = propose === "screenshare";
|
|
7205
|
+
if (isScreenShare && this._screenShareId && this._rtcPeerConnectionsMap.has(this._screenShareId)) throw new ScreenShareAlreadyActiveError(this._screenShareId);
|
|
6675
7206
|
let firstPeerConnectionError;
|
|
6676
7207
|
let rtcPeerConnController = null;
|
|
6677
7208
|
try {
|
|
@@ -6688,21 +7219,36 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6688
7219
|
this._rtcPeerConnections$.next(Array.from(this._rtcPeerConnectionsMap.values()));
|
|
6689
7220
|
this.subscribeTo(rtcPeerConnController.errors$, (error) => {
|
|
6690
7221
|
firstPeerConnectionError ??= error;
|
|
6691
|
-
this.
|
|
7222
|
+
this.reportLegError(error, rtcPeerConnController);
|
|
6692
7223
|
});
|
|
6693
|
-
|
|
7224
|
+
const pc = rtcPeerConnController;
|
|
7225
|
+
await firstValueFrom(merge(pc.localMediaSettled$.pipe(take(1), switchMap(() => pc.connectionState$.pipe(filter((state) => state === "connected"), take(1), timeout(DEFAULT_AUX_LEG_CONNECT_TIMEOUT_MS)))), this.legError$(pc.id)).pipe(takeUntil(merge(this.destroyed$, pc.destroyed$))));
|
|
6694
7226
|
if (isScreenShare) this._screenShareStatus$.next("started");
|
|
6695
7227
|
logger$16.info(`[WebRTCManager] Additional peer connection connected (${propose}).`);
|
|
6696
7228
|
return rtcPeerConnController.id;
|
|
6697
7229
|
} catch (error) {
|
|
6698
|
-
|
|
6699
|
-
|
|
6700
|
-
if (
|
|
7230
|
+
const cancelled = error instanceof AuxiliaryLegCancelledError;
|
|
7231
|
+
const aborted = error instanceof EmptyError && !firstPeerConnectionError;
|
|
7232
|
+
if (!cancelled && !aborted) logger$16.warn("[WebRTCManager] Error initializing additional peer connection:", error);
|
|
7233
|
+
if (rtcPeerConnController && this._rtcPeerConnectionsMap.has(rtcPeerConnController.id)) {
|
|
7234
|
+
rtcPeerConnController.destroy();
|
|
7235
|
+
this._rtcPeerConnectionsMap.delete(rtcPeerConnController.id);
|
|
7236
|
+
this._rtcPeerConnections$.next(Array.from(this._rtcPeerConnectionsMap.values()));
|
|
7237
|
+
}
|
|
7238
|
+
if (isScreenShare) {
|
|
7239
|
+
this._screenShareId = void 0;
|
|
7240
|
+
this._screenShareStatus$.next("none");
|
|
7241
|
+
}
|
|
7242
|
+
if (cancelled) {
|
|
7243
|
+
logger$16.debug("[WebRTCManager] Additional peer connection removed before connecting.");
|
|
7244
|
+
throw error;
|
|
7245
|
+
}
|
|
6701
7246
|
if (firstPeerConnectionError) throw firstPeerConnectionError instanceof MediaAccessError && firstPeerConnectionError.originalError instanceof Error ? firstPeerConnectionError.originalError : firstPeerConnectionError;
|
|
6702
7247
|
if (error instanceof EmptyError) {
|
|
6703
7248
|
logger$16.debug("[WebRTCManager] Additional peer connection aborted before connecting.");
|
|
6704
7249
|
return;
|
|
6705
7250
|
}
|
|
7251
|
+
if (error instanceof TimeoutError) throw new AuxiliaryLegTimeoutError(propose, error);
|
|
6706
7252
|
throw error instanceof Error ? error : new Error(String(error), { cause: error });
|
|
6707
7253
|
}
|
|
6708
7254
|
}
|
|
@@ -6719,7 +7265,10 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6719
7265
|
if (removeTrack) return this.mainPeerConnection.stopTrackSender(removeTrack, { updateTransceiverDirection: true });
|
|
6720
7266
|
}
|
|
6721
7267
|
async removeScreenMedia() {
|
|
6722
|
-
if (!["starting", "started"].includes(this._screenShareStatus$.value))
|
|
7268
|
+
if (!["starting", "started"].includes(this._screenShareStatus$.value)) {
|
|
7269
|
+
logger$16.warn("[WebRTCManager] No active screen share to stop.");
|
|
7270
|
+
return;
|
|
7271
|
+
}
|
|
6723
7272
|
if (!this._screenShareId) {
|
|
6724
7273
|
logger$16.debug("[WebRTCManager] No screen share peer connection found.");
|
|
6725
7274
|
return;
|
|
@@ -6734,6 +7283,10 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6734
7283
|
try {
|
|
6735
7284
|
if (rtcPeerConnController) await this.executeVertoBye(rtcPeerConnController);
|
|
6736
7285
|
} finally {
|
|
7286
|
+
if (rtcPeerConnController && rtcPeerConnController.connectionState !== "connected") this._legErrors$.next({
|
|
7287
|
+
legId: id,
|
|
7288
|
+
error: new AuxiliaryLegCancelledError(rtcPeerConnController.propose)
|
|
7289
|
+
});
|
|
6737
7290
|
rtcPeerConnController?.destroy();
|
|
6738
7291
|
this._rtcPeerConnectionsMap.delete(id);
|
|
6739
7292
|
this._rtcPeerConnections$.next(Array.from(this._rtcPeerConnectionsMap.values()));
|
|
@@ -6748,7 +7301,10 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6748
7301
|
await this.executeVerto(VertoBye({
|
|
6749
7302
|
...causeParams,
|
|
6750
7303
|
dialogParams: this.dialogParams(rtcPeerConnController)
|
|
6751
|
-
})
|
|
7304
|
+
}), {
|
|
7305
|
+
callID: rtcPeerConnController.id,
|
|
7306
|
+
node_id: rtcPeerConnController.nodeId ?? void 0
|
|
7307
|
+
});
|
|
6752
7308
|
} catch (error) {
|
|
6753
7309
|
logger$16.warn("[WebRTCManager] Call might already be disconnected, error sending Verto bye:", error);
|
|
6754
7310
|
throw error;
|
|
@@ -7631,14 +8187,35 @@ function mosToQualityLevel(mos) {
|
|
|
7631
8187
|
}
|
|
7632
8188
|
|
|
7633
8189
|
//#endregion
|
|
7634
|
-
//#region src/
|
|
7635
|
-
const logger$12 = getLogger();
|
|
8190
|
+
//#region src/utils/unwrapVertoReply.ts
|
|
7636
8191
|
/**
|
|
7637
|
-
*
|
|
7638
|
-
*
|
|
7639
|
-
*
|
|
8192
|
+
* Unwrap a method's own reply from a `webrtc.verto` envelope.
|
|
8193
|
+
*
|
|
8194
|
+
* A control verb sent in-dialog comes back nested two levels deep:
|
|
8195
|
+
*
|
|
8196
|
+
* ```
|
|
8197
|
+
* { result: { node_id, code, result: { jsonrpc, id, result: <method payload> } } }
|
|
8198
|
+
* ```
|
|
8199
|
+
*
|
|
8200
|
+
* whereas the routed transport resolves the method payload directly under
|
|
8201
|
+
* `.result`. Readers want the latter shape, and the difference is silent when it
|
|
8202
|
+
* is wrong — `response.result.layouts` simply evaluates to `undefined` against the
|
|
8203
|
+
* envelope, so the data arrives, nothing throws, and the caller sees an empty
|
|
8204
|
+
* value. That exact failure produced an empty layout dropdown with a successful
|
|
8205
|
+
* request behind it.
|
|
8206
|
+
*
|
|
8207
|
+
* Accepting both shapes here keeps every reader indifferent to which transport
|
|
8208
|
+
* produced the response. Anything that is not a nested envelope (a plain ack, or
|
|
8209
|
+
* an already-unwrapped reply) passes through untouched.
|
|
7640
8210
|
*/
|
|
7641
|
-
|
|
8211
|
+
function unwrapVertoReply(response) {
|
|
8212
|
+
const inner = getValueFrom(response, "result.result");
|
|
8213
|
+
return inner && typeof inner === "object" && "result" in inner ? inner : response;
|
|
8214
|
+
}
|
|
8215
|
+
|
|
8216
|
+
//#endregion
|
|
8217
|
+
//#region src/core/entities/Call.ts
|
|
8218
|
+
const logger$12 = getLogger();
|
|
7642
8219
|
/**
|
|
7643
8220
|
* Ratio between the critical and warning RTT spike multipliers.
|
|
7644
8221
|
* Warning threshold = baseline * warningMultiplier (default 3x)
|
|
@@ -7734,8 +8311,11 @@ var WebRTCCall = class extends Destroyable {
|
|
|
7734
8311
|
emitError(callError) {
|
|
7735
8312
|
if (this._status$.value === "destroyed" || this._status$.value === "failed") return;
|
|
7736
8313
|
this._errors$.next(callError);
|
|
7737
|
-
if (callError.fatal) {
|
|
8314
|
+
if (callError.fatal && this._status$.value !== "disconnecting") {
|
|
7738
8315
|
this._status$.next("failed");
|
|
8316
|
+
this.vertoManager.bye().catch((error) => {
|
|
8317
|
+
logger$12.debug("[Call] fatal-teardown bye failed (signaling likely already dead):", error);
|
|
8318
|
+
});
|
|
7739
8319
|
this.destroy();
|
|
7740
8320
|
}
|
|
7741
8321
|
}
|
|
@@ -7790,7 +8370,7 @@ var WebRTCCall = class extends Destroyable {
|
|
|
7790
8370
|
/** Toggles the call lock state, preventing or allowing new participants from joining. */
|
|
7791
8371
|
async toggleLock() {
|
|
7792
8372
|
const method = this.locked ? "call.unlock" : "call.lock";
|
|
7793
|
-
await this.executeMethod(this.
|
|
8373
|
+
await this.executeMethod(this.callSelf, method, {});
|
|
7794
8374
|
}
|
|
7795
8375
|
/**
|
|
7796
8376
|
* Toggles the hold state of the call (pauses/resumes local media transmission).
|
|
@@ -7839,14 +8419,25 @@ var WebRTCCall = class extends Destroyable {
|
|
|
7839
8419
|
*
|
|
7840
8420
|
* Constructs call context (node_id, call_id, member_id) and sends the RPC request.
|
|
7841
8421
|
*
|
|
7842
|
-
* @param target - Target
|
|
8422
|
+
* @param target - Target {@link MemberTarget} triple, or the local member's
|
|
8423
|
+
* ID string for self-operations (any other string is rejected — a bare
|
|
8424
|
+
* member id cannot carry the remote member's own call context).
|
|
7843
8425
|
* @param method - Verto method name (e.g. `'call.mute'`, `'call.member.remove'`).
|
|
7844
8426
|
* @param args - Parameters for the RPC method.
|
|
7845
8427
|
* @returns The RPC response.
|
|
8428
|
+
* @throws {CallNotReadyError} If the call has no self member context yet.
|
|
8429
|
+
* @throws {InvalidParams} If a string target is not the local member's ID.
|
|
7846
8430
|
* @throws {JSONRPCError} If the RPC call returns an error.
|
|
7847
8431
|
*/
|
|
7848
8432
|
async executeMethod(target, method, args) {
|
|
7849
|
-
const
|
|
8433
|
+
const self = this.callSelf;
|
|
8434
|
+
if (typeof target === "string" && target !== self.member_id) throw new InvalidParams(`Target member ID ${target} does not match call's self member ID ${self.member_id}`);
|
|
8435
|
+
if (this.clientSession.callControl === "in-dialog") return this.executeMethodInDialog(target, method, args);
|
|
8436
|
+
const params = {
|
|
8437
|
+
...args,
|
|
8438
|
+
self,
|
|
8439
|
+
target: typeof target === "string" ? self : target
|
|
8440
|
+
};
|
|
7850
8441
|
const request = buildRPCRequest({
|
|
7851
8442
|
method,
|
|
7852
8443
|
params
|
|
@@ -7860,29 +8451,98 @@ var WebRTCCall = class extends Destroyable {
|
|
|
7860
8451
|
throw error;
|
|
7861
8452
|
}
|
|
7862
8453
|
}
|
|
7863
|
-
|
|
7864
|
-
|
|
7865
|
-
|
|
7866
|
-
|
|
7867
|
-
|
|
7868
|
-
|
|
7869
|
-
|
|
7870
|
-
|
|
7871
|
-
|
|
7872
|
-
|
|
7873
|
-
|
|
7874
|
-
|
|
7875
|
-
|
|
7876
|
-
|
|
7877
|
-
};
|
|
7878
|
-
|
|
7879
|
-
|
|
7880
|
-
|
|
7881
|
-
|
|
7882
|
-
|
|
8454
|
+
/**
|
|
8455
|
+
* `executeMethod` for a call opened with `callControl: 'in-dialog'`.
|
|
8456
|
+
*
|
|
8457
|
+
* Translates the routed transport's calling convention into the in-dialog one. No
|
|
8458
|
+
* `self` tuple is sent, but a `target` is — the same {call_id, member_id} the routed
|
|
8459
|
+
* transport puts in `target` (minus node_id), for self-ops and cross-member ops alike.
|
|
8460
|
+
*
|
|
8461
|
+
* Target shapes are per-verb and irregular, so they are centralised here rather
|
|
8462
|
+
* than left to callers: most verbs take a singular `target`, `call.member.remove`
|
|
8463
|
+
* takes a plural `targets` array, and `call.member.position.set` takes a flat
|
|
8464
|
+
* `targets` of `{call_id, position}` — the one verb keyed on call_id rather than
|
|
8465
|
+
* member_id, so the member triple `Participant.setPosition` built is unwrapped.
|
|
8466
|
+
*/
|
|
8467
|
+
async executeMethodInDialog(target, method, args) {
|
|
8468
|
+
const control = { ...args };
|
|
8469
|
+
if (method === "call.member.position.set") control.targets = (args.targets ?? []).map((entry) => ({
|
|
8470
|
+
call_id: entry.target?.call_id ?? entry.call_id,
|
|
8471
|
+
position: entry.position
|
|
8472
|
+
}));
|
|
8473
|
+
else {
|
|
8474
|
+
const member = typeof target === "object" ? {
|
|
8475
|
+
call_id: target.call_id,
|
|
8476
|
+
member_id: target.member_id
|
|
8477
|
+
} : {
|
|
7883
8478
|
call_id: this.id,
|
|
7884
8479
|
member_id: target
|
|
7885
|
-
}
|
|
8480
|
+
};
|
|
8481
|
+
if (method === "call.member.remove") control.targets = [member];
|
|
8482
|
+
else control.target = member;
|
|
8483
|
+
}
|
|
8484
|
+
return this.sendCommand(method, control);
|
|
8485
|
+
}
|
|
8486
|
+
/**
|
|
8487
|
+
* Sends a `call.*` control verb **in-dialog** via `verto.info`, as an alternative
|
|
8488
|
+
* to the routed {@link executeMethod} transport.
|
|
8489
|
+
*
|
|
8490
|
+
* Why both exist: `executeMethod` addresses the member with an explicit
|
|
8491
|
+
* `{node_id, call_id, member_id}` tuple, which does not resolve for every conference,
|
|
8492
|
+
* so the op can fail. An in-dialog frame carries the verb on the member's own
|
|
8493
|
+
* signaling channel instead, so control works without the client needing to know how
|
|
8494
|
+
* the conference is hosted.
|
|
8495
|
+
*
|
|
8496
|
+
* The trade-off is reach: the in-dialog transport is only accepted for calls that
|
|
8497
|
+
* join a conference over SWML (e.g. an SWML `join_conference`); use the routed
|
|
8498
|
+
* default otherwise.
|
|
8499
|
+
*
|
|
8500
|
+
* `params` are sent verbatim — nothing is built for you, which includes the target.
|
|
8501
|
+
* **A self-directed op still needs one**, or it is refused; name yourself explicitly:
|
|
8502
|
+
*
|
|
8503
|
+
* ```ts
|
|
8504
|
+
* const { call_id, member_id } = call.self.target;
|
|
8505
|
+
* await call.sendCommand('call.mute', { channels: ['audio'], target: { call_id, member_id } });
|
|
8506
|
+
* ```
|
|
8507
|
+
*
|
|
8508
|
+
* Never include `node_id` — only the two ids. The shapes are per-verb: most take a
|
|
8509
|
+
* singular `target`, `call.member.remove` takes a plural `targets` array, and
|
|
8510
|
+
* `call.member.position.set` takes a flat `targets: [{call_id, position}]` (the one
|
|
8511
|
+
* verb keyed on `call_id` rather than `member_id`). Verbs that act on the call as a
|
|
8512
|
+
* whole, or that the SDK does not wrap at all, take no target.
|
|
8513
|
+
*
|
|
8514
|
+
* For the typed alternative that handles all of this, create the client with
|
|
8515
|
+
* `callControl: 'in-dialog'` and use the ordinary `Call`/`Participant` methods.
|
|
8516
|
+
*
|
|
8517
|
+
* @internal Not part of the supported surface while the in-dialog transport is still
|
|
8518
|
+
* rolling out. `WebRTCCall` is exported from the package entry, so without this tag
|
|
8519
|
+
* TypeDoc publishes the method — and the example above — as public API.
|
|
8520
|
+
*
|
|
8521
|
+
* @param method - A `call.*` method name (e.g. `'call.mute'`).
|
|
8522
|
+
* @param params - Method parameters, sent verbatim.
|
|
8523
|
+
* @returns The method's own reply, unwrapped from the `verto.info` envelope.
|
|
8524
|
+
* @throws {JSONRPCError} If the control op fails.
|
|
8525
|
+
*/
|
|
8526
|
+
async sendCommand(method, params = {}) {
|
|
8527
|
+
return unwrapVertoReply(await this.vertoManager.sendCallControl(method, params));
|
|
8528
|
+
}
|
|
8529
|
+
/**
|
|
8530
|
+
* The local leg's member triple — sent as `self` in every member RPC
|
|
8531
|
+
* envelope, and as the `target` of call-scoped self-operations (e.g. lock,
|
|
8532
|
+
* layout).
|
|
8533
|
+
*
|
|
8534
|
+
* @throws {CallNotReadyError} Before `call.joined` delivers the self member
|
|
8535
|
+
* context (`selfId`/`nodeId`) — an RPC without it cannot be routed, so fail
|
|
8536
|
+
* fast instead of sending a doomed request.
|
|
8537
|
+
*/
|
|
8538
|
+
get callSelf() {
|
|
8539
|
+
const node_id = this.nodeId;
|
|
8540
|
+
const member_id = this.vertoManager.selfId;
|
|
8541
|
+
if (!node_id || !member_id) throw new CallNotReadyError(this.id);
|
|
8542
|
+
return {
|
|
8543
|
+
node_id,
|
|
8544
|
+
call_id: this.id,
|
|
8545
|
+
member_id
|
|
7886
8546
|
};
|
|
7887
8547
|
}
|
|
7888
8548
|
/** Observable of the current call status (e.g. `'ringing'`, `'connected'`). */
|
|
@@ -8038,13 +8698,13 @@ var WebRTCCall = class extends Destroyable {
|
|
|
8038
8698
|
get bandwidthConstrained$() {
|
|
8039
8699
|
return this.deferEmission(this._bandwidthConstrained$.asObservable());
|
|
8040
8700
|
}
|
|
8041
|
-
/** Observable that emits when server
|
|
8701
|
+
/** Observable that emits when the server pushes media params. */
|
|
8042
8702
|
get mediaParamsUpdated$() {
|
|
8043
8703
|
return this.deferEmission(this._mediaParamsUpdated$.asObservable());
|
|
8044
8704
|
}
|
|
8045
8705
|
/**
|
|
8046
8706
|
* @internal Emit a media params update event.
|
|
8047
|
-
* Called by the VertoManager when server
|
|
8707
|
+
* Called by the VertoManager when the server pushes media params.
|
|
8048
8708
|
*/
|
|
8049
8709
|
emitMediaParamsUpdated(event) {
|
|
8050
8710
|
this._mediaParamsUpdated$.next(event);
|
|
@@ -8252,6 +8912,10 @@ var WebRTCCall = class extends Destroyable {
|
|
|
8252
8912
|
get selfId$() {
|
|
8253
8913
|
return this.vertoManager.selfId$;
|
|
8254
8914
|
}
|
|
8915
|
+
/** @internal Lets call creation bound the media and signalling phases apart. */
|
|
8916
|
+
get localMediaSettled$() {
|
|
8917
|
+
return this.vertoManager.localMediaSettled$;
|
|
8918
|
+
}
|
|
8255
8919
|
/** Local participant's member ID, or `null` if not joined. */
|
|
8256
8920
|
get selfId() {
|
|
8257
8921
|
return this.vertoManager.selfId;
|
|
@@ -8445,12 +9109,17 @@ var WebRTCCall = class extends Destroyable {
|
|
|
8445
9109
|
*
|
|
8446
9110
|
* **These operations are NOT atomic.** The layout is applied first, then each
|
|
8447
9111
|
* member position sequentially, so members may briefly flash into their
|
|
8448
|
-
* default slots before being moved to the requested positions.
|
|
9112
|
+
* default slots before being moved to the requested positions. Targeted
|
|
9113
|
+
* members are validated upfront, though: when any of them has no
|
|
9114
|
+
* {@link Participant.target | member call context} yet, the whole call
|
|
9115
|
+
* rejects before any request is sent and the layout is left unchanged.
|
|
8449
9116
|
*
|
|
8450
9117
|
* @param layout - Layout name (must be one of {@link layouts}).
|
|
8451
9118
|
* @param positions - Optional map of member IDs to {@link VideoPosition} values.
|
|
8452
9119
|
* When omitted or empty, only the layout is changed.
|
|
8453
9120
|
* @throws {InvalidParams} If the layout is not in the available {@link layouts}.
|
|
9121
|
+
* @throws {ParticipantNotReadyError} If a targeted member's call context has
|
|
9122
|
+
* not been received yet — thrown before any request is sent.
|
|
8454
9123
|
*
|
|
8455
9124
|
* @example
|
|
8456
9125
|
* ```ts
|
|
@@ -8461,18 +9130,19 @@ var WebRTCCall = class extends Destroyable {
|
|
|
8461
9130
|
*/
|
|
8462
9131
|
async setLayout(layout, positions) {
|
|
8463
9132
|
if (!this.layouts.includes(layout)) throw new 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) {
|
|
9133
|
+
const targets = [];
|
|
9134
|
+
for (const [memberId, position] of Object.entries(positions ?? {})) {
|
|
8469
9135
|
const participant = this.participants.find((p) => p.id === memberId);
|
|
8470
9136
|
if (!participant) {
|
|
8471
9137
|
logger$12.warn(`[Call] setLayout: member ${memberId} not found in participants; skipping position ${position}`);
|
|
8472
9138
|
continue;
|
|
8473
9139
|
}
|
|
8474
|
-
|
|
9140
|
+
participant.target;
|
|
9141
|
+
targets.push([participant, position]);
|
|
8475
9142
|
}
|
|
9143
|
+
const selfId = await firstValueFrom(this.selfId$.pipe(filter((id) => id !== null)));
|
|
9144
|
+
await this.executeMethod(selfId, "call.layout.set", { layout });
|
|
9145
|
+
for (const [participant, position] of targets) await participant.setPosition(position);
|
|
8476
9146
|
}
|
|
8477
9147
|
/**
|
|
8478
9148
|
* Transfers the call to another destination.
|
|
@@ -8574,17 +9244,28 @@ var WebRTCCall = class extends Destroyable {
|
|
|
8574
9244
|
* (notably iOS Safari) fall back to re-acquiring the track with the new
|
|
8575
9245
|
* constraint set and plumbing the replacement through the local audio
|
|
8576
9246
|
* pipeline if one is active.
|
|
9247
|
+
*
|
|
9248
|
+
* @returns whether the constraint reached the microphone. `false` is an
|
|
9249
|
+
* outcome rather than an error — a leg sending media the SDK did not capture
|
|
9250
|
+
* is left alone — so a UI that reflects the toggle must read it. Any failure
|
|
9251
|
+
* behind a `false` is also reported on {@link errors$}.
|
|
8577
9252
|
*/
|
|
8578
9253
|
async setEchoCancellation(enabled) {
|
|
8579
|
-
|
|
9254
|
+
return this.vertoManager.updateMediaConstraints({ audio: { echoCancellation: enabled } });
|
|
8580
9255
|
}
|
|
8581
|
-
/**
|
|
9256
|
+
/**
|
|
9257
|
+
* Toggle browser noise suppression on the local mic at runtime.
|
|
9258
|
+
* @returns whether the constraint reached the microphone.
|
|
9259
|
+
*/
|
|
8582
9260
|
async setNoiseSuppression(enabled) {
|
|
8583
|
-
|
|
9261
|
+
return this.vertoManager.updateMediaConstraints({ audio: { noiseSuppression: enabled } });
|
|
8584
9262
|
}
|
|
8585
|
-
/**
|
|
9263
|
+
/**
|
|
9264
|
+
* Toggle browser automatic gain control on the local mic at runtime.
|
|
9265
|
+
* @returns whether the constraint reached the microphone.
|
|
9266
|
+
*/
|
|
8586
9267
|
async setAutoGainControl(enabled) {
|
|
8587
|
-
|
|
9268
|
+
return this.vertoManager.updateMediaConstraints({ audio: { autoGainControl: enabled } });
|
|
8588
9269
|
}
|
|
8589
9270
|
/**
|
|
8590
9271
|
* Observable of the aggregate remote audio level, 0..1 RMS. The server
|
|
@@ -8641,6 +9322,9 @@ var WebRTCCall = class extends Destroyable {
|
|
|
8641
9322
|
/**
|
|
8642
9323
|
* Infers the semantic error category from a raw Error thrown by VertoManager
|
|
8643
9324
|
* or an RTCPeerConnection layer.
|
|
9325
|
+
*
|
|
9326
|
+
* Pure function — exported for unit testing.
|
|
9327
|
+
* @internal
|
|
8644
9328
|
*/
|
|
8645
9329
|
function inferCallErrorKind(error) {
|
|
8646
9330
|
if (error instanceof RPCTimeoutError) return "timeout";
|
|
@@ -8659,7 +9343,14 @@ const RECOVERABLE_RPC_CODES = new Set([
|
|
|
8659
9343
|
RPC_ERROR_AUTHENTICATION_FAILED,
|
|
8660
9344
|
RPC_ERROR_INVALID_PARAMS
|
|
8661
9345
|
]);
|
|
8662
|
-
/**
|
|
9346
|
+
/**
|
|
9347
|
+
* A *fallback*: callers knowing which leg failed pass an explicit `fatal` and
|
|
9348
|
+
* never reach here, so the default-fatal branch only sees call- and main-leg
|
|
9349
|
+
* errors. Auxiliary legs go through `WebRTCVertoManager.reportLegError`.
|
|
9350
|
+
*
|
|
9351
|
+
* Pure function — exported for unit testing.
|
|
9352
|
+
* @internal
|
|
9353
|
+
*/
|
|
8663
9354
|
function isFatalError(error) {
|
|
8664
9355
|
if (error instanceof VertoPongError) return false;
|
|
8665
9356
|
if (error instanceof MediaTrackError) return false;
|
|
@@ -8694,7 +9385,9 @@ var CallFactory = class {
|
|
|
8694
9385
|
kind: inferCallErrorKind(error),
|
|
8695
9386
|
fatal: options$1?.fatal ?? isFatalError(error),
|
|
8696
9387
|
error,
|
|
8697
|
-
callId: callInstance.id
|
|
9388
|
+
callId: callInstance.id,
|
|
9389
|
+
...options$1?.leg ? { leg: options$1.leg } : {},
|
|
9390
|
+
...options$1?.legId ? { legId: options$1.legId } : {}
|
|
8698
9391
|
};
|
|
8699
9392
|
callInstance.emitError(callError);
|
|
8700
9393
|
},
|
|
@@ -9162,6 +9855,30 @@ const logger$9 = getLogger();
|
|
|
9162
9855
|
function shouldAbortDial(callError) {
|
|
9163
9856
|
return callError.fatal || !(callError.error instanceof MediaAccessError);
|
|
9164
9857
|
}
|
|
9858
|
+
/**
|
|
9859
|
+
* Wait for a dialed call to be ready, or for the failure that stops it.
|
|
9860
|
+
*
|
|
9861
|
+
* Local media acquisition is deliberately unbounded: a permission prompt or a
|
|
9862
|
+
* device picker is human time, and `getUserMedia` cannot be cancelled anyway.
|
|
9863
|
+
* The clock starts only once acquisition settles, so a slow human never spends
|
|
9864
|
+
* the server's budget.
|
|
9865
|
+
*
|
|
9866
|
+
* `merge` rather than `race`, because the two legs settle asymmetrically. A
|
|
9867
|
+
* fatal acquisition failure reports the error and then destroys the call in the
|
|
9868
|
+
* same synchronous step; `errors$` defers delivery by a microtask while
|
|
9869
|
+
* `localMediaSettled$` completes immediately. Under `race` that bare completion
|
|
9870
|
+
* ended the wait first and `dial()` rejected with an RxJS `EmptyError`, burying
|
|
9871
|
+
* the `NotAllowedError` applications are told to inspect. Under `merge` the
|
|
9872
|
+
* completed leg is simply spent, and the queued error — enqueued before the
|
|
9873
|
+
* completion, so delivered before it — arrives to reject the wait. A dial
|
|
9874
|
+
* abandoned with no error at all still ends both legs, and the resulting
|
|
9875
|
+
* `EmptyError` remains the benign-cancel signal.
|
|
9876
|
+
*
|
|
9877
|
+
* Exported for unit testing.
|
|
9878
|
+
*/
|
|
9879
|
+
async function awaitDialReady(session, signalingTimeoutMs) {
|
|
9880
|
+
return firstValueFrom(merge(session.localMediaSettled$.pipe(take(1), switchMap(() => session.selfId$.pipe(filter((id) => Boolean(id)), take(1), timeout(signalingTimeoutMs)))), session.errors$.pipe(filter(shouldAbortDial), take(1), switchMap((callError) => throwError(() => callError.error)))));
|
|
9881
|
+
}
|
|
9165
9882
|
const getAddressSearchURI = (options) => {
|
|
9166
9883
|
const to = options.to?.split("?")[0];
|
|
9167
9884
|
const from$1 = options.from?.startsWith("subscriber://") ? options.from.replace("subscriber://", "") : options.from;
|
|
@@ -9178,7 +9895,6 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
9178
9895
|
this.authorizationStateKey = authorizationStateKey;
|
|
9179
9896
|
this.attachManager = attachManager;
|
|
9180
9897
|
this.dpopManager = dpopManager;
|
|
9181
|
-
this.callCreateTimeout = 6e3;
|
|
9182
9898
|
this.agent = `signalwire-js/4.0.0`;
|
|
9183
9899
|
this.eventAcks = true;
|
|
9184
9900
|
this.authorizationState$ = this.createReplaySubject(1);
|
|
@@ -9187,6 +9903,7 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
9187
9903
|
minor: 0,
|
|
9188
9904
|
revision: 0
|
|
9189
9905
|
};
|
|
9906
|
+
this.callControl = "routed";
|
|
9190
9907
|
this._authorization$ = this.createBehaviorSubject(void 0);
|
|
9191
9908
|
this._errors$ = this.createReplaySubject(1);
|
|
9192
9909
|
this._authState$ = this.createBehaviorSubject({ kind: "unauthenticated" });
|
|
@@ -9387,21 +10104,18 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
9387
10104
|
}
|
|
9388
10105
|
async handleAuthenticationError(error) {
|
|
9389
10106
|
logger$9.error("Authentication error:", error);
|
|
9390
|
-
const isRecoverableAuthError = error instanceof JSONRPCError && (error.code === RPC_ERROR_REQUESTER_VALIDATION_FAILED || error.code === RPC_ERROR_INVALID_PARAMS || error.code === RPC_ERROR_AUTHENTICATION_FAILED);
|
|
10107
|
+
const isRecoverableAuthError$1 = error instanceof JSONRPCError && (error.code === RPC_ERROR_REQUESTER_VALIDATION_FAILED || error.code === RPC_ERROR_INVALID_PARAMS || error.code === RPC_ERROR_AUTHENTICATION_FAILED);
|
|
9391
10108
|
const hasStoredState = await firstValueFrom(this.authorizationState$.pipe(take(1))) !== void 0;
|
|
9392
|
-
if (isRecoverableAuthError && hasStoredState) {
|
|
10109
|
+
if (isRecoverableAuthError$1 && hasStoredState) {
|
|
9393
10110
|
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
|
-
}
|
|
10111
|
+
await this.discardResumeStateAndReconnect();
|
|
9401
10112
|
} else this._errors$.next(error);
|
|
9402
10113
|
}
|
|
9403
10114
|
/**
|
|
9404
|
-
* Clear the resume state (authorization_state + protocol)
|
|
10115
|
+
* Clear the resume state (authorization_state + protocol) and ask the
|
|
10116
|
+
* transport to reconnect. The `connected` event re-triggers
|
|
10117
|
+
* `authenticate()`, which now has no stored state and so performs a fresh
|
|
10118
|
+
* connect.
|
|
9405
10119
|
*
|
|
9406
10120
|
* This is the stale-auth-state recovery helper used by handleAuthError:
|
|
9407
10121
|
* the server rejected a reconnect, so the resume state is discarded and a
|
|
@@ -9409,9 +10123,24 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
9409
10123
|
* session lives on through the reconnect and reattachCalls() needs the
|
|
9410
10124
|
* stored call references afterwards. Do NOT add detachAll() here.
|
|
9411
10125
|
*
|
|
10126
|
+
* Connect-time recovery only. A *request* refused on an already
|
|
10127
|
+
* authenticated session is never healed here: dropping the resume state
|
|
10128
|
+
* destroys the association between the socket and the previous session,
|
|
10129
|
+
* which is what reattach depends on. That path mints a fresh credential and
|
|
10130
|
+
* reauthenticates instead (see `SignalWire.recoverAndRetry`).
|
|
10131
|
+
*
|
|
9412
10132
|
* For public teardown (disconnect/destroy), use {@link teardownSessionState}
|
|
9413
10133
|
* instead, which clears the attach records as well.
|
|
9414
10134
|
*/
|
|
10135
|
+
async discardResumeStateAndReconnect() {
|
|
10136
|
+
try {
|
|
10137
|
+
await this.cleanupStoredConnectionParams();
|
|
10138
|
+
} catch (cleanupError) {
|
|
10139
|
+
logger$9.error("Failed to cleanup stored connection params:", cleanupError);
|
|
10140
|
+
} finally {
|
|
10141
|
+
this.transport.reconnect();
|
|
10142
|
+
}
|
|
10143
|
+
}
|
|
9415
10144
|
async cleanupStoredConnectionParams() {
|
|
9416
10145
|
await this.transport.setProtocol(void 0);
|
|
9417
10146
|
await this.updateAuthorizationStateInStorage(void 0);
|
|
@@ -9485,11 +10214,15 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
9485
10214
|
const isReconnect = hasReconnectState && storedToken;
|
|
9486
10215
|
let dpopToken;
|
|
9487
10216
|
if (isReconnect) logger$9.debug("[Session] Reconnecting with stored jwt_token + authorization_state");
|
|
9488
|
-
else
|
|
9489
|
-
|
|
9490
|
-
|
|
10217
|
+
else {
|
|
10218
|
+
const credential = this.getCredential();
|
|
10219
|
+
const credentialExpired = credential.expiry_at !== void 0 && credential.expiry_at <= Date.now() + CREDENTIAL_EXPIRY_SKEW_MS;
|
|
10220
|
+
if (this.onBeforeReconnect && (this.clientBound || credentialExpired)) {
|
|
10221
|
+
logger$9.debug("[Session] Refreshing credentials before fresh connect");
|
|
10222
|
+
await this.onBeforeReconnect();
|
|
10223
|
+
}
|
|
9491
10224
|
}
|
|
9492
|
-
if (
|
|
10225
|
+
if (this.dpopManager?.initialized) try {
|
|
9493
10226
|
dpopToken = await this.dpopManager.createRpcProof({ method: "signalwire.connect" });
|
|
9494
10227
|
} catch (error) {
|
|
9495
10228
|
if (this.clientBound) throw error;
|
|
@@ -9522,6 +10255,7 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
9522
10255
|
});
|
|
9523
10256
|
if (response.protocol) await this.transport.setProtocol(response.protocol);
|
|
9524
10257
|
this._authorization$.next(response.authorization);
|
|
10258
|
+
if (response.authorization.cnf?.jkt) this._wasClientBound = true;
|
|
9525
10259
|
this._iceServers$.next(response.ice_servers ?? []);
|
|
9526
10260
|
this._authState$.next({ kind: "authenticated" });
|
|
9527
10261
|
logger$9.debug("[Session] Authentication completed successfully");
|
|
@@ -9588,7 +10322,7 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
9588
10322
|
to: destinationURI,
|
|
9589
10323
|
...options
|
|
9590
10324
|
});
|
|
9591
|
-
await
|
|
10325
|
+
await awaitDialReady(callSession, DEFAULT_CALL_SIGNALING_TIMEOUT_MS);
|
|
9592
10326
|
this._calls$.next({
|
|
9593
10327
|
[`${callSession.id}`]: callSession,
|
|
9594
10328
|
...this._calls$.value
|
|
@@ -9641,12 +10375,23 @@ var ClientSessionWrapper = class {
|
|
|
9641
10375
|
get authenticated() {
|
|
9642
10376
|
return this.clientSessionManager.authenticated;
|
|
9643
10377
|
}
|
|
10378
|
+
/**
|
|
10379
|
+
* Whether the session is using a Client Bound SAT (DPoP). Sticky — set
|
|
10380
|
+
* when the binding is established or restored from a resumed session's
|
|
10381
|
+
* server authorization.
|
|
10382
|
+
*/
|
|
10383
|
+
get clientBound() {
|
|
10384
|
+
return this.clientSessionManager.clientBound;
|
|
10385
|
+
}
|
|
9644
10386
|
get signalingEvent$() {
|
|
9645
10387
|
return this.clientSessionManager.signalingEvent$;
|
|
9646
10388
|
}
|
|
9647
10389
|
get iceServers() {
|
|
9648
10390
|
return this.clientSessionManager.iceServers;
|
|
9649
10391
|
}
|
|
10392
|
+
get callControl() {
|
|
10393
|
+
return this.clientSessionManager.callControl;
|
|
10394
|
+
}
|
|
9650
10395
|
async execute(request, options) {
|
|
9651
10396
|
return this.clientSessionManager.execute(request, options);
|
|
9652
10397
|
}
|
|
@@ -9841,7 +10586,7 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
9841
10586
|
await session.reauthenticate(tokenData.token, rpcProof, { clientBound: true });
|
|
9842
10587
|
updateCredential({ token: tokenData.token });
|
|
9843
10588
|
logger$7.info("[DeviceToken] Client Bound SAT activated successfully");
|
|
9844
|
-
this.
|
|
10589
|
+
this.emitCurrentToken(tokenData);
|
|
9845
10590
|
return { activated: true };
|
|
9846
10591
|
} catch (error) {
|
|
9847
10592
|
logger$7.error("[DeviceToken] Failed to activate Client Bound SAT:", error);
|
|
@@ -9853,6 +10598,21 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
9853
10598
|
}
|
|
9854
10599
|
}
|
|
9855
10600
|
/**
|
|
10601
|
+
* Emit a freshly received token to the reactive pipeline, stamping an
|
|
10602
|
+
* absolute `expires_at` when the response carried only `expires_in`.
|
|
10603
|
+
* Resolving the expiry at RECEIVE time (not at read time) is what lets
|
|
10604
|
+
* {@link refreshNowIfDue} detect due-ness on resume: a bare `expires_in`
|
|
10605
|
+
* re-resolved later would always compute a full TTL from "now" and never
|
|
10606
|
+
* cross the refresh buffer.
|
|
10607
|
+
*/
|
|
10608
|
+
emitCurrentToken(token) {
|
|
10609
|
+
const stamped = token.expires_at ? token : {
|
|
10610
|
+
...token,
|
|
10611
|
+
expires_at: resolveExpiresAt(token)
|
|
10612
|
+
};
|
|
10613
|
+
this._currentToken$.next(stamped);
|
|
10614
|
+
}
|
|
10615
|
+
/**
|
|
9856
10616
|
* Returns true when the cached token has enough headroom before expiry to
|
|
9857
10617
|
* be safely reused on reactivation. The headroom matches the refresh
|
|
9858
10618
|
* buffer, so a token within the refresh window is treated as stale (the
|
|
@@ -9870,7 +10630,7 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
9870
10630
|
method: "POST",
|
|
9871
10631
|
uri: DEVICE_TOKEN_ENDPOINT
|
|
9872
10632
|
});
|
|
9873
|
-
const response = await this.http.request({
|
|
10633
|
+
const response = await this.http().request({
|
|
9874
10634
|
url: DEVICE_TOKEN_ENDPOINT,
|
|
9875
10635
|
...POST_PARAMS,
|
|
9876
10636
|
body: JSON.stringify({
|
|
@@ -9897,7 +10657,7 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
9897
10657
|
uri: DEVICE_REFRESH_ENDPOINT,
|
|
9898
10658
|
accessToken: currentToken
|
|
9899
10659
|
});
|
|
9900
|
-
const response = await this.http.request({
|
|
10660
|
+
const response = await this.http().request({
|
|
9901
10661
|
url: DEVICE_REFRESH_ENDPOINT,
|
|
9902
10662
|
...POST_PARAMS,
|
|
9903
10663
|
body: JSON.stringify({
|
|
@@ -9945,7 +10705,7 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
9945
10705
|
const currentToken = this.getCredential().token;
|
|
9946
10706
|
if (!currentToken) throw new TokenRefreshError("No current token available for refresh");
|
|
9947
10707
|
const newTokenData = await this.retryRefresh(session, currentToken, updateCredential);
|
|
9948
|
-
this.
|
|
10708
|
+
this.emitCurrentToken(newTokenData);
|
|
9949
10709
|
} catch (error) {
|
|
9950
10710
|
logger$7.error("[DeviceToken] Automatic Client Bound SAT refresh failed:", error);
|
|
9951
10711
|
this.errorHandler(error instanceof TokenRefreshError ? error : new TokenRefreshError("Automatic token refresh failed", error));
|
|
@@ -9972,6 +10732,22 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
9972
10732
|
throw lastError instanceof Error ? lastError : new TokenRefreshError("All refresh retries exhausted", lastError);
|
|
9973
10733
|
}
|
|
9974
10734
|
/**
|
|
10735
|
+
* Force an immediate refresh when the cached Client Bound SAT is already
|
|
10736
|
+
* past its refresh window. Called on resume from suspension where
|
|
10737
|
+
* background-tab throttling can delay the reactive timer past the buffer.
|
|
10738
|
+
* A no-op when no token is cached or it still has headroom; the normal
|
|
10739
|
+
* {@link executeRefresh} guards (paused / in-progress / unauthenticated)
|
|
10740
|
+
* still apply.
|
|
10741
|
+
*/
|
|
10742
|
+
refreshNowIfDue() {
|
|
10743
|
+
const token = this._currentToken$.value;
|
|
10744
|
+
if (!token) return;
|
|
10745
|
+
if (resolveExpiresAt(token) * 1e3 - Date.now() <= DEVICE_TOKEN_REFRESH_BUFFER_MS) {
|
|
10746
|
+
logger$7.debug("[DeviceToken] Resume: cached SAT past refresh window; refreshing now");
|
|
10747
|
+
this.executeRefresh();
|
|
10748
|
+
}
|
|
10749
|
+
}
|
|
10750
|
+
/**
|
|
9975
10751
|
* Stops the reactive refresh pipeline from firing. Use when the underlying
|
|
9976
10752
|
* session is being torn down (e.g., during {@link SignalWire.disconnect})
|
|
9977
10753
|
* so a scheduled refresh cannot fire against a destroyed session.
|
|
@@ -10022,6 +10798,7 @@ var CredentialRefreshCoordinator = class extends Destroyable {
|
|
|
10022
10798
|
this.deps = deps;
|
|
10023
10799
|
this._activating = false;
|
|
10024
10800
|
this._activationGeneration = 0;
|
|
10801
|
+
this._developerRefreshInProgress = false;
|
|
10025
10802
|
if (dpopManager?.initialized) this._deviceTokenManager = (deps.deviceTokenManagerFactory ?? defaultDeviceTokenManagerFactory)(dpopManager, deps.http, (error) => deps.notifier.onError(error), () => deps.store.read());
|
|
10026
10803
|
}
|
|
10027
10804
|
/** True when the Client Bound SAT path is available (DPoP initialized). */
|
|
@@ -10040,28 +10817,120 @@ var CredentialRefreshCoordinator = class extends Destroyable {
|
|
|
10040
10817
|
* invokes `deps.onRefreshExhausted` so the orchestrator can disconnect.
|
|
10041
10818
|
*/
|
|
10042
10819
|
scheduleDeveloperRefresh(provider, expiresAt, attempt = 0) {
|
|
10820
|
+
this._activeProvider = provider;
|
|
10043
10821
|
if (this._developerTimerId !== void 0) clearTimeout(this._developerTimerId);
|
|
10044
10822
|
const refreshInterval = attempt === 0 ? Math.max(expiresAt - Date.now() - CREDENTIAL_REFRESH_BUFFER_MS, 1e3) : Math.min(CREDENTIAL_REFRESH_RETRY_BASE_MS * Math.pow(2, attempt) * (.5 + Math.random() * .5), CREDENTIAL_REFRESH_MAX_DELAY_MS);
|
|
10045
|
-
this._developerTimerId = setTimeout(
|
|
10823
|
+
this._developerTimerId = setTimeout(() => {
|
|
10824
|
+
this._developerTimerId = void 0;
|
|
10825
|
+
this.executeDeveloperRefresh(provider, expiresAt, attempt);
|
|
10826
|
+
}, refreshInterval);
|
|
10827
|
+
}
|
|
10828
|
+
/**
|
|
10829
|
+
* Runs the developer-provided refresh once: mints a new credential, stores
|
|
10830
|
+
* and persists it, reauthenticates the live session (via the notifier), and
|
|
10831
|
+
* reschedules against the new expiry. On failure retries with backoff up to
|
|
10832
|
+
* {@link CREDENTIAL_REFRESH_MAX_RETRIES}, then signals exhaustion.
|
|
10833
|
+
*
|
|
10834
|
+
* Shared by the scheduled timer tick and {@link forceRefreshIfDue}. The
|
|
10835
|
+
* `_developerRefreshInProgress` guard prevents the two from overlapping.
|
|
10836
|
+
*/
|
|
10837
|
+
async executeDeveloperRefresh(provider, expiresAt, attempt) {
|
|
10838
|
+
if (this._developerRefreshInProgress) {
|
|
10839
|
+
logger$6.debug("[Coordinator] Developer refresh already in progress; skipping");
|
|
10840
|
+
return;
|
|
10841
|
+
}
|
|
10842
|
+
this._developerRefreshInProgress = true;
|
|
10843
|
+
try {
|
|
10844
|
+
const newCredentials = await this.refreshCredential(provider);
|
|
10845
|
+
this.deps.store.write(newCredentials);
|
|
10846
|
+
this.deps.store.persist(newCredentials);
|
|
10046
10847
|
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}/${CREDENTIAL_REFRESH_MAX_RETRIES}):`, error);
|
|
10056
|
-
this.deps.notifier.onError(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
10057
|
-
if (nextAttempt < 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 TokenRefreshError("Credential refresh failed after max retries"));
|
|
10061
|
-
this.deps.notifier.onRefreshExhausted();
|
|
10062
|
-
}
|
|
10848
|
+
await this.deps.notifier.onCredentialRefreshed(newCredentials);
|
|
10849
|
+
} catch (reauthError) {
|
|
10850
|
+
logger$6.warn("[Coordinator] onCredentialRefreshed rejected (non-fatal):", reauthError);
|
|
10063
10851
|
}
|
|
10064
|
-
|
|
10852
|
+
logger$6.info("[Coordinator] Credentials refreshed successfully.");
|
|
10853
|
+
if (newCredentials.expiry_at) this.scheduleDeveloperRefresh(provider, newCredentials.expiry_at, 0);
|
|
10854
|
+
} catch (error) {
|
|
10855
|
+
const nextAttempt = attempt + 1;
|
|
10856
|
+
logger$6.error(`[Coordinator] Credential refresh failed (attempt ${nextAttempt}/${CREDENTIAL_REFRESH_MAX_RETRIES}):`, error);
|
|
10857
|
+
this.deps.notifier.onError(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
10858
|
+
if (nextAttempt < CREDENTIAL_REFRESH_MAX_RETRIES) this.scheduleDeveloperRefresh(provider, expiresAt, nextAttempt);
|
|
10859
|
+
else {
|
|
10860
|
+
logger$6.error("[Coordinator] Credential refresh exhausted all retries. Disconnecting.");
|
|
10861
|
+
this.deps.notifier.onError(new TokenRefreshError("Credential refresh failed after max retries"));
|
|
10862
|
+
this.deps.notifier.onRefreshExhausted();
|
|
10863
|
+
}
|
|
10864
|
+
} finally {
|
|
10865
|
+
this._developerRefreshInProgress = false;
|
|
10866
|
+
}
|
|
10867
|
+
}
|
|
10868
|
+
/**
|
|
10869
|
+
* Force an immediate refresh when the current credential is already past its
|
|
10870
|
+
* scheduled refresh window. Called on resume from suspension, where
|
|
10871
|
+
* background-tab timer throttling can delay the armed refresh well past
|
|
10872
|
+
* expiry, leaving the live session stale.
|
|
10873
|
+
*
|
|
10874
|
+
* Routes to whichever mechanism is armed: the developer timer if armed,
|
|
10875
|
+
* otherwise the Client Bound SAT pipeline. A no-op when nothing is due.
|
|
10876
|
+
*/
|
|
10877
|
+
forceRefreshIfDue() {
|
|
10878
|
+
if (this._developerTimerId !== void 0 && this._activeProvider) {
|
|
10879
|
+
const expiry = this.deps.store.read().expiry_at;
|
|
10880
|
+
if (expiry !== void 0 && Date.now() >= expiry - CREDENTIAL_REFRESH_BUFFER_MS) {
|
|
10881
|
+
logger$6.debug("[Coordinator] Resume: credential past refresh window; forcing refresh");
|
|
10882
|
+
clearTimeout(this._developerTimerId);
|
|
10883
|
+
this._developerTimerId = void 0;
|
|
10884
|
+
this.executeDeveloperRefresh(this._activeProvider, expiry, 0);
|
|
10885
|
+
}
|
|
10886
|
+
return;
|
|
10887
|
+
}
|
|
10888
|
+
this._deviceTokenManager?.refreshNowIfDue();
|
|
10889
|
+
}
|
|
10890
|
+
/**
|
|
10891
|
+
* Sync the credential's expiry from the server-provided authorization (the
|
|
10892
|
+
* `signalwire.connect` result). SATs are opaque JWE, so
|
|
10893
|
+
* `fabric_subscriber.expires_at` is the authoritative expiry of the token
|
|
10894
|
+
* the session actually connected with — the provider-reported `expiry_at`
|
|
10895
|
+
* is only a hint (and may be wrong or absent). Corrects the stored
|
|
10896
|
+
* credential and re-arms the developer refresh timer against the real
|
|
10897
|
+
* deadline when the provider supports `refresh()`.
|
|
10898
|
+
*/
|
|
10899
|
+
syncExpiryFromAuthorization(authorization, provider) {
|
|
10900
|
+
const expiresAtSec = authorization?.fabric_subscriber?.expires_at;
|
|
10901
|
+
if (!expiresAtSec) return;
|
|
10902
|
+
const expiryAt = expiresAtSec * 1e3;
|
|
10903
|
+
const credential = this.deps.store.read();
|
|
10904
|
+
if (credential.expiry_at === expiryAt) return;
|
|
10905
|
+
logger$6.debug(`[Coordinator] Correcting credential expiry from server authorization: ${new Date(expiryAt).toISOString()}`);
|
|
10906
|
+
const updated = {
|
|
10907
|
+
...credential,
|
|
10908
|
+
expiry_at: expiryAt
|
|
10909
|
+
};
|
|
10910
|
+
this.deps.store.write(updated);
|
|
10911
|
+
this.deps.store.persist(updated);
|
|
10912
|
+
if (provider?.refresh) this.scheduleDeveloperRefresh(provider, expiryAt);
|
|
10913
|
+
}
|
|
10914
|
+
/**
|
|
10915
|
+
* Invoke `provider.refresh()` deduped against any concurrent developer
|
|
10916
|
+
* refresh. Concurrent callers — the scheduled tick, a resume-forced refresh,
|
|
10917
|
+
* and the orchestrator's -32003 recovery / reconnect re-mint — share one
|
|
10918
|
+
* in-flight promise, so a provider backed by one-time-use rotating refresh
|
|
10919
|
+
* tokens is never invoked twice in parallel.
|
|
10920
|
+
*
|
|
10921
|
+
* The caller owns applying the returned credential (store write, session
|
|
10922
|
+
* reauth, rescheduling); this method only serializes the network call.
|
|
10923
|
+
*/
|
|
10924
|
+
async refreshCredential(provider) {
|
|
10925
|
+
if (this._refreshInFlight) return this._refreshInFlight;
|
|
10926
|
+
if (!provider.refresh) throw new InvalidCredentialsError("Credential provider does not support refresh");
|
|
10927
|
+
const run = provider.refresh();
|
|
10928
|
+
this._refreshInFlight = run;
|
|
10929
|
+
const clear = () => {
|
|
10930
|
+
if (this._refreshInFlight === run) this._refreshInFlight = void 0;
|
|
10931
|
+
};
|
|
10932
|
+
run.then(clear, clear);
|
|
10933
|
+
return run;
|
|
10065
10934
|
}
|
|
10066
10935
|
/**
|
|
10067
10936
|
* Cancels any scheduled developer-provided refresh. Idempotent.
|
|
@@ -10841,6 +11710,13 @@ var TransportManager = class extends Destroyable {
|
|
|
10841
11710
|
//#endregion
|
|
10842
11711
|
//#region src/clients/SignalWire.ts
|
|
10843
11712
|
const logger$1 = getLogger();
|
|
11713
|
+
/**
|
|
11714
|
+
* Storage key for the client-bound marker. The SAT and authorization_state are
|
|
11715
|
+
* both opaque to the SDK, so on a page reload the preflight recovery — which
|
|
11716
|
+
* runs before any session exists — has no other way to know the session was
|
|
11717
|
+
* client-bound. See {@link SignalWire.persistClientBoundMarker}.
|
|
11718
|
+
*/
|
|
11719
|
+
const CLIENT_BOUND_STORAGE_KEY = "sw:client_bound";
|
|
10844
11720
|
const buildOptionsFromDestination = (destination) => {
|
|
10845
11721
|
if (typeof destination === "string") {
|
|
10846
11722
|
const queryStartIndex = destination.indexOf("?");
|
|
@@ -10884,6 +11760,7 @@ var SignalWire = class extends Destroyable {
|
|
|
10884
11760
|
this.preferences = new ClientPreferences();
|
|
10885
11761
|
this._user$ = this.createBehaviorSubject(void 0);
|
|
10886
11762
|
this._directory$ = this.createBehaviorSubject(void 0);
|
|
11763
|
+
this._credentialRecovered = false;
|
|
10887
11764
|
this._isConnected$ = this.createBehaviorSubject(false);
|
|
10888
11765
|
this._isRegistered$ = this.createBehaviorSubject(false);
|
|
10889
11766
|
this._errors$ = this.createReplaySubject(1);
|
|
@@ -10922,6 +11799,16 @@ var SignalWire = class extends Destroyable {
|
|
|
10922
11799
|
});
|
|
10923
11800
|
}
|
|
10924
11801
|
/**
|
|
11802
|
+
* Build the refresh path's own HTTP controller, against whatever host is current.
|
|
11803
|
+
*
|
|
11804
|
+
* Called on first use rather than up front, so `apiHost` already reflects the
|
|
11805
|
+
* token's `ch` claim. Same credential source as the container's controller — only
|
|
11806
|
+
* the instance, and therefore its observable streams, is separate.
|
|
11807
|
+
*/
|
|
11808
|
+
createRefreshHttpController() {
|
|
11809
|
+
return new HTTPRequestController(this._deps.apiHost, () => this._deps.credential);
|
|
11810
|
+
}
|
|
11811
|
+
/**
|
|
10925
11812
|
* Initializes DPoP if not already set up. Returns the fingerprint on success.
|
|
10926
11813
|
*/
|
|
10927
11814
|
async initDPoP() {
|
|
@@ -10948,11 +11835,19 @@ var SignalWire = class extends Destroyable {
|
|
|
10948
11835
|
async resolveCredentials() {
|
|
10949
11836
|
const fingerprint = await this.initDPoP();
|
|
10950
11837
|
this._refreshCoordinator = new CredentialRefreshCoordinator(this._dpopManager, {
|
|
10951
|
-
http:
|
|
11838
|
+
http: () => {
|
|
11839
|
+
if (!this._refreshHttp || this._refreshHttpHost !== this._deps.apiHost) {
|
|
11840
|
+
this._refreshHttp?.destroy();
|
|
11841
|
+
this._refreshHttp = this.createRefreshHttpController();
|
|
11842
|
+
this._refreshHttpHost = this._deps.apiHost;
|
|
11843
|
+
}
|
|
11844
|
+
return this._refreshHttp;
|
|
11845
|
+
},
|
|
10952
11846
|
notifier: {
|
|
10953
11847
|
onError: (error) => this._errors$.next(error),
|
|
10954
11848
|
onWarning: (warning) => this._warnings$.next(warning),
|
|
10955
|
-
onRefreshExhausted: () => void this.disconnect()
|
|
11849
|
+
onRefreshExhausted: () => void this.disconnect(),
|
|
11850
|
+
onCredentialRefreshed: async (credential) => this.reauthenticateLiveSession(credential)
|
|
10956
11851
|
},
|
|
10957
11852
|
store: {
|
|
10958
11853
|
read: () => this._deps.credential,
|
|
@@ -10964,6 +11859,7 @@ var SignalWire = class extends Destroyable {
|
|
|
10964
11859
|
...this._deps.credential,
|
|
10965
11860
|
...partial
|
|
10966
11861
|
};
|
|
11862
|
+
this.persistCredential(this._deps.credential);
|
|
10967
11863
|
},
|
|
10968
11864
|
persist: (credential) => this.persistCredential(credential)
|
|
10969
11865
|
}
|
|
@@ -11009,20 +11905,181 @@ var SignalWire = class extends Destroyable {
|
|
|
11009
11905
|
}
|
|
11010
11906
|
this._deps.credential = _credentials;
|
|
11011
11907
|
this.persistCredential(_credentials);
|
|
11012
|
-
|
|
11013
|
-
|
|
11908
|
+
await this.reauthenticateLiveSession(_credentials);
|
|
11909
|
+
}
|
|
11910
|
+
/**
|
|
11911
|
+
* Reauthenticate the currently-open session with a freshly obtained
|
|
11912
|
+
* credential so the new token takes effect on the live socket immediately —
|
|
11913
|
+
* not just on the next reconnect. No-op when the session is not
|
|
11914
|
+
* connected/authenticated or the credential carries no token (e.g. an
|
|
11915
|
+
* authorization-state-only refresh). Non-fatal: reauth failures surface on
|
|
11916
|
+
* `errors$` without aborting the refresh that triggered this.
|
|
11917
|
+
*/
|
|
11918
|
+
async reauthenticateLiveSession(credential) {
|
|
11919
|
+
if (!this.isConnected || !this._clientSession.authenticated || !credential.token) return;
|
|
11920
|
+
try {
|
|
11921
|
+
await this._clientSession.reauthenticate(credential.token);
|
|
11014
11922
|
logger$1.info("[SignalWire] Session refreshed with new credentials.");
|
|
11015
11923
|
} catch (error) {
|
|
11016
11924
|
logger$1.error("[SignalWire] Failed to refresh session with new credentials:", error);
|
|
11017
11925
|
this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
11018
11926
|
}
|
|
11019
11927
|
}
|
|
11928
|
+
/**
|
|
11929
|
+
* Recover a session the server is refusing: mint a fresh credential,
|
|
11930
|
+
* reauthenticate the live session with it, and retry the operation.
|
|
11931
|
+
*
|
|
11932
|
+
* The connection is deliberately kept. A reload authenticates the new socket
|
|
11933
|
+
* against the persisted `authorization_state`, and that handshake is what
|
|
11934
|
+
* associates the socket with the previous session — the association reattach
|
|
11935
|
+
* depends on. `signalwire.reauthenticate` swaps the credential *on that same
|
|
11936
|
+
* session*, so recovery never touches the resume state. Discarding it would
|
|
11937
|
+
* heal the credential by destroying the very thing the caller is trying to
|
|
11938
|
+
* get back to.
|
|
11939
|
+
*
|
|
11940
|
+
* The operation is still the verdict, never the RPC. Reauthenticating with
|
|
11941
|
+
* the in-memory token is accepted by a resume even while requests stay
|
|
11942
|
+
* refused, because the persisted `authorization_state` short-circuits token
|
|
11943
|
+
* validation — and `signalwire.reauthenticate` with a *freshly minted* token
|
|
11944
|
+
* has also been observed accepted while `subscriber.online` keeps being
|
|
11945
|
+
* refused (staging run 33826974634). Both look like success and are not.
|
|
11946
|
+
*
|
|
11947
|
+
* @returns the operation's value, or the reason recovery could not deliver
|
|
11948
|
+
* one. `error` is undefined when there was no way to mint at all.
|
|
11949
|
+
*/
|
|
11950
|
+
async recoverAndRetry(operation) {
|
|
11951
|
+
if (!await this.remintAndReauthenticate()) return { ok: false };
|
|
11952
|
+
try {
|
|
11953
|
+
const value = await operation();
|
|
11954
|
+
this._credentialRecovered = true;
|
|
11955
|
+
return {
|
|
11956
|
+
ok: true,
|
|
11957
|
+
value
|
|
11958
|
+
};
|
|
11959
|
+
} catch (error) {
|
|
11960
|
+
logger$1.warn("[SignalWire] Reauthentication was accepted but the operation is still refused:", error);
|
|
11961
|
+
return {
|
|
11962
|
+
ok: false,
|
|
11963
|
+
error
|
|
11964
|
+
};
|
|
11965
|
+
}
|
|
11966
|
+
}
|
|
11967
|
+
/**
|
|
11968
|
+
* Re-mint a credential and adopt it only if the live session accepts it.
|
|
11969
|
+
*
|
|
11970
|
+
* The mechanism follows the binding: a client-bound session re-mints a bound
|
|
11971
|
+
* base SAT through `authenticate()` with the DPoP fingerprint, because the
|
|
11972
|
+
* developer refresh handler would hand back an unbound token and silently
|
|
11973
|
+
* degrade the session. An unbound session uses the refresh handler. Rotation
|
|
11974
|
+
* cost is not a reason to skip this — the only reason is having no mechanism.
|
|
11975
|
+
*
|
|
11976
|
+
* @returns whether the session is now running on a freshly accepted credential.
|
|
11977
|
+
*/
|
|
11978
|
+
async remintAndReauthenticate() {
|
|
11979
|
+
const provider = this._credentialProvider;
|
|
11980
|
+
if (!provider) return false;
|
|
11981
|
+
const { clientBound } = this._clientSession;
|
|
11982
|
+
if (!clientBound && !provider.refresh) {
|
|
11983
|
+
logger$1.debug("[SignalWire] [SW-NO-REFRESH-HANDLER] Unbound session with no refresh handler; cannot re-mint.");
|
|
11984
|
+
return false;
|
|
11985
|
+
}
|
|
11986
|
+
try {
|
|
11987
|
+
const newCredentials = clientBound ? await provider.authenticate(this._dpopManager?.initialized ? { fingerprint: this._dpopManager.fingerprint } : void 0) : await this.remintCredential(provider);
|
|
11988
|
+
if (!newCredentials.token) {
|
|
11989
|
+
logger$1.warn("[SignalWire] Re-minted credential has no token; keeping the current one.");
|
|
11990
|
+
return false;
|
|
11991
|
+
}
|
|
11992
|
+
await this._clientSession.reauthenticate(newCredentials.token);
|
|
11993
|
+
this._deps.credential = newCredentials;
|
|
11994
|
+
this.persistCredential(newCredentials);
|
|
11995
|
+
if (newCredentials.expiry_at && provider.refresh) this._refreshCoordinator?.scheduleDeveloperRefresh(provider, newCredentials.expiry_at);
|
|
11996
|
+
return true;
|
|
11997
|
+
} catch (error) {
|
|
11998
|
+
logger$1.warn("[SignalWire] Re-mint recovery failed:", error);
|
|
11999
|
+
return false;
|
|
12000
|
+
}
|
|
12001
|
+
}
|
|
12002
|
+
/**
|
|
12003
|
+
* Re-mint a credential via `provider.refresh()`, routed through the
|
|
12004
|
+
* coordinator's shared in-flight guard so concurrent re-mint paths (a
|
|
12005
|
+
* scheduled/resume refresh, -32003 recovery, and reconnect) never fire a
|
|
12006
|
+
* second `provider.refresh()` in parallel — which rotating one-time-use
|
|
12007
|
+
* refresh tokens reject. Falls back to a direct call only if the coordinator
|
|
12008
|
+
* has not been constructed yet.
|
|
12009
|
+
*/
|
|
12010
|
+
async remintCredential(provider) {
|
|
12011
|
+
if (this._refreshCoordinator) return this._refreshCoordinator.refreshCredential(provider);
|
|
12012
|
+
if (!provider.refresh) throw new InvalidCredentialsError("Credential provider does not support refresh");
|
|
12013
|
+
return provider.refresh();
|
|
12014
|
+
}
|
|
12015
|
+
/**
|
|
12016
|
+
* Re-mint credentials before a fresh (re)connect (`onBeforeReconnect` hook).
|
|
12017
|
+
* The session invokes this only when it is client-bound OR the in-memory
|
|
12018
|
+
* token is expired. The re-mint mechanism depends on the binding:
|
|
12019
|
+
* - Client-bound: `authenticate()` with the DPoP fingerprint to obtain a
|
|
12020
|
+
* fresh base SAT the upcoming reconnect can re-bind (the
|
|
12021
|
+
* DeviceTokenManager re-activates afterwards).
|
|
12022
|
+
* - Unbound: the developer's non-interactive `refresh()` handler.
|
|
12023
|
+
* `authenticate()` is deliberately NOT used here — it may be interactive
|
|
12024
|
+
* (a login prompt) and must not fire on a background reconnect.
|
|
12025
|
+
*
|
|
12026
|
+
* Rejects on failure so the session aborts the reconnect rather than
|
|
12027
|
+
* replaying a stale token.
|
|
12028
|
+
*/
|
|
12029
|
+
async refreshCredentialForReconnect() {
|
|
12030
|
+
if (!this._credentialProvider) return;
|
|
12031
|
+
try {
|
|
12032
|
+
let newCredentials;
|
|
12033
|
+
if (this._clientSession?.clientBound ?? await this.wasClientBound()) {
|
|
12034
|
+
logger$1.debug("[SignalWire] Re-minting client-bound base SAT before reconnect");
|
|
12035
|
+
newCredentials = await this._credentialProvider.authenticate(this._dpopManager?.initialized ? { fingerprint: this._dpopManager.fingerprint } : void 0);
|
|
12036
|
+
} else if (this._credentialProvider.refresh) {
|
|
12037
|
+
logger$1.debug("[SignalWire] Refreshing unbound credential before reconnect");
|
|
12038
|
+
newCredentials = await this.remintCredential(this._credentialProvider);
|
|
12039
|
+
} else {
|
|
12040
|
+
logger$1.warn("[SignalWire] [SW-NO-REFRESH-HANDLER] Token expired on reconnect but no refresh handler; reconnecting with the existing token.");
|
|
12041
|
+
return;
|
|
12042
|
+
}
|
|
12043
|
+
if (!newCredentials.token) {
|
|
12044
|
+
logger$1.warn("[SignalWire] Re-minted credential has no token; keeping the existing credential for reconnect.");
|
|
12045
|
+
return;
|
|
12046
|
+
}
|
|
12047
|
+
this._deps.credential = newCredentials;
|
|
12048
|
+
this.persistCredential(newCredentials);
|
|
12049
|
+
if (newCredentials.expiry_at && this._credentialProvider.refresh) this._refreshCoordinator?.scheduleDeveloperRefresh(this._credentialProvider, newCredentials.expiry_at);
|
|
12050
|
+
logger$1.debug("[SignalWire] Credential refreshed successfully for reconnect");
|
|
12051
|
+
} catch (error) {
|
|
12052
|
+
logger$1.error("[SignalWire] Failed to refresh credentials for reconnect:", error);
|
|
12053
|
+
this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
12054
|
+
throw error;
|
|
12055
|
+
}
|
|
12056
|
+
}
|
|
11020
12057
|
/** Persist credential to localStorage when persistSession is enabled. */
|
|
11021
12058
|
persistCredential(credential) {
|
|
11022
12059
|
if (!credential.token) return;
|
|
11023
12060
|
this._deps.storage.setItem("sw:cached_credential", credential);
|
|
11024
12061
|
if (this._deps.persistSession) this._deps.storage.setItem("sw:cached_credential", credential, "local");
|
|
11025
12062
|
}
|
|
12063
|
+
/**
|
|
12064
|
+
* Persist whether the session is client-bound, mirroring the credential's
|
|
12065
|
+
* storage scopes so it survives a reload. The preflight recovery reads it
|
|
12066
|
+
* before any session exists to decide whether to re-bind via `authenticate()`
|
|
12067
|
+
* or refresh an unbound token; the marker tracks the latest binding, so an
|
|
12068
|
+
* unbound reconnect clears a stale marker from an earlier client-bound login.
|
|
12069
|
+
*/
|
|
12070
|
+
persistClientBoundMarker(bound) {
|
|
12071
|
+
const scopes = this._deps.persistSession ? ["session", "local"] : ["session"];
|
|
12072
|
+
for (const scope of scopes) if (bound) this._deps.storage.setItem(CLIENT_BOUND_STORAGE_KEY, true, scope);
|
|
12073
|
+
else this._deps.storage.removeItem(CLIENT_BOUND_STORAGE_KEY, scope);
|
|
12074
|
+
}
|
|
12075
|
+
/** Read the persisted client-bound marker (see {@link persistClientBoundMarker}). */
|
|
12076
|
+
async wasClientBound() {
|
|
12077
|
+
const scopes = this._deps.persistSession ? ["local", "session"] : ["session"];
|
|
12078
|
+
for (const scope of scopes) try {
|
|
12079
|
+
if (await this._deps.storage.getItem(CLIENT_BOUND_STORAGE_KEY, scope)) return true;
|
|
12080
|
+
} catch {}
|
|
12081
|
+
return false;
|
|
12082
|
+
}
|
|
11026
12083
|
async init() {
|
|
11027
12084
|
this._user$.next(new User(this._deps.http));
|
|
11028
12085
|
if (!this._options.skipConnection) await this.connect();
|
|
@@ -11046,6 +12103,42 @@ var SignalWire = class extends Destroyable {
|
|
|
11046
12103
|
} catch (error) {
|
|
11047
12104
|
logger$1.error("[SignalWire] Failed to reattach calls:", error);
|
|
11048
12105
|
this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
12106
|
+
} finally {
|
|
12107
|
+
this._credentialRecovered = false;
|
|
12108
|
+
}
|
|
12109
|
+
}
|
|
12110
|
+
/**
|
|
12111
|
+
* Fetch the authenticated user profile, recovering a stale credential.
|
|
12112
|
+
*
|
|
12113
|
+
* On a reload the persisted credential can be expired. Unlike the WS resume —
|
|
12114
|
+
* which the server accepts against the persisted `authorization_state` even
|
|
12115
|
+
* with an expired token — this REST preflight has no such short-circuit and is
|
|
12116
|
+
* refused (401). There is no session yet to reauthenticate, so recovery
|
|
12117
|
+
* re-mints the credential through the provider ({@link refreshCredentialForReconnect})
|
|
12118
|
+
* and retries with a FRESH {@link User}: Fetchable memoizes its result
|
|
12119
|
+
* (shareReplay), so reusing the instance would replay the 401 instead of
|
|
12120
|
+
* re-fetching with the new token. Without the user id the transport/session —
|
|
12121
|
+
* and the reattach a reload is trying to preserve — cannot even be addressed.
|
|
12122
|
+
*/
|
|
12123
|
+
async fetchUserOrRecover() {
|
|
12124
|
+
const fetchUser = async (user$1) => {
|
|
12125
|
+
if (!await firstValueFrom(user$1.fetched$)) throw new UnexpectedError("Failed to fetch user information - fetched$ emitted false");
|
|
12126
|
+
this._deps.user = user$1;
|
|
12127
|
+
};
|
|
12128
|
+
const user = this._user$.value;
|
|
12129
|
+
if (!user) throw new UnexpectedError("User not initialized before connect");
|
|
12130
|
+
try {
|
|
12131
|
+
await fetchUser(user);
|
|
12132
|
+
} catch (firstError) {
|
|
12133
|
+
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.`);
|
|
12134
|
+
try {
|
|
12135
|
+
await this.refreshCredentialForReconnect();
|
|
12136
|
+
const refetched = new User(this._deps.http);
|
|
12137
|
+
await fetchUser(refetched);
|
|
12138
|
+
this._user$.next(refetched);
|
|
12139
|
+
} catch (retryError) {
|
|
12140
|
+
throw new UnexpectedError("Error fetching user information", { cause: retryError });
|
|
12141
|
+
}
|
|
11049
12142
|
}
|
|
11050
12143
|
}
|
|
11051
12144
|
/**
|
|
@@ -11087,40 +12180,23 @@ var SignalWire = class extends Destroyable {
|
|
|
11087
12180
|
*/
|
|
11088
12181
|
async connect() {
|
|
11089
12182
|
await this.teardownTransportAndSession();
|
|
11090
|
-
|
|
11091
|
-
const user = this._user$.value;
|
|
11092
|
-
if (!user) throw new UnexpectedError("User not initialized before connect");
|
|
11093
|
-
if (!await firstValueFrom(user.fetched$)) throw new 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 UnexpectedError("Error fetching user information", { cause: error });
|
|
11098
|
-
}
|
|
12183
|
+
await this.fetchUserOrRecover();
|
|
11099
12184
|
const errorHandler = (error) => {
|
|
11100
12185
|
this._errors$.next(error);
|
|
11101
12186
|
};
|
|
11102
12187
|
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);
|
|
12188
|
+
this._attachManager = new AttachManager(this._deps.storage, this._deps.deviceController, PreferencesContainer.instance.reconnectCallsTimeout, this._deps.attachedCallsKey, () => this._credentialRecovered);
|
|
11104
12189
|
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
12190
|
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
|
-
};
|
|
12191
|
+
this._clientSession.callControl = this._options.callControl ?? "routed";
|
|
12192
|
+
this._clientSession.onBeforeReconnect = async () => this.refreshCredentialForReconnect();
|
|
11121
12193
|
this.subscribeTo(this._clientSession.errors$, (error) => {
|
|
11122
12194
|
this._errors$.next(error);
|
|
11123
12195
|
});
|
|
12196
|
+
this.subscribeTo(this._clientSession.authorization$, (authorization) => {
|
|
12197
|
+
this._refreshCoordinator?.syncExpiryFromAuthorization(authorization, this._credentialProvider);
|
|
12198
|
+
this.persistClientBoundMarker(Boolean(authorization?.cnf?.jkt));
|
|
12199
|
+
});
|
|
11124
12200
|
await this._clientSession.connect();
|
|
11125
12201
|
await this._refreshCoordinator?.activate(this._deps.user, this._clientSession);
|
|
11126
12202
|
this.subscribeTo(this._clientSession.authenticated$.pipe(skip(1), filter(Boolean)), async () => {
|
|
@@ -11283,6 +12359,13 @@ var SignalWire = class extends Destroyable {
|
|
|
11283
12359
|
}
|
|
11284
12360
|
try {
|
|
11285
12361
|
this._visibilityController = new VisibilityController();
|
|
12362
|
+
this.subscribeTo(this._visibilityController.visibilityChange$.pipe(filter((event) => event.to === "visible")), () => {
|
|
12363
|
+
try {
|
|
12364
|
+
this._refreshCoordinator?.forceRefreshIfDue();
|
|
12365
|
+
} catch (error) {
|
|
12366
|
+
logger$1.warn("[SignalWire] Resume credential revalidation failed (non-fatal):", error);
|
|
12367
|
+
}
|
|
12368
|
+
});
|
|
11286
12369
|
this.subscribeTo(this._visibilityController.visibilityChange$.pipe(filter((event) => event.to === "visible" && PreferencesContainer.instance.refreshDevicesOnVisible)), () => {
|
|
11287
12370
|
logger$1.debug("[SignalWire] Page visible, re-enumerating devices");
|
|
11288
12371
|
try {
|
|
@@ -11294,7 +12377,7 @@ var SignalWire = class extends Destroyable {
|
|
|
11294
12377
|
logger$1.warn("[SignalWire] Failed to initialize VisibilityController:", error);
|
|
11295
12378
|
}
|
|
11296
12379
|
try {
|
|
11297
|
-
this._diagnosticsCollector = new DiagnosticsCollector({ sdkVersion: "4.0.0-rc.
|
|
12380
|
+
this._diagnosticsCollector = new DiagnosticsCollector({ sdkVersion: "4.0.0-rc.3" });
|
|
11298
12381
|
} catch (error) {
|
|
11299
12382
|
logger$1.warn("[SignalWire] Failed to initialize DiagnosticsCollector:", error);
|
|
11300
12383
|
}
|
|
@@ -11365,21 +12448,22 @@ var SignalWire = class extends Destroyable {
|
|
|
11365
12448
|
this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
11366
12449
|
throw error;
|
|
11367
12450
|
}
|
|
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()");
|
|
12451
|
+
logger$1.debug("[SignalWire] Failed to register user, attempting credential recovery...");
|
|
12452
|
+
const outcome = await this.recoverAndRetry(async () => {
|
|
11372
12453
|
await this._transport.execute(RPCExecute({
|
|
11373
12454
|
method: "subscriber.online",
|
|
11374
12455
|
params: {}
|
|
11375
12456
|
}));
|
|
12457
|
+
});
|
|
12458
|
+
if (outcome.ok) {
|
|
12459
|
+
logger$1.debug("[SignalWire] Recovery restored registration");
|
|
11376
12460
|
this._isRegistered$.next(true);
|
|
11377
|
-
|
|
11378
|
-
logger$1.error("[SignalWire] Reauthentication failed during register():", reauthError);
|
|
11379
|
-
const registerError = new InvalidCredentialsError("Failed to register user, and reauthentication attempt also failed. Please check your credentials.", { cause: reauthError instanceof Error ? reauthError : new Error(String(reauthError), { cause: reauthError }) });
|
|
11380
|
-
this._errors$.next(registerError);
|
|
11381
|
-
throw registerError;
|
|
12461
|
+
return;
|
|
11382
12462
|
}
|
|
12463
|
+
const failureCause = outcome.error ?? error;
|
|
12464
|
+
const registerError = new 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 }) });
|
|
12465
|
+
this._errors$.next(registerError);
|
|
12466
|
+
throw registerError;
|
|
11383
12467
|
}
|
|
11384
12468
|
}
|
|
11385
12469
|
/**
|
|
@@ -11410,6 +12494,11 @@ var SignalWire = class extends Destroyable {
|
|
|
11410
12494
|
* Returns a {@link Call} in `'ringing'` state. Subscribe to {@link Call.status$}
|
|
11411
12495
|
* to track progression through `'connected'` → `'disconnected'`.
|
|
11412
12496
|
*
|
|
12497
|
+
* Local media acquisition is deliberately unbounded: an unanswered permission
|
|
12498
|
+
* prompt leaves this promise pending indefinitely, so apply your own bound if
|
|
12499
|
+
* your UI needs one. The 12 s signaling budget starts only once acquisition
|
|
12500
|
+
* settles.
|
|
12501
|
+
*
|
|
11413
12502
|
* @param destination - Address URI string (e.g. `'/public/my-room'`) or {@link Address} instance.
|
|
11414
12503
|
* @param options - Media and dial options (audio/video, device constraints). Overrides defaults.
|
|
11415
12504
|
* @returns The created {@link Call} instance.
|
|
@@ -11432,7 +12521,18 @@ var SignalWire = class extends Destroyable {
|
|
|
11432
12521
|
};
|
|
11433
12522
|
await this.waitAuthentication();
|
|
11434
12523
|
logger$1.debug("[SignalWire] Dialing with options:", computed_options);
|
|
11435
|
-
|
|
12524
|
+
try {
|
|
12525
|
+
return await this._clientSession.createOutboundCall(destination, computed_options);
|
|
12526
|
+
} catch (error) {
|
|
12527
|
+
if (!isRecoverableAuthError(error)) throw error;
|
|
12528
|
+
logger$1.debug("[SignalWire] Dial hit a recoverable auth error; recovering the session and retrying");
|
|
12529
|
+
const outcome = await this.recoverAndRetry(async () => {
|
|
12530
|
+
await this.waitAuthentication();
|
|
12531
|
+
return this._clientSession.createOutboundCall(destination, computed_options);
|
|
12532
|
+
});
|
|
12533
|
+
if (outcome.ok) return outcome.value;
|
|
12534
|
+
throw outcome.error ?? error;
|
|
12535
|
+
}
|
|
11436
12536
|
}
|
|
11437
12537
|
/**
|
|
11438
12538
|
* Runs a multi-phase connectivity test against the given destination.
|
|
@@ -11713,10 +12813,13 @@ var SignalWire = class extends Destroyable {
|
|
|
11713
12813
|
destroy() {
|
|
11714
12814
|
this._refreshCoordinator?.destroy();
|
|
11715
12815
|
this._refreshCoordinator = void 0;
|
|
12816
|
+
this._refreshHttp?.destroy();
|
|
12817
|
+
this._refreshHttp = void 0;
|
|
11716
12818
|
this._dpopManager?.destroy();
|
|
11717
|
-
this._clientSession
|
|
11718
|
-
|
|
11719
|
-
this.
|
|
12819
|
+
const session = this._clientSession;
|
|
12820
|
+
session?.teardownSessionState();
|
|
12821
|
+
this._transport?.destroy();
|
|
12822
|
+
session?.destroy();
|
|
11720
12823
|
try {
|
|
11721
12824
|
this._networkMonitor?.destroy();
|
|
11722
12825
|
} catch {}
|
|
@@ -11828,7 +12931,7 @@ var StaticCredentialProvider = class {
|
|
|
11828
12931
|
/**
|
|
11829
12932
|
* Library version from package.json, injected at build time.
|
|
11830
12933
|
*/
|
|
11831
|
-
const version = "4.0.0-rc.
|
|
12934
|
+
const version = "4.0.0-rc.3";
|
|
11832
12935
|
/**
|
|
11833
12936
|
* Flag indicating the library has been loaded and is ready to use.
|
|
11834
12937
|
* For UMD builds: `window.SignalWire.ready`
|
|
@@ -11850,12 +12953,12 @@ const ready = true;
|
|
|
11850
12953
|
*/
|
|
11851
12954
|
const emitReadyEvent = () => {
|
|
11852
12955
|
if (typeof window !== "undefined") {
|
|
11853
|
-
const event = new CustomEvent("signalwire:js:ready", { detail: { version: "4.0.0-rc.
|
|
12956
|
+
const event = new CustomEvent("signalwire:js:ready", { detail: { version: "4.0.0-rc.3" } });
|
|
11854
12957
|
window.dispatchEvent(event);
|
|
11855
12958
|
}
|
|
11856
12959
|
};
|
|
11857
12960
|
emitReadyEvent();
|
|
11858
12961
|
|
|
11859
12962
|
//#endregion
|
|
11860
|
-
export { Address, CallCreateError, ClientPreferences, CollectionFetchError, DPoPInitError, DeviceTokenError, EmbedTokenCredentialProvider, InvalidCredentialsError, MediaAccessError, MediaTrackError, MessageParseError, OverconstrainedFallbackError, Participant, PreflightError, RecoveryError, SelfCapabilities, SelfParticipant, SignalWire, StaticCredentialProvider, TokenRefreshError, UnexpectedError, User, VertoPongError, WebRTCCall, embeddableCall, getLogger, isSelfParticipant, ready, setDebugOptions, setLogLevel, setLogger, version };
|
|
12963
|
+
export { Address, AuxiliaryLegCancelledError, AuxiliaryLegTimeoutError, CallCreateError, CallNotReadyError, ClientPreferences, CollectionFetchError, DPoPInitError, DeviceTokenError, EmbedTokenCredentialProvider, InvalidCredentialsError, MediaAccessError, MediaTrackError, MessageParseError, OverconstrainedFallbackError, Participant, ParticipantNotReadyError, PreflightError, RecoveryError, ScreenShareAlreadyActiveError, SelfCapabilities, SelfParticipant, SignalWire, StaticCredentialProvider, TokenRefreshError, UnexpectedError, User, VertoPongError, WebRTCCall, embeddableCall, getLogger, isSelfParticipant, ready, setDebugOptions, setLogLevel, setLogger, version };
|
|
11861
12964
|
//# sourceMappingURL=index.mjs.map
|