@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/browser.umd.js
CHANGED
|
@@ -4192,7 +4192,7 @@ var require_merge$1 = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
4192
4192
|
var empty_1$7 = require_empty();
|
|
4193
4193
|
var args_1$8 = require_args();
|
|
4194
4194
|
var from_1$4 = require_from();
|
|
4195
|
-
function merge$
|
|
4195
|
+
function merge$7() {
|
|
4196
4196
|
var args = [];
|
|
4197
4197
|
for (var _i = 0; _i < arguments.length; _i++) args[_i] = arguments[_i];
|
|
4198
4198
|
var scheduler = args_1$8.popScheduler(args);
|
|
@@ -4200,7 +4200,7 @@ var require_merge$1 = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
4200
4200
|
var sources = args;
|
|
4201
4201
|
return !sources.length ? empty_1$7.EMPTY : sources.length === 1 ? innerFrom_1$31.innerFrom(sources[0]) : mergeAll_1$3.mergeAll(concurrent)(from_1$4.from(sources, scheduler));
|
|
4202
4202
|
}
|
|
4203
|
-
exports.merge = merge$
|
|
4203
|
+
exports.merge = merge$7;
|
|
4204
4204
|
}));
|
|
4205
4205
|
|
|
4206
4206
|
//#endregion
|
|
@@ -4331,13 +4331,13 @@ var require_race$1 = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
4331
4331
|
var innerFrom_1$28 = require_innerFrom();
|
|
4332
4332
|
var argsOrArgArray_1$4 = require_argsOrArgArray();
|
|
4333
4333
|
var OperatorSubscriber_1$48 = require_OperatorSubscriber();
|
|
4334
|
-
function race$
|
|
4334
|
+
function race$4() {
|
|
4335
4335
|
var sources = [];
|
|
4336
4336
|
for (var _i = 0; _i < arguments.length; _i++) sources[_i] = arguments[_i];
|
|
4337
4337
|
sources = argsOrArgArray_1$4.argsOrArgArray(sources);
|
|
4338
4338
|
return sources.length === 1 ? innerFrom_1$28.innerFrom(sources[0]) : new Observable_1$6.Observable(raceInit(sources));
|
|
4339
4339
|
}
|
|
4340
|
-
exports.race = race$
|
|
4340
|
+
exports.race = race$4;
|
|
4341
4341
|
function raceInit(sources) {
|
|
4342
4342
|
return function(subscriber) {
|
|
4343
4343
|
var subscriptions = [];
|
|
@@ -6082,7 +6082,7 @@ var require_merge = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
6082
6082
|
var mergeAll_1$2 = require_mergeAll();
|
|
6083
6083
|
var args_1$3 = require_args();
|
|
6084
6084
|
var from_1$1 = require_from();
|
|
6085
|
-
function merge$
|
|
6085
|
+
function merge$6() {
|
|
6086
6086
|
var args = [];
|
|
6087
6087
|
for (var _i = 0; _i < arguments.length; _i++) args[_i] = arguments[_i];
|
|
6088
6088
|
var scheduler = args_1$3.popScheduler(args);
|
|
@@ -6091,7 +6091,7 @@ var require_merge = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
6091
6091
|
mergeAll_1$2.mergeAll(concurrent)(from_1$1.from(__spreadArray$8([source], __read$8(args)), scheduler)).subscribe(subscriber);
|
|
6092
6092
|
});
|
|
6093
6093
|
}
|
|
6094
|
-
exports.merge = merge$
|
|
6094
|
+
exports.merge = merge$6;
|
|
6095
6095
|
}));
|
|
6096
6096
|
|
|
6097
6097
|
//#endregion
|
|
@@ -6938,7 +6938,7 @@ var require_switchMap = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
6938
6938
|
var innerFrom_1$6 = require_innerFrom();
|
|
6939
6939
|
var lift_1$13 = require_lift();
|
|
6940
6940
|
var OperatorSubscriber_1$11 = require_OperatorSubscriber();
|
|
6941
|
-
function switchMap$
|
|
6941
|
+
function switchMap$8(project, resultSelector) {
|
|
6942
6942
|
return lift_1$13.operate(function(source, subscriber) {
|
|
6943
6943
|
var innerSubscriber = null;
|
|
6944
6944
|
var index = 0;
|
|
@@ -6962,7 +6962,7 @@ var require_switchMap = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
6962
6962
|
}));
|
|
6963
6963
|
});
|
|
6964
6964
|
}
|
|
6965
|
-
exports.switchMap = switchMap$
|
|
6965
|
+
exports.switchMap = switchMap$8;
|
|
6966
6966
|
}));
|
|
6967
6967
|
|
|
6968
6968
|
//#endregion
|
|
@@ -9028,6 +9028,25 @@ const DEFAULT_ICE_CANDIDATE_TIMEOUT_MS = 600;
|
|
|
9028
9028
|
const DEFAULT_ICE_GATHERING_TIMEOUT_MS = 6e3;
|
|
9029
9029
|
const DEFAULT_RECONNECT_CALLS_TIMEOUT_MS = 300 * 1e3;
|
|
9030
9030
|
const DEFAULT_CONNECTION_TIMEOUT_MS = 1e4;
|
|
9031
|
+
/**
|
|
9032
|
+
* How long call setup may wait for the network, measured from local media
|
|
9033
|
+
* settling until the member id arrives.
|
|
9034
|
+
*
|
|
9035
|
+
* Local media acquisition is deliberately NOT inside this: a permission prompt
|
|
9036
|
+
* or a device picker is human time, and a human may take as long as they like
|
|
9037
|
+
* without spending the server's budget. The clock starts when acquisition ends.
|
|
9038
|
+
*
|
|
9039
|
+
* Must exceed `iceGatheringTimeout + the RPC timeout`, which both run inside it.
|
|
9040
|
+
*/
|
|
9041
|
+
const DEFAULT_CALL_SIGNALING_TIMEOUT_MS = 12e3;
|
|
9042
|
+
/**
|
|
9043
|
+
* How long an auxiliary leg (screen share, additional device) may take to
|
|
9044
|
+
* connect once its media is in hand.
|
|
9045
|
+
*
|
|
9046
|
+
* One bound for both: the picker is human time and sits outside it. What remains
|
|
9047
|
+
* — offer, ICE, invite, answer, DTLS — is the same work for either leg kind.
|
|
9048
|
+
*/
|
|
9049
|
+
const DEFAULT_AUX_LEG_CONNECT_TIMEOUT_MS = 15e3;
|
|
9031
9050
|
const DEFAULT_RECONNECT_DELAY_MIN_MS = 100;
|
|
9032
9051
|
const DEFAULT_RECONNECT_DELAY_MAX_MS = 3e3;
|
|
9033
9052
|
const DEFAULT_DEVICE_DEBOUNCE_TIME_MS = 1500;
|
|
@@ -9055,6 +9074,14 @@ const CREDENTIAL_REFRESH_MAX_DELAY_MS = 3e4;
|
|
|
9055
9074
|
/** Buffer in milliseconds before token expiry to trigger refresh. */
|
|
9056
9075
|
const CREDENTIAL_REFRESH_BUFFER_MS = 5e3;
|
|
9057
9076
|
/**
|
|
9077
|
+
* Clock-skew allowance (ms) for treating an in-memory credential as expired
|
|
9078
|
+
* when deciding whether a fresh (re)connect must re-mint the token before
|
|
9079
|
+
* authenticating. A token within this window of its `expiry_at` is treated as
|
|
9080
|
+
* stale so the reconnect re-mints via the credential provider instead of
|
|
9081
|
+
* replaying a dead token (which the server rejects with -32003).
|
|
9082
|
+
*/
|
|
9083
|
+
const CREDENTIAL_EXPIRY_SKEW_MS = 3e4;
|
|
9084
|
+
/**
|
|
9058
9085
|
* Maximum time the coordinator will wait for `DeviceTokenManager.activate()`
|
|
9059
9086
|
* to resolve before treating the activation as failed and falling back to
|
|
9060
9087
|
* the developer-provided refresh path. Prevents a wedged HTTP layer from
|
|
@@ -9073,6 +9100,8 @@ const MEDIA_ACCESS_DENIAL_NAMES = [
|
|
|
9073
9100
|
"SecurityError",
|
|
9074
9101
|
"PermissionDeniedError"
|
|
9075
9102
|
];
|
|
9103
|
+
/** Error names browsers use when the capture hardware is already held exclusively. */
|
|
9104
|
+
const MEDIA_DEVICE_IN_USE_NAMES = ["NotReadableError", "TrackStartError"];
|
|
9076
9105
|
/** Default polling interval for RTCPeerConnection.getStats() in milliseconds. */
|
|
9077
9106
|
const DEFAULT_STATS_POLLING_INTERVAL_MS = 1e3;
|
|
9078
9107
|
/** Number of initial samples used to build a baseline for spike detection. */
|
|
@@ -9287,6 +9316,20 @@ var CallCreateError = class extends Error {
|
|
|
9287
9316
|
this.name = "CallCreateError";
|
|
9288
9317
|
}
|
|
9289
9318
|
};
|
|
9319
|
+
var CallNotReadyError = class extends Error {
|
|
9320
|
+
constructor(callId, options) {
|
|
9321
|
+
super(`Call "${callId}" has no self member context yet: selfId/nodeId have not been received from the server`, options);
|
|
9322
|
+
this.callId = callId;
|
|
9323
|
+
this.name = "CallNotReadyError";
|
|
9324
|
+
}
|
|
9325
|
+
};
|
|
9326
|
+
var ParticipantNotReadyError = class extends Error {
|
|
9327
|
+
constructor(memberId, options) {
|
|
9328
|
+
super(`Participant "${memberId}" has no call context yet: its member state (call_id/node_id) has not been received from the server`, options);
|
|
9329
|
+
this.memberId = memberId;
|
|
9330
|
+
this.name = "ParticipantNotReadyError";
|
|
9331
|
+
}
|
|
9332
|
+
};
|
|
9290
9333
|
var JSONRPCError = class extends Error {
|
|
9291
9334
|
constructor(code, message, data, options, requestId) {
|
|
9292
9335
|
super(message, options);
|
|
@@ -9358,6 +9401,32 @@ var CollectionFetchError = class extends Error {
|
|
|
9358
9401
|
this.name = "CollectionFetchError";
|
|
9359
9402
|
}
|
|
9360
9403
|
};
|
|
9404
|
+
/**
|
|
9405
|
+
* An auxiliary leg did not connect within its budget. Typed rather than a bare
|
|
9406
|
+
* RxJS `TimeoutError` so the leg and cause survive.
|
|
9407
|
+
*/
|
|
9408
|
+
var AuxiliaryLegTimeoutError = class extends Error {
|
|
9409
|
+
constructor(leg, originalError) {
|
|
9410
|
+
super(`Timed out waiting for the ${leg} connection to be established`, { cause: originalError });
|
|
9411
|
+
this.leg = leg;
|
|
9412
|
+
this.originalError = originalError;
|
|
9413
|
+
this.name = "AuxiliaryLegTimeoutError";
|
|
9414
|
+
}
|
|
9415
|
+
};
|
|
9416
|
+
/**
|
|
9417
|
+
* An auxiliary leg was removed before it finished connecting.
|
|
9418
|
+
*
|
|
9419
|
+
* Typed rather than a bare resolve so a caller awaiting the start can tell a
|
|
9420
|
+
* cancel apart from a share that actually came up — the public methods return
|
|
9421
|
+
* `void`, so the promise is the only signal they have.
|
|
9422
|
+
*/
|
|
9423
|
+
var AuxiliaryLegCancelledError = class extends Error {
|
|
9424
|
+
constructor(leg) {
|
|
9425
|
+
super(`The ${leg} leg was removed before it finished connecting`);
|
|
9426
|
+
this.leg = leg;
|
|
9427
|
+
this.name = "AuxiliaryLegCancelledError";
|
|
9428
|
+
}
|
|
9429
|
+
};
|
|
9361
9430
|
var MediaTrackError = class extends Error {
|
|
9362
9431
|
constructor(operation, kind, originalError) {
|
|
9363
9432
|
super(`Media track ${operation} failed for ${kind}`, { cause: originalError instanceof Error ? originalError : void 0 });
|
|
@@ -9371,6 +9440,10 @@ var MediaTrackError = class extends Error {
|
|
|
9371
9440
|
function isMediaAccessDenial(originalError) {
|
|
9372
9441
|
return originalError instanceof Error && MEDIA_ACCESS_DENIAL_NAMES.includes(originalError.name);
|
|
9373
9442
|
}
|
|
9443
|
+
/** True when a `getUserMedia` rejection means the hardware is already held exclusively. */
|
|
9444
|
+
function isMediaDeviceInUse(originalError) {
|
|
9445
|
+
return originalError instanceof Error && MEDIA_DEVICE_IN_USE_NAMES.includes(originalError.name);
|
|
9446
|
+
}
|
|
9374
9447
|
/**
|
|
9375
9448
|
* Failure to acquire local media (camera, microphone, or screen capture)
|
|
9376
9449
|
* via `getUserMedia`/`getDisplayMedia`.
|
|
@@ -9394,6 +9467,22 @@ var MediaAccessError = class extends Error {
|
|
|
9394
9467
|
return isMediaAccessDenial(this.originalError);
|
|
9395
9468
|
}
|
|
9396
9469
|
};
|
|
9470
|
+
/**
|
|
9471
|
+
* Thrown by `startScreenShare()` when the call is already sharing a screen.
|
|
9472
|
+
*
|
|
9473
|
+
* A call carries at most one screen share. Accepting a second one would
|
|
9474
|
+
* overwrite the only reference the SDK holds to the first, leaving it
|
|
9475
|
+
* capturing and sending with no way to stop it — so the second request is
|
|
9476
|
+
* rejected and the live share is left untouched. Call `stopScreenShare()`
|
|
9477
|
+
* first to replace it.
|
|
9478
|
+
*/
|
|
9479
|
+
var ScreenShareAlreadyActiveError = class extends Error {
|
|
9480
|
+
constructor(screenShareId, options) {
|
|
9481
|
+
super(`A screen share is already active on this call (${screenShareId}). Call stopScreenShare() before starting another one.`, options);
|
|
9482
|
+
this.screenShareId = screenShareId;
|
|
9483
|
+
this.name = "ScreenShareAlreadyActiveError";
|
|
9484
|
+
}
|
|
9485
|
+
};
|
|
9397
9486
|
var DPoPInitError = class extends Error {
|
|
9398
9487
|
constructor(originalError, message = "Failed to initialize DPoP key pair") {
|
|
9399
9488
|
super(message, { cause: originalError instanceof Error ? originalError : void 0 });
|
|
@@ -9634,9 +9723,9 @@ var require_loglevel = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
9634
9723
|
defaultLogger$1 = new Logger();
|
|
9635
9724
|
defaultLogger$1.getLogger = function getLogger$1(name) {
|
|
9636
9725
|
if (typeof name !== "symbol" && typeof name !== "string" || name === "") throw new TypeError("You must supply a name when creating a logger.");
|
|
9637
|
-
var logger$
|
|
9638
|
-
if (!logger$
|
|
9639
|
-
return logger$
|
|
9726
|
+
var logger$34 = _loggersByName[name];
|
|
9727
|
+
if (!logger$34) logger$34 = _loggersByName[name] = new Logger(name, defaultLogger$1.methodFactory);
|
|
9728
|
+
return logger$34;
|
|
9640
9729
|
};
|
|
9641
9730
|
var _log = typeof window !== undefinedType ? window.log : void 0;
|
|
9642
9731
|
defaultLogger$1.noConflict = function() {
|
|
@@ -9672,8 +9761,8 @@ const defaultLoggerLevel = defaultLogger.levels.WARN;
|
|
|
9672
9761
|
defaultLogger.setLevel(defaultLoggerLevel);
|
|
9673
9762
|
let userLogger = null;
|
|
9674
9763
|
/** Replace the built-in logger with a custom implementation. Pass `null` to restore defaults. */
|
|
9675
|
-
const setLogger = (logger$
|
|
9676
|
-
userLogger = logger$
|
|
9764
|
+
const setLogger = (logger$34) => {
|
|
9765
|
+
userLogger = logger$34;
|
|
9677
9766
|
};
|
|
9678
9767
|
let debugOptions = {};
|
|
9679
9768
|
/** Configure debug options (e.g., `{ logWsTraffic: true }`). */
|
|
@@ -9717,8 +9806,8 @@ const wsTraffic = (options) => {
|
|
|
9717
9806
|
loggerInstance.debug(`${options.type.toUpperCase()}: \n`, msg, "\n");
|
|
9718
9807
|
};
|
|
9719
9808
|
const getLogger = () => {
|
|
9720
|
-
const logger$
|
|
9721
|
-
return new Proxy(logger$
|
|
9809
|
+
const logger$34 = getLoggerInstance();
|
|
9810
|
+
return new Proxy(logger$34, { get(_target, prop, _receiver) {
|
|
9722
9811
|
if (prop === "wsTraffic") return wsTraffic;
|
|
9723
9812
|
const instance = getLoggerInstance();
|
|
9724
9813
|
const value = Reflect.get(instance, prop);
|
|
@@ -9770,7 +9859,7 @@ const asyncRetry = async ({ asyncCallable, maxRetries: retries = DEFAULT_MAX_RET
|
|
|
9770
9859
|
|
|
9771
9860
|
//#endregion
|
|
9772
9861
|
//#region src/controllers/HTTPRequestController.ts
|
|
9773
|
-
const logger$
|
|
9862
|
+
const logger$33 = getLogger();
|
|
9774
9863
|
const GET_PARAMS = {
|
|
9775
9864
|
method: "GET",
|
|
9776
9865
|
headers: { Accept: "application/json" }
|
|
@@ -9834,7 +9923,7 @@ var HTTPRequestController = class HTTPRequestController extends Destroyable {
|
|
|
9834
9923
|
this._responses$.next(response);
|
|
9835
9924
|
return response;
|
|
9836
9925
|
} catch (error) {
|
|
9837
|
-
logger$
|
|
9926
|
+
logger$33.error("[HTTPRequestController] Request error:", error);
|
|
9838
9927
|
this._status$.next("error");
|
|
9839
9928
|
const err = error instanceof Error ? error : new Error("HTTP request failed", { cause: error });
|
|
9840
9929
|
this._errors$.next(err);
|
|
@@ -9861,7 +9950,7 @@ var HTTPRequestController = class HTTPRequestController extends Destroyable {
|
|
|
9861
9950
|
const url = this.buildURL(request.url);
|
|
9862
9951
|
const headers = this.buildHeaders(request.headers);
|
|
9863
9952
|
const timeout$5 = request.timeout ?? this.requestTimeout;
|
|
9864
|
-
logger$
|
|
9953
|
+
logger$33.debug("[HTTPRequestController] Executing request:", {
|
|
9865
9954
|
method: request.method,
|
|
9866
9955
|
url,
|
|
9867
9956
|
headers: Object.keys(headers).reduce((acc, key) => {
|
|
@@ -9881,7 +9970,7 @@ var HTTPRequestController = class HTTPRequestController extends Destroyable {
|
|
|
9881
9970
|
});
|
|
9882
9971
|
clearTimeout(timeoutId);
|
|
9883
9972
|
const httpResponse = await this.convertResponse(response);
|
|
9884
|
-
logger$
|
|
9973
|
+
logger$33.debug("[HTTPRequestController] Response received:", {
|
|
9885
9974
|
status: response.status,
|
|
9886
9975
|
statusText: response.statusText,
|
|
9887
9976
|
headers: [...response.headers.entries()],
|
|
@@ -9891,7 +9980,7 @@ var HTTPRequestController = class HTTPRequestController extends Destroyable {
|
|
|
9891
9980
|
} catch (error) {
|
|
9892
9981
|
clearTimeout(timeoutId);
|
|
9893
9982
|
if (error instanceof Error && error.name === "AbortError") throw new RequestTimeoutError(`Request timeout after ${timeout$5}ms`, { cause: error });
|
|
9894
|
-
logger$
|
|
9983
|
+
logger$33.error("[HTTPRequestController] Request failed:", error);
|
|
9895
9984
|
throw error;
|
|
9896
9985
|
}
|
|
9897
9986
|
}
|
|
@@ -9905,8 +9994,8 @@ var HTTPRequestController = class HTTPRequestController extends Destroyable {
|
|
|
9905
9994
|
const credential = this.getCredential();
|
|
9906
9995
|
if (credential.token) {
|
|
9907
9996
|
headers.Authorization = `Bearer ${credential.token}`;
|
|
9908
|
-
logger$
|
|
9909
|
-
} else logger$
|
|
9997
|
+
logger$33.debug("[HTTPRequestController] Using Bearer token auth, token length:", credential.token.length);
|
|
9998
|
+
} else logger$33.warn("[HTTPRequestController] No credentials available for authentication");
|
|
9910
9999
|
return headers;
|
|
9911
10000
|
}
|
|
9912
10001
|
/**
|
|
@@ -10054,7 +10143,7 @@ function fromMsToSec(milliseconds) {
|
|
|
10054
10143
|
|
|
10055
10144
|
//#endregion
|
|
10056
10145
|
//#region src/containers/PreferencesContainer.ts
|
|
10057
|
-
const logger$
|
|
10146
|
+
const logger$32 = getLogger();
|
|
10058
10147
|
var PreferencesContainer = class PreferencesContainer {
|
|
10059
10148
|
static get instance() {
|
|
10060
10149
|
this._instance ??= new PreferencesContainer();
|
|
@@ -10716,7 +10805,7 @@ var ClientPreferences = class {
|
|
|
10716
10805
|
if (!this._storage) return;
|
|
10717
10806
|
const data = collectStoredPreferences();
|
|
10718
10807
|
this._storage.setItem(PREFERENCES_STORAGE_KEY, data, "local").catch((error) => {
|
|
10719
|
-
logger$
|
|
10808
|
+
logger$32.error(`[ClientPreferences] Failed to save preferences: ${String(error)}`);
|
|
10720
10809
|
});
|
|
10721
10810
|
}
|
|
10722
10811
|
/** Loads preferences from storage and applies them to the container. */
|
|
@@ -10725,7 +10814,7 @@ var ClientPreferences = class {
|
|
|
10725
10814
|
this._storage.getItem(PREFERENCES_STORAGE_KEY, "local").then((stored) => {
|
|
10726
10815
|
if (stored) applyStoredPreferences(stored);
|
|
10727
10816
|
}).catch((error) => {
|
|
10728
|
-
logger$
|
|
10817
|
+
logger$32.error(`[ClientPreferences] Failed to load preferences: ${String(error)}`);
|
|
10729
10818
|
});
|
|
10730
10819
|
}
|
|
10731
10820
|
};
|
|
@@ -10747,7 +10836,7 @@ function toError(value) {
|
|
|
10747
10836
|
//#endregion
|
|
10748
10837
|
//#region src/controllers/NavigatorDeviceController.ts
|
|
10749
10838
|
var import_cjs$29 = require_cjs();
|
|
10750
|
-
const logger$
|
|
10839
|
+
const logger$31 = getLogger();
|
|
10751
10840
|
/** Maps a device kind to its storage key. */
|
|
10752
10841
|
const DEVICE_STORAGE_KEYS = {
|
|
10753
10842
|
audioinput: DEVICE_STORAGE_KEY_AUDIO_INPUT,
|
|
@@ -10769,7 +10858,7 @@ var NavigatorDeviceController = class extends Destroyable {
|
|
|
10769
10858
|
super();
|
|
10770
10859
|
this.webRTCApiProvider = webRTCApiProvider;
|
|
10771
10860
|
this.deviceChangeHandler = () => {
|
|
10772
|
-
logger$
|
|
10861
|
+
logger$31.debug("[DeviceController] Device change detected");
|
|
10773
10862
|
this.enumerateDevices();
|
|
10774
10863
|
};
|
|
10775
10864
|
this._devicesState$ = this.createBehaviorSubject(initialDevicesState);
|
|
@@ -10834,13 +10923,13 @@ var NavigatorDeviceController = class extends Destroyable {
|
|
|
10834
10923
|
return this.cachedObservable("videoInputDevices$", () => this._devicesState$.pipe((0, import_cjs$29.map)((state) => state.videoinput), (0, import_cjs$29.distinctUntilChanged)(), (0, import_cjs$29.takeUntil)(this.destroyed$)));
|
|
10835
10924
|
}
|
|
10836
10925
|
get selectedAudioInputDevice$() {
|
|
10837
|
-
return this.cachedObservable("selectedAudioInputDevice$", () => this._selectedDevicesState$.asObservable().pipe((0, import_cjs$29.map)((state) => state.audioinput), (0, import_cjs$29.distinctUntilChanged)(), (0, import_cjs$29.takeUntil)(this.destroyed$), (0, import_cjs$29.tap)((info) => logger$
|
|
10926
|
+
return this.cachedObservable("selectedAudioInputDevice$", () => this._selectedDevicesState$.asObservable().pipe((0, import_cjs$29.map)((state) => state.audioinput), (0, import_cjs$29.distinctUntilChanged)(), (0, import_cjs$29.takeUntil)(this.destroyed$), (0, import_cjs$29.tap)((info) => logger$31.debug("[DeviceController] Selected audio input device changed:", info))));
|
|
10838
10927
|
}
|
|
10839
10928
|
get selectedAudioOutputDevice$() {
|
|
10840
|
-
return this.cachedObservable("selectedAudioOutputDevice$", () => this._selectedDevicesState$.asObservable().pipe((0, import_cjs$29.map)((state) => state.audiooutput), (0, import_cjs$29.distinctUntilChanged)(), (0, import_cjs$29.takeUntil)(this.destroyed$), (0, import_cjs$29.tap)((info) => logger$
|
|
10929
|
+
return this.cachedObservable("selectedAudioOutputDevice$", () => this._selectedDevicesState$.asObservable().pipe((0, import_cjs$29.map)((state) => state.audiooutput), (0, import_cjs$29.distinctUntilChanged)(), (0, import_cjs$29.takeUntil)(this.destroyed$), (0, import_cjs$29.tap)((info) => logger$31.debug("[DeviceController] Selected audio output device changed:", info))));
|
|
10841
10930
|
}
|
|
10842
10931
|
get selectedVideoInputDevice$() {
|
|
10843
|
-
return this.cachedObservable("selectedVideoInputDevice$", () => this._selectedDevicesState$.asObservable().pipe((0, import_cjs$29.map)((state) => state.videoinput), (0, import_cjs$29.distinctUntilChanged)(), (0, import_cjs$29.takeUntil)(this.destroyed$), (0, import_cjs$29.tap)((info) => logger$
|
|
10932
|
+
return this.cachedObservable("selectedVideoInputDevice$", () => this._selectedDevicesState$.asObservable().pipe((0, import_cjs$29.map)((state) => state.videoinput), (0, import_cjs$29.distinctUntilChanged)(), (0, import_cjs$29.takeUntil)(this.destroyed$), (0, import_cjs$29.tap)((info) => logger$31.debug("[DeviceController] Selected video input device changed:", info))));
|
|
10844
10933
|
}
|
|
10845
10934
|
get selectedAudioInputDevice() {
|
|
10846
10935
|
if (this._audioInputDisabled$.value) return null;
|
|
@@ -10915,7 +11004,7 @@ var NavigatorDeviceController = class extends Destroyable {
|
|
|
10915
11004
|
if (device) this.persistDeviceSelection("audioinput", device);
|
|
10916
11005
|
}
|
|
10917
11006
|
selectVideoInputDevice(device) {
|
|
10918
|
-
logger$
|
|
11007
|
+
logger$31.debug("[DeviceController] Setting selected video input device:", device);
|
|
10919
11008
|
if (this._videoInputDisabled$.value && device) this._videoInputDisabled$.next(false);
|
|
10920
11009
|
const previous = this._selectedDevicesState$.value.videoinput;
|
|
10921
11010
|
if (previous && previous.deviceId !== device?.deviceId) this._deviceHistory.push("videoinput", previous);
|
|
@@ -10972,7 +11061,7 @@ var NavigatorDeviceController = class extends Destroyable {
|
|
|
10972
11061
|
}
|
|
10973
11062
|
const fromHistory = this._deviceHistory.findInHistory(kind, devices);
|
|
10974
11063
|
if (fromHistory) {
|
|
10975
|
-
logger$
|
|
11064
|
+
logger$31.debug(`[DeviceController] Device disappeared, falling back to history: ${fromHistory.label}`);
|
|
10976
11065
|
this.emitDeviceRecovered(kind, selected, fromHistory, "device_disconnected");
|
|
10977
11066
|
return fromHistory;
|
|
10978
11067
|
}
|
|
@@ -11025,7 +11114,7 @@ var NavigatorDeviceController = class extends Destroyable {
|
|
|
11025
11114
|
try {
|
|
11026
11115
|
await this._storageManager.setItem(DEVICE_STORAGE_KEYS[kind], stored, "local");
|
|
11027
11116
|
} catch (error) {
|
|
11028
|
-
logger$
|
|
11117
|
+
logger$31.error(`[DeviceController] Failed to persist device selection for ${kind}:`, error);
|
|
11029
11118
|
}
|
|
11030
11119
|
}
|
|
11031
11120
|
async loadPersistedDevices() {
|
|
@@ -11041,7 +11130,7 @@ var NavigatorDeviceController = class extends Destroyable {
|
|
|
11041
11130
|
[kind]: stored
|
|
11042
11131
|
};
|
|
11043
11132
|
} catch (error) {
|
|
11044
|
-
logger$
|
|
11133
|
+
logger$31.error(`[DeviceController] Failed to load persisted device for ${kind}:`, error);
|
|
11045
11134
|
}
|
|
11046
11135
|
}
|
|
11047
11136
|
/** Clears device history, persisted selections, and re-enumerates devices. */
|
|
@@ -11059,7 +11148,7 @@ var NavigatorDeviceController = class extends Destroyable {
|
|
|
11059
11148
|
this.disableDeviceMonitoring();
|
|
11060
11149
|
this.webRTCApiProvider.mediaDevices.addEventListener("devicechange", this.deviceChangeHandler);
|
|
11061
11150
|
if (PreferencesContainer.instance.devicePollingInterval > 0) this._devicesPoolingSubscription = (0, import_cjs$29.interval)(PreferencesContainer.instance.devicePollingInterval).subscribe(() => {
|
|
11062
|
-
logger$
|
|
11151
|
+
logger$31.debug("[DeviceController] Polling devices due to interval");
|
|
11063
11152
|
this.enumerateDevices();
|
|
11064
11153
|
});
|
|
11065
11154
|
this.enumerateDevices();
|
|
@@ -11085,13 +11174,13 @@ var NavigatorDeviceController = class extends Destroyable {
|
|
|
11085
11174
|
videoinput: []
|
|
11086
11175
|
});
|
|
11087
11176
|
this._devicesState$.next(devicesByKind);
|
|
11088
|
-
logger$
|
|
11177
|
+
logger$31.debug("[DeviceController] Devices enumerated:", {
|
|
11089
11178
|
audioInputs: devicesByKind.audioinput.length,
|
|
11090
11179
|
audioOutputs: devicesByKind.audiooutput.length,
|
|
11091
11180
|
videoInputs: devicesByKind.videoinput.length
|
|
11092
11181
|
});
|
|
11093
11182
|
} catch (error) {
|
|
11094
|
-
logger$
|
|
11183
|
+
logger$31.error("[DeviceController] Failed to enumerate devices:", error);
|
|
11095
11184
|
this._errors$.next(toError(error));
|
|
11096
11185
|
}
|
|
11097
11186
|
}
|
|
@@ -11107,7 +11196,7 @@ var NavigatorDeviceController = class extends Destroyable {
|
|
|
11107
11196
|
stream.getTracks().forEach((t) => t.stop());
|
|
11108
11197
|
return capabilities;
|
|
11109
11198
|
} catch (error) {
|
|
11110
|
-
logger$
|
|
11199
|
+
logger$31.error("[DeviceController] Failed to get device capabilities:", error);
|
|
11111
11200
|
this._errors$.next(toError(error));
|
|
11112
11201
|
throw error;
|
|
11113
11202
|
}
|
|
@@ -11358,7 +11447,7 @@ var DependencyContainer = class {
|
|
|
11358
11447
|
|
|
11359
11448
|
//#endregion
|
|
11360
11449
|
//#region src/controllers/CryptoController.ts
|
|
11361
|
-
const logger$
|
|
11450
|
+
const logger$30 = getLogger();
|
|
11362
11451
|
const DPOP_DB_NAME = "sw-dpop";
|
|
11363
11452
|
const DPOP_DB_VERSION = 1;
|
|
11364
11453
|
const DPOP_STORE_NAME = "keys";
|
|
@@ -11417,7 +11506,7 @@ async function loadKeyPairFromDB() {
|
|
|
11417
11506
|
tx.oncomplete = () => db.close();
|
|
11418
11507
|
});
|
|
11419
11508
|
} catch (error) {
|
|
11420
|
-
logger$
|
|
11509
|
+
logger$30.warn("[DPoP] Failed to load key pair from IndexedDB:", error);
|
|
11421
11510
|
return null;
|
|
11422
11511
|
}
|
|
11423
11512
|
}
|
|
@@ -11437,7 +11526,7 @@ async function saveKeyPairToDB(keyPair) {
|
|
|
11437
11526
|
};
|
|
11438
11527
|
});
|
|
11439
11528
|
} catch (error) {
|
|
11440
|
-
logger$
|
|
11529
|
+
logger$30.warn("[DPoP] Failed to save key pair to IndexedDB:", error);
|
|
11441
11530
|
}
|
|
11442
11531
|
}
|
|
11443
11532
|
async function deleteKeyPairFromDB() {
|
|
@@ -11456,7 +11545,7 @@ async function deleteKeyPairFromDB() {
|
|
|
11456
11545
|
};
|
|
11457
11546
|
});
|
|
11458
11547
|
} catch (error) {
|
|
11459
|
-
logger$
|
|
11548
|
+
logger$30.warn("[DPoP] Failed to delete key pair from IndexedDB:", error);
|
|
11460
11549
|
}
|
|
11461
11550
|
}
|
|
11462
11551
|
/**
|
|
@@ -11516,13 +11605,13 @@ var CryptoController = class {
|
|
|
11516
11605
|
this._publicJwk = await crypto.subtle.exportKey("jwk", stored.publicKey);
|
|
11517
11606
|
this._fingerprint = await computeJwkThumbprint(this._publicJwk);
|
|
11518
11607
|
this._initialized = true;
|
|
11519
|
-
logger$
|
|
11608
|
+
logger$30.debug("[DPoP] Key pair restored from IndexedDB, fingerprint:", this._fingerprint);
|
|
11520
11609
|
return this._fingerprint;
|
|
11521
11610
|
} catch (error) {
|
|
11522
|
-
logger$
|
|
11611
|
+
logger$30.warn("[DPoP] Stored key pair unusable, generating new one:", error);
|
|
11523
11612
|
await deleteKeyPairFromDB();
|
|
11524
11613
|
}
|
|
11525
|
-
logger$
|
|
11614
|
+
logger$30.debug("[DPoP] Generating RSA key pair");
|
|
11526
11615
|
this._keyPair = await crypto.subtle.generateKey({
|
|
11527
11616
|
name: "RSASSA-PKCS1-v1_5",
|
|
11528
11617
|
modulusLength: 2048,
|
|
@@ -11537,7 +11626,7 @@ var CryptoController = class {
|
|
|
11537
11626
|
this._fingerprint = await computeJwkThumbprint(this._publicJwk);
|
|
11538
11627
|
this._initialized = true;
|
|
11539
11628
|
await saveKeyPairToDB(this._keyPair);
|
|
11540
|
-
logger$
|
|
11629
|
+
logger$30.debug("[DPoP] Key pair generated and persisted, fingerprint:", this._fingerprint);
|
|
11541
11630
|
return this._fingerprint;
|
|
11542
11631
|
}
|
|
11543
11632
|
/**
|
|
@@ -11603,7 +11692,7 @@ var CryptoController = class {
|
|
|
11603
11692
|
this._fingerprint = null;
|
|
11604
11693
|
this._initialized = false;
|
|
11605
11694
|
deleteKeyPairFromDB();
|
|
11606
|
-
logger$
|
|
11695
|
+
logger$30.debug("[DPoP] Controller destroyed");
|
|
11607
11696
|
}
|
|
11608
11697
|
get publicJwk() {
|
|
11609
11698
|
if (!this._publicJwk) throw new DPoPInitError("CryptoController not initialized. Call init() first.");
|
|
@@ -11627,7 +11716,7 @@ var CryptoController = class {
|
|
|
11627
11716
|
//#endregion
|
|
11628
11717
|
//#region src/controllers/NetworkMonitor.ts
|
|
11629
11718
|
var import_cjs$28 = require_cjs();
|
|
11630
|
-
const logger$
|
|
11719
|
+
const logger$29 = getLogger();
|
|
11631
11720
|
/**
|
|
11632
11721
|
* Safely check whether we are running in a browser environment
|
|
11633
11722
|
* with `window` and the relevant event targets.
|
|
@@ -11684,7 +11773,7 @@ var NetworkMonitor = class extends Destroyable {
|
|
|
11684
11773
|
}
|
|
11685
11774
|
attachListeners() {
|
|
11686
11775
|
if (!hasBrowserNetworkEvents()) {
|
|
11687
|
-
logger$
|
|
11776
|
+
logger$29.debug("NetworkMonitor: no browser environment detected, skipping event listeners");
|
|
11688
11777
|
return;
|
|
11689
11778
|
}
|
|
11690
11779
|
window.addEventListener("online", this._onOnline);
|
|
@@ -11692,7 +11781,7 @@ var NetworkMonitor = class extends Destroyable {
|
|
|
11692
11781
|
const connection = getNetworkConnection();
|
|
11693
11782
|
if (connection) connection.addEventListener("change", this._onConnectionChange);
|
|
11694
11783
|
this._listenersAttached = true;
|
|
11695
|
-
logger$
|
|
11784
|
+
logger$29.debug("NetworkMonitor: event listeners attached");
|
|
11696
11785
|
}
|
|
11697
11786
|
removeListeners() {
|
|
11698
11787
|
if (!this._listenersAttached) return;
|
|
@@ -11703,10 +11792,10 @@ var NetworkMonitor = class extends Destroyable {
|
|
|
11703
11792
|
if (connection) connection.removeEventListener("change", this._onConnectionChange);
|
|
11704
11793
|
}
|
|
11705
11794
|
this._listenersAttached = false;
|
|
11706
|
-
logger$
|
|
11795
|
+
logger$29.debug("NetworkMonitor: event listeners removed");
|
|
11707
11796
|
}
|
|
11708
11797
|
handleOnline() {
|
|
11709
|
-
logger$
|
|
11798
|
+
logger$29.info("NetworkMonitor: browser went online");
|
|
11710
11799
|
this._isOnline$.next(true);
|
|
11711
11800
|
this._networkChange$.next({
|
|
11712
11801
|
type: "online",
|
|
@@ -11715,7 +11804,7 @@ var NetworkMonitor = class extends Destroyable {
|
|
|
11715
11804
|
});
|
|
11716
11805
|
}
|
|
11717
11806
|
handleOffline() {
|
|
11718
|
-
logger$
|
|
11807
|
+
logger$29.info("NetworkMonitor: browser went offline");
|
|
11719
11808
|
this._isOnline$.next(false);
|
|
11720
11809
|
this._networkChange$.next({
|
|
11721
11810
|
type: "offline",
|
|
@@ -11724,7 +11813,7 @@ var NetworkMonitor = class extends Destroyable {
|
|
|
11724
11813
|
}
|
|
11725
11814
|
handleConnectionChange() {
|
|
11726
11815
|
const networkType = getNetworkType();
|
|
11727
|
-
logger$
|
|
11816
|
+
logger$29.info(`NetworkMonitor: connection changed — effectiveType=${networkType ?? "unknown"}`);
|
|
11728
11817
|
this._networkChange$.next({
|
|
11729
11818
|
type: "connection_change",
|
|
11730
11819
|
timestamp: Date.now(),
|
|
@@ -11840,7 +11929,7 @@ function getNavigatorMediaDevices() {
|
|
|
11840
11929
|
//#endregion
|
|
11841
11930
|
//#region src/controllers/PreflightRunner.ts
|
|
11842
11931
|
var import_cjs$27 = require_cjs();
|
|
11843
|
-
const logger$
|
|
11932
|
+
const logger$28 = getLogger();
|
|
11844
11933
|
const DEFAULT_MEDIA_TEST_DURATION_S = 10;
|
|
11845
11934
|
const ICE_GATHERING_TIMEOUT_MS = 1e4;
|
|
11846
11935
|
const SIGNALING_RTT_TIMEOUT_MS = 5e3;
|
|
@@ -11889,7 +11978,7 @@ var PreflightRunner = class extends Destroyable {
|
|
|
11889
11978
|
if (!this._options.skipMediaTest) try {
|
|
11890
11979
|
bandwidth = await this.testMediaBandwidth(destination);
|
|
11891
11980
|
} catch (error) {
|
|
11892
|
-
logger$
|
|
11981
|
+
logger$28.warn("[PreflightRunner] Media bandwidth test failed:", error);
|
|
11893
11982
|
warnings.push("Media bandwidth test failed");
|
|
11894
11983
|
}
|
|
11895
11984
|
return {
|
|
@@ -11901,7 +11990,7 @@ var PreflightRunner = class extends Destroyable {
|
|
|
11901
11990
|
warnings
|
|
11902
11991
|
};
|
|
11903
11992
|
} catch (error) {
|
|
11904
|
-
logger$
|
|
11993
|
+
logger$28.error("[PreflightRunner] Preflight test failed:", error);
|
|
11905
11994
|
throw new PreflightError("preflight", error instanceof Error ? error : new Error(String(error)));
|
|
11906
11995
|
} finally {
|
|
11907
11996
|
this.destroy();
|
|
@@ -11932,7 +12021,7 @@ var PreflightRunner = class extends Destroyable {
|
|
|
11932
12021
|
if (track.kind === "video" && track.readyState === "live") videoWorking = true;
|
|
11933
12022
|
}
|
|
11934
12023
|
} catch (error) {
|
|
11935
|
-
logger$
|
|
12024
|
+
logger$28.warn("[PreflightRunner] Device test failed:", error);
|
|
11936
12025
|
} finally {
|
|
11937
12026
|
if (audioStream) audioStream.getTracks().forEach((t) => t.stop());
|
|
11938
12027
|
}
|
|
@@ -11990,7 +12079,7 @@ var PreflightRunner = class extends Destroyable {
|
|
|
11990
12079
|
rttMs
|
|
11991
12080
|
};
|
|
11992
12081
|
} catch (error) {
|
|
11993
|
-
logger$
|
|
12082
|
+
logger$28.warn("[PreflightRunner] ICE connectivity test failed:", error);
|
|
11994
12083
|
return {
|
|
11995
12084
|
type: "failed",
|
|
11996
12085
|
turnReachable: false,
|
|
@@ -12038,7 +12127,7 @@ var PreflightRunner = class extends Destroyable {
|
|
|
12038
12127
|
//#endregion
|
|
12039
12128
|
//#region src/controllers/VisibilityController.ts
|
|
12040
12129
|
var import_cjs$26 = require_cjs();
|
|
12041
|
-
const logger$
|
|
12130
|
+
const logger$27 = getLogger();
|
|
12042
12131
|
/**
|
|
12043
12132
|
* Checks whether the document visibility API is available.
|
|
12044
12133
|
*/
|
|
@@ -12075,8 +12164,8 @@ var VisibilityController = class extends Destroyable {
|
|
|
12075
12164
|
this._boundHandler = this._handleVisibilityChange.bind(this);
|
|
12076
12165
|
if (this._hasVisibilityApi) {
|
|
12077
12166
|
document.addEventListener("visibilitychange", this._boundHandler);
|
|
12078
|
-
logger$
|
|
12079
|
-
} else logger$
|
|
12167
|
+
logger$27.debug("VisibilityController: listening for visibilitychange events");
|
|
12168
|
+
} else logger$27.debug("VisibilityController: document visibility API not available, defaulting to visible");
|
|
12080
12169
|
}
|
|
12081
12170
|
/**
|
|
12082
12171
|
* Observable of the current visibility state.
|
|
@@ -12101,7 +12190,7 @@ var VisibilityController = class extends Destroyable {
|
|
|
12101
12190
|
destroy() {
|
|
12102
12191
|
if (this._hasVisibilityApi) {
|
|
12103
12192
|
document.removeEventListener("visibilitychange", this._boundHandler);
|
|
12104
|
-
logger$
|
|
12193
|
+
logger$27.debug("VisibilityController: removed visibilitychange listener");
|
|
12105
12194
|
}
|
|
12106
12195
|
super.destroy();
|
|
12107
12196
|
}
|
|
@@ -12119,7 +12208,7 @@ var VisibilityController = class extends Destroyable {
|
|
|
12119
12208
|
timestamp: Date.now()
|
|
12120
12209
|
};
|
|
12121
12210
|
this._visibilityChange$.next(changeEvent);
|
|
12122
|
-
logger$
|
|
12211
|
+
logger$27.debug("VisibilityController: visibility changed", {
|
|
12123
12212
|
from: previousState,
|
|
12124
12213
|
to: newState
|
|
12125
12214
|
});
|
|
@@ -12358,15 +12447,57 @@ const RPCEventAckResponse = (id) => makeRPCResponse({
|
|
|
12358
12447
|
result: {}
|
|
12359
12448
|
});
|
|
12360
12449
|
|
|
12450
|
+
//#endregion
|
|
12451
|
+
//#region src/utils/authRecovery.ts
|
|
12452
|
+
/**
|
|
12453
|
+
* Walk an error's `error`/`cause` chain looking for a {@link JSONRPCError}.
|
|
12454
|
+
* Errors thrown by call creation are wrapped (e.g. `CallCreateError`), so the
|
|
12455
|
+
* underlying signaling error is nested. Bounded by a visited set to guard
|
|
12456
|
+
* against cyclic causes.
|
|
12457
|
+
*/
|
|
12458
|
+
function findJSONRPCError(error) {
|
|
12459
|
+
const seen = /* @__PURE__ */ new Set();
|
|
12460
|
+
let current = error;
|
|
12461
|
+
while (current instanceof Error && !seen.has(current)) {
|
|
12462
|
+
seen.add(current);
|
|
12463
|
+
if (current instanceof JSONRPCError) return current;
|
|
12464
|
+
current = current.error ?? current.cause;
|
|
12465
|
+
}
|
|
12466
|
+
}
|
|
12467
|
+
/**
|
|
12468
|
+
* Whether an error is a session-recoverable authentication failure
|
|
12469
|
+
* (`-32002` authentication failed or `-32003` requester validation failed)
|
|
12470
|
+
* that a credential re-mint + retry can heal.
|
|
12471
|
+
*/
|
|
12472
|
+
function isRecoverableAuthError(error) {
|
|
12473
|
+
const rpcError = findJSONRPCError(error);
|
|
12474
|
+
return rpcError !== void 0 && (rpcError.code === RPC_ERROR_REQUESTER_VALIDATION_FAILED || rpcError.code === RPC_ERROR_AUTHENTICATION_FAILED);
|
|
12475
|
+
}
|
|
12476
|
+
/**
|
|
12477
|
+
* Whether an error is specifically a requester-validation rejection
|
|
12478
|
+
* (`-32003`) — the server refusing the session's credential.
|
|
12479
|
+
*
|
|
12480
|
+
* Narrower than {@link isRecoverableAuthError} on purpose. `-32002` is
|
|
12481
|
+
* overloaded server-side: a rejected reattach arrives as `-32002` with
|
|
12482
|
+
* `cause: INVALID_MSG_UNSPECIFIED` and message `CALL ERROR`, which is a
|
|
12483
|
+
* call-level rejection and says nothing about the credential. Use this where
|
|
12484
|
+
* the decision must not be fooled by that, such as deciding whether retrying
|
|
12485
|
+
* an operation could possibly succeed.
|
|
12486
|
+
*/
|
|
12487
|
+
function isRequesterValidationError(error) {
|
|
12488
|
+
return findJSONRPCError(error)?.code === RPC_ERROR_REQUESTER_VALIDATION_FAILED;
|
|
12489
|
+
}
|
|
12490
|
+
|
|
12361
12491
|
//#endregion
|
|
12362
12492
|
//#region src/managers/AttachManager.ts
|
|
12363
|
-
const logger$
|
|
12493
|
+
const logger$26 = getLogger();
|
|
12364
12494
|
var AttachManager = class {
|
|
12365
|
-
constructor(storage, deviceController, reconnectCallsTimeout, attachKey) {
|
|
12495
|
+
constructor(storage, deviceController, reconnectCallsTimeout, attachKey, credentialRecovered) {
|
|
12366
12496
|
this.storage = storage;
|
|
12367
12497
|
this.deviceController = deviceController;
|
|
12368
12498
|
this.reconnectCallsTimeout = reconnectCallsTimeout;
|
|
12369
12499
|
this.attachKey = attachKey;
|
|
12500
|
+
this.credentialRecovered = credentialRecovered;
|
|
12370
12501
|
this.writeQueue = Promise.resolve();
|
|
12371
12502
|
}
|
|
12372
12503
|
async detachAll() {
|
|
@@ -12381,7 +12512,7 @@ var AttachManager = class {
|
|
|
12381
12512
|
try {
|
|
12382
12513
|
return await this.storage.getItem(this.attachKey) ?? {};
|
|
12383
12514
|
} catch (error) {
|
|
12384
|
-
logger$
|
|
12515
|
+
logger$26.warn("[AttachManager] Failed to retrieve attached calls from storage", error);
|
|
12385
12516
|
return {};
|
|
12386
12517
|
}
|
|
12387
12518
|
}
|
|
@@ -12389,7 +12520,7 @@ var AttachManager = class {
|
|
|
12389
12520
|
try {
|
|
12390
12521
|
await this.storage.setItem(this.attachKey, attached);
|
|
12391
12522
|
} catch (error) {
|
|
12392
|
-
logger$
|
|
12523
|
+
logger$26.warn("[AttachManager] Failed to write attached calls to storage", error);
|
|
12393
12524
|
}
|
|
12394
12525
|
}
|
|
12395
12526
|
/**
|
|
@@ -12408,11 +12539,39 @@ var AttachManager = class {
|
|
|
12408
12539
|
}
|
|
12409
12540
|
async attach(call) {
|
|
12410
12541
|
if (!call.to) {
|
|
12411
|
-
logger$
|
|
12542
|
+
logger$26.warn("[AttachManager] Skip attach for calls with no destination");
|
|
12412
12543
|
return;
|
|
12413
12544
|
}
|
|
12545
|
+
const attachment = this.buildAttachment(call, call.to);
|
|
12546
|
+
await this.mutate((attached) => ({
|
|
12547
|
+
...attached,
|
|
12548
|
+
[call.id]: attachment
|
|
12549
|
+
}));
|
|
12550
|
+
}
|
|
12551
|
+
/**
|
|
12552
|
+
* Keep an already-stored call's reference alive and current — the periodic
|
|
12553
|
+
* refresh the `verto.ping` keepalive drives.
|
|
12554
|
+
*
|
|
12555
|
+
* Only ever updates: a call with no record is one nothing wants reattached,
|
|
12556
|
+
* and re-creating it here would undo a `detach`. That matters because a ping
|
|
12557
|
+
* can land in the window between `bye()` detaching and the call being torn
|
|
12558
|
+
* down, and a record revived there survives the hangup — so the next page
|
|
12559
|
+
* load dials a call nobody is on. The existence check and the write share
|
|
12560
|
+
* one {@link mutate} turn, so a concurrent detach cannot slip between them.
|
|
12561
|
+
*/
|
|
12562
|
+
async refresh(call) {
|
|
12563
|
+
if (!call.to) return;
|
|
12414
12564
|
const destination = call.to;
|
|
12415
|
-
|
|
12565
|
+
await this.mutate((attached) => {
|
|
12566
|
+
if (!Object.hasOwn(attached, call.id)) return attached;
|
|
12567
|
+
return {
|
|
12568
|
+
...attached,
|
|
12569
|
+
[call.id]: this.buildAttachment(call, destination)
|
|
12570
|
+
};
|
|
12571
|
+
});
|
|
12572
|
+
}
|
|
12573
|
+
buildAttachment(call, destination) {
|
|
12574
|
+
return {
|
|
12416
12575
|
nodeId: call.nodeId,
|
|
12417
12576
|
destination,
|
|
12418
12577
|
mediaDirections: call.mediaDirections,
|
|
@@ -12420,10 +12579,6 @@ var AttachManager = class {
|
|
|
12420
12579
|
videoInputDevice: call.mediaDirections.video !== "inactive" ? this.deviceController.selectedVideoInputDevice : null,
|
|
12421
12580
|
attachedAt: Date.now()
|
|
12422
12581
|
};
|
|
12423
|
-
await this.mutate((attached) => ({
|
|
12424
|
-
...attached,
|
|
12425
|
-
[call.id]: attachment
|
|
12426
|
-
}));
|
|
12427
12582
|
}
|
|
12428
12583
|
async detach(call) {
|
|
12429
12584
|
await this.mutate((attached) => {
|
|
@@ -12446,8 +12601,14 @@ var AttachManager = class {
|
|
|
12446
12601
|
* rejecting. Once that fix is deployed, this will work for both
|
|
12447
12602
|
* page reloads and WebSocket reconnects.
|
|
12448
12603
|
*
|
|
12449
|
-
*
|
|
12450
|
-
*
|
|
12604
|
+
* A failed reattach does NOT generally cost the stored reference. It is
|
|
12605
|
+
* discarded only when the server denied the reattach on a session whose
|
|
12606
|
+
* credential it had already accepted — a verified reauthentication followed
|
|
12607
|
+
* by a refusal is the server saying the call is gone, and that is the one
|
|
12608
|
+
* refusal worth acting on. Until then the credential may be what is being
|
|
12609
|
+
* refused, and the record is the only way a later reload can try again;
|
|
12610
|
+
* keeping it costs nothing, since `detachExpired` reaps it once it is older
|
|
12611
|
+
* than `reconnectCallsTimeout`.
|
|
12451
12612
|
*/
|
|
12452
12613
|
async reattachCalls() {
|
|
12453
12614
|
const attached = await this.readAttached();
|
|
@@ -12456,25 +12617,31 @@ var AttachManager = class {
|
|
|
12456
12617
|
const { destination } = attachment;
|
|
12457
12618
|
const options = this.buildCallOptions(attachment);
|
|
12458
12619
|
let succeeded = false;
|
|
12620
|
+
let refusedOnCredentials = false;
|
|
12459
12621
|
for (let attempt = 1; attempt <= 3; attempt++) try {
|
|
12460
12622
|
await this.session.createOutboundCall(destination, {
|
|
12461
12623
|
callId,
|
|
12462
12624
|
...options
|
|
12463
12625
|
});
|
|
12464
|
-
logger$
|
|
12626
|
+
logger$26.info(`[AttachManager] Reattached call ${callId} (attempt ${attempt})`);
|
|
12465
12627
|
succeeded = true;
|
|
12466
12628
|
break;
|
|
12467
12629
|
} catch (error) {
|
|
12468
|
-
logger$
|
|
12630
|
+
logger$26.warn(`[AttachManager] Reattach attempt ${attempt}/3 failed for call ${callId}:`, error);
|
|
12631
|
+
if (isRequesterValidationError(error)) {
|
|
12632
|
+
refusedOnCredentials = true;
|
|
12633
|
+
logger$26.warn(`[AttachManager] Reattach of ${callId} was refused on credentials; not retrying.`);
|
|
12634
|
+
break;
|
|
12635
|
+
}
|
|
12469
12636
|
if (attempt < 3) await new Promise((r) => setTimeout(r, (attempt + 1) * 1e3));
|
|
12470
12637
|
}
|
|
12471
|
-
if (!succeeded) {
|
|
12472
|
-
logger$
|
|
12638
|
+
if (!succeeded) if (this.credentialRecovered() && !refusedOnCredentials) {
|
|
12639
|
+
logger$26.warn(`[AttachManager] Reattach of ${callId} was denied after a verified reauthentication, removing reference`);
|
|
12473
12640
|
await this.detach({
|
|
12474
12641
|
id: callId,
|
|
12475
12642
|
mediaDirections: attachment.mediaDirections
|
|
12476
12643
|
});
|
|
12477
|
-
}
|
|
12644
|
+
} else logger$26.warn(`[AttachManager] Reattach failed for call ${callId}; keeping the reference (credential refused or never proven good)`);
|
|
12478
12645
|
}
|
|
12479
12646
|
}
|
|
12480
12647
|
/**
|
|
@@ -12575,12 +12742,12 @@ var require_race = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
12575
12742
|
exports.race = void 0;
|
|
12576
12743
|
var argsOrArgArray_1 = require_argsOrArgArray();
|
|
12577
12744
|
var raceWith_1$1 = require_raceWith();
|
|
12578
|
-
function race$
|
|
12745
|
+
function race$3() {
|
|
12579
12746
|
var args = [];
|
|
12580
12747
|
for (var _i = 0; _i < arguments.length; _i++) args[_i] = arguments[_i];
|
|
12581
12748
|
return raceWith_1$1.raceWith.apply(void 0, __spreadArray([], __read(argsOrArgArray_1.argsOrArgArray(args))));
|
|
12582
12749
|
}
|
|
12583
|
-
exports.race = race$
|
|
12750
|
+
exports.race = race$3;
|
|
12584
12751
|
}));
|
|
12585
12752
|
|
|
12586
12753
|
//#endregion
|
|
@@ -13640,7 +13807,7 @@ function toggleHandraiseMethod(is) {
|
|
|
13640
13807
|
|
|
13641
13808
|
//#endregion
|
|
13642
13809
|
//#region src/core/entities/Participant.ts
|
|
13643
|
-
const logger$
|
|
13810
|
+
const logger$25 = getLogger();
|
|
13644
13811
|
const initialState = {};
|
|
13645
13812
|
/**
|
|
13646
13813
|
* Represents a participant in a call.
|
|
@@ -13650,9 +13817,9 @@ const initialState = {};
|
|
|
13650
13817
|
* the local participant with additional device control.
|
|
13651
13818
|
*/
|
|
13652
13819
|
var Participant = class extends Destroyable {
|
|
13653
|
-
constructor(id,
|
|
13820
|
+
constructor(id, callExecuteMethod, deviceController) {
|
|
13654
13821
|
super();
|
|
13655
|
-
this.
|
|
13822
|
+
this.callExecuteMethod = callExecuteMethod;
|
|
13656
13823
|
this.deviceController = deviceController;
|
|
13657
13824
|
this._state$ = this.createBehaviorSubject(initialState);
|
|
13658
13825
|
this.id = id;
|
|
@@ -13874,22 +14041,55 @@ var Participant = class extends Destroyable {
|
|
|
13874
14041
|
get value() {
|
|
13875
14042
|
return this._state$.value;
|
|
13876
14043
|
}
|
|
14044
|
+
/**
|
|
14045
|
+
* Target triple for member RPCs, built from the participant's own state.
|
|
14046
|
+
* The backend locates the member's session by the target `call_id`/`node_id`,
|
|
14047
|
+
* so this must always be the participant's own call context — never the
|
|
14048
|
+
* local call's id (issue #19400).
|
|
14049
|
+
*
|
|
14050
|
+
* Reading it doubles as a readiness probe: it throws until the first full
|
|
14051
|
+
* member event (`member.joined`/`member.updated` or the `call.joined`
|
|
14052
|
+
* roster) arrives, and never regresses afterwards.
|
|
14053
|
+
*
|
|
14054
|
+
* @throws {ParticipantNotReadyError} If the member state has not been
|
|
14055
|
+
* received yet (e.g. a participant first seen via `member.talking`) — an
|
|
14056
|
+
* empty call context can never address the member, so fail fast instead of
|
|
14057
|
+
* sending a doomed RPC.
|
|
14058
|
+
*/
|
|
14059
|
+
get target() {
|
|
14060
|
+
const { call_id, node_id } = this._state$.value;
|
|
14061
|
+
if (!call_id || !node_id) throw new ParticipantNotReadyError(this.id);
|
|
14062
|
+
return {
|
|
14063
|
+
member_id: this.id,
|
|
14064
|
+
call_id,
|
|
14065
|
+
node_id
|
|
14066
|
+
};
|
|
14067
|
+
}
|
|
14068
|
+
/**
|
|
14069
|
+
* Executes a member RPC against this participant, injecting its own
|
|
14070
|
+
* {@link target} as the target.
|
|
14071
|
+
*
|
|
14072
|
+
* @throws {ParticipantNotReadyError} Via {@link target}, when the
|
|
14073
|
+
* member state has not been received yet.
|
|
14074
|
+
*/
|
|
14075
|
+
async executeMethod(method, args) {
|
|
14076
|
+
return this.callExecuteMethod(this.target, method, args);
|
|
14077
|
+
}
|
|
13877
14078
|
/** Toggles the deafened state (mutes/unmutes incoming audio). */
|
|
13878
14079
|
async toggleDeaf() {
|
|
13879
|
-
|
|
13880
|
-
await this.executeMethod(this.id, method, {});
|
|
14080
|
+
await this.executeMethod(toggleDeafMethod(this.deaf), {});
|
|
13881
14081
|
}
|
|
13882
14082
|
/** Toggles the hand-raised state. */
|
|
13883
14083
|
async toggleHandraise() {
|
|
13884
|
-
await this.executeMethod(
|
|
14084
|
+
await this.executeMethod(toggleHandraiseMethod(this.handraised), {});
|
|
13885
14085
|
}
|
|
13886
14086
|
/** Mutes the participant's audio. */
|
|
13887
14087
|
async mute() {
|
|
13888
|
-
await this.executeMethod(
|
|
14088
|
+
await this.executeMethod("call.mute", { channels: ["audio"] });
|
|
13889
14089
|
}
|
|
13890
14090
|
/** Unmutes the participant's audio. */
|
|
13891
14091
|
async unmute() {
|
|
13892
|
-
await this.executeMethod(
|
|
14092
|
+
await this.executeMethod("call.unmute", { channels: ["audio"] });
|
|
13893
14093
|
}
|
|
13894
14094
|
/** Toggles the participant's audio mute state. */
|
|
13895
14095
|
async toggleMute() {
|
|
@@ -13897,11 +14097,11 @@ var Participant = class extends Destroyable {
|
|
|
13897
14097
|
}
|
|
13898
14098
|
/** Mutes the participant's video. */
|
|
13899
14099
|
async muteVideo() {
|
|
13900
|
-
await this.executeMethod(
|
|
14100
|
+
await this.executeMethod("call.mute", { channels: ["video"] });
|
|
13901
14101
|
}
|
|
13902
14102
|
/** Unmutes the participant's video. */
|
|
13903
14103
|
async unmuteVideo() {
|
|
13904
|
-
await this.executeMethod(
|
|
14104
|
+
await this.executeMethod("call.unmute", { channels: ["video"] });
|
|
13905
14105
|
}
|
|
13906
14106
|
/** Toggles the participant's video mute state. */
|
|
13907
14107
|
async toggleMuteVideo() {
|
|
@@ -13909,7 +14109,7 @@ var Participant = class extends Destroyable {
|
|
|
13909
14109
|
}
|
|
13910
14110
|
/** Toggles echo cancellation on the audio input. */
|
|
13911
14111
|
async toggleEchoCancellation() {
|
|
13912
|
-
await this.executeMethod(
|
|
14112
|
+
await this.executeMethod("call.audioflags.set", {
|
|
13913
14113
|
echo_cancellation: !this.echoCancellation,
|
|
13914
14114
|
auto_gain: this.autoGain,
|
|
13915
14115
|
noise_suppression: this.noiseSuppression
|
|
@@ -13917,7 +14117,7 @@ var Participant = class extends Destroyable {
|
|
|
13917
14117
|
}
|
|
13918
14118
|
/** Toggles automatic gain control on the audio input. */
|
|
13919
14119
|
async toggleAudioInputAutoGain() {
|
|
13920
|
-
await this.executeMethod(
|
|
14120
|
+
await this.executeMethod("call.audioflags.set", {
|
|
13921
14121
|
echo_cancellation: this.echoCancellation,
|
|
13922
14122
|
auto_gain: !this.autoGain,
|
|
13923
14123
|
noise_suppression: this.noiseSuppression
|
|
@@ -13925,7 +14125,7 @@ var Participant = class extends Destroyable {
|
|
|
13925
14125
|
}
|
|
13926
14126
|
/** Toggles noise suppression on the audio input. */
|
|
13927
14127
|
async toggleNoiseSuppression() {
|
|
13928
|
-
await this.executeMethod(
|
|
14128
|
+
await this.executeMethod("call.audioflags.set", {
|
|
13929
14129
|
echo_cancellation: this.echoCancellation,
|
|
13930
14130
|
auto_gain: this.autoGain,
|
|
13931
14131
|
noise_suppression: !this.noiseSuppression
|
|
@@ -13933,7 +14133,7 @@ var Participant = class extends Destroyable {
|
|
|
13933
14133
|
}
|
|
13934
14134
|
/** Toggles low-bitrate mode for this participant's media. */
|
|
13935
14135
|
async toggleLowbitrate() {
|
|
13936
|
-
await this.executeMethod(
|
|
14136
|
+
await this.executeMethod("call.lowbitrate.set", { lowbitrate: !this.lowbitrate });
|
|
13937
14137
|
}
|
|
13938
14138
|
/**
|
|
13939
14139
|
* Adjusts the **conference-only** microphone energy gate / sensitivity level
|
|
@@ -13949,7 +14149,7 @@ var Participant = class extends Destroyable {
|
|
|
13949
14149
|
* (integer, larger values are more sensitive).
|
|
13950
14150
|
*/
|
|
13951
14151
|
async setAudioInputSensitivity(value) {
|
|
13952
|
-
await this.executeMethod(
|
|
14152
|
+
await this.executeMethod("call.microphone.sensitivity.set", { sensitivity: value });
|
|
13953
14153
|
}
|
|
13954
14154
|
/**
|
|
13955
14155
|
* Sets the **server-side** microphone volume on this participant's bridged
|
|
@@ -13962,7 +14162,7 @@ var Participant = class extends Destroyable {
|
|
|
13962
14162
|
* @param value - Volume level (0-100).
|
|
13963
14163
|
*/
|
|
13964
14164
|
async setAudioInputVolume(value) {
|
|
13965
|
-
await this.executeMethod(
|
|
14165
|
+
await this.executeMethod("call.microphone.volume.set", { volume: value });
|
|
13966
14166
|
}
|
|
13967
14167
|
/**
|
|
13968
14168
|
* Sets the **server-side** speaker volume on this participant's bridged call
|
|
@@ -13976,45 +14176,31 @@ var Participant = class extends Destroyable {
|
|
|
13976
14176
|
* @param value - Volume level (0-100).
|
|
13977
14177
|
*/
|
|
13978
14178
|
async setAudioOutputVolume(value) {
|
|
13979
|
-
await this.executeMethod(
|
|
14179
|
+
await this.executeMethod("call.speaker.volume.set", { volume: value });
|
|
13980
14180
|
}
|
|
13981
14181
|
/**
|
|
13982
14182
|
* Sets the participant's position in the video layout.
|
|
13983
14183
|
*
|
|
13984
|
-
* Requires the `member.position` capability. The gateway
|
|
13985
|
-
*
|
|
13986
|
-
*
|
|
13987
|
-
*
|
|
13988
|
-
* guarantee a visible change: the backend silently returns `200` (no-op) for
|
|
13989
|
-
* non-conference targets.
|
|
14184
|
+
* Requires the `member.position` capability. The gateway requires a
|
|
14185
|
+
* `targets` array of `{ target, position }` entries (issue #19400). A
|
|
14186
|
+
* resolved promise does not guarantee a visible change: the backend silently
|
|
14187
|
+
* returns `200` (no-op) for non-conference targets.
|
|
13990
14188
|
*
|
|
13991
14189
|
* @param value - The {@link VideoPosition} to assign (e.g. `'auto'`, `'reserved-0'`).
|
|
13992
14190
|
*/
|
|
13993
14191
|
async setPosition(value) {
|
|
13994
|
-
|
|
13995
|
-
|
|
13996
|
-
member_id: this.id,
|
|
13997
|
-
call_id: state.call_id ?? "",
|
|
13998
|
-
node_id: state.node_id ?? ""
|
|
13999
|
-
};
|
|
14000
|
-
await this.executeMethod(target, "call.member.position.set", { targets: [{
|
|
14001
|
-
target,
|
|
14192
|
+
await this.executeMethod("call.member.position.set", { targets: [{
|
|
14193
|
+
target: this.target,
|
|
14002
14194
|
position: value
|
|
14003
14195
|
}] });
|
|
14004
14196
|
}
|
|
14005
14197
|
/** Removes this participant from the call. */
|
|
14006
14198
|
async remove() {
|
|
14007
|
-
|
|
14008
|
-
const target = {
|
|
14009
|
-
member_id: this.id,
|
|
14010
|
-
call_id: state.call_id ?? "",
|
|
14011
|
-
node_id: state.node_id ?? ""
|
|
14012
|
-
};
|
|
14013
|
-
await this.executeMethod(target, "call.member.remove", {});
|
|
14199
|
+
await this.executeMethod("call.member.remove", { targets: [this.target] });
|
|
14014
14200
|
}
|
|
14015
14201
|
/** Ends the call for this participant. */
|
|
14016
14202
|
async end() {
|
|
14017
|
-
await this.executeMethod(
|
|
14203
|
+
await this.executeMethod("call.end", {});
|
|
14018
14204
|
}
|
|
14019
14205
|
/**
|
|
14020
14206
|
* Replaces custom metadata for this participant.
|
|
@@ -14034,7 +14220,7 @@ var Participant = class extends Destroyable {
|
|
|
14034
14220
|
}
|
|
14035
14221
|
/** Destroys the participant, releasing all subscriptions and references. */
|
|
14036
14222
|
destroy() {
|
|
14037
|
-
this.
|
|
14223
|
+
this.callExecuteMethod = void 0;
|
|
14038
14224
|
super.destroy();
|
|
14039
14225
|
}
|
|
14040
14226
|
};
|
|
@@ -14046,8 +14232,8 @@ var Participant = class extends Destroyable {
|
|
|
14046
14232
|
*/
|
|
14047
14233
|
var SelfParticipant = class extends Participant {
|
|
14048
14234
|
/** @internal */
|
|
14049
|
-
constructor(id,
|
|
14050
|
-
super(id,
|
|
14235
|
+
constructor(id, callExecuteMethod, vertoManager, deviceController) {
|
|
14236
|
+
super(id, callExecuteMethod, deviceController);
|
|
14051
14237
|
this.vertoManager = vertoManager;
|
|
14052
14238
|
this._studioAudio$ = this.createBehaviorSubject(false);
|
|
14053
14239
|
this.capabilities = new SelfCapabilities();
|
|
@@ -14071,7 +14257,7 @@ var SelfParticipant = class extends Participant {
|
|
|
14071
14257
|
async enableStudioAudio() {
|
|
14072
14258
|
if (this._studioAudio$.value) return;
|
|
14073
14259
|
this._studioAudio$.next(true);
|
|
14074
|
-
await this.executeMethod(
|
|
14260
|
+
await this.executeMethod("call.audioflags.set", {
|
|
14075
14261
|
echo_cancellation: false,
|
|
14076
14262
|
auto_gain: false,
|
|
14077
14263
|
noise_suppression: false
|
|
@@ -14084,7 +14270,7 @@ var SelfParticipant = class extends Participant {
|
|
|
14084
14270
|
async disableStudioAudio() {
|
|
14085
14271
|
if (!this._studioAudio$.value) return;
|
|
14086
14272
|
this._studioAudio$.next(false);
|
|
14087
|
-
await this.executeMethod(
|
|
14273
|
+
await this.executeMethod("call.audioflags.set", {
|
|
14088
14274
|
echo_cancellation: true,
|
|
14089
14275
|
auto_gain: true,
|
|
14090
14276
|
noise_suppression: true
|
|
@@ -14093,17 +14279,27 @@ var SelfParticipant = class extends Participant {
|
|
|
14093
14279
|
/**
|
|
14094
14280
|
* Starts sharing the local screen.
|
|
14095
14281
|
*
|
|
14282
|
+
* A call carries at most one screen share. Read `screenShareStatus` before
|
|
14283
|
+
* calling and treat `'starting'`/`'stopping'` as busy.
|
|
14284
|
+
*
|
|
14096
14285
|
* The call is unaffected when acquisition fails.
|
|
14097
14286
|
*
|
|
14287
|
+
* @param options - Pass `{ audio: true }` to also request the shared
|
|
14288
|
+
* surface's audio. Defaults to video only.
|
|
14289
|
+
* @throws {ScreenShareAlreadyActiveError} When this call is already
|
|
14290
|
+
* sharing a screen. Call {@link stopScreenShare} before starting another.
|
|
14291
|
+
* @throws {AuxiliaryLegCancelledError} When {@link stopScreenShare} removes
|
|
14292
|
+
* the share before its leg finishes connecting.
|
|
14098
14293
|
* @throws The raw `getDisplayMedia` error. A dismissed picker or a
|
|
14099
14294
|
* permission denial rejects with a `NotAllowedError` `DOMException` —
|
|
14100
14295
|
* inspect `error.name` to tell benign cancels apart from real failures.
|
|
14101
14296
|
*/
|
|
14102
|
-
async startScreenShare() {
|
|
14297
|
+
async startScreenShare(options) {
|
|
14103
14298
|
try {
|
|
14104
|
-
await this.vertoManager.addScreenMedia();
|
|
14299
|
+
await this.vertoManager.addScreenMedia(options);
|
|
14105
14300
|
} catch (error) {
|
|
14106
|
-
logger$
|
|
14301
|
+
if (error instanceof AuxiliaryLegCancelledError) logger$25.debug("[Participant.startScreenShare] Screen share cancelled before connecting.");
|
|
14302
|
+
else logger$25.error("[Participant.startScreenShare] Screen share error:", error);
|
|
14107
14303
|
throw error;
|
|
14108
14304
|
}
|
|
14109
14305
|
}
|
|
@@ -14124,14 +14320,18 @@ var SelfParticipant = class extends Participant {
|
|
|
14124
14320
|
*
|
|
14125
14321
|
* The call is unaffected when acquisition fails.
|
|
14126
14322
|
*
|
|
14323
|
+
* @throws {AuxiliaryLegCancelledError} When {@link removeAdditionalDevice}
|
|
14324
|
+
* removes the device before its leg finishes connecting.
|
|
14127
14325
|
* @throws The raw `getUserMedia` error (e.g. `NotAllowedError` on
|
|
14128
|
-
* permission denial) — inspect `error.name` to decide how to react
|
|
14326
|
+
* permission denial) — inspect `error.name` to decide how to react — or
|
|
14327
|
+
* `AuxiliaryLegTimeoutError` if the leg does not connect in time.
|
|
14129
14328
|
*/
|
|
14130
14329
|
async addAdditionalDevice(options) {
|
|
14131
14330
|
try {
|
|
14132
14331
|
await this.vertoManager.addInputDevice(options);
|
|
14133
14332
|
} catch (error) {
|
|
14134
|
-
logger$
|
|
14333
|
+
if (error instanceof AuxiliaryLegCancelledError) logger$25.debug("[Participant.addAdditionalDevice] Device removed before connecting.");
|
|
14334
|
+
else logger$25.error("[Participant.addAdditionalDevice] Additional device error:", error);
|
|
14135
14335
|
throw error;
|
|
14136
14336
|
}
|
|
14137
14337
|
}
|
|
@@ -14166,22 +14366,31 @@ var SelfParticipant = class extends Participant {
|
|
|
14166
14366
|
this.deviceController.selectAudioInputDevice(device);
|
|
14167
14367
|
if (options.savePreference) PreferencesContainer.instance.preferredAudioInput = device;
|
|
14168
14368
|
}
|
|
14169
|
-
/**
|
|
14369
|
+
/**
|
|
14370
|
+
* Updates the audio input track constraints for the active call.
|
|
14371
|
+
* @returns whether the constraints reached the media the call is sending.
|
|
14372
|
+
*/
|
|
14170
14373
|
async setAudioInputDeviceConstraints(constraints) {
|
|
14171
|
-
|
|
14374
|
+
return this.vertoManager.updateMediaConstraints({ audio: constraints });
|
|
14172
14375
|
}
|
|
14173
|
-
/**
|
|
14376
|
+
/**
|
|
14377
|
+
* Updates both audio and video input track constraints for the active call.
|
|
14378
|
+
* @returns whether both kinds took the constraints.
|
|
14379
|
+
*/
|
|
14174
14380
|
async setInputDevicesConstraints(constraints) {
|
|
14175
|
-
|
|
14381
|
+
return this.vertoManager.updateMediaConstraints(constraints);
|
|
14176
14382
|
}
|
|
14177
14383
|
/** Selects the video input device for future calls. Optionally saves as a preference. */
|
|
14178
14384
|
selectVideoInputDevice(device, options = {}) {
|
|
14179
14385
|
this.deviceController.selectVideoInputDevice(device);
|
|
14180
14386
|
if (options.savePreference) PreferencesContainer.instance.preferredVideoInput = device;
|
|
14181
14387
|
}
|
|
14182
|
-
/**
|
|
14388
|
+
/**
|
|
14389
|
+
* Updates the video input track constraints for the active call.
|
|
14390
|
+
* @returns whether the constraints reached the media the call is sending.
|
|
14391
|
+
*/
|
|
14183
14392
|
async setVideoInputDeviceConstraints(constraints) {
|
|
14184
|
-
|
|
14393
|
+
return this.vertoManager.updateMediaConstraints({ video: constraints });
|
|
14185
14394
|
}
|
|
14186
14395
|
/** Selects the audio output device. Optionally saves as a preference. */
|
|
14187
14396
|
selectAudioOutputDevice(device, options = {}) {
|
|
@@ -14194,7 +14403,7 @@ var SelfParticipant = class extends Participant {
|
|
|
14194
14403
|
*/
|
|
14195
14404
|
exitStudioModeIfActive() {
|
|
14196
14405
|
if (this._studioAudio$.value) {
|
|
14197
|
-
logger$
|
|
14406
|
+
logger$25.debug("[SelfParticipant] Exiting studio audio mode due to individual flag toggle");
|
|
14198
14407
|
this._studioAudio$.next(false);
|
|
14199
14408
|
}
|
|
14200
14409
|
}
|
|
@@ -14218,7 +14427,7 @@ var SelfParticipant = class extends Participant {
|
|
|
14218
14427
|
try {
|
|
14219
14428
|
await super.mute();
|
|
14220
14429
|
} catch (error) {
|
|
14221
|
-
logger$
|
|
14430
|
+
logger$25.warn("[Participant.toggleAudioInput] Server Error while muting audio input, proceeding with local toggle anyway", error);
|
|
14222
14431
|
} finally {
|
|
14223
14432
|
this.vertoManager.muteMainAudioInputDevice();
|
|
14224
14433
|
}
|
|
@@ -14228,7 +14437,7 @@ var SelfParticipant = class extends Participant {
|
|
|
14228
14437
|
try {
|
|
14229
14438
|
await super.unmute();
|
|
14230
14439
|
} catch (error) {
|
|
14231
|
-
logger$
|
|
14440
|
+
logger$25.warn("[Participant.toggleAudioInput] Server Error while unmuting audio input, proceeding with local toggle anyway", error);
|
|
14232
14441
|
} finally {
|
|
14233
14442
|
await this.vertoManager.unmuteMainAudioInputDevice();
|
|
14234
14443
|
}
|
|
@@ -14238,7 +14447,7 @@ var SelfParticipant = class extends Participant {
|
|
|
14238
14447
|
try {
|
|
14239
14448
|
await super.muteVideo();
|
|
14240
14449
|
} catch (error) {
|
|
14241
|
-
logger$
|
|
14450
|
+
logger$25.warn("[Participant.toggleVideoInput] Server Error while muting video input, proceeding with local toggle anyway", error);
|
|
14242
14451
|
} finally {
|
|
14243
14452
|
this.vertoManager.muteMainVideoInputDevice();
|
|
14244
14453
|
}
|
|
@@ -14248,7 +14457,7 @@ var SelfParticipant = class extends Participant {
|
|
|
14248
14457
|
try {
|
|
14249
14458
|
await super.unmuteVideo();
|
|
14250
14459
|
} catch (error) {
|
|
14251
|
-
logger$
|
|
14460
|
+
logger$25.warn("[Participant.toggleVideoInput] Server Error while unmuting video input, proceeding with local toggle anyway", error);
|
|
14252
14461
|
} finally {
|
|
14253
14462
|
await this.vertoManager.unmuteMainVideoInputDevice();
|
|
14254
14463
|
}
|
|
@@ -14453,7 +14662,7 @@ function filterAs(predicate, resultPath) {
|
|
|
14453
14662
|
//#endregion
|
|
14454
14663
|
//#region src/operators/throwOnRPCError.ts
|
|
14455
14664
|
var import_cjs$21 = require_cjs();
|
|
14456
|
-
const logger$
|
|
14665
|
+
const logger$24 = getLogger();
|
|
14457
14666
|
/**
|
|
14458
14667
|
* RxJS operator that throws a {@link JSONRPCError} when the RPC response contains an error.
|
|
14459
14668
|
* Passes successful responses through unchanged.
|
|
@@ -14461,14 +14670,14 @@ const logger$23 = getLogger();
|
|
|
14461
14670
|
function throwOnRPCError() {
|
|
14462
14671
|
return (0, import_cjs$21.map)((response) => {
|
|
14463
14672
|
if (response.error) {
|
|
14464
|
-
logger$
|
|
14673
|
+
logger$24.error("[throwOnRPCError] RPC error response:", {
|
|
14465
14674
|
code: response.error.code,
|
|
14466
14675
|
message: response.error.message,
|
|
14467
14676
|
data: response.error.data
|
|
14468
14677
|
});
|
|
14469
14678
|
throw new JSONRPCError(response.error.code, response.error.message, response.error.data);
|
|
14470
14679
|
}
|
|
14471
|
-
logger$
|
|
14680
|
+
logger$24.debug("[throwOnRPCError] RPC successful response:", response);
|
|
14472
14681
|
return response;
|
|
14473
14682
|
});
|
|
14474
14683
|
}
|
|
@@ -14476,7 +14685,7 @@ function throwOnRPCError() {
|
|
|
14476
14685
|
//#endregion
|
|
14477
14686
|
//#region src/managers/CallEventsManager.ts
|
|
14478
14687
|
var import_cjs$20 = require_cjs();
|
|
14479
|
-
const logger$
|
|
14688
|
+
const logger$23 = getLogger();
|
|
14480
14689
|
const initialSessionState = {};
|
|
14481
14690
|
/** @internal */
|
|
14482
14691
|
var CallEventsManager = class extends Destroyable {
|
|
@@ -14580,7 +14789,7 @@ var CallEventsManager = class extends Destroyable {
|
|
|
14580
14789
|
}
|
|
14581
14790
|
initSubscriptions() {
|
|
14582
14791
|
this.subscribeTo(this.callJoinedEvent$, (callJoinedEvent) => {
|
|
14583
|
-
logger$
|
|
14792
|
+
logger$23.debug("[CallEventsManager] Handling call.joined event for call/session IDs:", {
|
|
14584
14793
|
callId: callJoinedEvent.call_id,
|
|
14585
14794
|
roomSessionId: callJoinedEvent.room_session_id
|
|
14586
14795
|
});
|
|
@@ -14607,19 +14816,19 @@ var CallEventsManager = class extends Destroyable {
|
|
|
14607
14816
|
if (this._self$.value?.capabilities.setLayout) this.updateLayouts();
|
|
14608
14817
|
});
|
|
14609
14818
|
this.subscribeTo(this.memberUpdates$, (member) => {
|
|
14610
|
-
logger$
|
|
14819
|
+
logger$23.debug("[CallEventsManager] Handling member update event for member ID:", member);
|
|
14611
14820
|
this.upsertParticipant(member);
|
|
14612
14821
|
});
|
|
14613
14822
|
this.subscribeTo(this.webRtcCallSession.memberLeft$, (memberLeftEvent) => {
|
|
14614
|
-
logger$
|
|
14823
|
+
logger$23.debug("[CallEventsManager] Handling member.left event for member ID:", memberLeftEvent.member.member_id);
|
|
14615
14824
|
const participants = { ...this._participants$.value };
|
|
14616
14825
|
if (memberLeftEvent.member.member_id in participants) {
|
|
14617
14826
|
delete participants[memberLeftEvent.member.member_id];
|
|
14618
14827
|
this._participants$.next(participants);
|
|
14619
|
-
} else logger$
|
|
14828
|
+
} else logger$23.warn(`[CallEventsManager] Received member.left event for unknown member ID: ${memberLeftEvent.member.member_id}`);
|
|
14620
14829
|
});
|
|
14621
14830
|
this.subscribeTo(this.webRtcCallSession.callUpdated$, (callUpdatedEvent) => {
|
|
14622
|
-
logger$
|
|
14831
|
+
logger$23.debug("[CallEventsManager] Handling call.updated event:", callUpdatedEvent);
|
|
14623
14832
|
const roomSession = callUpdatedEvent.room_session;
|
|
14624
14833
|
this._sessionState$.next({
|
|
14625
14834
|
...this._sessionState$.value,
|
|
@@ -14634,7 +14843,7 @@ var CallEventsManager = class extends Destroyable {
|
|
|
14634
14843
|
});
|
|
14635
14844
|
});
|
|
14636
14845
|
this.subscribeTo(this.layoutChangedEvent$, (layoutChangedEvent) => {
|
|
14637
|
-
logger$
|
|
14846
|
+
logger$23.debug("[CallEventsManager] Handling layout.changed event:", layoutChangedEvent);
|
|
14638
14847
|
this._sessionState$.next({
|
|
14639
14848
|
...this._sessionState$.value,
|
|
14640
14849
|
layout_name: layoutChangedEvent.id,
|
|
@@ -14644,10 +14853,10 @@ var CallEventsManager = class extends Destroyable {
|
|
|
14644
14853
|
});
|
|
14645
14854
|
}
|
|
14646
14855
|
updateParticipantPositions(layoutChangedEvent) {
|
|
14647
|
-
if (Object.keys(this._participants$.value).length > 0 && !layoutChangedEvent.layers.some((layer) => !!layer.member_id)) logger$
|
|
14856
|
+
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.");
|
|
14648
14857
|
layoutChangedEvent.layers.filter((layer) => !!layer.member_id).filter((layer) => {
|
|
14649
14858
|
if (!(layer.member_id in this._participants$.value)) {
|
|
14650
|
-
logger$
|
|
14859
|
+
logger$23.warn(`[CallEventsManager] Skipping layout layer for unknown member_id: ${layer.member_id}`);
|
|
14651
14860
|
return false;
|
|
14652
14861
|
}
|
|
14653
14862
|
return true;
|
|
@@ -14665,12 +14874,17 @@ var CallEventsManager = class extends Destroyable {
|
|
|
14665
14874
|
updateLayouts() {
|
|
14666
14875
|
if (!this.selfId) return;
|
|
14667
14876
|
this.webRtcCallSession.executeMethod(this.selfId, "call.layout.list", {}).then((response) => {
|
|
14877
|
+
const layouts = response.result?.layouts;
|
|
14878
|
+
if (!layouts) {
|
|
14879
|
+
logger$23.warn("[CallEventsManager] Layout list response carried no layouts; keeping current layouts");
|
|
14880
|
+
return;
|
|
14881
|
+
}
|
|
14668
14882
|
this._sessionState$.next({
|
|
14669
14883
|
...this._sessionState$.value,
|
|
14670
|
-
layouts
|
|
14884
|
+
layouts
|
|
14671
14885
|
});
|
|
14672
14886
|
}).catch((error) => {
|
|
14673
|
-
logger$
|
|
14887
|
+
logger$23.error("[CallEventsManager] Error fetching layouts:", error);
|
|
14674
14888
|
});
|
|
14675
14889
|
}
|
|
14676
14890
|
updateParticipants(members) {
|
|
@@ -14686,7 +14900,7 @@ var CallEventsManager = class extends Destroyable {
|
|
|
14686
14900
|
}
|
|
14687
14901
|
const participant = this._participants$.value[member.member_id];
|
|
14688
14902
|
const oldValue = participant.value;
|
|
14689
|
-
logger$
|
|
14903
|
+
logger$23.debug("[CallEventsManager] Updating participant:", member.member_id, {
|
|
14690
14904
|
oldValue,
|
|
14691
14905
|
newValue: member
|
|
14692
14906
|
});
|
|
@@ -14699,17 +14913,17 @@ var CallEventsManager = class extends Destroyable {
|
|
|
14699
14913
|
}
|
|
14700
14914
|
get callJoinedEvent$() {
|
|
14701
14915
|
return this.cachedObservable("callJoinedEvent$", () => this.webRtcCallSession.callEvent$.pipe((0, import_cjs$20.filter)(isCallJoinedPayload), (0, import_cjs$20.tap)((event) => {
|
|
14702
|
-
logger$
|
|
14916
|
+
logger$23.debug("[CallEventsManager] Call joined event:", event);
|
|
14703
14917
|
})));
|
|
14704
14918
|
}
|
|
14705
14919
|
get layoutChangedEvent$() {
|
|
14706
14920
|
return this.cachedObservable("layoutChangedEvent$", () => this.webRtcCallSession.callEvent$.pipe(filterAs(isLayoutChangedPayload, "layout"), (0, import_cjs$20.tap)((event) => {
|
|
14707
|
-
logger$
|
|
14921
|
+
logger$23.debug("[CallEventsManager] Layout changed event:", event);
|
|
14708
14922
|
})));
|
|
14709
14923
|
}
|
|
14710
14924
|
get memberUpdates$() {
|
|
14711
14925
|
return this.cachedObservable("memberUpdates$", () => (0, import_cjs$20.merge)(this.webRtcCallSession.memberJoined$, this.webRtcCallSession.memberUpdated$, this.webRtcCallSession.memberTalking$).pipe((0, import_cjs$20.map)((event) => event.member), (0, import_cjs$20.tap)((event) => {
|
|
14712
|
-
logger$
|
|
14926
|
+
logger$23.debug("[CallEventsManager] Member update event:", event);
|
|
14713
14927
|
})));
|
|
14714
14928
|
}
|
|
14715
14929
|
destroy() {
|
|
@@ -14728,6 +14942,111 @@ var CallEventsManager = class extends Destroyable {
|
|
|
14728
14942
|
}
|
|
14729
14943
|
};
|
|
14730
14944
|
|
|
14945
|
+
//#endregion
|
|
14946
|
+
//#region src/controllers/ConstraintFallbackHelper.ts
|
|
14947
|
+
/**
|
|
14948
|
+
* ConstraintFallbackHelper - Provides getUserMedia with automatic constraint
|
|
14949
|
+
* fallback on OverconstrainedError.
|
|
14950
|
+
*
|
|
14951
|
+
* When a specific device ID is requested, the helper tries progressively
|
|
14952
|
+
* looser constraints:
|
|
14953
|
+
* 1. `{ deviceId: { exact: deviceId } }` -- exact match
|
|
14954
|
+
* 2. `{ deviceId: deviceId }` -- preferred (browser may pick another)
|
|
14955
|
+
* 3. `{}` -- no constraint, browser default
|
|
14956
|
+
*
|
|
14957
|
+
* This prevents stale device IDs from blocking call setup.
|
|
14958
|
+
*
|
|
14959
|
+
* @see Section 5.8 and Section 11 of the Implementation Guide
|
|
14960
|
+
*/
|
|
14961
|
+
const logger$22 = getLogger();
|
|
14962
|
+
/**
|
|
14963
|
+
* Attempts getUserMedia with progressively looser constraints.
|
|
14964
|
+
*
|
|
14965
|
+
* The function tries three levels of constraint specificity for the given
|
|
14966
|
+
* device kind. Each level is only attempted if the previous one fails with
|
|
14967
|
+
* an OverconstrainedError. Non-OverconstrainedError failures (e.g.,
|
|
14968
|
+
* NotAllowedError) are thrown immediately without fallback.
|
|
14969
|
+
*
|
|
14970
|
+
* @param mediaDevices - Anything exposing `getUserMedia` (a full
|
|
14971
|
+
* `WebRTCMediaDevices`, or a shim wrapping one)
|
|
14972
|
+
* @param constraints - The full MediaStreamConstraints to use as a base
|
|
14973
|
+
* @param kind - Which track kind to apply fallback to ('audio' | 'video')
|
|
14974
|
+
* @param deviceId - The device ID to try (if undefined, calls getUserMedia as-is)
|
|
14975
|
+
* @returns The stream and the fallback level that succeeded
|
|
14976
|
+
* @throws When all fallback levels fail, or when a non-OverconstrainedError occurs
|
|
14977
|
+
*/
|
|
14978
|
+
async function getUserMediaWithFallback(mediaDevices, constraints, kind, deviceId) {
|
|
14979
|
+
if (!deviceId) return {
|
|
14980
|
+
stream: await mediaDevices.getUserMedia(constraints),
|
|
14981
|
+
fallbackLevel: "default"
|
|
14982
|
+
};
|
|
14983
|
+
const baseConstraints = typeof constraints[kind] === "object" ? constraints[kind] : {};
|
|
14984
|
+
try {
|
|
14985
|
+
const exactConstraints = {
|
|
14986
|
+
...constraints,
|
|
14987
|
+
[kind]: {
|
|
14988
|
+
...baseConstraints,
|
|
14989
|
+
deviceId: { exact: deviceId }
|
|
14990
|
+
}
|
|
14991
|
+
};
|
|
14992
|
+
return {
|
|
14993
|
+
stream: await mediaDevices.getUserMedia(exactConstraints),
|
|
14994
|
+
fallbackLevel: "exact"
|
|
14995
|
+
};
|
|
14996
|
+
} catch (error) {
|
|
14997
|
+
if (!isOverconstrainedError(error)) throw error;
|
|
14998
|
+
logger$22.debug(`[ConstraintFallbackHelper] Exact constraint failed for ${kind}, trying preferred`, { deviceId });
|
|
14999
|
+
}
|
|
15000
|
+
try {
|
|
15001
|
+
const preferredConstraints = {
|
|
15002
|
+
...constraints,
|
|
15003
|
+
[kind]: {
|
|
15004
|
+
...baseConstraints,
|
|
15005
|
+
deviceId
|
|
15006
|
+
}
|
|
15007
|
+
};
|
|
15008
|
+
return {
|
|
15009
|
+
stream: await mediaDevices.getUserMedia(preferredConstraints),
|
|
15010
|
+
fallbackLevel: "preferred"
|
|
15011
|
+
};
|
|
15012
|
+
} catch (error) {
|
|
15013
|
+
if (!isOverconstrainedError(error)) throw error;
|
|
15014
|
+
logger$22.debug(`[ConstraintFallbackHelper] Preferred constraint failed for ${kind}, trying default`, { deviceId });
|
|
15015
|
+
}
|
|
15016
|
+
try {
|
|
15017
|
+
const defaultConstraints = {
|
|
15018
|
+
...constraints,
|
|
15019
|
+
[kind]: { ...baseConstraints }
|
|
15020
|
+
};
|
|
15021
|
+
if (typeof defaultConstraints[kind] === "object") {
|
|
15022
|
+
const { deviceId: _removed, ...rest } = defaultConstraints[kind];
|
|
15023
|
+
defaultConstraints[kind] = rest;
|
|
15024
|
+
}
|
|
15025
|
+
const stream = await mediaDevices.getUserMedia(defaultConstraints);
|
|
15026
|
+
logger$22.warn(`[ConstraintFallbackHelper] Fell back to browser default for ${kind}`, { requestedDeviceId: deviceId });
|
|
15027
|
+
return {
|
|
15028
|
+
stream,
|
|
15029
|
+
fallbackLevel: "default"
|
|
15030
|
+
};
|
|
15031
|
+
} catch (error) {
|
|
15032
|
+
logger$22.error(`[ConstraintFallbackHelper] All fallback levels exhausted for ${kind}`, {
|
|
15033
|
+
deviceId,
|
|
15034
|
+
error
|
|
15035
|
+
});
|
|
15036
|
+
throw error;
|
|
15037
|
+
}
|
|
15038
|
+
}
|
|
15039
|
+
/**
|
|
15040
|
+
* Checks whether an error is an OverconstrainedError.
|
|
15041
|
+
*
|
|
15042
|
+
* Browsers may throw either a native OverconstrainedError or a DOMException
|
|
15043
|
+
* with a specific name.
|
|
15044
|
+
*/
|
|
15045
|
+
function isOverconstrainedError(error) {
|
|
15046
|
+
if (error instanceof Error) return error.name === "OverconstrainedError" || error.name === "ConstraintNotSatisfiedError";
|
|
15047
|
+
return false;
|
|
15048
|
+
}
|
|
15049
|
+
|
|
14731
15050
|
//#endregion
|
|
14732
15051
|
//#region src/helpers/SDPHelper.ts
|
|
14733
15052
|
/**
|
|
@@ -15286,6 +15605,7 @@ var LocalStreamController = class extends Destroyable {
|
|
|
15286
15605
|
this._localAudioTracks$ = this.createBehaviorSubject([]);
|
|
15287
15606
|
this._localVideoTracks$ = this.createBehaviorSubject([]);
|
|
15288
15607
|
this._mediaTrackEnded$ = this.createSubject();
|
|
15608
|
+
this._trackOrigins = /* @__PURE__ */ new WeakMap();
|
|
15289
15609
|
}
|
|
15290
15610
|
get localStream$() {
|
|
15291
15611
|
return this._localStream$.asObservable().pipe((0, import_cjs$17.takeUntil)(this.destroyed$));
|
|
@@ -15308,6 +15628,22 @@ var LocalStreamController = class extends Destroyable {
|
|
|
15308
15628
|
get localVideoTracks() {
|
|
15309
15629
|
return this._localVideoTracks$.value;
|
|
15310
15630
|
}
|
|
15631
|
+
tagTracks(tracks, origin) {
|
|
15632
|
+
for (const track of tracks) this._trackOrigins.set(track, origin);
|
|
15633
|
+
}
|
|
15634
|
+
setTrackOrigin(track, origin) {
|
|
15635
|
+
this._trackOrigins.set(track, origin);
|
|
15636
|
+
}
|
|
15637
|
+
getTrackOrigin(track) {
|
|
15638
|
+
return this._trackOrigins.get(track);
|
|
15639
|
+
}
|
|
15640
|
+
/**
|
|
15641
|
+
* Fail-safe: an unrecorded track reads as not-a-device-capture, so a missed
|
|
15642
|
+
* tagging site leaves media alone rather than destroying it.
|
|
15643
|
+
*/
|
|
15644
|
+
isDeviceCapture(track) {
|
|
15645
|
+
return this._trackOrigins.get(track) === "device";
|
|
15646
|
+
}
|
|
15311
15647
|
/**
|
|
15312
15648
|
* Build the local media stream based on the provided options.
|
|
15313
15649
|
*/
|
|
@@ -15317,13 +15653,16 @@ var LocalStreamController = class extends Destroyable {
|
|
|
15317
15653
|
if (this.options.inputAudioStream ?? this.options.inputVideoStream) {
|
|
15318
15654
|
const tracks = [...this.options.inputAudioStream?.getTracks() ?? [], ...this.options.inputVideoStream?.getTracks() ?? []];
|
|
15319
15655
|
stream = new MediaStream(tracks);
|
|
15656
|
+
this.tagTracks(tracks, "application");
|
|
15320
15657
|
} else if (this.options.propose === "screenshare") {
|
|
15321
|
-
|
|
15658
|
+
const audio = this.options.screenShareAudio ?? false;
|
|
15659
|
+
logger$19.debug("[LocalStreamController] Requesting display media for screen sharing with audio:", audio);
|
|
15322
15660
|
stream = await this.options.getDisplayMedia({
|
|
15323
15661
|
video: true,
|
|
15324
|
-
audio
|
|
15662
|
+
audio
|
|
15325
15663
|
});
|
|
15326
15664
|
logger$19.debug("[LocalStreamController] Screen share media obtained:", stream);
|
|
15665
|
+
this.tagTracks(stream.getTracks(), "display");
|
|
15327
15666
|
} else {
|
|
15328
15667
|
const constraints = {
|
|
15329
15668
|
audio: this.options.inputAudioDeviceConstraints,
|
|
@@ -15332,6 +15671,7 @@ var LocalStreamController = class extends Destroyable {
|
|
|
15332
15671
|
logger$19.debug("[LocalStreamController] Requesting user media with constraints:", constraints);
|
|
15333
15672
|
stream = await this.options.getUserMedia(constraints);
|
|
15334
15673
|
logger$19.debug("[LocalStreamController] User media obtained:", stream);
|
|
15674
|
+
this.tagTracks(stream.getTracks(), "device");
|
|
15335
15675
|
}
|
|
15336
15676
|
this._localStream$.next(stream);
|
|
15337
15677
|
this._localAudioTracks$.next(stream.getAudioTracks());
|
|
@@ -15341,10 +15681,13 @@ var LocalStreamController = class extends Destroyable {
|
|
|
15341
15681
|
/**
|
|
15342
15682
|
* Add a local media track to the local stream.
|
|
15343
15683
|
* @param track - The MediaStreamTrack to add
|
|
15684
|
+
* @param origin - Defaults to `'device'`; every internal caller passes a
|
|
15685
|
+
* fresh `getUserMedia` capture.
|
|
15344
15686
|
* @returns The MediaStream (either existing or newly created)
|
|
15345
15687
|
*/
|
|
15346
|
-
addTrack(track) {
|
|
15688
|
+
addTrack(track, origin = "device") {
|
|
15347
15689
|
const localStream = this._localStream$.value ?? new MediaStream();
|
|
15690
|
+
this._trackOrigins.set(track, origin);
|
|
15348
15691
|
track.addEventListener("ended", this.mediaTrackEndedHandler);
|
|
15349
15692
|
localStream.addTrack(track);
|
|
15350
15693
|
this._localStream$.next(localStream);
|
|
@@ -15614,15 +15957,27 @@ var TransceiverController = class extends Destroyable {
|
|
|
15614
15957
|
for (let i = 0; i < Number(msStreamsNumber); i++) this.peerConnection.addTransceiver("video", { direction: "recvonly" });
|
|
15615
15958
|
}
|
|
15616
15959
|
}
|
|
15960
|
+
/**
|
|
15961
|
+
* @returns whether every live sender of the kind took the constraints. A
|
|
15962
|
+
* skipped non-device sender, an exhausted fallback, and having no live sender
|
|
15963
|
+
* at all all report `false` — `mediaParamsUpdated.applied` is built from this,
|
|
15964
|
+
* and an application told `true` cannot tell a working push from a no-op.
|
|
15965
|
+
*/
|
|
15617
15966
|
async updateSendersConstraints(kind, constraints) {
|
|
15618
15967
|
if (!constraints) {
|
|
15619
15968
|
this.stopTrackSender(kind);
|
|
15620
|
-
return
|
|
15969
|
+
return false;
|
|
15621
15970
|
}
|
|
15622
15971
|
const senders = this.peerConnection.getSenders().filter((sender) => sender.track?.kind === kind && sender.track.readyState === "live");
|
|
15972
|
+
let applied = senders.length > 0;
|
|
15623
15973
|
for (const sender of senders) {
|
|
15624
15974
|
const { track } = sender;
|
|
15625
15975
|
if (track) {
|
|
15976
|
+
if (!this.options.localStreamController.isDeviceCapture(track)) {
|
|
15977
|
+
logger$18.debug(`[TransceiverController] Skipping ${kind} constraints for a non-device track (origin: ${this.options.localStreamController.getTrackOrigin(track) ?? "unrecorded"}), track ${track.id}`);
|
|
15978
|
+
applied = false;
|
|
15979
|
+
continue;
|
|
15980
|
+
}
|
|
15626
15981
|
const constraintsToApply = {
|
|
15627
15982
|
...track.getConstraints(),
|
|
15628
15983
|
...constraints
|
|
@@ -15638,33 +15993,39 @@ var TransceiverController = class extends Destroyable {
|
|
|
15638
15993
|
} catch (fallbackError) {
|
|
15639
15994
|
logger$18.warn(`[TransceiverController] Track replacement fallback also failed for ${kind} track:`, fallbackError);
|
|
15640
15995
|
this.options.onError?.(new MediaTrackError("updateSendersConstraints", kind, fallbackError));
|
|
15996
|
+
applied = false;
|
|
15641
15997
|
}
|
|
15642
15998
|
}
|
|
15643
15999
|
}
|
|
15644
16000
|
}
|
|
16001
|
+
return applied;
|
|
15645
16002
|
}
|
|
15646
16003
|
/**
|
|
15647
|
-
* Fallback when applyConstraints fails
|
|
15648
|
-
* one via getUserMedia with the merged constraints (preserving the current
|
|
15649
|
-
* deviceId), replace the sender track, and update the localStream.
|
|
16004
|
+
* Fallback when applyConstraints fails, which on iOS Safari it silently does.
|
|
15650
16005
|
*
|
|
15651
|
-
*
|
|
15652
|
-
*
|
|
16006
|
+
* Order matters: acquiring before stopping means a failed acquisition leaves
|
|
16007
|
+
* the existing media playing. The deviceId goes through the fallback ladder
|
|
16008
|
+
* rather than pinned `{ exact }`, so a stale id degrades instead of failing.
|
|
15653
16009
|
*/
|
|
15654
16010
|
async replaceTrackFallback(sender, oldTrack, kind, mergedConstraints) {
|
|
15655
16011
|
const { deviceId } = oldTrack.getSettings();
|
|
15656
|
-
const
|
|
15657
|
-
|
|
15658
|
-
|
|
15659
|
-
|
|
15660
|
-
|
|
16012
|
+
const { stream, fallbackLevel } = await getUserMediaWithFallback({ getUserMedia: this.options.getUserMedia }, { [kind]: mergedConstraints }, kind, deviceId);
|
|
16013
|
+
const newTrack = stream.getTracks().find((t) => t.kind === kind);
|
|
16014
|
+
if (!newTrack) {
|
|
16015
|
+
stream.getTracks().forEach((t) => t.stop());
|
|
16016
|
+
throw new MediaTrackError("replaceTrackFallback", kind, /* @__PURE__ */ new Error("getUserMedia returned no track of the requested kind"));
|
|
16017
|
+
}
|
|
16018
|
+
try {
|
|
16019
|
+
await sender.replaceTrack(newTrack);
|
|
16020
|
+
} catch (error) {
|
|
16021
|
+
stream.getTracks().forEach((t) => t.stop());
|
|
16022
|
+
throw error;
|
|
16023
|
+
}
|
|
16024
|
+
const oldTrackId = oldTrack.id;
|
|
16025
|
+
this.options.localStreamController.removeTrack(oldTrackId);
|
|
15661
16026
|
oldTrack.stop();
|
|
15662
|
-
this.options.localStreamController.removeTrack(trackId);
|
|
15663
|
-
const newTrack = (await this.options.getUserMedia({ [kind]: constraintsWithDevice })).getTracks().find((t) => t.kind === kind);
|
|
15664
|
-
if (!newTrack) throw new MediaTrackError("replaceTrackFallback", kind, /* @__PURE__ */ new Error("getUserMedia returned no track of the requested kind"));
|
|
15665
|
-
await sender.replaceTrack(newTrack);
|
|
15666
16027
|
this.options.localStreamController.addTrack(newTrack);
|
|
15667
|
-
logger$18.debug(`[TransceiverController] Track replacement fallback succeeded for ${kind}. New track: ${newTrack.id}`);
|
|
16028
|
+
logger$18.debug(`[TransceiverController] Track replacement fallback succeeded for ${kind} (deviceId fallback level: ${fallbackLevel}). New track: ${newTrack.id}`);
|
|
15668
16029
|
}
|
|
15669
16030
|
getMediaDirections() {
|
|
15670
16031
|
if (this.peerConnection.connectionState === "connected") return this.peerConnection.getTransceivers().reduce((acc, transceiver) => {
|
|
@@ -15734,48 +16095,44 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
15734
16095
|
this.negotiationNeeded$.next();
|
|
15735
16096
|
};
|
|
15736
16097
|
this.updateSelectedInputDevice = async (kind, deviceInfo) => {
|
|
16098
|
+
const { localStream } = this;
|
|
16099
|
+
if (!localStream) {
|
|
16100
|
+
logger$17.warn("[RTCPeerConnectionController] No local stream available to update input device.");
|
|
16101
|
+
return;
|
|
16102
|
+
}
|
|
16103
|
+
const currentTrack = localStream.getTracks().find((track) => track.kind === kind);
|
|
16104
|
+
if (!currentTrack) {
|
|
16105
|
+
logger$17.debug(`[RTCPeerConnectionController] No ${kind} track to switch.`);
|
|
16106
|
+
return;
|
|
16107
|
+
}
|
|
16108
|
+
if (!deviceInfo) {
|
|
16109
|
+
logger$17.debug(`[RTCPeerConnectionController] ${kind} input device selected: none`);
|
|
16110
|
+
this.stopTrackSender(kind);
|
|
16111
|
+
return;
|
|
16112
|
+
}
|
|
16113
|
+
const constraints = {
|
|
16114
|
+
...currentTrack.getConstraints(),
|
|
16115
|
+
...this.deviceController.deviceInfoToConstraints(deviceInfo)
|
|
16116
|
+
};
|
|
15737
16117
|
try {
|
|
15738
|
-
const
|
|
15739
|
-
|
|
15740
|
-
|
|
15741
|
-
return;
|
|
15742
|
-
}
|
|
15743
|
-
logger$17.debug(`[RTCPeerConnectionController] Updating selected ${kind} input device:`, localStream.getTracks());
|
|
15744
|
-
const track = localStream.getTracks().find((track$1) => track$1.kind === kind);
|
|
15745
|
-
if (track) {
|
|
15746
|
-
this.transceiverController?.stopTrackSender(kind);
|
|
15747
|
-
this.localStreamController.removeTrack(track.id);
|
|
15748
|
-
logger$17.debug(`[RTCPeerConnectionController] Stopped existing ${kind} track: ${track.id}`, localStream.getTracks());
|
|
15749
|
-
if (!deviceInfo) {
|
|
15750
|
-
logger$17.debug(`[RTCPeerConnectionController] ${kind} input device selected: none`);
|
|
15751
|
-
return;
|
|
15752
|
-
}
|
|
15753
|
-
const streamTrack = (await this.getUserMedia({ [kind]: {
|
|
15754
|
-
...track.getConstraints(),
|
|
15755
|
-
...this.deviceController.deviceInfoToConstraints(deviceInfo)
|
|
15756
|
-
} })).getTracks().find((t) => t.kind === kind);
|
|
15757
|
-
if (streamTrack) {
|
|
15758
|
-
logger$17.debug(`[RTCPeerConnectionController] Adding new ${kind} track: ${streamTrack.id}`);
|
|
15759
|
-
this.localStreamController.addTrack(streamTrack);
|
|
15760
|
-
await this.transceiverController?.replaceSenderTrack(kind, streamTrack);
|
|
15761
|
-
logger$17.debug(`[RTCPeerConnectionController] Added new ${kind} track: ${streamTrack.id}`, this.localStream?.getTracks());
|
|
15762
|
-
}
|
|
15763
|
-
}
|
|
15764
|
-
logger$17.debug(`[RTCPeerConnectionController] ${kind} input device selected:`, deviceInfo?.label);
|
|
16118
|
+
const newTrack = await this.acquireInputTrack(kind, constraints, deviceInfo, currentTrack);
|
|
16119
|
+
await this.attachInputTrack(kind, newTrack, currentTrack);
|
|
16120
|
+
logger$17.debug(`[RTCPeerConnectionController] ${kind} input device selected:`, deviceInfo.label, newTrack.id);
|
|
15765
16121
|
} catch (error) {
|
|
15766
16122
|
logger$17.error(`[RTCPeerConnectionController] Failed to select ${kind} input device:`, error);
|
|
15767
16123
|
this._errors$.next(new MediaTrackError("updateSelectedInputDevice", kind, error));
|
|
15768
|
-
throw error;
|
|
15769
16124
|
}
|
|
15770
16125
|
};
|
|
15771
16126
|
this._isNegotiating$ = this.createBehaviorSubject(false);
|
|
15772
16127
|
this._memberId = null;
|
|
16128
|
+
this._nodeId = null;
|
|
15773
16129
|
this._iceConnectionState$ = this.createReplaySubject(1);
|
|
15774
16130
|
this._connectionState$ = this.createReplaySubject(1);
|
|
15775
16131
|
this._signalingState$ = this.createReplaySubject(1);
|
|
15776
16132
|
this._iceGatheringState$ = this.createReplaySubject(1);
|
|
15777
16133
|
this._errors$ = this.createReplaySubject(1);
|
|
15778
16134
|
this._iceCandidates$ = this.createReplaySubject(1);
|
|
16135
|
+
this._localMediaSettled$ = this.createReplaySubject(1);
|
|
15779
16136
|
this._initialized$ = this.createReplaySubject(1);
|
|
15780
16137
|
this._remoteDescription$ = this.createReplaySubject(1);
|
|
15781
16138
|
this._remoteStream$ = this.createBehaviorSubject(null);
|
|
@@ -15808,6 +16165,7 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
15808
16165
|
inputVideoStream: this.options.inputVideoStream,
|
|
15809
16166
|
inputAudioDeviceConstraints: this.inputAudioDeviceConstraints,
|
|
15810
16167
|
inputVideoDeviceConstraints: this.inputVideoDeviceConstraints,
|
|
16168
|
+
screenShareAudio: this.options.screenShareAudio,
|
|
15811
16169
|
getUserMedia: async (constraints) => this.getUserMedia(constraints),
|
|
15812
16170
|
getDisplayMedia: async (options$1) => this.getDisplayMedia(options$1)
|
|
15813
16171
|
});
|
|
@@ -15834,6 +16192,13 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
15834
16192
|
get memberId() {
|
|
15835
16193
|
return this._memberId;
|
|
15836
16194
|
}
|
|
16195
|
+
/** The node this leg's invite landed on — auxiliary legs are placed independently. */
|
|
16196
|
+
setNodeId(nodeId) {
|
|
16197
|
+
this._nodeId = nodeId;
|
|
16198
|
+
}
|
|
16199
|
+
get nodeId() {
|
|
16200
|
+
return this._nodeId;
|
|
16201
|
+
}
|
|
15837
16202
|
stopTrackSender(kind, options = { updateTransceiverDirection: false }) {
|
|
15838
16203
|
const audioCovered = kind === "audio" || kind === "both";
|
|
15839
16204
|
if (audioCovered && this._localAudioPipeline) this.stopRawAudioInputForPipeline();
|
|
@@ -15843,10 +16208,7 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
15843
16208
|
}
|
|
15844
16209
|
stopRawAudioInputForPipeline() {
|
|
15845
16210
|
const rawTracks = this.localStreamController.localAudioTracks;
|
|
15846
|
-
for (const track of rawTracks) if (track.readyState === "live")
|
|
15847
|
-
track.stop();
|
|
15848
|
-
this.localStreamController.removeTrack(track.id);
|
|
15849
|
-
}
|
|
16211
|
+
for (const track of rawTracks) if (track.readyState === "live") this.localStreamController.removeTrack(track.id);
|
|
15850
16212
|
this._localAudioPipeline?.setInputTrack(null);
|
|
15851
16213
|
}
|
|
15852
16214
|
get isNegotiating$() {
|
|
@@ -15879,6 +16241,10 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
15879
16241
|
get remoteDescription$() {
|
|
15880
16242
|
return this.cachedObservable("remoteDescription$", () => this._remoteDescription$.asObservable().pipe((0, import_cjs$16.takeUntil)(this.destroyed$)));
|
|
15881
16243
|
}
|
|
16244
|
+
/** Emits once local media is settled — acquired, or knowingly receive-only. */
|
|
16245
|
+
get localMediaSettled$() {
|
|
16246
|
+
return this.cachedObservable("localMediaSettled$", () => this._localMediaSettled$.asObservable().pipe((0, import_cjs$16.takeUntil)(this.destroyed$)));
|
|
16247
|
+
}
|
|
15882
16248
|
get localStream$() {
|
|
15883
16249
|
return this.cachedObservable("localStream$", () => this.localStreamController.localStream$.pipe((0, import_cjs$16.takeUntil)(this.destroyed$)));
|
|
15884
16250
|
}
|
|
@@ -15906,6 +16272,9 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
15906
16272
|
get propose() {
|
|
15907
16273
|
return this.options.propose ?? "main";
|
|
15908
16274
|
}
|
|
16275
|
+
get connectionState() {
|
|
16276
|
+
return this.peerConnection?.connectionState;
|
|
16277
|
+
}
|
|
15909
16278
|
get isAdditionalDevice() {
|
|
15910
16279
|
return this.propose === "additional-device";
|
|
15911
16280
|
}
|
|
@@ -16034,7 +16403,7 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
16034
16403
|
this._isNegotiating$.next(true);
|
|
16035
16404
|
await this._setRemoteDescription(this.sdpInit);
|
|
16036
16405
|
} else {
|
|
16037
|
-
await this.setupTrackHandling();
|
|
16406
|
+
if (!await this.setupTrackHandling()) return;
|
|
16038
16407
|
this._initialized$.next(true);
|
|
16039
16408
|
}
|
|
16040
16409
|
} catch (error) {
|
|
@@ -16158,7 +16527,10 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
16158
16527
|
inputVideoDeviceConstraints: this.inputVideoDeviceConstraints
|
|
16159
16528
|
});
|
|
16160
16529
|
}
|
|
16161
|
-
await this.setupLocalTracks()
|
|
16530
|
+
if (!await this.setupLocalTracks()) {
|
|
16531
|
+
logger$17.debug("[RTCPeerConnectionController] Inbound answer abandoned; the connection went away.");
|
|
16532
|
+
return;
|
|
16533
|
+
}
|
|
16162
16534
|
const { answerOptions } = this;
|
|
16163
16535
|
logger$17.debug("[RTCPeerConnectionController] Creating inbound answer with options:", answerOptions);
|
|
16164
16536
|
await this.createAnswer(answerOptions);
|
|
@@ -16296,35 +16668,52 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
16296
16668
|
}
|
|
16297
16669
|
/**
|
|
16298
16670
|
* Setup track handling for remote tracks.
|
|
16671
|
+
*
|
|
16672
|
+
* @returns `false` when the connection went away while local media was being
|
|
16673
|
+
* acquired — see {@link setupLocalTracks}.
|
|
16299
16674
|
*/
|
|
16300
16675
|
async setupTrackHandling() {
|
|
16301
16676
|
if (!this.peerConnection) throw new DependencyError("RTCPeerConnection is not initialized");
|
|
16302
|
-
await this.setupLocalTracks();
|
|
16677
|
+
if (!await this.setupLocalTracks()) return false;
|
|
16303
16678
|
await this.setupRemoteTracks();
|
|
16679
|
+
return true;
|
|
16304
16680
|
}
|
|
16681
|
+
/**
|
|
16682
|
+
* @returns `false` when the connection was torn down while getUserMedia was
|
|
16683
|
+
* in flight. The acquisition is not cancellable, so the caller must stop
|
|
16684
|
+
* rather than go on to touch a peer connection that is closed or gone.
|
|
16685
|
+
*/
|
|
16305
16686
|
async setupLocalTracks() {
|
|
16306
16687
|
logger$17.debug("[RTCPeerConnectionController] Setting up local tracks/transceivers.");
|
|
16307
16688
|
if (this.hasNoLocalMediaToSend()) {
|
|
16308
16689
|
if (!this.receiveAudio && !this.receiveVideo) throw new InvalidParams("Call requests no media: enable audio/video or receiveAudio/receiveVideo");
|
|
16309
16690
|
logger$17.debug("[RTCPeerConnectionController] No local media requested; negotiating receive-only.");
|
|
16310
16691
|
this.setupReceiveOnlyTransceivers();
|
|
16311
|
-
|
|
16692
|
+
this._localMediaSettled$.next();
|
|
16693
|
+
return true;
|
|
16312
16694
|
}
|
|
16313
16695
|
let localStream;
|
|
16314
16696
|
try {
|
|
16315
16697
|
localStream = this.localStream ?? await this.localStreamController.buildLocalStream();
|
|
16316
16698
|
} catch (error) {
|
|
16317
16699
|
this.handleLocalMediaFailure(error);
|
|
16318
|
-
|
|
16700
|
+
this._localMediaSettled$.next();
|
|
16701
|
+
return true;
|
|
16319
16702
|
}
|
|
16703
|
+
if (!this.peerConnection || this.peerConnection.signalingState === "closed") {
|
|
16704
|
+
logger$17.debug("[RTCPeerConnectionController] Local media arrived after teardown; releasing it.");
|
|
16705
|
+
localStream.getTracks().forEach((track) => track.stop());
|
|
16706
|
+
return false;
|
|
16707
|
+
}
|
|
16708
|
+
this._localMediaSettled$.next();
|
|
16320
16709
|
if (this.transceiverController?.useAddStream ?? false) {
|
|
16321
16710
|
logger$17.warn("[RTCPeerConnectionController] Using deprecated addStream API to add local stream.");
|
|
16322
|
-
this.peerConnection
|
|
16711
|
+
this.peerConnection.addStream(localStream);
|
|
16323
16712
|
if (!this.isNegotiating) {
|
|
16324
16713
|
logger$17.debug("[RTCPeerConnectionController] Forcing negotiationneeded after local tracks setup.");
|
|
16325
16714
|
this.negotiationNeeded$.next();
|
|
16326
16715
|
}
|
|
16327
|
-
return;
|
|
16716
|
+
return true;
|
|
16328
16717
|
}
|
|
16329
16718
|
for (const kind of ["audio", "video"]) {
|
|
16330
16719
|
const tracks = (kind === "audio" ? localStream.getAudioTracks() : localStream.getVideoTracks()).map((track, index) => ({
|
|
@@ -16338,10 +16727,11 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
16338
16727
|
await this.transceiverController?.setupTransceiverSender(track, localStream, transceivers[index]);
|
|
16339
16728
|
} else {
|
|
16340
16729
|
logger$17.debug(`[RTCPeerConnectionController] Using addTrack for local ${kind} track:`, track.id);
|
|
16341
|
-
this.peerConnection
|
|
16730
|
+
this.peerConnection.addTrack(track, localStream);
|
|
16342
16731
|
}
|
|
16343
16732
|
}
|
|
16344
16733
|
}
|
|
16734
|
+
return true;
|
|
16345
16735
|
}
|
|
16346
16736
|
/** True for a main connection with no local media to send. */
|
|
16347
16737
|
hasNoLocalMediaToSend() {
|
|
@@ -16430,6 +16820,61 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
16430
16820
|
this._localAudioPipeline.setInputTrack(newTrack);
|
|
16431
16821
|
}
|
|
16432
16822
|
/**
|
|
16823
|
+
* Capture the newly selected device, leaving the current capture running.
|
|
16824
|
+
*
|
|
16825
|
+
* A rejection must leave the current track sending, so nothing is released
|
|
16826
|
+
* until the replacement is in hand. The one exception is hardware that admits
|
|
16827
|
+
* a single opener — a phone's front and back cameras, typically — which
|
|
16828
|
+
* rejects the second capture until the first is closed.
|
|
16829
|
+
*/
|
|
16830
|
+
async acquireInputTrack(kind, constraints, deviceInfo, currentTrack) {
|
|
16831
|
+
try {
|
|
16832
|
+
return await this.captureTrack(kind, constraints, deviceInfo.deviceId);
|
|
16833
|
+
} catch (error) {
|
|
16834
|
+
if (!isMediaDeviceInUse(error)) throw error;
|
|
16835
|
+
logger$17.warn(`[RTCPeerConnectionController] ${kind} device is held exclusively; releasing the current capture to retry:`, error);
|
|
16836
|
+
const previousDeviceId = currentTrack.getSettings().deviceId;
|
|
16837
|
+
this.stopTrackSender(kind);
|
|
16838
|
+
try {
|
|
16839
|
+
return await this.captureTrack(kind, constraints, deviceInfo.deviceId);
|
|
16840
|
+
} catch (retryError) {
|
|
16841
|
+
await this.restorePreviousInputTrack(kind, constraints, previousDeviceId, currentTrack);
|
|
16842
|
+
throw retryError;
|
|
16843
|
+
}
|
|
16844
|
+
}
|
|
16845
|
+
}
|
|
16846
|
+
async captureTrack(kind, constraints, deviceId) {
|
|
16847
|
+
const { stream, fallbackLevel } = await getUserMediaWithFallback({ getUserMedia: async (c) => this.getUserMedia(c) }, { [kind]: constraints }, kind, deviceId);
|
|
16848
|
+
const track = stream.getTracks().find((t) => t.kind === kind);
|
|
16849
|
+
if (!track) {
|
|
16850
|
+
stream.getTracks().forEach((t) => t.stop());
|
|
16851
|
+
throw new MediaTrackError("updateSelectedInputDevice", kind, /* @__PURE__ */ new Error("getUserMedia returned no track of the requested kind"));
|
|
16852
|
+
}
|
|
16853
|
+
if (fallbackLevel !== "exact") logger$17.warn(`[RTCPeerConnectionController] ${kind} device acquired at fallback level '${fallbackLevel}'; the capture may not be the requested device.`);
|
|
16854
|
+
return track;
|
|
16855
|
+
}
|
|
16856
|
+
/** Best-effort return to the device that was released for an exclusive retry. */
|
|
16857
|
+
async restorePreviousInputTrack(kind, constraints, previousDeviceId, releasedTrack) {
|
|
16858
|
+
try {
|
|
16859
|
+
const restored = await this.captureTrack(kind, constraints, previousDeviceId);
|
|
16860
|
+
await this.attachInputTrack(kind, restored, releasedTrack);
|
|
16861
|
+
} catch (error) {
|
|
16862
|
+
logger$17.error(`[RTCPeerConnectionController] Failed to restore the previous ${kind} device:`, error);
|
|
16863
|
+
}
|
|
16864
|
+
}
|
|
16865
|
+
async attachInputTrack(kind, newTrack, oldTrack) {
|
|
16866
|
+
const pipelineOwnsAudio = kind === "audio" && this._localAudioPipeline;
|
|
16867
|
+
if (!pipelineOwnsAudio) try {
|
|
16868
|
+
await this.transceiverController?.replaceSenderTrack(kind, newTrack);
|
|
16869
|
+
} catch (error) {
|
|
16870
|
+
newTrack.stop();
|
|
16871
|
+
throw error;
|
|
16872
|
+
}
|
|
16873
|
+
this.localStreamController.removeTrack(oldTrack.id);
|
|
16874
|
+
this.localStreamController.addTrack(newTrack);
|
|
16875
|
+
if (pipelineOwnsAudio) this._localAudioPipeline?.setInputTrack(newTrack);
|
|
16876
|
+
}
|
|
16877
|
+
/**
|
|
16433
16878
|
* Return the lazily-created {@link LocalAudioPipeline}, constructing it on
|
|
16434
16879
|
* first access. On creation the current audio sender's track is routed
|
|
16435
16880
|
* through the pipeline (input → gain → analyser → destination) and the
|
|
@@ -16462,6 +16907,7 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
16462
16907
|
const sender = (this.transceiverController?.audioTransceivers.at(0))?.sender ?? this.peerConnection.getSenders().find((s) => s.track?.kind === "audio");
|
|
16463
16908
|
if (!sender || !raw) return;
|
|
16464
16909
|
try {
|
|
16910
|
+
this.localStreamController.setTrackOrigin(this._localAudioPipeline.outputTrack, "processed");
|
|
16465
16911
|
await sender.replaceTrack(this._localAudioPipeline.outputTrack);
|
|
16466
16912
|
} catch (error) {
|
|
16467
16913
|
logger$17.warn("[RTCPeerConnectionController] Failed to route audio sender through pipeline:", error);
|
|
@@ -16522,42 +16968,73 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
16522
16968
|
for (const existingTrack of existingTracks) this.removeLocalTrack(existingTrack.id);
|
|
16523
16969
|
this.addLocalTrack(track);
|
|
16524
16970
|
}
|
|
16525
|
-
async updateSendersConstraints(kind, constraints) {
|
|
16526
|
-
await this.transceiverController?.updateSendersConstraints(kind, constraints);
|
|
16527
|
-
}
|
|
16528
16971
|
/**
|
|
16529
|
-
*
|
|
16530
|
-
*
|
|
16531
|
-
*
|
|
16532
|
-
*
|
|
16972
|
+
* @returns whether the constraints reached the media the leg is sending.
|
|
16973
|
+
*
|
|
16974
|
+
* With the pipeline engaged the audio sender carries the processed
|
|
16975
|
+
* destination track, so the sender scan would find nothing it may touch and
|
|
16976
|
+
* every audio constraint API would silently no-op. The constraints belong to
|
|
16977
|
+
* the pipeline's device source, which is the capture that sender ultimately
|
|
16978
|
+
* carries.
|
|
16533
16979
|
*/
|
|
16534
|
-
async
|
|
16535
|
-
|
|
16536
|
-
|
|
16537
|
-
|
|
16538
|
-
|
|
16539
|
-
|
|
16540
|
-
|
|
16541
|
-
const oldTrack = sender.track;
|
|
16542
|
-
if (!oldTrack) continue;
|
|
16543
|
-
const { deviceId } = oldTrack.getSettings();
|
|
16544
|
-
const mergedConstraints = {
|
|
16545
|
-
...oldTrack.getConstraints(),
|
|
16546
|
-
...constraints,
|
|
16547
|
-
...deviceId ? { deviceId: { exact: deviceId } } : {}
|
|
16548
|
-
};
|
|
16549
|
-
const trackId = oldTrack.id;
|
|
16550
|
-
oldTrack.stop();
|
|
16551
|
-
this.localStreamController.removeTrack(trackId);
|
|
16552
|
-
const newTrack = (await this.getUserMedia({ audio: mergedConstraints })).getAudioTracks()[0];
|
|
16553
|
-
await sender.replaceTrack(newTrack);
|
|
16554
|
-
this.localStreamController.addTrack(newTrack);
|
|
16555
|
-
logger$17.debug(`[RTCPeerConnectionController] Audio track replaced for server-pushed params. New track: ${newTrack.id}`);
|
|
16980
|
+
async updateSendersConstraints(kind, constraints) {
|
|
16981
|
+
if (kind === "audio" && this._localAudioPipeline) {
|
|
16982
|
+
if (!constraints) {
|
|
16983
|
+
this.stopTrackSender("audio");
|
|
16984
|
+
return false;
|
|
16985
|
+
}
|
|
16986
|
+
return this.applyPipelineSourceConstraints(constraints);
|
|
16556
16987
|
}
|
|
16988
|
+
return await this.transceiverController?.updateSendersConstraints(kind, constraints) ?? false;
|
|
16557
16989
|
}
|
|
16558
16990
|
/**
|
|
16559
|
-
*
|
|
16560
|
-
*
|
|
16991
|
+
* Mirror of the sender path for a piped audio leg: same merge, same fallback
|
|
16992
|
+
* ladder, same device-capture invariant — but the swap target is the pipeline
|
|
16993
|
+
* input, so the sender keeps emitting the pipeline's output track and its
|
|
16994
|
+
* identity survives the change.
|
|
16995
|
+
*/
|
|
16996
|
+
async applyPipelineSourceConstraints(constraints) {
|
|
16997
|
+
const pipeline = this._localAudioPipeline;
|
|
16998
|
+
const source = this.localStreamController.localAudioTracks.at(0);
|
|
16999
|
+
if (!pipeline || !source) {
|
|
17000
|
+
logger$17.debug("[RTCPeerConnectionController] No pipeline input to constrain.");
|
|
17001
|
+
return false;
|
|
17002
|
+
}
|
|
17003
|
+
if (!this.localStreamController.isDeviceCapture(source)) {
|
|
17004
|
+
logger$17.debug(`[RTCPeerConnectionController] Skipping audio constraints for a non-device pipeline input (origin: ${this.localStreamController.getTrackOrigin(source) ?? "unrecorded"}).`);
|
|
17005
|
+
return false;
|
|
17006
|
+
}
|
|
17007
|
+
const merged = {
|
|
17008
|
+
...source.getConstraints(),
|
|
17009
|
+
...constraints
|
|
17010
|
+
};
|
|
17011
|
+
try {
|
|
17012
|
+
await source.applyConstraints(merged);
|
|
17013
|
+
logger$17.debug("[RTCPeerConnectionController] Pipeline input constraints updated:", merged);
|
|
17014
|
+
return true;
|
|
17015
|
+
} catch (error) {
|
|
17016
|
+
logger$17.warn("[RTCPeerConnectionController] applyConstraints failed on the pipeline input, re-acquiring:", error);
|
|
17017
|
+
}
|
|
17018
|
+
try {
|
|
17019
|
+
const { stream } = await getUserMediaWithFallback({ getUserMedia: async (c) => this.getUserMedia(c) }, { audio: merged }, "audio", source.getSettings().deviceId);
|
|
17020
|
+
const newTrack = stream.getAudioTracks().at(0);
|
|
17021
|
+
if (!newTrack) {
|
|
17022
|
+
stream.getTracks().forEach((track) => track.stop());
|
|
17023
|
+
throw new Error("getUserMedia returned no audio track");
|
|
17024
|
+
}
|
|
17025
|
+
this.localStreamController.removeTrack(source.id);
|
|
17026
|
+
this.localStreamController.addTrack(newTrack);
|
|
17027
|
+
pipeline.setInputTrack(newTrack);
|
|
17028
|
+
return true;
|
|
17029
|
+
} catch (error) {
|
|
17030
|
+
logger$17.warn("[RTCPeerConnectionController] Failed to re-acquire the pipeline input for constraints:", error);
|
|
17031
|
+
this._errors$.next(new MediaTrackError("updateSendersConstraints", "audio", error));
|
|
17032
|
+
return false;
|
|
17033
|
+
}
|
|
17034
|
+
}
|
|
17035
|
+
/**
|
|
17036
|
+
* Clean up resources and close the peer connection.
|
|
17037
|
+
* Completes all observables to prevent memory leaks.
|
|
16561
17038
|
*/
|
|
16562
17039
|
destroy() {
|
|
16563
17040
|
logger$17.debug(`[RTCPeerConnectionController] Destroying RTCPeerConnectionController. ${this.propose}`);
|
|
@@ -16672,6 +17149,48 @@ const logger$16 = getLogger();
|
|
|
16672
17149
|
function resolveInviteNodeId(args) {
|
|
16673
17150
|
return args.isInvite && !args.reattach && !args.explicitNodeId ? "" : args.currentNodeId ?? "";
|
|
16674
17151
|
}
|
|
17152
|
+
/**
|
|
17153
|
+
* Surface the real outcome of a `webrtc.verto` reply.
|
|
17154
|
+
*
|
|
17155
|
+
* A webrtc.verto response nests several envelopes, each keyed by a verto-style
|
|
17156
|
+
* string `code` ("200" ok, "400"/etc. fail) rather than a JSON-RPC `error`. An outer
|
|
17157
|
+
* layer reports only whether the frame was delivered; an inner layer carries the op's
|
|
17158
|
+
* own outcome:
|
|
17159
|
+
*
|
|
17160
|
+
* response.result = { code:"200", result:{…} } ← delivery acknowledgement
|
|
17161
|
+
* .result = { jsonrpc, id, result:{…} } ← the reply payload
|
|
17162
|
+
* .result = { code:"400", message:"Bad request" } ← the actual op outcome
|
|
17163
|
+
*
|
|
17164
|
+
* A failure can appear at any layer (delivery refused, or the op itself rejected
|
|
17165
|
+
* deeper down), so walk every nested `.result` object and return the FIRST non-2xx
|
|
17166
|
+
* `code` with its message. Returns null when every `code` seen is 2xx or absent —
|
|
17167
|
+
* i.e. the op succeeded. This is the only way to detect that e.g. a mute/kick was
|
|
17168
|
+
* rejected, since the outer delivery `code` is "200" (delivered) even then.
|
|
17169
|
+
*
|
|
17170
|
+
* Pure function — exported for unit testing.
|
|
17171
|
+
*/
|
|
17172
|
+
function findNestedVertoFailure(response) {
|
|
17173
|
+
let node = response;
|
|
17174
|
+
while (node !== null && typeof node === "object") {
|
|
17175
|
+
const obj = node;
|
|
17176
|
+
const err = obj.error;
|
|
17177
|
+
if (err !== null && typeof err === "object") {
|
|
17178
|
+
const e = err;
|
|
17179
|
+
const errCode = typeof e.code === "string" || typeof e.code === "number" ? String(e.code) : void 0;
|
|
17180
|
+
if (errCode !== void 0 && !/^2\d\d$/.test(errCode)) return {
|
|
17181
|
+
code: errCode,
|
|
17182
|
+
message: typeof e.message === "string" ? e.message : void 0
|
|
17183
|
+
};
|
|
17184
|
+
}
|
|
17185
|
+
const code = typeof obj.code === "string" || typeof obj.code === "number" ? String(obj.code) : void 0;
|
|
17186
|
+
if (code !== void 0 && !/^2\d\d$/.test(code)) return {
|
|
17187
|
+
code,
|
|
17188
|
+
message: typeof obj.message === "string" ? obj.message : void 0
|
|
17189
|
+
};
|
|
17190
|
+
node = obj.result !== null && typeof obj.result === "object" ? obj.result : null;
|
|
17191
|
+
}
|
|
17192
|
+
return null;
|
|
17193
|
+
}
|
|
16675
17194
|
var VertoManager = class extends Destroyable {
|
|
16676
17195
|
constructor(callSession) {
|
|
16677
17196
|
super();
|
|
@@ -16694,7 +17213,7 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
16694
17213
|
this._signalingStatus$ = this.createReplaySubject(1);
|
|
16695
17214
|
this._screenShareStatus$ = this.createBehaviorSubject("none");
|
|
16696
17215
|
this._rtcPeerConnectionsMap = /* @__PURE__ */ new Map();
|
|
16697
|
-
this.
|
|
17216
|
+
this._legErrors$ = this.createSubject();
|
|
16698
17217
|
this._nodeId$ = this.createBehaviorSubject(options.nodeId ?? null);
|
|
16699
17218
|
this.onError = options.onError;
|
|
16700
17219
|
this.onModifyFailed = options.onModifyFailed;
|
|
@@ -16742,6 +17261,10 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
16742
17261
|
get selfId$() {
|
|
16743
17262
|
return this._selfId$.asObservable();
|
|
16744
17263
|
}
|
|
17264
|
+
/** Separates the media phase of call creation from the signalling phase. */
|
|
17265
|
+
get localMediaSettled$() {
|
|
17266
|
+
return this.mainPeerConnection.localMediaSettled$;
|
|
17267
|
+
}
|
|
16745
17268
|
get localStream() {
|
|
16746
17269
|
return this._rtcPeerConnectionsMap.get(this.webRtcCallSession.id)?.localStream ?? null;
|
|
16747
17270
|
}
|
|
@@ -16798,35 +17321,95 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
16798
17321
|
const { mediaParams, callID } = event;
|
|
16799
17322
|
const rtcPeerConnController = this._rtcPeerConnectionsMap.get(callID);
|
|
16800
17323
|
const { audio, video } = mediaParams;
|
|
16801
|
-
|
|
16802
|
-
|
|
16803
|
-
|
|
16804
|
-
|
|
16805
|
-
|
|
16806
|
-
audio,
|
|
16807
|
-
video,
|
|
16808
|
-
timestamp: Date.now()
|
|
16809
|
-
});
|
|
16810
|
-
} catch (error) {
|
|
16811
|
-
logger$16.warn("[WebRTCManager] Error applying server-pushed media params:", error);
|
|
16812
|
-
this.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
16813
|
-
}
|
|
16814
|
-
})();
|
|
17324
|
+
if (!rtcPeerConnController) {
|
|
17325
|
+
logger$16.warn(`[WebRTCManager] Ignoring server-pushed media params for unknown leg ${callID}`);
|
|
17326
|
+
return;
|
|
17327
|
+
}
|
|
17328
|
+
this.applyServerMediaParams(rtcPeerConnController, audio, video);
|
|
16815
17329
|
});
|
|
16816
17330
|
this.subscribeTo(this.vertoPing$, (vertoPing) => {
|
|
16817
|
-
this.attachManager.
|
|
17331
|
+
this.attachManager.refresh(this.buildAttachableCall());
|
|
16818
17332
|
this.sendVertoPong(vertoPing);
|
|
16819
17333
|
});
|
|
16820
17334
|
}
|
|
16821
17335
|
/**
|
|
17336
|
+
* An auxiliary-leg failure must never destroy the call; main-leg and
|
|
17337
|
+
* call-level errors keep `CallFactory.isFatalError`'s classification.
|
|
17338
|
+
*
|
|
17339
|
+
* Every site holding a peer connection reports through here, so the invariant
|
|
17340
|
+
* is structural rather than per-call-site — which is how cloud-product#20523
|
|
17341
|
+
* happened, with only one of fourteen sites passing `{ fatal: false }`.
|
|
17342
|
+
*
|
|
17343
|
+
* `override` composes rather than replaces: a caller may force non-fatal for a
|
|
17344
|
+
* reason of its own (a `verto.info` frame is best-effort whichever leg carries
|
|
17345
|
+
* it), and an auxiliary leg stays non-fatal regardless.
|
|
17346
|
+
*/
|
|
17347
|
+
reportLegError(error, rtcPeerConnController, override) {
|
|
17348
|
+
const leg = rtcPeerConnController?.propose;
|
|
17349
|
+
const legId = rtcPeerConnController?.id;
|
|
17350
|
+
const auxiliary = Boolean(rtcPeerConnController) && !rtcPeerConnController?.isMainDevice;
|
|
17351
|
+
this.onError?.(error, {
|
|
17352
|
+
...override?.fatal === false || auxiliary ? { fatal: false } : {},
|
|
17353
|
+
...leg ? { leg } : {},
|
|
17354
|
+
...legId ? { legId } : {}
|
|
17355
|
+
});
|
|
17356
|
+
if (legId) this._legErrors$.next({
|
|
17357
|
+
legId,
|
|
17358
|
+
error
|
|
17359
|
+
});
|
|
17360
|
+
}
|
|
17361
|
+
/**
|
|
17362
|
+
* Errors reported for one leg, as a stream that fails with them.
|
|
17363
|
+
*
|
|
17364
|
+
* Signaling failures are reported, never thrown — so nothing that waits on a
|
|
17365
|
+
* leg's progress would otherwise learn of a rejected invite. Merging this in
|
|
17366
|
+
* lets the wait end with the reason the server gave.
|
|
17367
|
+
*/
|
|
17368
|
+
legError$(legId) {
|
|
17369
|
+
return this._legErrors$.pipe((0, import_cjs$15.filter)((report) => report.legId === legId), (0, import_cjs$15.map)((report) => {
|
|
17370
|
+
throw report.error;
|
|
17371
|
+
}));
|
|
17372
|
+
}
|
|
17373
|
+
/**
|
|
17374
|
+
* Audio and video are applied independently so a failure in one cannot
|
|
17375
|
+
* suppress the other, and `mediaParamsUpdated` is emitted whatever happens —
|
|
17376
|
+
* an application should not be starved of the params by a constraint failure.
|
|
17377
|
+
*/
|
|
17378
|
+
async applyServerMediaParams(rtcPeerConnController, audio, video) {
|
|
17379
|
+
const failures = [];
|
|
17380
|
+
let applied = true;
|
|
17381
|
+
if (audio) try {
|
|
17382
|
+
applied = await rtcPeerConnController.updateSendersConstraints("audio", audio) && applied;
|
|
17383
|
+
} catch (error) {
|
|
17384
|
+
applied = false;
|
|
17385
|
+
failures.push(toError(error));
|
|
17386
|
+
}
|
|
17387
|
+
if (video) try {
|
|
17388
|
+
applied = await rtcPeerConnController.updateSendersConstraints("video", video) && applied;
|
|
17389
|
+
} catch (error) {
|
|
17390
|
+
applied = false;
|
|
17391
|
+
failures.push(toError(error));
|
|
17392
|
+
}
|
|
17393
|
+
this.webRtcCallSession.emitMediaParamsUpdated({
|
|
17394
|
+
audio,
|
|
17395
|
+
video,
|
|
17396
|
+
timestamp: Date.now(),
|
|
17397
|
+
applied
|
|
17398
|
+
});
|
|
17399
|
+
for (const failure of failures) {
|
|
17400
|
+
logger$16.warn("[WebRTCManager] Error applying server-pushed media params:", failure);
|
|
17401
|
+
this.reportLegError(failure, rtcPeerConnController, { fatal: false });
|
|
17402
|
+
}
|
|
17403
|
+
}
|
|
17404
|
+
/**
|
|
16822
17405
|
* Set node_id/selfId only when the current value is null.
|
|
16823
17406
|
*
|
|
16824
17407
|
* During reattach, `call.joined` and `verto.answer` events can deliver
|
|
16825
17408
|
* these identifiers before the `verto.invite` RPC response (`CALL CREATED`)
|
|
16826
17409
|
* arrives. These methods let early events populate them eagerly so that
|
|
16827
17410
|
* downstream RPC calls (e.g. `call.layout.list`) don't fail with empty
|
|
16828
|
-
* identifiers. `processInviteResponse()` remains the authoritative source
|
|
16829
|
-
*
|
|
17411
|
+
* identifiers. `processInviteResponse()` remains the authoritative source and
|
|
17412
|
+
* overwrites unconditionally — for selfId, on the main leg only.
|
|
16830
17413
|
*/
|
|
16831
17414
|
setNodeIdIfNull(nodeId) {
|
|
16832
17415
|
if (!this._nodeId$.value && nodeId) {
|
|
@@ -16849,24 +17432,33 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
16849
17432
|
this.onError?.(new VertoPongError(error));
|
|
16850
17433
|
}
|
|
16851
17434
|
}
|
|
17435
|
+
/**
|
|
17436
|
+
* @returns whether the constraints reached the media the call is sending.
|
|
17437
|
+
* `false` is an outcome, not a failure: the leg may have no live sender of
|
|
17438
|
+
* the kind, or carry media the SDK did not capture and may not replace.
|
|
17439
|
+
* A failure behind it still reaches the call's `errors$`, so a caller that
|
|
17440
|
+
* ignores this value learns of it there.
|
|
17441
|
+
*/
|
|
16852
17442
|
async updateMediaConstraints(options = {}) {
|
|
16853
17443
|
const { audio, video } = options;
|
|
17444
|
+
let applied = true;
|
|
16854
17445
|
try {
|
|
16855
|
-
if (audio) await this.mainPeerConnection.updateSendersConstraints("audio", audio);
|
|
16856
|
-
if (video) await this.mainPeerConnection.updateSendersConstraints("video", video);
|
|
17446
|
+
if (audio) applied = await this.mainPeerConnection.updateSendersConstraints("audio", audio) && applied;
|
|
17447
|
+
if (video) applied = await this.mainPeerConnection.updateSendersConstraints("video", video) && applied;
|
|
16857
17448
|
} catch (error) {
|
|
16858
17449
|
logger$16.warn("[WebRTCManager] Error updating media constraints:", error);
|
|
16859
|
-
this.
|
|
17450
|
+
this.reportLegError(toError(error), this.mainPeerConnection);
|
|
16860
17451
|
throw error;
|
|
16861
17452
|
}
|
|
17453
|
+
return applied;
|
|
16862
17454
|
}
|
|
16863
17455
|
get selfId() {
|
|
16864
17456
|
return this._selfId$.value;
|
|
16865
17457
|
}
|
|
16866
17458
|
/** Build an AttachableCall from the current call state. */
|
|
16867
|
-
buildAttachableCall(idOverride) {
|
|
17459
|
+
buildAttachableCall(idOverride, nodeIdOverride) {
|
|
16868
17460
|
return {
|
|
16869
|
-
nodeId: this.nodeId ?? void 0,
|
|
17461
|
+
nodeId: nodeIdOverride ?? this.nodeId ?? void 0,
|
|
16870
17462
|
id: idOverride ?? this.webRtcCallSession.id,
|
|
16871
17463
|
to: this.webRtcCallSession.to,
|
|
16872
17464
|
mediaDirections: this.webRtcCallSession.mediaDirections
|
|
@@ -16996,24 +17588,49 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
16996
17588
|
get vertoPing$() {
|
|
16997
17589
|
return this.cachedObservable("vertoPing$", () => this.webRtcCallSession.webrtcMessages$.pipe(filterAs(isVertoPingInnerParams, "params"), (0, import_cjs$15.takeUntil)(this.destroyed$)));
|
|
16998
17590
|
}
|
|
17591
|
+
/**
|
|
17592
|
+
* Send a member-control op in-dialog via verto.info.
|
|
17593
|
+
*
|
|
17594
|
+
* The control payload rides in `params.command` — a sibling of `dialogParams`,
|
|
17595
|
+
* at the same level as `dtmf` in {@link sendDigits} — and the inner verto.info
|
|
17596
|
+
* is matched to this call's channel by `dialogParams.callID`, the in-dialog
|
|
17597
|
+
* convention for member-scoped frames. Because it is delivered on the dialog
|
|
17598
|
+
* itself, control lands on the call's own channel with no {node_id,call_id,member_id}
|
|
17599
|
+
* "self" tuple to get wrong. The outer webrtc.verto envelope (added by executeVerto)
|
|
17600
|
+
* still carries the own-leg callID + node_id for session routing.
|
|
17601
|
+
*
|
|
17602
|
+
* Keep `command` OUT of `dialogParams`: it is read at the params level, and
|
|
17603
|
+
* filterVertoParams rewrites/filters dialogParams keys but passes params-level
|
|
17604
|
+
* keys through verbatim.
|
|
17605
|
+
*/
|
|
17606
|
+
async sendCallControl(method, params) {
|
|
17607
|
+
const response = await this.executeVerto(VertoInfo({
|
|
17608
|
+
dialogParams: { callID: this.webRtcCallSession.id },
|
|
17609
|
+
command: {
|
|
17610
|
+
method,
|
|
17611
|
+
params
|
|
17612
|
+
}
|
|
17613
|
+
}));
|
|
17614
|
+
const failure = findNestedVertoFailure(response);
|
|
17615
|
+
if (failure) throw new JSONRPCError(Number.parseInt(failure.code, 10) || 0, `Call control "${method}" failed (code ${failure.code})${failure.message ? `: ${failure.message}` : ""}`, void 0);
|
|
17616
|
+
return response;
|
|
17617
|
+
}
|
|
16999
17618
|
async executeVerto(message, optionals = {}) {
|
|
17000
|
-
const
|
|
17619
|
+
const params = {
|
|
17001
17620
|
callID: optionals.callID ?? this.webRtcCallSession.id,
|
|
17002
17621
|
node_id: optionals.node_id ?? this._nodeId$.value ?? "",
|
|
17003
17622
|
message,
|
|
17004
17623
|
subscribe: optionals.subscribe
|
|
17005
|
-
}
|
|
17624
|
+
};
|
|
17625
|
+
const webrtcVertoMessage = WebrtcVerto(params);
|
|
17006
17626
|
const response = await this.webRtcCallSession.execute(webrtcVertoMessage);
|
|
17007
|
-
|
|
17008
|
-
const error = new JSONRPCError(response.error.code, response.error.message, response.error.data);
|
|
17009
|
-
this.onError?.(error);
|
|
17010
|
-
return response;
|
|
17011
|
-
}
|
|
17627
|
+
const nonFatal = message.method === "verto.info" ? { fatal: false } : void 0;
|
|
17012
17628
|
const innerResult = getValueFrom(response, "result.result");
|
|
17013
|
-
|
|
17014
|
-
|
|
17015
|
-
|
|
17016
|
-
|
|
17629
|
+
const failure = response.error ?? innerResult?.error;
|
|
17630
|
+
if (failure) {
|
|
17631
|
+
const error = new JSONRPCError(failure.code, failure.message, failure.data);
|
|
17632
|
+
if (message.method === "verto.invite" || message.method === "verto.answer") throw error;
|
|
17633
|
+
this.reportLegError(error, this._rtcPeerConnectionsMap.get(params.callID), nonFatal);
|
|
17017
17634
|
}
|
|
17018
17635
|
return response;
|
|
17019
17636
|
}
|
|
@@ -17032,8 +17649,9 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
17032
17649
|
default:
|
|
17033
17650
|
}
|
|
17034
17651
|
} catch (error) {
|
|
17652
|
+
if (vertoMethod === "verto.answer") throw error;
|
|
17035
17653
|
logger$16.error(`[WebRTCManager] Error sending Verto ${vertoMethod}:`, error);
|
|
17036
|
-
this.
|
|
17654
|
+
this.reportLegError(toError(error), rtcPeerConnController);
|
|
17037
17655
|
if (vertoMethod === "verto.modify") this.onModifyFailed?.();
|
|
17038
17656
|
}
|
|
17039
17657
|
}
|
|
@@ -17049,7 +17667,7 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
17049
17667
|
} catch (error) {
|
|
17050
17668
|
logger$16.warn("[WebRTCManager] Error processing modify response:", error);
|
|
17051
17669
|
const modifyError = error instanceof Error ? error : new Error(String(error), { cause: error });
|
|
17052
|
-
this.
|
|
17670
|
+
this.reportLegError(modifyError, rtcPeerConnController);
|
|
17053
17671
|
}
|
|
17054
17672
|
}
|
|
17055
17673
|
}
|
|
@@ -17062,37 +17680,35 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
17062
17680
|
status,
|
|
17063
17681
|
signalingError
|
|
17064
17682
|
});
|
|
17065
|
-
this.
|
|
17683
|
+
this.reportLegError(signalingError, null, { fatal: false });
|
|
17066
17684
|
return;
|
|
17067
17685
|
}
|
|
17068
17686
|
if (rtcPeerConnController.isMainDevice) this._signalingStatus$.next(status);
|
|
17069
17687
|
}
|
|
17070
17688
|
processInviteResponse(response, rtcPeerConnController) {
|
|
17071
|
-
if (
|
|
17689
|
+
if (getValueFrom(response, "result.result.result.message") === "CALL CREATED") {
|
|
17072
17690
|
this.emitMainSignalingStatus(rtcPeerConnController.id, "trying");
|
|
17073
|
-
|
|
17691
|
+
const nodeId = getValueFrom(response, "result.node_id") ?? null;
|
|
17074
17692
|
const memberId = getValueFrom(response, "result.result.result.memberID") ?? null;
|
|
17075
|
-
const callId = getValueFrom(response, "result.result.result.callID")
|
|
17693
|
+
const callId = getValueFrom(response, "result.result.result.callID");
|
|
17076
17694
|
logger$16.debug("[WebRTCManager] Verto invite response:", {
|
|
17077
17695
|
callId,
|
|
17078
17696
|
memberId,
|
|
17079
17697
|
response
|
|
17080
17698
|
});
|
|
17081
|
-
this._selfId$.next(memberId);
|
|
17082
17699
|
rtcPeerConnController.setMemberId(memberId);
|
|
17083
|
-
|
|
17084
|
-
|
|
17085
|
-
this.
|
|
17086
|
-
|
|
17087
|
-
|
|
17088
|
-
|
|
17089
|
-
|
|
17700
|
+
rtcPeerConnController.setNodeId(nodeId);
|
|
17701
|
+
if (rtcPeerConnController.isMainDevice) {
|
|
17702
|
+
this._selfId$.next(memberId);
|
|
17703
|
+
this._nodeId$.next(nodeId);
|
|
17704
|
+
this.attachManager.attach(this.buildAttachableCall(callId, nodeId ?? void 0));
|
|
17705
|
+
}
|
|
17706
|
+
if (callId) this.webRtcCallSession.addCallId(callId);
|
|
17090
17707
|
logger$16.info("[WebRTCManager] Verto invite successful");
|
|
17091
17708
|
logger$16.debug(`[WebRTCManager] nodeid: ${this._nodeId$.value}, selfId: ${this._selfId$.value}`);
|
|
17092
17709
|
} else {
|
|
17093
17710
|
logger$16.error("[WebRTCManager] Verto invite failed:", response);
|
|
17094
|
-
|
|
17095
|
-
this.onError?.(inviteError);
|
|
17711
|
+
this.reportLegError(/* @__PURE__ */ new Error("Verto invite failed: unexpected response"), rtcPeerConnController);
|
|
17096
17712
|
}
|
|
17097
17713
|
}
|
|
17098
17714
|
get RTCPeerConnectionConfig() {
|
|
@@ -17131,7 +17747,7 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
17131
17747
|
this._rtcPeerConnectionsMap.set(rtcPeerConnController.id, rtcPeerConnController);
|
|
17132
17748
|
this._rtcPeerConnections$.next(Array.from(this._rtcPeerConnectionsMap.values()));
|
|
17133
17749
|
this.subscribeTo(rtcPeerConnController.errors$, (error) => {
|
|
17134
|
-
this.
|
|
17750
|
+
this.reportLegError(error, rtcPeerConnController);
|
|
17135
17751
|
});
|
|
17136
17752
|
if (options.initOffer) this.handleInboundAnswer(rtcPeerConnController);
|
|
17137
17753
|
}
|
|
@@ -17160,7 +17776,7 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
17160
17776
|
await rtcPeerConnController.acceptInbound(answerOptions);
|
|
17161
17777
|
} catch (error) {
|
|
17162
17778
|
logger$16.error("[WebRTCManager] Error creating inbound answer:", error);
|
|
17163
|
-
this.
|
|
17779
|
+
this.reportLegError(toError(error), rtcPeerConnController);
|
|
17164
17780
|
}
|
|
17165
17781
|
}
|
|
17166
17782
|
}
|
|
@@ -17232,7 +17848,7 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
17232
17848
|
isInvite: isVertoInviteMessage(vertoMessage),
|
|
17233
17849
|
reattach: this.webRtcCallSession.options.reattach === true,
|
|
17234
17850
|
explicitNodeId: this.webRtcCallSession.options.nodeId,
|
|
17235
|
-
currentNodeId: this._nodeId$.value
|
|
17851
|
+
currentNodeId: rtcPeerConnController.nodeId ?? this._nodeId$.value
|
|
17236
17852
|
}),
|
|
17237
17853
|
subscribe
|
|
17238
17854
|
};
|
|
@@ -17264,7 +17880,7 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
17264
17880
|
await this.attachManager.attach(this.buildAttachableCall());
|
|
17265
17881
|
} catch (error) {
|
|
17266
17882
|
logger$16.error("[WebRTCManager] Error sending Verto answer:", error);
|
|
17267
|
-
this.
|
|
17883
|
+
this.reportLegError(toError(error), rtcPeerConnectionController);
|
|
17268
17884
|
await rtcPeerConnectionController.updateAnswerStatus({ status: "failed" });
|
|
17269
17885
|
}
|
|
17270
17886
|
}
|
|
@@ -17336,15 +17952,19 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
17336
17952
|
await this.mainPeerConnection.restoreTrackSender(deviceKind);
|
|
17337
17953
|
} else {
|
|
17338
17954
|
const error = new InvalidParams("No valid device to be added");
|
|
17339
|
-
this.
|
|
17955
|
+
this.reportLegError(error, this.mainPeerConnection);
|
|
17340
17956
|
throw error;
|
|
17341
17957
|
}
|
|
17342
17958
|
}
|
|
17343
|
-
async addScreenMedia(options = {
|
|
17344
|
-
await this.initAdditionalPeerConnection("screenshare",
|
|
17959
|
+
async addScreenMedia(options = {}) {
|
|
17960
|
+
await this.initAdditionalPeerConnection("screenshare", {
|
|
17961
|
+
audio: false,
|
|
17962
|
+
screenShareAudio: options.audio ?? false
|
|
17963
|
+
});
|
|
17345
17964
|
}
|
|
17346
17965
|
async initAdditionalPeerConnection(propose, options) {
|
|
17347
17966
|
const isScreenShare = propose === "screenshare";
|
|
17967
|
+
if (isScreenShare && this._screenShareId && this._rtcPeerConnectionsMap.has(this._screenShareId)) throw new ScreenShareAlreadyActiveError(this._screenShareId);
|
|
17348
17968
|
let firstPeerConnectionError;
|
|
17349
17969
|
let rtcPeerConnController = null;
|
|
17350
17970
|
try {
|
|
@@ -17361,21 +17981,36 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
17361
17981
|
this._rtcPeerConnections$.next(Array.from(this._rtcPeerConnectionsMap.values()));
|
|
17362
17982
|
this.subscribeTo(rtcPeerConnController.errors$, (error) => {
|
|
17363
17983
|
firstPeerConnectionError ??= error;
|
|
17364
|
-
this.
|
|
17984
|
+
this.reportLegError(error, rtcPeerConnController);
|
|
17365
17985
|
});
|
|
17366
|
-
|
|
17986
|
+
const pc = rtcPeerConnController;
|
|
17987
|
+
await (0, import_cjs$15.firstValueFrom)((0, import_cjs$15.merge)(pc.localMediaSettled$.pipe((0, import_cjs$15.take)(1), (0, import_cjs$15.switchMap)(() => pc.connectionState$.pipe((0, import_cjs$15.filter)((state) => state === "connected"), (0, import_cjs$15.take)(1), (0, import_cjs$15.timeout)(DEFAULT_AUX_LEG_CONNECT_TIMEOUT_MS)))), this.legError$(pc.id)).pipe((0, import_cjs$15.takeUntil)((0, import_cjs$15.merge)(this.destroyed$, pc.destroyed$))));
|
|
17367
17988
|
if (isScreenShare) this._screenShareStatus$.next("started");
|
|
17368
17989
|
logger$16.info(`[WebRTCManager] Additional peer connection connected (${propose}).`);
|
|
17369
17990
|
return rtcPeerConnController.id;
|
|
17370
17991
|
} catch (error) {
|
|
17371
|
-
|
|
17372
|
-
|
|
17373
|
-
if (
|
|
17992
|
+
const cancelled = error instanceof AuxiliaryLegCancelledError;
|
|
17993
|
+
const aborted = error instanceof import_cjs$15.EmptyError && !firstPeerConnectionError;
|
|
17994
|
+
if (!cancelled && !aborted) logger$16.warn("[WebRTCManager] Error initializing additional peer connection:", error);
|
|
17995
|
+
if (rtcPeerConnController && this._rtcPeerConnectionsMap.has(rtcPeerConnController.id)) {
|
|
17996
|
+
rtcPeerConnController.destroy();
|
|
17997
|
+
this._rtcPeerConnectionsMap.delete(rtcPeerConnController.id);
|
|
17998
|
+
this._rtcPeerConnections$.next(Array.from(this._rtcPeerConnectionsMap.values()));
|
|
17999
|
+
}
|
|
18000
|
+
if (isScreenShare) {
|
|
18001
|
+
this._screenShareId = void 0;
|
|
18002
|
+
this._screenShareStatus$.next("none");
|
|
18003
|
+
}
|
|
18004
|
+
if (cancelled) {
|
|
18005
|
+
logger$16.debug("[WebRTCManager] Additional peer connection removed before connecting.");
|
|
18006
|
+
throw error;
|
|
18007
|
+
}
|
|
17374
18008
|
if (firstPeerConnectionError) throw firstPeerConnectionError instanceof MediaAccessError && firstPeerConnectionError.originalError instanceof Error ? firstPeerConnectionError.originalError : firstPeerConnectionError;
|
|
17375
18009
|
if (error instanceof import_cjs$15.EmptyError) {
|
|
17376
18010
|
logger$16.debug("[WebRTCManager] Additional peer connection aborted before connecting.");
|
|
17377
18011
|
return;
|
|
17378
18012
|
}
|
|
18013
|
+
if (error instanceof import_cjs$15.TimeoutError) throw new AuxiliaryLegTimeoutError(propose, error);
|
|
17379
18014
|
throw error instanceof Error ? error : new Error(String(error), { cause: error });
|
|
17380
18015
|
}
|
|
17381
18016
|
}
|
|
@@ -17392,7 +18027,10 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
17392
18027
|
if (removeTrack) return this.mainPeerConnection.stopTrackSender(removeTrack, { updateTransceiverDirection: true });
|
|
17393
18028
|
}
|
|
17394
18029
|
async removeScreenMedia() {
|
|
17395
|
-
if (!["starting", "started"].includes(this._screenShareStatus$.value))
|
|
18030
|
+
if (!["starting", "started"].includes(this._screenShareStatus$.value)) {
|
|
18031
|
+
logger$16.warn("[WebRTCManager] No active screen share to stop.");
|
|
18032
|
+
return;
|
|
18033
|
+
}
|
|
17396
18034
|
if (!this._screenShareId) {
|
|
17397
18035
|
logger$16.debug("[WebRTCManager] No screen share peer connection found.");
|
|
17398
18036
|
return;
|
|
@@ -17407,6 +18045,10 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
17407
18045
|
try {
|
|
17408
18046
|
if (rtcPeerConnController) await this.executeVertoBye(rtcPeerConnController);
|
|
17409
18047
|
} finally {
|
|
18048
|
+
if (rtcPeerConnController && rtcPeerConnController.connectionState !== "connected") this._legErrors$.next({
|
|
18049
|
+
legId: id,
|
|
18050
|
+
error: new AuxiliaryLegCancelledError(rtcPeerConnController.propose)
|
|
18051
|
+
});
|
|
17410
18052
|
rtcPeerConnController?.destroy();
|
|
17411
18053
|
this._rtcPeerConnectionsMap.delete(id);
|
|
17412
18054
|
this._rtcPeerConnections$.next(Array.from(this._rtcPeerConnectionsMap.values()));
|
|
@@ -17421,7 +18063,10 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
17421
18063
|
await this.executeVerto(VertoBye({
|
|
17422
18064
|
...causeParams,
|
|
17423
18065
|
dialogParams: this.dialogParams(rtcPeerConnController)
|
|
17424
|
-
})
|
|
18066
|
+
}), {
|
|
18067
|
+
callID: rtcPeerConnController.id,
|
|
18068
|
+
node_id: rtcPeerConnController.nodeId ?? void 0
|
|
18069
|
+
});
|
|
17425
18070
|
} catch (error) {
|
|
17426
18071
|
logger$16.warn("[WebRTCManager] Call might already be disconnected, error sending Verto bye:", error);
|
|
17427
18072
|
throw error;
|
|
@@ -18300,17 +18945,38 @@ function mosToQualityLevel(mos) {
|
|
|
18300
18945
|
return "critical";
|
|
18301
18946
|
}
|
|
18302
18947
|
|
|
18948
|
+
//#endregion
|
|
18949
|
+
//#region src/utils/unwrapVertoReply.ts
|
|
18950
|
+
/**
|
|
18951
|
+
* Unwrap a method's own reply from a `webrtc.verto` envelope.
|
|
18952
|
+
*
|
|
18953
|
+
* A control verb sent in-dialog comes back nested two levels deep:
|
|
18954
|
+
*
|
|
18955
|
+
* ```
|
|
18956
|
+
* { result: { node_id, code, result: { jsonrpc, id, result: <method payload> } } }
|
|
18957
|
+
* ```
|
|
18958
|
+
*
|
|
18959
|
+
* whereas the routed transport resolves the method payload directly under
|
|
18960
|
+
* `.result`. Readers want the latter shape, and the difference is silent when it
|
|
18961
|
+
* is wrong — `response.result.layouts` simply evaluates to `undefined` against the
|
|
18962
|
+
* envelope, so the data arrives, nothing throws, and the caller sees an empty
|
|
18963
|
+
* value. That exact failure produced an empty layout dropdown with a successful
|
|
18964
|
+
* request behind it.
|
|
18965
|
+
*
|
|
18966
|
+
* Accepting both shapes here keeps every reader indifferent to which transport
|
|
18967
|
+
* produced the response. Anything that is not a nested envelope (a plain ack, or
|
|
18968
|
+
* an already-unwrapped reply) passes through untouched.
|
|
18969
|
+
*/
|
|
18970
|
+
function unwrapVertoReply(response) {
|
|
18971
|
+
const inner = getValueFrom(response, "result.result");
|
|
18972
|
+
return inner && typeof inner === "object" && "result" in inner ? inner : response;
|
|
18973
|
+
}
|
|
18974
|
+
|
|
18303
18975
|
//#endregion
|
|
18304
18976
|
//#region src/core/entities/Call.ts
|
|
18305
18977
|
var import_cjs$11 = require_cjs();
|
|
18306
18978
|
const logger$12 = getLogger();
|
|
18307
18979
|
/**
|
|
18308
|
-
* Verto method for setting member layout positions. Its gateway DTO requires a
|
|
18309
|
-
* `targets` array whose entries are `{ target, position }` (NOT bare targets),
|
|
18310
|
-
* so {@link WebRTCCall.buildMethodParams} special-cases it. See issue #19400.
|
|
18311
|
-
*/
|
|
18312
|
-
const POSITION_SET_METHOD = "call.member.position.set";
|
|
18313
|
-
/**
|
|
18314
18980
|
* Ratio between the critical and warning RTT spike multipliers.
|
|
18315
18981
|
* Warning threshold = baseline * warningMultiplier (default 3x)
|
|
18316
18982
|
* Critical threshold = baseline * warningMultiplier * RTT_CRITICAL_TO_WARNING_RATIO
|
|
@@ -18405,8 +19071,11 @@ var WebRTCCall = class extends Destroyable {
|
|
|
18405
19071
|
emitError(callError) {
|
|
18406
19072
|
if (this._status$.value === "destroyed" || this._status$.value === "failed") return;
|
|
18407
19073
|
this._errors$.next(callError);
|
|
18408
|
-
if (callError.fatal) {
|
|
19074
|
+
if (callError.fatal && this._status$.value !== "disconnecting") {
|
|
18409
19075
|
this._status$.next("failed");
|
|
19076
|
+
this.vertoManager.bye().catch((error) => {
|
|
19077
|
+
logger$12.debug("[Call] fatal-teardown bye failed (signaling likely already dead):", error);
|
|
19078
|
+
});
|
|
18410
19079
|
this.destroy();
|
|
18411
19080
|
}
|
|
18412
19081
|
}
|
|
@@ -18461,7 +19130,7 @@ var WebRTCCall = class extends Destroyable {
|
|
|
18461
19130
|
/** Toggles the call lock state, preventing or allowing new participants from joining. */
|
|
18462
19131
|
async toggleLock() {
|
|
18463
19132
|
const method = this.locked ? "call.unlock" : "call.lock";
|
|
18464
|
-
await this.executeMethod(this.
|
|
19133
|
+
await this.executeMethod(this.callSelf, method, {});
|
|
18465
19134
|
}
|
|
18466
19135
|
/**
|
|
18467
19136
|
* Toggles the hold state of the call (pauses/resumes local media transmission).
|
|
@@ -18510,14 +19179,25 @@ var WebRTCCall = class extends Destroyable {
|
|
|
18510
19179
|
*
|
|
18511
19180
|
* Constructs call context (node_id, call_id, member_id) and sends the RPC request.
|
|
18512
19181
|
*
|
|
18513
|
-
* @param target - Target
|
|
19182
|
+
* @param target - Target {@link MemberTarget} triple, or the local member's
|
|
19183
|
+
* ID string for self-operations (any other string is rejected — a bare
|
|
19184
|
+
* member id cannot carry the remote member's own call context).
|
|
18514
19185
|
* @param method - Verto method name (e.g. `'call.mute'`, `'call.member.remove'`).
|
|
18515
19186
|
* @param args - Parameters for the RPC method.
|
|
18516
19187
|
* @returns The RPC response.
|
|
19188
|
+
* @throws {CallNotReadyError} If the call has no self member context yet.
|
|
19189
|
+
* @throws {InvalidParams} If a string target is not the local member's ID.
|
|
18517
19190
|
* @throws {JSONRPCError} If the RPC call returns an error.
|
|
18518
19191
|
*/
|
|
18519
19192
|
async executeMethod(target, method, args) {
|
|
18520
|
-
const
|
|
19193
|
+
const self = this.callSelf;
|
|
19194
|
+
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}`);
|
|
19195
|
+
if (this.clientSession.callControl === "in-dialog") return this.executeMethodInDialog(target, method, args);
|
|
19196
|
+
const params = {
|
|
19197
|
+
...args,
|
|
19198
|
+
self,
|
|
19199
|
+
target: typeof target === "string" ? self : target
|
|
19200
|
+
};
|
|
18521
19201
|
const request = buildRPCRequest({
|
|
18522
19202
|
method,
|
|
18523
19203
|
params
|
|
@@ -18531,29 +19211,98 @@ var WebRTCCall = class extends Destroyable {
|
|
|
18531
19211
|
throw error;
|
|
18532
19212
|
}
|
|
18533
19213
|
}
|
|
18534
|
-
|
|
18535
|
-
|
|
18536
|
-
|
|
18537
|
-
|
|
18538
|
-
|
|
18539
|
-
|
|
18540
|
-
|
|
18541
|
-
|
|
18542
|
-
|
|
18543
|
-
|
|
18544
|
-
|
|
18545
|
-
|
|
18546
|
-
|
|
18547
|
-
|
|
18548
|
-
};
|
|
18549
|
-
|
|
18550
|
-
|
|
18551
|
-
|
|
18552
|
-
|
|
18553
|
-
|
|
19214
|
+
/**
|
|
19215
|
+
* `executeMethod` for a call opened with `callControl: 'in-dialog'`.
|
|
19216
|
+
*
|
|
19217
|
+
* Translates the routed transport's calling convention into the in-dialog one. No
|
|
19218
|
+
* `self` tuple is sent, but a `target` is — the same {call_id, member_id} the routed
|
|
19219
|
+
* transport puts in `target` (minus node_id), for self-ops and cross-member ops alike.
|
|
19220
|
+
*
|
|
19221
|
+
* Target shapes are per-verb and irregular, so they are centralised here rather
|
|
19222
|
+
* than left to callers: most verbs take a singular `target`, `call.member.remove`
|
|
19223
|
+
* takes a plural `targets` array, and `call.member.position.set` takes a flat
|
|
19224
|
+
* `targets` of `{call_id, position}` — the one verb keyed on call_id rather than
|
|
19225
|
+
* member_id, so the member triple `Participant.setPosition` built is unwrapped.
|
|
19226
|
+
*/
|
|
19227
|
+
async executeMethodInDialog(target, method, args) {
|
|
19228
|
+
const control = { ...args };
|
|
19229
|
+
if (method === "call.member.position.set") control.targets = (args.targets ?? []).map((entry) => ({
|
|
19230
|
+
call_id: entry.target?.call_id ?? entry.call_id,
|
|
19231
|
+
position: entry.position
|
|
19232
|
+
}));
|
|
19233
|
+
else {
|
|
19234
|
+
const member = typeof target === "object" ? {
|
|
19235
|
+
call_id: target.call_id,
|
|
19236
|
+
member_id: target.member_id
|
|
19237
|
+
} : {
|
|
18554
19238
|
call_id: this.id,
|
|
18555
19239
|
member_id: target
|
|
18556
|
-
}
|
|
19240
|
+
};
|
|
19241
|
+
if (method === "call.member.remove") control.targets = [member];
|
|
19242
|
+
else control.target = member;
|
|
19243
|
+
}
|
|
19244
|
+
return this.sendCommand(method, control);
|
|
19245
|
+
}
|
|
19246
|
+
/**
|
|
19247
|
+
* Sends a `call.*` control verb **in-dialog** via `verto.info`, as an alternative
|
|
19248
|
+
* to the routed {@link executeMethod} transport.
|
|
19249
|
+
*
|
|
19250
|
+
* Why both exist: `executeMethod` addresses the member with an explicit
|
|
19251
|
+
* `{node_id, call_id, member_id}` tuple, which does not resolve for every conference,
|
|
19252
|
+
* so the op can fail. An in-dialog frame carries the verb on the member's own
|
|
19253
|
+
* signaling channel instead, so control works without the client needing to know how
|
|
19254
|
+
* the conference is hosted.
|
|
19255
|
+
*
|
|
19256
|
+
* The trade-off is reach: the in-dialog transport is only accepted for calls that
|
|
19257
|
+
* join a conference over SWML (e.g. an SWML `join_conference`); use the routed
|
|
19258
|
+
* default otherwise.
|
|
19259
|
+
*
|
|
19260
|
+
* `params` are sent verbatim — nothing is built for you, which includes the target.
|
|
19261
|
+
* **A self-directed op still needs one**, or it is refused; name yourself explicitly:
|
|
19262
|
+
*
|
|
19263
|
+
* ```ts
|
|
19264
|
+
* const { call_id, member_id } = call.self.target;
|
|
19265
|
+
* await call.sendCommand('call.mute', { channels: ['audio'], target: { call_id, member_id } });
|
|
19266
|
+
* ```
|
|
19267
|
+
*
|
|
19268
|
+
* Never include `node_id` — only the two ids. The shapes are per-verb: most take a
|
|
19269
|
+
* singular `target`, `call.member.remove` takes a plural `targets` array, and
|
|
19270
|
+
* `call.member.position.set` takes a flat `targets: [{call_id, position}]` (the one
|
|
19271
|
+
* verb keyed on `call_id` rather than `member_id`). Verbs that act on the call as a
|
|
19272
|
+
* whole, or that the SDK does not wrap at all, take no target.
|
|
19273
|
+
*
|
|
19274
|
+
* For the typed alternative that handles all of this, create the client with
|
|
19275
|
+
* `callControl: 'in-dialog'` and use the ordinary `Call`/`Participant` methods.
|
|
19276
|
+
*
|
|
19277
|
+
* @internal Not part of the supported surface while the in-dialog transport is still
|
|
19278
|
+
* rolling out. `WebRTCCall` is exported from the package entry, so without this tag
|
|
19279
|
+
* TypeDoc publishes the method — and the example above — as public API.
|
|
19280
|
+
*
|
|
19281
|
+
* @param method - A `call.*` method name (e.g. `'call.mute'`).
|
|
19282
|
+
* @param params - Method parameters, sent verbatim.
|
|
19283
|
+
* @returns The method's own reply, unwrapped from the `verto.info` envelope.
|
|
19284
|
+
* @throws {JSONRPCError} If the control op fails.
|
|
19285
|
+
*/
|
|
19286
|
+
async sendCommand(method, params = {}) {
|
|
19287
|
+
return unwrapVertoReply(await this.vertoManager.sendCallControl(method, params));
|
|
19288
|
+
}
|
|
19289
|
+
/**
|
|
19290
|
+
* The local leg's member triple — sent as `self` in every member RPC
|
|
19291
|
+
* envelope, and as the `target` of call-scoped self-operations (e.g. lock,
|
|
19292
|
+
* layout).
|
|
19293
|
+
*
|
|
19294
|
+
* @throws {CallNotReadyError} Before `call.joined` delivers the self member
|
|
19295
|
+
* context (`selfId`/`nodeId`) — an RPC without it cannot be routed, so fail
|
|
19296
|
+
* fast instead of sending a doomed request.
|
|
19297
|
+
*/
|
|
19298
|
+
get callSelf() {
|
|
19299
|
+
const node_id = this.nodeId;
|
|
19300
|
+
const member_id = this.vertoManager.selfId;
|
|
19301
|
+
if (!node_id || !member_id) throw new CallNotReadyError(this.id);
|
|
19302
|
+
return {
|
|
19303
|
+
node_id,
|
|
19304
|
+
call_id: this.id,
|
|
19305
|
+
member_id
|
|
18557
19306
|
};
|
|
18558
19307
|
}
|
|
18559
19308
|
/** Observable of the current call status (e.g. `'ringing'`, `'connected'`). */
|
|
@@ -18709,13 +19458,13 @@ var WebRTCCall = class extends Destroyable {
|
|
|
18709
19458
|
get bandwidthConstrained$() {
|
|
18710
19459
|
return this.deferEmission(this._bandwidthConstrained$.asObservable());
|
|
18711
19460
|
}
|
|
18712
|
-
/** Observable that emits when server
|
|
19461
|
+
/** Observable that emits when the server pushes media params. */
|
|
18713
19462
|
get mediaParamsUpdated$() {
|
|
18714
19463
|
return this.deferEmission(this._mediaParamsUpdated$.asObservable());
|
|
18715
19464
|
}
|
|
18716
19465
|
/**
|
|
18717
19466
|
* @internal Emit a media params update event.
|
|
18718
|
-
* Called by the VertoManager when server
|
|
19467
|
+
* Called by the VertoManager when the server pushes media params.
|
|
18719
19468
|
*/
|
|
18720
19469
|
emitMediaParamsUpdated(event) {
|
|
18721
19470
|
this._mediaParamsUpdated$.next(event);
|
|
@@ -18923,6 +19672,10 @@ var WebRTCCall = class extends Destroyable {
|
|
|
18923
19672
|
get selfId$() {
|
|
18924
19673
|
return this.vertoManager.selfId$;
|
|
18925
19674
|
}
|
|
19675
|
+
/** @internal Lets call creation bound the media and signalling phases apart. */
|
|
19676
|
+
get localMediaSettled$() {
|
|
19677
|
+
return this.vertoManager.localMediaSettled$;
|
|
19678
|
+
}
|
|
18926
19679
|
/** Local participant's member ID, or `null` if not joined. */
|
|
18927
19680
|
get selfId() {
|
|
18928
19681
|
return this.vertoManager.selfId;
|
|
@@ -19116,12 +19869,17 @@ var WebRTCCall = class extends Destroyable {
|
|
|
19116
19869
|
*
|
|
19117
19870
|
* **These operations are NOT atomic.** The layout is applied first, then each
|
|
19118
19871
|
* member position sequentially, so members may briefly flash into their
|
|
19119
|
-
* default slots before being moved to the requested positions.
|
|
19872
|
+
* default slots before being moved to the requested positions. Targeted
|
|
19873
|
+
* members are validated upfront, though: when any of them has no
|
|
19874
|
+
* {@link Participant.target | member call context} yet, the whole call
|
|
19875
|
+
* rejects before any request is sent and the layout is left unchanged.
|
|
19120
19876
|
*
|
|
19121
19877
|
* @param layout - Layout name (must be one of {@link layouts}).
|
|
19122
19878
|
* @param positions - Optional map of member IDs to {@link VideoPosition} values.
|
|
19123
19879
|
* When omitted or empty, only the layout is changed.
|
|
19124
19880
|
* @throws {InvalidParams} If the layout is not in the available {@link layouts}.
|
|
19881
|
+
* @throws {ParticipantNotReadyError} If a targeted member's call context has
|
|
19882
|
+
* not been received yet — thrown before any request is sent.
|
|
19125
19883
|
*
|
|
19126
19884
|
* @example
|
|
19127
19885
|
* ```ts
|
|
@@ -19132,18 +19890,19 @@ var WebRTCCall = class extends Destroyable {
|
|
|
19132
19890
|
*/
|
|
19133
19891
|
async setLayout(layout, positions) {
|
|
19134
19892
|
if (!this.layouts.includes(layout)) throw new InvalidParams(`Layout ${layout} is not available in the current call layouts: ${this.layouts.join(", ")}`);
|
|
19135
|
-
const
|
|
19136
|
-
|
|
19137
|
-
const positionEntries = Object.entries(positions ?? {});
|
|
19138
|
-
if (positionEntries.length === 0) return;
|
|
19139
|
-
for (const [memberId, position] of positionEntries) {
|
|
19893
|
+
const targets = [];
|
|
19894
|
+
for (const [memberId, position] of Object.entries(positions ?? {})) {
|
|
19140
19895
|
const participant = this.participants.find((p) => p.id === memberId);
|
|
19141
19896
|
if (!participant) {
|
|
19142
19897
|
logger$12.warn(`[Call] setLayout: member ${memberId} not found in participants; skipping position ${position}`);
|
|
19143
19898
|
continue;
|
|
19144
19899
|
}
|
|
19145
|
-
|
|
19900
|
+
participant.target;
|
|
19901
|
+
targets.push([participant, position]);
|
|
19146
19902
|
}
|
|
19903
|
+
const selfId = await (0, import_cjs$11.firstValueFrom)(this.selfId$.pipe((0, import_cjs$11.filter)((id) => id !== null)));
|
|
19904
|
+
await this.executeMethod(selfId, "call.layout.set", { layout });
|
|
19905
|
+
for (const [participant, position] of targets) await participant.setPosition(position);
|
|
19147
19906
|
}
|
|
19148
19907
|
/**
|
|
19149
19908
|
* Transfers the call to another destination.
|
|
@@ -19245,17 +20004,28 @@ var WebRTCCall = class extends Destroyable {
|
|
|
19245
20004
|
* (notably iOS Safari) fall back to re-acquiring the track with the new
|
|
19246
20005
|
* constraint set and plumbing the replacement through the local audio
|
|
19247
20006
|
* pipeline if one is active.
|
|
20007
|
+
*
|
|
20008
|
+
* @returns whether the constraint reached the microphone. `false` is an
|
|
20009
|
+
* outcome rather than an error — a leg sending media the SDK did not capture
|
|
20010
|
+
* is left alone — so a UI that reflects the toggle must read it. Any failure
|
|
20011
|
+
* behind a `false` is also reported on {@link errors$}.
|
|
19248
20012
|
*/
|
|
19249
20013
|
async setEchoCancellation(enabled) {
|
|
19250
|
-
|
|
20014
|
+
return this.vertoManager.updateMediaConstraints({ audio: { echoCancellation: enabled } });
|
|
19251
20015
|
}
|
|
19252
|
-
/**
|
|
20016
|
+
/**
|
|
20017
|
+
* Toggle browser noise suppression on the local mic at runtime.
|
|
20018
|
+
* @returns whether the constraint reached the microphone.
|
|
20019
|
+
*/
|
|
19253
20020
|
async setNoiseSuppression(enabled) {
|
|
19254
|
-
|
|
20021
|
+
return this.vertoManager.updateMediaConstraints({ audio: { noiseSuppression: enabled } });
|
|
19255
20022
|
}
|
|
19256
|
-
/**
|
|
20023
|
+
/**
|
|
20024
|
+
* Toggle browser automatic gain control on the local mic at runtime.
|
|
20025
|
+
* @returns whether the constraint reached the microphone.
|
|
20026
|
+
*/
|
|
19257
20027
|
async setAutoGainControl(enabled) {
|
|
19258
|
-
|
|
20028
|
+
return this.vertoManager.updateMediaConstraints({ audio: { autoGainControl: enabled } });
|
|
19259
20029
|
}
|
|
19260
20030
|
/**
|
|
19261
20031
|
* Observable of the aggregate remote audio level, 0..1 RMS. The server
|
|
@@ -19312,6 +20082,9 @@ var WebRTCCall = class extends Destroyable {
|
|
|
19312
20082
|
/**
|
|
19313
20083
|
* Infers the semantic error category from a raw Error thrown by VertoManager
|
|
19314
20084
|
* or an RTCPeerConnection layer.
|
|
20085
|
+
*
|
|
20086
|
+
* Pure function — exported for unit testing.
|
|
20087
|
+
* @internal
|
|
19315
20088
|
*/
|
|
19316
20089
|
function inferCallErrorKind(error) {
|
|
19317
20090
|
if (error instanceof RPCTimeoutError) return "timeout";
|
|
@@ -19330,7 +20103,14 @@ const RECOVERABLE_RPC_CODES = new Set([
|
|
|
19330
20103
|
RPC_ERROR_AUTHENTICATION_FAILED,
|
|
19331
20104
|
RPC_ERROR_INVALID_PARAMS
|
|
19332
20105
|
]);
|
|
19333
|
-
/**
|
|
20106
|
+
/**
|
|
20107
|
+
* A *fallback*: callers knowing which leg failed pass an explicit `fatal` and
|
|
20108
|
+
* never reach here, so the default-fatal branch only sees call- and main-leg
|
|
20109
|
+
* errors. Auxiliary legs go through `WebRTCVertoManager.reportLegError`.
|
|
20110
|
+
*
|
|
20111
|
+
* Pure function — exported for unit testing.
|
|
20112
|
+
* @internal
|
|
20113
|
+
*/
|
|
19334
20114
|
function isFatalError(error) {
|
|
19335
20115
|
if (error instanceof VertoPongError) return false;
|
|
19336
20116
|
if (error instanceof MediaTrackError) return false;
|
|
@@ -19365,7 +20145,9 @@ var CallFactory = class {
|
|
|
19365
20145
|
kind: inferCallErrorKind(error),
|
|
19366
20146
|
fatal: options$1?.fatal ?? isFatalError(error),
|
|
19367
20147
|
error,
|
|
19368
|
-
callId: callInstance.id
|
|
20148
|
+
callId: callInstance.id,
|
|
20149
|
+
...options$1?.leg ? { leg: options$1.leg } : {},
|
|
20150
|
+
...options$1?.legId ? { legId: options$1.legId } : {}
|
|
19369
20151
|
};
|
|
19370
20152
|
callInstance.emitError(callError);
|
|
19371
20153
|
},
|
|
@@ -19837,6 +20619,30 @@ const logger$9 = getLogger();
|
|
|
19837
20619
|
function shouldAbortDial(callError) {
|
|
19838
20620
|
return callError.fatal || !(callError.error instanceof MediaAccessError);
|
|
19839
20621
|
}
|
|
20622
|
+
/**
|
|
20623
|
+
* Wait for a dialed call to be ready, or for the failure that stops it.
|
|
20624
|
+
*
|
|
20625
|
+
* Local media acquisition is deliberately unbounded: a permission prompt or a
|
|
20626
|
+
* device picker is human time, and `getUserMedia` cannot be cancelled anyway.
|
|
20627
|
+
* The clock starts only once acquisition settles, so a slow human never spends
|
|
20628
|
+
* the server's budget.
|
|
20629
|
+
*
|
|
20630
|
+
* `merge` rather than `race`, because the two legs settle asymmetrically. A
|
|
20631
|
+
* fatal acquisition failure reports the error and then destroys the call in the
|
|
20632
|
+
* same synchronous step; `errors$` defers delivery by a microtask while
|
|
20633
|
+
* `localMediaSettled$` completes immediately. Under `race` that bare completion
|
|
20634
|
+
* ended the wait first and `dial()` rejected with an RxJS `EmptyError`, burying
|
|
20635
|
+
* the `NotAllowedError` applications are told to inspect. Under `merge` the
|
|
20636
|
+
* completed leg is simply spent, and the queued error — enqueued before the
|
|
20637
|
+
* completion, so delivered before it — arrives to reject the wait. A dial
|
|
20638
|
+
* abandoned with no error at all still ends both legs, and the resulting
|
|
20639
|
+
* `EmptyError` remains the benign-cancel signal.
|
|
20640
|
+
*
|
|
20641
|
+
* Exported for unit testing.
|
|
20642
|
+
*/
|
|
20643
|
+
async function awaitDialReady(session, signalingTimeoutMs) {
|
|
20644
|
+
return (0, import_cjs$7.firstValueFrom)((0, import_cjs$7.merge)(session.localMediaSettled$.pipe((0, import_cjs$7.take)(1), (0, import_cjs$7.switchMap)(() => session.selfId$.pipe((0, import_cjs$7.filter)((id) => Boolean(id)), (0, import_cjs$7.take)(1), (0, import_cjs$7.timeout)(signalingTimeoutMs)))), session.errors$.pipe((0, import_cjs$7.filter)(shouldAbortDial), (0, import_cjs$7.take)(1), (0, import_cjs$7.switchMap)((callError) => (0, import_cjs$7.throwError)(() => callError.error)))));
|
|
20645
|
+
}
|
|
19840
20646
|
const getAddressSearchURI = (options) => {
|
|
19841
20647
|
const to = options.to?.split("?")[0];
|
|
19842
20648
|
const from$9 = options.from?.startsWith("subscriber://") ? options.from.replace("subscriber://", "") : options.from;
|
|
@@ -19853,7 +20659,6 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
19853
20659
|
this.authorizationStateKey = authorizationStateKey;
|
|
19854
20660
|
this.attachManager = attachManager;
|
|
19855
20661
|
this.dpopManager = dpopManager;
|
|
19856
|
-
this.callCreateTimeout = 6e3;
|
|
19857
20662
|
this.agent = `signalwire-js/4.0.0`;
|
|
19858
20663
|
this.eventAcks = true;
|
|
19859
20664
|
this.authorizationState$ = this.createReplaySubject(1);
|
|
@@ -19862,6 +20667,7 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
19862
20667
|
minor: 0,
|
|
19863
20668
|
revision: 0
|
|
19864
20669
|
};
|
|
20670
|
+
this.callControl = "routed";
|
|
19865
20671
|
this._authorization$ = this.createBehaviorSubject(void 0);
|
|
19866
20672
|
this._errors$ = this.createReplaySubject(1);
|
|
19867
20673
|
this._authState$ = this.createBehaviorSubject({ kind: "unauthenticated" });
|
|
@@ -20062,21 +20868,18 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
20062
20868
|
}
|
|
20063
20869
|
async handleAuthenticationError(error) {
|
|
20064
20870
|
logger$9.error("Authentication error:", error);
|
|
20065
|
-
const isRecoverableAuthError = error instanceof JSONRPCError && (error.code === RPC_ERROR_REQUESTER_VALIDATION_FAILED || error.code === RPC_ERROR_INVALID_PARAMS || error.code === RPC_ERROR_AUTHENTICATION_FAILED);
|
|
20871
|
+
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);
|
|
20066
20872
|
const hasStoredState = await (0, import_cjs$7.firstValueFrom)(this.authorizationState$.pipe((0, import_cjs$7.take)(1))) !== void 0;
|
|
20067
|
-
if (isRecoverableAuthError && hasStoredState) {
|
|
20873
|
+
if (isRecoverableAuthError$1 && hasStoredState) {
|
|
20068
20874
|
logger$9.debug("[Session] Recoverable auth error — cleaning up stored state and reconnecting fresh");
|
|
20069
|
-
|
|
20070
|
-
await this.cleanupStoredConnectionParams();
|
|
20071
|
-
} catch (cleanupError) {
|
|
20072
|
-
logger$9.error("Failed to cleanup stored connection params:", cleanupError);
|
|
20073
|
-
} finally {
|
|
20074
|
-
this.transport.reconnect();
|
|
20075
|
-
}
|
|
20875
|
+
await this.discardResumeStateAndReconnect();
|
|
20076
20876
|
} else this._errors$.next(error);
|
|
20077
20877
|
}
|
|
20078
20878
|
/**
|
|
20079
|
-
* Clear the resume state (authorization_state + protocol)
|
|
20879
|
+
* Clear the resume state (authorization_state + protocol) and ask the
|
|
20880
|
+
* transport to reconnect. The `connected` event re-triggers
|
|
20881
|
+
* `authenticate()`, which now has no stored state and so performs a fresh
|
|
20882
|
+
* connect.
|
|
20080
20883
|
*
|
|
20081
20884
|
* This is the stale-auth-state recovery helper used by handleAuthError:
|
|
20082
20885
|
* the server rejected a reconnect, so the resume state is discarded and a
|
|
@@ -20084,9 +20887,24 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
20084
20887
|
* session lives on through the reconnect and reattachCalls() needs the
|
|
20085
20888
|
* stored call references afterwards. Do NOT add detachAll() here.
|
|
20086
20889
|
*
|
|
20890
|
+
* Connect-time recovery only. A *request* refused on an already
|
|
20891
|
+
* authenticated session is never healed here: dropping the resume state
|
|
20892
|
+
* destroys the association between the socket and the previous session,
|
|
20893
|
+
* which is what reattach depends on. That path mints a fresh credential and
|
|
20894
|
+
* reauthenticates instead (see `SignalWire.recoverAndRetry`).
|
|
20895
|
+
*
|
|
20087
20896
|
* For public teardown (disconnect/destroy), use {@link teardownSessionState}
|
|
20088
20897
|
* instead, which clears the attach records as well.
|
|
20089
20898
|
*/
|
|
20899
|
+
async discardResumeStateAndReconnect() {
|
|
20900
|
+
try {
|
|
20901
|
+
await this.cleanupStoredConnectionParams();
|
|
20902
|
+
} catch (cleanupError) {
|
|
20903
|
+
logger$9.error("Failed to cleanup stored connection params:", cleanupError);
|
|
20904
|
+
} finally {
|
|
20905
|
+
this.transport.reconnect();
|
|
20906
|
+
}
|
|
20907
|
+
}
|
|
20090
20908
|
async cleanupStoredConnectionParams() {
|
|
20091
20909
|
await this.transport.setProtocol(void 0);
|
|
20092
20910
|
await this.updateAuthorizationStateInStorage(void 0);
|
|
@@ -20160,11 +20978,15 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
20160
20978
|
const isReconnect = hasReconnectState && storedToken;
|
|
20161
20979
|
let dpopToken;
|
|
20162
20980
|
if (isReconnect) logger$9.debug("[Session] Reconnecting with stored jwt_token + authorization_state");
|
|
20163
|
-
else
|
|
20164
|
-
|
|
20165
|
-
|
|
20981
|
+
else {
|
|
20982
|
+
const credential = this.getCredential();
|
|
20983
|
+
const credentialExpired = credential.expiry_at !== void 0 && credential.expiry_at <= Date.now() + CREDENTIAL_EXPIRY_SKEW_MS;
|
|
20984
|
+
if (this.onBeforeReconnect && (this.clientBound || credentialExpired)) {
|
|
20985
|
+
logger$9.debug("[Session] Refreshing credentials before fresh connect");
|
|
20986
|
+
await this.onBeforeReconnect();
|
|
20987
|
+
}
|
|
20166
20988
|
}
|
|
20167
|
-
if (
|
|
20989
|
+
if (this.dpopManager?.initialized) try {
|
|
20168
20990
|
dpopToken = await this.dpopManager.createRpcProof({ method: "signalwire.connect" });
|
|
20169
20991
|
} catch (error) {
|
|
20170
20992
|
if (this.clientBound) throw error;
|
|
@@ -20197,6 +21019,7 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
20197
21019
|
});
|
|
20198
21020
|
if (response.protocol) await this.transport.setProtocol(response.protocol);
|
|
20199
21021
|
this._authorization$.next(response.authorization);
|
|
21022
|
+
if (response.authorization.cnf?.jkt) this._wasClientBound = true;
|
|
20200
21023
|
this._iceServers$.next(response.ice_servers ?? []);
|
|
20201
21024
|
this._authState$.next({ kind: "authenticated" });
|
|
20202
21025
|
logger$9.debug("[Session] Authentication completed successfully");
|
|
@@ -20263,7 +21086,7 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
20263
21086
|
to: destinationURI,
|
|
20264
21087
|
...options
|
|
20265
21088
|
});
|
|
20266
|
-
await (
|
|
21089
|
+
await awaitDialReady(callSession, DEFAULT_CALL_SIGNALING_TIMEOUT_MS);
|
|
20267
21090
|
this._calls$.next({
|
|
20268
21091
|
[`${callSession.id}`]: callSession,
|
|
20269
21092
|
...this._calls$.value
|
|
@@ -20316,12 +21139,23 @@ var ClientSessionWrapper = class {
|
|
|
20316
21139
|
get authenticated() {
|
|
20317
21140
|
return this.clientSessionManager.authenticated;
|
|
20318
21141
|
}
|
|
21142
|
+
/**
|
|
21143
|
+
* Whether the session is using a Client Bound SAT (DPoP). Sticky — set
|
|
21144
|
+
* when the binding is established or restored from a resumed session's
|
|
21145
|
+
* server authorization.
|
|
21146
|
+
*/
|
|
21147
|
+
get clientBound() {
|
|
21148
|
+
return this.clientSessionManager.clientBound;
|
|
21149
|
+
}
|
|
20319
21150
|
get signalingEvent$() {
|
|
20320
21151
|
return this.clientSessionManager.signalingEvent$;
|
|
20321
21152
|
}
|
|
20322
21153
|
get iceServers() {
|
|
20323
21154
|
return this.clientSessionManager.iceServers;
|
|
20324
21155
|
}
|
|
21156
|
+
get callControl() {
|
|
21157
|
+
return this.clientSessionManager.callControl;
|
|
21158
|
+
}
|
|
20325
21159
|
async execute(request, options) {
|
|
20326
21160
|
return this.clientSessionManager.execute(request, options);
|
|
20327
21161
|
}
|
|
@@ -20518,7 +21352,7 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
20518
21352
|
await session.reauthenticate(tokenData.token, rpcProof, { clientBound: true });
|
|
20519
21353
|
updateCredential({ token: tokenData.token });
|
|
20520
21354
|
logger$7.info("[DeviceToken] Client Bound SAT activated successfully");
|
|
20521
|
-
this.
|
|
21355
|
+
this.emitCurrentToken(tokenData);
|
|
20522
21356
|
return { activated: true };
|
|
20523
21357
|
} catch (error) {
|
|
20524
21358
|
logger$7.error("[DeviceToken] Failed to activate Client Bound SAT:", error);
|
|
@@ -20530,6 +21364,21 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
20530
21364
|
}
|
|
20531
21365
|
}
|
|
20532
21366
|
/**
|
|
21367
|
+
* Emit a freshly received token to the reactive pipeline, stamping an
|
|
21368
|
+
* absolute `expires_at` when the response carried only `expires_in`.
|
|
21369
|
+
* Resolving the expiry at RECEIVE time (not at read time) is what lets
|
|
21370
|
+
* {@link refreshNowIfDue} detect due-ness on resume: a bare `expires_in`
|
|
21371
|
+
* re-resolved later would always compute a full TTL from "now" and never
|
|
21372
|
+
* cross the refresh buffer.
|
|
21373
|
+
*/
|
|
21374
|
+
emitCurrentToken(token) {
|
|
21375
|
+
const stamped = token.expires_at ? token : {
|
|
21376
|
+
...token,
|
|
21377
|
+
expires_at: resolveExpiresAt(token)
|
|
21378
|
+
};
|
|
21379
|
+
this._currentToken$.next(stamped);
|
|
21380
|
+
}
|
|
21381
|
+
/**
|
|
20533
21382
|
* Returns true when the cached token has enough headroom before expiry to
|
|
20534
21383
|
* be safely reused on reactivation. The headroom matches the refresh
|
|
20535
21384
|
* buffer, so a token within the refresh window is treated as stale (the
|
|
@@ -20547,7 +21396,7 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
20547
21396
|
method: "POST",
|
|
20548
21397
|
uri: DEVICE_TOKEN_ENDPOINT
|
|
20549
21398
|
});
|
|
20550
|
-
const response = await this.http.request({
|
|
21399
|
+
const response = await this.http().request({
|
|
20551
21400
|
url: DEVICE_TOKEN_ENDPOINT,
|
|
20552
21401
|
...POST_PARAMS,
|
|
20553
21402
|
body: JSON.stringify({
|
|
@@ -20574,7 +21423,7 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
20574
21423
|
uri: DEVICE_REFRESH_ENDPOINT,
|
|
20575
21424
|
accessToken: currentToken
|
|
20576
21425
|
});
|
|
20577
|
-
const response = await this.http.request({
|
|
21426
|
+
const response = await this.http().request({
|
|
20578
21427
|
url: DEVICE_REFRESH_ENDPOINT,
|
|
20579
21428
|
...POST_PARAMS,
|
|
20580
21429
|
body: JSON.stringify({
|
|
@@ -20622,7 +21471,7 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
20622
21471
|
const currentToken = this.getCredential().token;
|
|
20623
21472
|
if (!currentToken) throw new TokenRefreshError("No current token available for refresh");
|
|
20624
21473
|
const newTokenData = await this.retryRefresh(session, currentToken, updateCredential);
|
|
20625
|
-
this.
|
|
21474
|
+
this.emitCurrentToken(newTokenData);
|
|
20626
21475
|
} catch (error) {
|
|
20627
21476
|
logger$7.error("[DeviceToken] Automatic Client Bound SAT refresh failed:", error);
|
|
20628
21477
|
this.errorHandler(error instanceof TokenRefreshError ? error : new TokenRefreshError("Automatic token refresh failed", error));
|
|
@@ -20649,6 +21498,22 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
20649
21498
|
throw lastError instanceof Error ? lastError : new TokenRefreshError("All refresh retries exhausted", lastError);
|
|
20650
21499
|
}
|
|
20651
21500
|
/**
|
|
21501
|
+
* Force an immediate refresh when the cached Client Bound SAT is already
|
|
21502
|
+
* past its refresh window. Called on resume from suspension where
|
|
21503
|
+
* background-tab throttling can delay the reactive timer past the buffer.
|
|
21504
|
+
* A no-op when no token is cached or it still has headroom; the normal
|
|
21505
|
+
* {@link executeRefresh} guards (paused / in-progress / unauthenticated)
|
|
21506
|
+
* still apply.
|
|
21507
|
+
*/
|
|
21508
|
+
refreshNowIfDue() {
|
|
21509
|
+
const token = this._currentToken$.value;
|
|
21510
|
+
if (!token) return;
|
|
21511
|
+
if (resolveExpiresAt(token) * 1e3 - Date.now() <= DEVICE_TOKEN_REFRESH_BUFFER_MS) {
|
|
21512
|
+
logger$7.debug("[DeviceToken] Resume: cached SAT past refresh window; refreshing now");
|
|
21513
|
+
this.executeRefresh();
|
|
21514
|
+
}
|
|
21515
|
+
}
|
|
21516
|
+
/**
|
|
20652
21517
|
* Stops the reactive refresh pipeline from firing. Use when the underlying
|
|
20653
21518
|
* session is being torn down (e.g., during {@link SignalWire.disconnect})
|
|
20654
21519
|
* so a scheduled refresh cannot fire against a destroyed session.
|
|
@@ -20699,6 +21564,7 @@ var CredentialRefreshCoordinator = class extends Destroyable {
|
|
|
20699
21564
|
this.deps = deps;
|
|
20700
21565
|
this._activating = false;
|
|
20701
21566
|
this._activationGeneration = 0;
|
|
21567
|
+
this._developerRefreshInProgress = false;
|
|
20702
21568
|
if (dpopManager?.initialized) this._deviceTokenManager = (deps.deviceTokenManagerFactory ?? defaultDeviceTokenManagerFactory)(dpopManager, deps.http, (error) => deps.notifier.onError(error), () => deps.store.read());
|
|
20703
21569
|
}
|
|
20704
21570
|
/** True when the Client Bound SAT path is available (DPoP initialized). */
|
|
@@ -20717,28 +21583,120 @@ var CredentialRefreshCoordinator = class extends Destroyable {
|
|
|
20717
21583
|
* invokes `deps.onRefreshExhausted` so the orchestrator can disconnect.
|
|
20718
21584
|
*/
|
|
20719
21585
|
scheduleDeveloperRefresh(provider, expiresAt, attempt = 0) {
|
|
21586
|
+
this._activeProvider = provider;
|
|
20720
21587
|
if (this._developerTimerId !== void 0) clearTimeout(this._developerTimerId);
|
|
20721
21588
|
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);
|
|
20722
|
-
this._developerTimerId = setTimeout(
|
|
21589
|
+
this._developerTimerId = setTimeout(() => {
|
|
21590
|
+
this._developerTimerId = void 0;
|
|
21591
|
+
this.executeDeveloperRefresh(provider, expiresAt, attempt);
|
|
21592
|
+
}, refreshInterval);
|
|
21593
|
+
}
|
|
21594
|
+
/**
|
|
21595
|
+
* Runs the developer-provided refresh once: mints a new credential, stores
|
|
21596
|
+
* and persists it, reauthenticates the live session (via the notifier), and
|
|
21597
|
+
* reschedules against the new expiry. On failure retries with backoff up to
|
|
21598
|
+
* {@link CREDENTIAL_REFRESH_MAX_RETRIES}, then signals exhaustion.
|
|
21599
|
+
*
|
|
21600
|
+
* Shared by the scheduled timer tick and {@link forceRefreshIfDue}. The
|
|
21601
|
+
* `_developerRefreshInProgress` guard prevents the two from overlapping.
|
|
21602
|
+
*/
|
|
21603
|
+
async executeDeveloperRefresh(provider, expiresAt, attempt) {
|
|
21604
|
+
if (this._developerRefreshInProgress) {
|
|
21605
|
+
logger$6.debug("[Coordinator] Developer refresh already in progress; skipping");
|
|
21606
|
+
return;
|
|
21607
|
+
}
|
|
21608
|
+
this._developerRefreshInProgress = true;
|
|
21609
|
+
try {
|
|
21610
|
+
const newCredentials = await this.refreshCredential(provider);
|
|
21611
|
+
this.deps.store.write(newCredentials);
|
|
21612
|
+
this.deps.store.persist(newCredentials);
|
|
20723
21613
|
try {
|
|
20724
|
-
|
|
20725
|
-
|
|
20726
|
-
|
|
20727
|
-
this.deps.store.persist(newCredentials);
|
|
20728
|
-
logger$6.info("[Coordinator] Credentials refreshed successfully.");
|
|
20729
|
-
if (newCredentials.expiry_at) this.scheduleDeveloperRefresh(provider, newCredentials.expiry_at, 0);
|
|
20730
|
-
} catch (error) {
|
|
20731
|
-
const nextAttempt = attempt + 1;
|
|
20732
|
-
logger$6.error(`[Coordinator] Credential refresh failed (attempt ${nextAttempt}/${CREDENTIAL_REFRESH_MAX_RETRIES}):`, error);
|
|
20733
|
-
this.deps.notifier.onError(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
20734
|
-
if (nextAttempt < CREDENTIAL_REFRESH_MAX_RETRIES) this.scheduleDeveloperRefresh(provider, expiresAt, nextAttempt);
|
|
20735
|
-
else {
|
|
20736
|
-
logger$6.error("[Coordinator] Credential refresh exhausted all retries. Disconnecting.");
|
|
20737
|
-
this.deps.notifier.onError(new TokenRefreshError("Credential refresh failed after max retries"));
|
|
20738
|
-
this.deps.notifier.onRefreshExhausted();
|
|
20739
|
-
}
|
|
21614
|
+
await this.deps.notifier.onCredentialRefreshed(newCredentials);
|
|
21615
|
+
} catch (reauthError) {
|
|
21616
|
+
logger$6.warn("[Coordinator] onCredentialRefreshed rejected (non-fatal):", reauthError);
|
|
20740
21617
|
}
|
|
20741
|
-
|
|
21618
|
+
logger$6.info("[Coordinator] Credentials refreshed successfully.");
|
|
21619
|
+
if (newCredentials.expiry_at) this.scheduleDeveloperRefresh(provider, newCredentials.expiry_at, 0);
|
|
21620
|
+
} catch (error) {
|
|
21621
|
+
const nextAttempt = attempt + 1;
|
|
21622
|
+
logger$6.error(`[Coordinator] Credential refresh failed (attempt ${nextAttempt}/${CREDENTIAL_REFRESH_MAX_RETRIES}):`, error);
|
|
21623
|
+
this.deps.notifier.onError(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
21624
|
+
if (nextAttempt < CREDENTIAL_REFRESH_MAX_RETRIES) this.scheduleDeveloperRefresh(provider, expiresAt, nextAttempt);
|
|
21625
|
+
else {
|
|
21626
|
+
logger$6.error("[Coordinator] Credential refresh exhausted all retries. Disconnecting.");
|
|
21627
|
+
this.deps.notifier.onError(new TokenRefreshError("Credential refresh failed after max retries"));
|
|
21628
|
+
this.deps.notifier.onRefreshExhausted();
|
|
21629
|
+
}
|
|
21630
|
+
} finally {
|
|
21631
|
+
this._developerRefreshInProgress = false;
|
|
21632
|
+
}
|
|
21633
|
+
}
|
|
21634
|
+
/**
|
|
21635
|
+
* Force an immediate refresh when the current credential is already past its
|
|
21636
|
+
* scheduled refresh window. Called on resume from suspension, where
|
|
21637
|
+
* background-tab timer throttling can delay the armed refresh well past
|
|
21638
|
+
* expiry, leaving the live session stale.
|
|
21639
|
+
*
|
|
21640
|
+
* Routes to whichever mechanism is armed: the developer timer if armed,
|
|
21641
|
+
* otherwise the Client Bound SAT pipeline. A no-op when nothing is due.
|
|
21642
|
+
*/
|
|
21643
|
+
forceRefreshIfDue() {
|
|
21644
|
+
if (this._developerTimerId !== void 0 && this._activeProvider) {
|
|
21645
|
+
const expiry = this.deps.store.read().expiry_at;
|
|
21646
|
+
if (expiry !== void 0 && Date.now() >= expiry - CREDENTIAL_REFRESH_BUFFER_MS) {
|
|
21647
|
+
logger$6.debug("[Coordinator] Resume: credential past refresh window; forcing refresh");
|
|
21648
|
+
clearTimeout(this._developerTimerId);
|
|
21649
|
+
this._developerTimerId = void 0;
|
|
21650
|
+
this.executeDeveloperRefresh(this._activeProvider, expiry, 0);
|
|
21651
|
+
}
|
|
21652
|
+
return;
|
|
21653
|
+
}
|
|
21654
|
+
this._deviceTokenManager?.refreshNowIfDue();
|
|
21655
|
+
}
|
|
21656
|
+
/**
|
|
21657
|
+
* Sync the credential's expiry from the server-provided authorization (the
|
|
21658
|
+
* `signalwire.connect` result). SATs are opaque JWE, so
|
|
21659
|
+
* `fabric_subscriber.expires_at` is the authoritative expiry of the token
|
|
21660
|
+
* the session actually connected with — the provider-reported `expiry_at`
|
|
21661
|
+
* is only a hint (and may be wrong or absent). Corrects the stored
|
|
21662
|
+
* credential and re-arms the developer refresh timer against the real
|
|
21663
|
+
* deadline when the provider supports `refresh()`.
|
|
21664
|
+
*/
|
|
21665
|
+
syncExpiryFromAuthorization(authorization, provider) {
|
|
21666
|
+
const expiresAtSec = authorization?.fabric_subscriber?.expires_at;
|
|
21667
|
+
if (!expiresAtSec) return;
|
|
21668
|
+
const expiryAt = expiresAtSec * 1e3;
|
|
21669
|
+
const credential = this.deps.store.read();
|
|
21670
|
+
if (credential.expiry_at === expiryAt) return;
|
|
21671
|
+
logger$6.debug(`[Coordinator] Correcting credential expiry from server authorization: ${new Date(expiryAt).toISOString()}`);
|
|
21672
|
+
const updated = {
|
|
21673
|
+
...credential,
|
|
21674
|
+
expiry_at: expiryAt
|
|
21675
|
+
};
|
|
21676
|
+
this.deps.store.write(updated);
|
|
21677
|
+
this.deps.store.persist(updated);
|
|
21678
|
+
if (provider?.refresh) this.scheduleDeveloperRefresh(provider, expiryAt);
|
|
21679
|
+
}
|
|
21680
|
+
/**
|
|
21681
|
+
* Invoke `provider.refresh()` deduped against any concurrent developer
|
|
21682
|
+
* refresh. Concurrent callers — the scheduled tick, a resume-forced refresh,
|
|
21683
|
+
* and the orchestrator's -32003 recovery / reconnect re-mint — share one
|
|
21684
|
+
* in-flight promise, so a provider backed by one-time-use rotating refresh
|
|
21685
|
+
* tokens is never invoked twice in parallel.
|
|
21686
|
+
*
|
|
21687
|
+
* The caller owns applying the returned credential (store write, session
|
|
21688
|
+
* reauth, rescheduling); this method only serializes the network call.
|
|
21689
|
+
*/
|
|
21690
|
+
async refreshCredential(provider) {
|
|
21691
|
+
if (this._refreshInFlight) return this._refreshInFlight;
|
|
21692
|
+
if (!provider.refresh) throw new InvalidCredentialsError("Credential provider does not support refresh");
|
|
21693
|
+
const run = provider.refresh();
|
|
21694
|
+
this._refreshInFlight = run;
|
|
21695
|
+
const clear = () => {
|
|
21696
|
+
if (this._refreshInFlight === run) this._refreshInFlight = void 0;
|
|
21697
|
+
};
|
|
21698
|
+
run.then(clear, clear);
|
|
21699
|
+
return run;
|
|
20742
21700
|
}
|
|
20743
21701
|
/**
|
|
20744
21702
|
* Cancels any scheduled developer-provided refresh. Idempotent.
|
|
@@ -21522,6 +22480,13 @@ var TransportManager = class extends Destroyable {
|
|
|
21522
22480
|
//#region src/clients/SignalWire.ts
|
|
21523
22481
|
var import_cjs$1 = require_cjs();
|
|
21524
22482
|
const logger$1 = getLogger();
|
|
22483
|
+
/**
|
|
22484
|
+
* Storage key for the client-bound marker. The SAT and authorization_state are
|
|
22485
|
+
* both opaque to the SDK, so on a page reload the preflight recovery — which
|
|
22486
|
+
* runs before any session exists — has no other way to know the session was
|
|
22487
|
+
* client-bound. See {@link SignalWire.persistClientBoundMarker}.
|
|
22488
|
+
*/
|
|
22489
|
+
const CLIENT_BOUND_STORAGE_KEY = "sw:client_bound";
|
|
21525
22490
|
const buildOptionsFromDestination = (destination) => {
|
|
21526
22491
|
if (typeof destination === "string") {
|
|
21527
22492
|
const queryStartIndex = destination.indexOf("?");
|
|
@@ -21565,6 +22530,7 @@ var SignalWire = class extends Destroyable {
|
|
|
21565
22530
|
this.preferences = new ClientPreferences();
|
|
21566
22531
|
this._user$ = this.createBehaviorSubject(void 0);
|
|
21567
22532
|
this._directory$ = this.createBehaviorSubject(void 0);
|
|
22533
|
+
this._credentialRecovered = false;
|
|
21568
22534
|
this._isConnected$ = this.createBehaviorSubject(false);
|
|
21569
22535
|
this._isRegistered$ = this.createBehaviorSubject(false);
|
|
21570
22536
|
this._errors$ = this.createReplaySubject(1);
|
|
@@ -21603,6 +22569,16 @@ var SignalWire = class extends Destroyable {
|
|
|
21603
22569
|
});
|
|
21604
22570
|
}
|
|
21605
22571
|
/**
|
|
22572
|
+
* Build the refresh path's own HTTP controller, against whatever host is current.
|
|
22573
|
+
*
|
|
22574
|
+
* Called on first use rather than up front, so `apiHost` already reflects the
|
|
22575
|
+
* token's `ch` claim. Same credential source as the container's controller — only
|
|
22576
|
+
* the instance, and therefore its observable streams, is separate.
|
|
22577
|
+
*/
|
|
22578
|
+
createRefreshHttpController() {
|
|
22579
|
+
return new HTTPRequestController(this._deps.apiHost, () => this._deps.credential);
|
|
22580
|
+
}
|
|
22581
|
+
/**
|
|
21606
22582
|
* Initializes DPoP if not already set up. Returns the fingerprint on success.
|
|
21607
22583
|
*/
|
|
21608
22584
|
async initDPoP() {
|
|
@@ -21629,11 +22605,19 @@ var SignalWire = class extends Destroyable {
|
|
|
21629
22605
|
async resolveCredentials() {
|
|
21630
22606
|
const fingerprint = await this.initDPoP();
|
|
21631
22607
|
this._refreshCoordinator = new CredentialRefreshCoordinator(this._dpopManager, {
|
|
21632
|
-
http:
|
|
22608
|
+
http: () => {
|
|
22609
|
+
if (!this._refreshHttp || this._refreshHttpHost !== this._deps.apiHost) {
|
|
22610
|
+
this._refreshHttp?.destroy();
|
|
22611
|
+
this._refreshHttp = this.createRefreshHttpController();
|
|
22612
|
+
this._refreshHttpHost = this._deps.apiHost;
|
|
22613
|
+
}
|
|
22614
|
+
return this._refreshHttp;
|
|
22615
|
+
},
|
|
21633
22616
|
notifier: {
|
|
21634
22617
|
onError: (error) => this._errors$.next(error),
|
|
21635
22618
|
onWarning: (warning) => this._warnings$.next(warning),
|
|
21636
|
-
onRefreshExhausted: () => void this.disconnect()
|
|
22619
|
+
onRefreshExhausted: () => void this.disconnect(),
|
|
22620
|
+
onCredentialRefreshed: async (credential) => this.reauthenticateLiveSession(credential)
|
|
21637
22621
|
},
|
|
21638
22622
|
store: {
|
|
21639
22623
|
read: () => this._deps.credential,
|
|
@@ -21645,6 +22629,7 @@ var SignalWire = class extends Destroyable {
|
|
|
21645
22629
|
...this._deps.credential,
|
|
21646
22630
|
...partial
|
|
21647
22631
|
};
|
|
22632
|
+
this.persistCredential(this._deps.credential);
|
|
21648
22633
|
},
|
|
21649
22634
|
persist: (credential) => this.persistCredential(credential)
|
|
21650
22635
|
}
|
|
@@ -21690,20 +22675,181 @@ var SignalWire = class extends Destroyable {
|
|
|
21690
22675
|
}
|
|
21691
22676
|
this._deps.credential = _credentials;
|
|
21692
22677
|
this.persistCredential(_credentials);
|
|
21693
|
-
|
|
21694
|
-
|
|
22678
|
+
await this.reauthenticateLiveSession(_credentials);
|
|
22679
|
+
}
|
|
22680
|
+
/**
|
|
22681
|
+
* Reauthenticate the currently-open session with a freshly obtained
|
|
22682
|
+
* credential so the new token takes effect on the live socket immediately —
|
|
22683
|
+
* not just on the next reconnect. No-op when the session is not
|
|
22684
|
+
* connected/authenticated or the credential carries no token (e.g. an
|
|
22685
|
+
* authorization-state-only refresh). Non-fatal: reauth failures surface on
|
|
22686
|
+
* `errors$` without aborting the refresh that triggered this.
|
|
22687
|
+
*/
|
|
22688
|
+
async reauthenticateLiveSession(credential) {
|
|
22689
|
+
if (!this.isConnected || !this._clientSession.authenticated || !credential.token) return;
|
|
22690
|
+
try {
|
|
22691
|
+
await this._clientSession.reauthenticate(credential.token);
|
|
21695
22692
|
logger$1.info("[SignalWire] Session refreshed with new credentials.");
|
|
21696
22693
|
} catch (error) {
|
|
21697
22694
|
logger$1.error("[SignalWire] Failed to refresh session with new credentials:", error);
|
|
21698
22695
|
this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
21699
22696
|
}
|
|
21700
22697
|
}
|
|
22698
|
+
/**
|
|
22699
|
+
* Recover a session the server is refusing: mint a fresh credential,
|
|
22700
|
+
* reauthenticate the live session with it, and retry the operation.
|
|
22701
|
+
*
|
|
22702
|
+
* The connection is deliberately kept. A reload authenticates the new socket
|
|
22703
|
+
* against the persisted `authorization_state`, and that handshake is what
|
|
22704
|
+
* associates the socket with the previous session — the association reattach
|
|
22705
|
+
* depends on. `signalwire.reauthenticate` swaps the credential *on that same
|
|
22706
|
+
* session*, so recovery never touches the resume state. Discarding it would
|
|
22707
|
+
* heal the credential by destroying the very thing the caller is trying to
|
|
22708
|
+
* get back to.
|
|
22709
|
+
*
|
|
22710
|
+
* The operation is still the verdict, never the RPC. Reauthenticating with
|
|
22711
|
+
* the in-memory token is accepted by a resume even while requests stay
|
|
22712
|
+
* refused, because the persisted `authorization_state` short-circuits token
|
|
22713
|
+
* validation — and `signalwire.reauthenticate` with a *freshly minted* token
|
|
22714
|
+
* has also been observed accepted while `subscriber.online` keeps being
|
|
22715
|
+
* refused (staging run 33826974634). Both look like success and are not.
|
|
22716
|
+
*
|
|
22717
|
+
* @returns the operation's value, or the reason recovery could not deliver
|
|
22718
|
+
* one. `error` is undefined when there was no way to mint at all.
|
|
22719
|
+
*/
|
|
22720
|
+
async recoverAndRetry(operation) {
|
|
22721
|
+
if (!await this.remintAndReauthenticate()) return { ok: false };
|
|
22722
|
+
try {
|
|
22723
|
+
const value = await operation();
|
|
22724
|
+
this._credentialRecovered = true;
|
|
22725
|
+
return {
|
|
22726
|
+
ok: true,
|
|
22727
|
+
value
|
|
22728
|
+
};
|
|
22729
|
+
} catch (error) {
|
|
22730
|
+
logger$1.warn("[SignalWire] Reauthentication was accepted but the operation is still refused:", error);
|
|
22731
|
+
return {
|
|
22732
|
+
ok: false,
|
|
22733
|
+
error
|
|
22734
|
+
};
|
|
22735
|
+
}
|
|
22736
|
+
}
|
|
22737
|
+
/**
|
|
22738
|
+
* Re-mint a credential and adopt it only if the live session accepts it.
|
|
22739
|
+
*
|
|
22740
|
+
* The mechanism follows the binding: a client-bound session re-mints a bound
|
|
22741
|
+
* base SAT through `authenticate()` with the DPoP fingerprint, because the
|
|
22742
|
+
* developer refresh handler would hand back an unbound token and silently
|
|
22743
|
+
* degrade the session. An unbound session uses the refresh handler. Rotation
|
|
22744
|
+
* cost is not a reason to skip this — the only reason is having no mechanism.
|
|
22745
|
+
*
|
|
22746
|
+
* @returns whether the session is now running on a freshly accepted credential.
|
|
22747
|
+
*/
|
|
22748
|
+
async remintAndReauthenticate() {
|
|
22749
|
+
const provider = this._credentialProvider;
|
|
22750
|
+
if (!provider) return false;
|
|
22751
|
+
const { clientBound } = this._clientSession;
|
|
22752
|
+
if (!clientBound && !provider.refresh) {
|
|
22753
|
+
logger$1.debug("[SignalWire] [SW-NO-REFRESH-HANDLER] Unbound session with no refresh handler; cannot re-mint.");
|
|
22754
|
+
return false;
|
|
22755
|
+
}
|
|
22756
|
+
try {
|
|
22757
|
+
const newCredentials = clientBound ? await provider.authenticate(this._dpopManager?.initialized ? { fingerprint: this._dpopManager.fingerprint } : void 0) : await this.remintCredential(provider);
|
|
22758
|
+
if (!newCredentials.token) {
|
|
22759
|
+
logger$1.warn("[SignalWire] Re-minted credential has no token; keeping the current one.");
|
|
22760
|
+
return false;
|
|
22761
|
+
}
|
|
22762
|
+
await this._clientSession.reauthenticate(newCredentials.token);
|
|
22763
|
+
this._deps.credential = newCredentials;
|
|
22764
|
+
this.persistCredential(newCredentials);
|
|
22765
|
+
if (newCredentials.expiry_at && provider.refresh) this._refreshCoordinator?.scheduleDeveloperRefresh(provider, newCredentials.expiry_at);
|
|
22766
|
+
return true;
|
|
22767
|
+
} catch (error) {
|
|
22768
|
+
logger$1.warn("[SignalWire] Re-mint recovery failed:", error);
|
|
22769
|
+
return false;
|
|
22770
|
+
}
|
|
22771
|
+
}
|
|
22772
|
+
/**
|
|
22773
|
+
* Re-mint a credential via `provider.refresh()`, routed through the
|
|
22774
|
+
* coordinator's shared in-flight guard so concurrent re-mint paths (a
|
|
22775
|
+
* scheduled/resume refresh, -32003 recovery, and reconnect) never fire a
|
|
22776
|
+
* second `provider.refresh()` in parallel — which rotating one-time-use
|
|
22777
|
+
* refresh tokens reject. Falls back to a direct call only if the coordinator
|
|
22778
|
+
* has not been constructed yet.
|
|
22779
|
+
*/
|
|
22780
|
+
async remintCredential(provider) {
|
|
22781
|
+
if (this._refreshCoordinator) return this._refreshCoordinator.refreshCredential(provider);
|
|
22782
|
+
if (!provider.refresh) throw new InvalidCredentialsError("Credential provider does not support refresh");
|
|
22783
|
+
return provider.refresh();
|
|
22784
|
+
}
|
|
22785
|
+
/**
|
|
22786
|
+
* Re-mint credentials before a fresh (re)connect (`onBeforeReconnect` hook).
|
|
22787
|
+
* The session invokes this only when it is client-bound OR the in-memory
|
|
22788
|
+
* token is expired. The re-mint mechanism depends on the binding:
|
|
22789
|
+
* - Client-bound: `authenticate()` with the DPoP fingerprint to obtain a
|
|
22790
|
+
* fresh base SAT the upcoming reconnect can re-bind (the
|
|
22791
|
+
* DeviceTokenManager re-activates afterwards).
|
|
22792
|
+
* - Unbound: the developer's non-interactive `refresh()` handler.
|
|
22793
|
+
* `authenticate()` is deliberately NOT used here — it may be interactive
|
|
22794
|
+
* (a login prompt) and must not fire on a background reconnect.
|
|
22795
|
+
*
|
|
22796
|
+
* Rejects on failure so the session aborts the reconnect rather than
|
|
22797
|
+
* replaying a stale token.
|
|
22798
|
+
*/
|
|
22799
|
+
async refreshCredentialForReconnect() {
|
|
22800
|
+
if (!this._credentialProvider) return;
|
|
22801
|
+
try {
|
|
22802
|
+
let newCredentials;
|
|
22803
|
+
if (this._clientSession?.clientBound ?? await this.wasClientBound()) {
|
|
22804
|
+
logger$1.debug("[SignalWire] Re-minting client-bound base SAT before reconnect");
|
|
22805
|
+
newCredentials = await this._credentialProvider.authenticate(this._dpopManager?.initialized ? { fingerprint: this._dpopManager.fingerprint } : void 0);
|
|
22806
|
+
} else if (this._credentialProvider.refresh) {
|
|
22807
|
+
logger$1.debug("[SignalWire] Refreshing unbound credential before reconnect");
|
|
22808
|
+
newCredentials = await this.remintCredential(this._credentialProvider);
|
|
22809
|
+
} else {
|
|
22810
|
+
logger$1.warn("[SignalWire] [SW-NO-REFRESH-HANDLER] Token expired on reconnect but no refresh handler; reconnecting with the existing token.");
|
|
22811
|
+
return;
|
|
22812
|
+
}
|
|
22813
|
+
if (!newCredentials.token) {
|
|
22814
|
+
logger$1.warn("[SignalWire] Re-minted credential has no token; keeping the existing credential for reconnect.");
|
|
22815
|
+
return;
|
|
22816
|
+
}
|
|
22817
|
+
this._deps.credential = newCredentials;
|
|
22818
|
+
this.persistCredential(newCredentials);
|
|
22819
|
+
if (newCredentials.expiry_at && this._credentialProvider.refresh) this._refreshCoordinator?.scheduleDeveloperRefresh(this._credentialProvider, newCredentials.expiry_at);
|
|
22820
|
+
logger$1.debug("[SignalWire] Credential refreshed successfully for reconnect");
|
|
22821
|
+
} catch (error) {
|
|
22822
|
+
logger$1.error("[SignalWire] Failed to refresh credentials for reconnect:", error);
|
|
22823
|
+
this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
22824
|
+
throw error;
|
|
22825
|
+
}
|
|
22826
|
+
}
|
|
21701
22827
|
/** Persist credential to localStorage when persistSession is enabled. */
|
|
21702
22828
|
persistCredential(credential) {
|
|
21703
22829
|
if (!credential.token) return;
|
|
21704
22830
|
this._deps.storage.setItem("sw:cached_credential", credential);
|
|
21705
22831
|
if (this._deps.persistSession) this._deps.storage.setItem("sw:cached_credential", credential, "local");
|
|
21706
22832
|
}
|
|
22833
|
+
/**
|
|
22834
|
+
* Persist whether the session is client-bound, mirroring the credential's
|
|
22835
|
+
* storage scopes so it survives a reload. The preflight recovery reads it
|
|
22836
|
+
* before any session exists to decide whether to re-bind via `authenticate()`
|
|
22837
|
+
* or refresh an unbound token; the marker tracks the latest binding, so an
|
|
22838
|
+
* unbound reconnect clears a stale marker from an earlier client-bound login.
|
|
22839
|
+
*/
|
|
22840
|
+
persistClientBoundMarker(bound) {
|
|
22841
|
+
const scopes = this._deps.persistSession ? ["session", "local"] : ["session"];
|
|
22842
|
+
for (const scope of scopes) if (bound) this._deps.storage.setItem(CLIENT_BOUND_STORAGE_KEY, true, scope);
|
|
22843
|
+
else this._deps.storage.removeItem(CLIENT_BOUND_STORAGE_KEY, scope);
|
|
22844
|
+
}
|
|
22845
|
+
/** Read the persisted client-bound marker (see {@link persistClientBoundMarker}). */
|
|
22846
|
+
async wasClientBound() {
|
|
22847
|
+
const scopes = this._deps.persistSession ? ["local", "session"] : ["session"];
|
|
22848
|
+
for (const scope of scopes) try {
|
|
22849
|
+
if (await this._deps.storage.getItem(CLIENT_BOUND_STORAGE_KEY, scope)) return true;
|
|
22850
|
+
} catch {}
|
|
22851
|
+
return false;
|
|
22852
|
+
}
|
|
21707
22853
|
async init() {
|
|
21708
22854
|
this._user$.next(new User(this._deps.http));
|
|
21709
22855
|
if (!this._options.skipConnection) await this.connect();
|
|
@@ -21727,6 +22873,42 @@ var SignalWire = class extends Destroyable {
|
|
|
21727
22873
|
} catch (error) {
|
|
21728
22874
|
logger$1.error("[SignalWire] Failed to reattach calls:", error);
|
|
21729
22875
|
this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
22876
|
+
} finally {
|
|
22877
|
+
this._credentialRecovered = false;
|
|
22878
|
+
}
|
|
22879
|
+
}
|
|
22880
|
+
/**
|
|
22881
|
+
* Fetch the authenticated user profile, recovering a stale credential.
|
|
22882
|
+
*
|
|
22883
|
+
* On a reload the persisted credential can be expired. Unlike the WS resume —
|
|
22884
|
+
* which the server accepts against the persisted `authorization_state` even
|
|
22885
|
+
* with an expired token — this REST preflight has no such short-circuit and is
|
|
22886
|
+
* refused (401). There is no session yet to reauthenticate, so recovery
|
|
22887
|
+
* re-mints the credential through the provider ({@link refreshCredentialForReconnect})
|
|
22888
|
+
* and retries with a FRESH {@link User}: Fetchable memoizes its result
|
|
22889
|
+
* (shareReplay), so reusing the instance would replay the 401 instead of
|
|
22890
|
+
* re-fetching with the new token. Without the user id the transport/session —
|
|
22891
|
+
* and the reattach a reload is trying to preserve — cannot even be addressed.
|
|
22892
|
+
*/
|
|
22893
|
+
async fetchUserOrRecover() {
|
|
22894
|
+
const fetchUser = async (user$1) => {
|
|
22895
|
+
if (!await (0, import_cjs$1.firstValueFrom)(user$1.fetched$)) throw new UnexpectedError("Failed to fetch user information - fetched$ emitted false");
|
|
22896
|
+
this._deps.user = user$1;
|
|
22897
|
+
};
|
|
22898
|
+
const user = this._user$.value;
|
|
22899
|
+
if (!user) throw new UnexpectedError("User not initialized before connect");
|
|
22900
|
+
try {
|
|
22901
|
+
await fetchUser(user);
|
|
22902
|
+
} catch (firstError) {
|
|
22903
|
+
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.`);
|
|
22904
|
+
try {
|
|
22905
|
+
await this.refreshCredentialForReconnect();
|
|
22906
|
+
const refetched = new User(this._deps.http);
|
|
22907
|
+
await fetchUser(refetched);
|
|
22908
|
+
this._user$.next(refetched);
|
|
22909
|
+
} catch (retryError) {
|
|
22910
|
+
throw new UnexpectedError("Error fetching user information", { cause: retryError });
|
|
22911
|
+
}
|
|
21730
22912
|
}
|
|
21731
22913
|
}
|
|
21732
22914
|
/**
|
|
@@ -21768,40 +22950,23 @@ var SignalWire = class extends Destroyable {
|
|
|
21768
22950
|
*/
|
|
21769
22951
|
async connect() {
|
|
21770
22952
|
await this.teardownTransportAndSession();
|
|
21771
|
-
|
|
21772
|
-
const user = this._user$.value;
|
|
21773
|
-
if (!user) throw new UnexpectedError("User not initialized before connect");
|
|
21774
|
-
if (!await (0, import_cjs$1.firstValueFrom)(user.fetched$)) throw new UnexpectedError("Failed to fetch user information - fetched$ emitted false");
|
|
21775
|
-
this._deps.user = user;
|
|
21776
|
-
} catch (error) {
|
|
21777
|
-
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.`);
|
|
21778
|
-
throw new UnexpectedError("Error fetching user information", { cause: error });
|
|
21779
|
-
}
|
|
22953
|
+
await this.fetchUserOrRecover();
|
|
21780
22954
|
const errorHandler = (error) => {
|
|
21781
22955
|
this._errors$.next(error);
|
|
21782
22956
|
};
|
|
21783
22957
|
this._transport = new TransportManager(this._deps.storage, this._deps.protocolKey, this._deps.WebSocket, PreferencesContainer.instance.relayHost ?? this._deps.relayHost, errorHandler);
|
|
21784
|
-
this._attachManager = new AttachManager(this._deps.storage, this._deps.deviceController, PreferencesContainer.instance.reconnectCallsTimeout, this._deps.attachedCallsKey);
|
|
22958
|
+
this._attachManager = new AttachManager(this._deps.storage, this._deps.deviceController, PreferencesContainer.instance.reconnectCallsTimeout, this._deps.attachedCallsKey, () => this._credentialRecovered);
|
|
21785
22959
|
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$);
|
|
21786
22960
|
this._publicSession = new ClientSessionWrapper(this._clientSession);
|
|
21787
|
-
this._clientSession.
|
|
21788
|
-
|
|
21789
|
-
try {
|
|
21790
|
-
const fingerprint = this._dpopManager?.initialized ? this._dpopManager.fingerprint : void 0;
|
|
21791
|
-
logger$1.debug("[SignalWire] Credential expired, refreshing before reconnect");
|
|
21792
|
-
const newCredentials = await this._credentialProvider.authenticate(fingerprint ? { fingerprint } : void 0);
|
|
21793
|
-
this._deps.credential = newCredentials;
|
|
21794
|
-
if (newCredentials.expiry_at && this._credentialProvider.refresh) this._refreshCoordinator?.scheduleDeveloperRefresh(this._credentialProvider, newCredentials.expiry_at);
|
|
21795
|
-
logger$1.debug("[SignalWire] Credential refreshed successfully for reconnect");
|
|
21796
|
-
} catch (error) {
|
|
21797
|
-
logger$1.error("[SignalWire] Failed to refresh credentials for reconnect:", error);
|
|
21798
|
-
this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
21799
|
-
throw error;
|
|
21800
|
-
}
|
|
21801
|
-
};
|
|
22961
|
+
this._clientSession.callControl = this._options.callControl ?? "routed";
|
|
22962
|
+
this._clientSession.onBeforeReconnect = async () => this.refreshCredentialForReconnect();
|
|
21802
22963
|
this.subscribeTo(this._clientSession.errors$, (error) => {
|
|
21803
22964
|
this._errors$.next(error);
|
|
21804
22965
|
});
|
|
22966
|
+
this.subscribeTo(this._clientSession.authorization$, (authorization) => {
|
|
22967
|
+
this._refreshCoordinator?.syncExpiryFromAuthorization(authorization, this._credentialProvider);
|
|
22968
|
+
this.persistClientBoundMarker(Boolean(authorization?.cnf?.jkt));
|
|
22969
|
+
});
|
|
21805
22970
|
await this._clientSession.connect();
|
|
21806
22971
|
await this._refreshCoordinator?.activate(this._deps.user, this._clientSession);
|
|
21807
22972
|
this.subscribeTo(this._clientSession.authenticated$.pipe((0, import_cjs$1.skip)(1), (0, import_cjs$1.filter)(Boolean)), async () => {
|
|
@@ -21964,6 +23129,13 @@ var SignalWire = class extends Destroyable {
|
|
|
21964
23129
|
}
|
|
21965
23130
|
try {
|
|
21966
23131
|
this._visibilityController = new VisibilityController();
|
|
23132
|
+
this.subscribeTo(this._visibilityController.visibilityChange$.pipe((0, import_cjs$1.filter)((event) => event.to === "visible")), () => {
|
|
23133
|
+
try {
|
|
23134
|
+
this._refreshCoordinator?.forceRefreshIfDue();
|
|
23135
|
+
} catch (error) {
|
|
23136
|
+
logger$1.warn("[SignalWire] Resume credential revalidation failed (non-fatal):", error);
|
|
23137
|
+
}
|
|
23138
|
+
});
|
|
21967
23139
|
this.subscribeTo(this._visibilityController.visibilityChange$.pipe((0, import_cjs$1.filter)((event) => event.to === "visible" && PreferencesContainer.instance.refreshDevicesOnVisible)), () => {
|
|
21968
23140
|
logger$1.debug("[SignalWire] Page visible, re-enumerating devices");
|
|
21969
23141
|
try {
|
|
@@ -21975,7 +23147,7 @@ var SignalWire = class extends Destroyable {
|
|
|
21975
23147
|
logger$1.warn("[SignalWire] Failed to initialize VisibilityController:", error);
|
|
21976
23148
|
}
|
|
21977
23149
|
try {
|
|
21978
|
-
this._diagnosticsCollector = new DiagnosticsCollector({ sdkVersion: "4.0.0-rc.
|
|
23150
|
+
this._diagnosticsCollector = new DiagnosticsCollector({ sdkVersion: "4.0.0-rc.3" });
|
|
21979
23151
|
} catch (error) {
|
|
21980
23152
|
logger$1.warn("[SignalWire] Failed to initialize DiagnosticsCollector:", error);
|
|
21981
23153
|
}
|
|
@@ -22046,21 +23218,22 @@ var SignalWire = class extends Destroyable {
|
|
|
22046
23218
|
this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
22047
23219
|
throw error;
|
|
22048
23220
|
}
|
|
22049
|
-
logger$1.debug("[SignalWire] Failed to register user,
|
|
22050
|
-
|
|
22051
|
-
await this._clientSession.reauthenticate(this._deps.credential.token);
|
|
22052
|
-
logger$1.debug("[SignalWire] Reauthentication successful, retrying register()");
|
|
23221
|
+
logger$1.debug("[SignalWire] Failed to register user, attempting credential recovery...");
|
|
23222
|
+
const outcome = await this.recoverAndRetry(async () => {
|
|
22053
23223
|
await this._transport.execute(RPCExecute({
|
|
22054
23224
|
method: "subscriber.online",
|
|
22055
23225
|
params: {}
|
|
22056
23226
|
}));
|
|
23227
|
+
});
|
|
23228
|
+
if (outcome.ok) {
|
|
23229
|
+
logger$1.debug("[SignalWire] Recovery restored registration");
|
|
22057
23230
|
this._isRegistered$.next(true);
|
|
22058
|
-
|
|
22059
|
-
logger$1.error("[SignalWire] Reauthentication failed during register():", reauthError);
|
|
22060
|
-
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 }) });
|
|
22061
|
-
this._errors$.next(registerError);
|
|
22062
|
-
throw registerError;
|
|
23231
|
+
return;
|
|
22063
23232
|
}
|
|
23233
|
+
const failureCause = outcome.error ?? error;
|
|
23234
|
+
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 }) });
|
|
23235
|
+
this._errors$.next(registerError);
|
|
23236
|
+
throw registerError;
|
|
22064
23237
|
}
|
|
22065
23238
|
}
|
|
22066
23239
|
/**
|
|
@@ -22091,6 +23264,11 @@ var SignalWire = class extends Destroyable {
|
|
|
22091
23264
|
* Returns a {@link Call} in `'ringing'` state. Subscribe to {@link Call.status$}
|
|
22092
23265
|
* to track progression through `'connected'` → `'disconnected'`.
|
|
22093
23266
|
*
|
|
23267
|
+
* Local media acquisition is deliberately unbounded: an unanswered permission
|
|
23268
|
+
* prompt leaves this promise pending indefinitely, so apply your own bound if
|
|
23269
|
+
* your UI needs one. The 12 s signaling budget starts only once acquisition
|
|
23270
|
+
* settles.
|
|
23271
|
+
*
|
|
22094
23272
|
* @param destination - Address URI string (e.g. `'/public/my-room'`) or {@link Address} instance.
|
|
22095
23273
|
* @param options - Media and dial options (audio/video, device constraints). Overrides defaults.
|
|
22096
23274
|
* @returns The created {@link Call} instance.
|
|
@@ -22113,7 +23291,18 @@ var SignalWire = class extends Destroyable {
|
|
|
22113
23291
|
};
|
|
22114
23292
|
await this.waitAuthentication();
|
|
22115
23293
|
logger$1.debug("[SignalWire] Dialing with options:", computed_options);
|
|
22116
|
-
|
|
23294
|
+
try {
|
|
23295
|
+
return await this._clientSession.createOutboundCall(destination, computed_options);
|
|
23296
|
+
} catch (error) {
|
|
23297
|
+
if (!isRecoverableAuthError(error)) throw error;
|
|
23298
|
+
logger$1.debug("[SignalWire] Dial hit a recoverable auth error; recovering the session and retrying");
|
|
23299
|
+
const outcome = await this.recoverAndRetry(async () => {
|
|
23300
|
+
await this.waitAuthentication();
|
|
23301
|
+
return this._clientSession.createOutboundCall(destination, computed_options);
|
|
23302
|
+
});
|
|
23303
|
+
if (outcome.ok) return outcome.value;
|
|
23304
|
+
throw outcome.error ?? error;
|
|
23305
|
+
}
|
|
22117
23306
|
}
|
|
22118
23307
|
/**
|
|
22119
23308
|
* Runs a multi-phase connectivity test against the given destination.
|
|
@@ -22394,10 +23583,13 @@ var SignalWire = class extends Destroyable {
|
|
|
22394
23583
|
destroy() {
|
|
22395
23584
|
this._refreshCoordinator?.destroy();
|
|
22396
23585
|
this._refreshCoordinator = void 0;
|
|
23586
|
+
this._refreshHttp?.destroy();
|
|
23587
|
+
this._refreshHttp = void 0;
|
|
22397
23588
|
this._dpopManager?.destroy();
|
|
22398
|
-
this._clientSession
|
|
22399
|
-
|
|
22400
|
-
this.
|
|
23589
|
+
const session = this._clientSession;
|
|
23590
|
+
session?.teardownSessionState();
|
|
23591
|
+
this._transport?.destroy();
|
|
23592
|
+
session?.destroy();
|
|
22401
23593
|
try {
|
|
22402
23594
|
this._networkMonitor?.destroy();
|
|
22403
23595
|
} catch {}
|
|
@@ -22510,7 +23702,7 @@ var StaticCredentialProvider = class {
|
|
|
22510
23702
|
/**
|
|
22511
23703
|
* Library version from package.json, injected at build time.
|
|
22512
23704
|
*/
|
|
22513
|
-
const version = "4.0.0-rc.
|
|
23705
|
+
const version = "4.0.0-rc.3";
|
|
22514
23706
|
/**
|
|
22515
23707
|
* Flag indicating the library has been loaded and is ready to use.
|
|
22516
23708
|
* For UMD builds: `window.SignalWire.ready`
|
|
@@ -22532,7 +23724,7 @@ const ready = true;
|
|
|
22532
23724
|
*/
|
|
22533
23725
|
const emitReadyEvent = () => {
|
|
22534
23726
|
if (typeof window !== "undefined") {
|
|
22535
|
-
const event = new CustomEvent("signalwire:js:ready", { detail: { version: "4.0.0-rc.
|
|
23727
|
+
const event = new CustomEvent("signalwire:js:ready", { detail: { version: "4.0.0-rc.3" } });
|
|
22536
23728
|
window.dispatchEvent(event);
|
|
22537
23729
|
}
|
|
22538
23730
|
};
|
|
@@ -22557,7 +23749,10 @@ if (typeof process === "undefined") globalThis.process = { env: { NODE_ENV: "pro
|
|
|
22557
23749
|
|
|
22558
23750
|
//#endregion
|
|
22559
23751
|
exports.Address = Address;
|
|
23752
|
+
exports.AuxiliaryLegCancelledError = AuxiliaryLegCancelledError;
|
|
23753
|
+
exports.AuxiliaryLegTimeoutError = AuxiliaryLegTimeoutError;
|
|
22560
23754
|
exports.CallCreateError = CallCreateError;
|
|
23755
|
+
exports.CallNotReadyError = CallNotReadyError;
|
|
22561
23756
|
exports.ClientPreferences = ClientPreferences;
|
|
22562
23757
|
exports.CollectionFetchError = CollectionFetchError;
|
|
22563
23758
|
exports.DPoPInitError = DPoPInitError;
|
|
@@ -22569,8 +23764,10 @@ exports.MediaTrackError = MediaTrackError;
|
|
|
22569
23764
|
exports.MessageParseError = MessageParseError;
|
|
22570
23765
|
exports.OverconstrainedFallbackError = OverconstrainedFallbackError;
|
|
22571
23766
|
exports.Participant = Participant;
|
|
23767
|
+
exports.ParticipantNotReadyError = ParticipantNotReadyError;
|
|
22572
23768
|
exports.PreflightError = PreflightError;
|
|
22573
23769
|
exports.RecoveryError = RecoveryError;
|
|
23770
|
+
exports.ScreenShareAlreadyActiveError = ScreenShareAlreadyActiveError;
|
|
22574
23771
|
exports.SelfCapabilities = SelfCapabilities;
|
|
22575
23772
|
exports.SelfParticipant = SelfParticipant;
|
|
22576
23773
|
exports.SignalWire = SignalWire;
|