@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.mjs
CHANGED
|
@@ -4186,7 +4186,7 @@ var require_merge$1 = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
4186
4186
|
var empty_1$7 = require_empty();
|
|
4187
4187
|
var args_1$8 = require_args();
|
|
4188
4188
|
var from_1$4 = require_from();
|
|
4189
|
-
function merge$
|
|
4189
|
+
function merge$7() {
|
|
4190
4190
|
var args = [];
|
|
4191
4191
|
for (var _i = 0; _i < arguments.length; _i++) args[_i] = arguments[_i];
|
|
4192
4192
|
var scheduler = args_1$8.popScheduler(args);
|
|
@@ -4194,7 +4194,7 @@ var require_merge$1 = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
4194
4194
|
var sources = args;
|
|
4195
4195
|
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));
|
|
4196
4196
|
}
|
|
4197
|
-
exports.merge = merge$
|
|
4197
|
+
exports.merge = merge$7;
|
|
4198
4198
|
}));
|
|
4199
4199
|
|
|
4200
4200
|
//#endregion
|
|
@@ -4325,13 +4325,13 @@ var require_race$1 = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
4325
4325
|
var innerFrom_1$28 = require_innerFrom();
|
|
4326
4326
|
var argsOrArgArray_1$4 = require_argsOrArgArray();
|
|
4327
4327
|
var OperatorSubscriber_1$48 = require_OperatorSubscriber();
|
|
4328
|
-
function race$
|
|
4328
|
+
function race$4() {
|
|
4329
4329
|
var sources = [];
|
|
4330
4330
|
for (var _i = 0; _i < arguments.length; _i++) sources[_i] = arguments[_i];
|
|
4331
4331
|
sources = argsOrArgArray_1$4.argsOrArgArray(sources);
|
|
4332
4332
|
return sources.length === 1 ? innerFrom_1$28.innerFrom(sources[0]) : new Observable_1$6.Observable(raceInit(sources));
|
|
4333
4333
|
}
|
|
4334
|
-
exports.race = race$
|
|
4334
|
+
exports.race = race$4;
|
|
4335
4335
|
function raceInit(sources) {
|
|
4336
4336
|
return function(subscriber) {
|
|
4337
4337
|
var subscriptions = [];
|
|
@@ -6076,7 +6076,7 @@ var require_merge = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
6076
6076
|
var mergeAll_1$2 = require_mergeAll();
|
|
6077
6077
|
var args_1$3 = require_args();
|
|
6078
6078
|
var from_1$1 = require_from();
|
|
6079
|
-
function merge$
|
|
6079
|
+
function merge$6() {
|
|
6080
6080
|
var args = [];
|
|
6081
6081
|
for (var _i = 0; _i < arguments.length; _i++) args[_i] = arguments[_i];
|
|
6082
6082
|
var scheduler = args_1$3.popScheduler(args);
|
|
@@ -6085,7 +6085,7 @@ var require_merge = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
6085
6085
|
mergeAll_1$2.mergeAll(concurrent)(from_1$1.from(__spreadArray$8([source], __read$8(args)), scheduler)).subscribe(subscriber);
|
|
6086
6086
|
});
|
|
6087
6087
|
}
|
|
6088
|
-
exports.merge = merge$
|
|
6088
|
+
exports.merge = merge$6;
|
|
6089
6089
|
}));
|
|
6090
6090
|
|
|
6091
6091
|
//#endregion
|
|
@@ -6932,7 +6932,7 @@ var require_switchMap = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
6932
6932
|
var innerFrom_1$6 = require_innerFrom();
|
|
6933
6933
|
var lift_1$13 = require_lift();
|
|
6934
6934
|
var OperatorSubscriber_1$11 = require_OperatorSubscriber();
|
|
6935
|
-
function switchMap$
|
|
6935
|
+
function switchMap$8(project, resultSelector) {
|
|
6936
6936
|
return lift_1$13.operate(function(source, subscriber) {
|
|
6937
6937
|
var innerSubscriber = null;
|
|
6938
6938
|
var index = 0;
|
|
@@ -6956,7 +6956,7 @@ var require_switchMap = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
6956
6956
|
}));
|
|
6957
6957
|
});
|
|
6958
6958
|
}
|
|
6959
|
-
exports.switchMap = switchMap$
|
|
6959
|
+
exports.switchMap = switchMap$8;
|
|
6960
6960
|
}));
|
|
6961
6961
|
|
|
6962
6962
|
//#endregion
|
|
@@ -9022,6 +9022,25 @@ const DEFAULT_ICE_CANDIDATE_TIMEOUT_MS = 600;
|
|
|
9022
9022
|
const DEFAULT_ICE_GATHERING_TIMEOUT_MS = 6e3;
|
|
9023
9023
|
const DEFAULT_RECONNECT_CALLS_TIMEOUT_MS = 300 * 1e3;
|
|
9024
9024
|
const DEFAULT_CONNECTION_TIMEOUT_MS = 1e4;
|
|
9025
|
+
/**
|
|
9026
|
+
* How long call setup may wait for the network, measured from local media
|
|
9027
|
+
* settling until the member id arrives.
|
|
9028
|
+
*
|
|
9029
|
+
* Local media acquisition is deliberately NOT inside this: a permission prompt
|
|
9030
|
+
* or a device picker is human time, and a human may take as long as they like
|
|
9031
|
+
* without spending the server's budget. The clock starts when acquisition ends.
|
|
9032
|
+
*
|
|
9033
|
+
* Must exceed `iceGatheringTimeout + the RPC timeout`, which both run inside it.
|
|
9034
|
+
*/
|
|
9035
|
+
const DEFAULT_CALL_SIGNALING_TIMEOUT_MS = 12e3;
|
|
9036
|
+
/**
|
|
9037
|
+
* How long an auxiliary leg (screen share, additional device) may take to
|
|
9038
|
+
* connect once its media is in hand.
|
|
9039
|
+
*
|
|
9040
|
+
* One bound for both: the picker is human time and sits outside it. What remains
|
|
9041
|
+
* — offer, ICE, invite, answer, DTLS — is the same work for either leg kind.
|
|
9042
|
+
*/
|
|
9043
|
+
const DEFAULT_AUX_LEG_CONNECT_TIMEOUT_MS = 15e3;
|
|
9025
9044
|
const DEFAULT_RECONNECT_DELAY_MIN_MS = 100;
|
|
9026
9045
|
const DEFAULT_RECONNECT_DELAY_MAX_MS = 3e3;
|
|
9027
9046
|
const DEFAULT_DEVICE_DEBOUNCE_TIME_MS = 1500;
|
|
@@ -9049,6 +9068,14 @@ const CREDENTIAL_REFRESH_MAX_DELAY_MS = 3e4;
|
|
|
9049
9068
|
/** Buffer in milliseconds before token expiry to trigger refresh. */
|
|
9050
9069
|
const CREDENTIAL_REFRESH_BUFFER_MS = 5e3;
|
|
9051
9070
|
/**
|
|
9071
|
+
* Clock-skew allowance (ms) for treating an in-memory credential as expired
|
|
9072
|
+
* when deciding whether a fresh (re)connect must re-mint the token before
|
|
9073
|
+
* authenticating. A token within this window of its `expiry_at` is treated as
|
|
9074
|
+
* stale so the reconnect re-mints via the credential provider instead of
|
|
9075
|
+
* replaying a dead token (which the server rejects with -32003).
|
|
9076
|
+
*/
|
|
9077
|
+
const CREDENTIAL_EXPIRY_SKEW_MS = 3e4;
|
|
9078
|
+
/**
|
|
9052
9079
|
* Maximum time the coordinator will wait for `DeviceTokenManager.activate()`
|
|
9053
9080
|
* to resolve before treating the activation as failed and falling back to
|
|
9054
9081
|
* the developer-provided refresh path. Prevents a wedged HTTP layer from
|
|
@@ -9067,6 +9094,8 @@ const MEDIA_ACCESS_DENIAL_NAMES = [
|
|
|
9067
9094
|
"SecurityError",
|
|
9068
9095
|
"PermissionDeniedError"
|
|
9069
9096
|
];
|
|
9097
|
+
/** Error names browsers use when the capture hardware is already held exclusively. */
|
|
9098
|
+
const MEDIA_DEVICE_IN_USE_NAMES = ["NotReadableError", "TrackStartError"];
|
|
9070
9099
|
/** Default polling interval for RTCPeerConnection.getStats() in milliseconds. */
|
|
9071
9100
|
const DEFAULT_STATS_POLLING_INTERVAL_MS = 1e3;
|
|
9072
9101
|
/** Number of initial samples used to build a baseline for spike detection. */
|
|
@@ -9281,6 +9310,20 @@ var CallCreateError = class extends Error {
|
|
|
9281
9310
|
this.name = "CallCreateError";
|
|
9282
9311
|
}
|
|
9283
9312
|
};
|
|
9313
|
+
var CallNotReadyError = class extends Error {
|
|
9314
|
+
constructor(callId, options) {
|
|
9315
|
+
super(`Call "${callId}" has no self member context yet: selfId/nodeId have not been received from the server`, options);
|
|
9316
|
+
this.callId = callId;
|
|
9317
|
+
this.name = "CallNotReadyError";
|
|
9318
|
+
}
|
|
9319
|
+
};
|
|
9320
|
+
var ParticipantNotReadyError = class extends Error {
|
|
9321
|
+
constructor(memberId, options) {
|
|
9322
|
+
super(`Participant "${memberId}" has no call context yet: its member state (call_id/node_id) has not been received from the server`, options);
|
|
9323
|
+
this.memberId = memberId;
|
|
9324
|
+
this.name = "ParticipantNotReadyError";
|
|
9325
|
+
}
|
|
9326
|
+
};
|
|
9284
9327
|
var JSONRPCError = class extends Error {
|
|
9285
9328
|
constructor(code, message, data, options, requestId) {
|
|
9286
9329
|
super(message, options);
|
|
@@ -9352,6 +9395,32 @@ var CollectionFetchError = class extends Error {
|
|
|
9352
9395
|
this.name = "CollectionFetchError";
|
|
9353
9396
|
}
|
|
9354
9397
|
};
|
|
9398
|
+
/**
|
|
9399
|
+
* An auxiliary leg did not connect within its budget. Typed rather than a bare
|
|
9400
|
+
* RxJS `TimeoutError` so the leg and cause survive.
|
|
9401
|
+
*/
|
|
9402
|
+
var AuxiliaryLegTimeoutError = class extends Error {
|
|
9403
|
+
constructor(leg, originalError) {
|
|
9404
|
+
super(`Timed out waiting for the ${leg} connection to be established`, { cause: originalError });
|
|
9405
|
+
this.leg = leg;
|
|
9406
|
+
this.originalError = originalError;
|
|
9407
|
+
this.name = "AuxiliaryLegTimeoutError";
|
|
9408
|
+
}
|
|
9409
|
+
};
|
|
9410
|
+
/**
|
|
9411
|
+
* An auxiliary leg was removed before it finished connecting.
|
|
9412
|
+
*
|
|
9413
|
+
* Typed rather than a bare resolve so a caller awaiting the start can tell a
|
|
9414
|
+
* cancel apart from a share that actually came up — the public methods return
|
|
9415
|
+
* `void`, so the promise is the only signal they have.
|
|
9416
|
+
*/
|
|
9417
|
+
var AuxiliaryLegCancelledError = class extends Error {
|
|
9418
|
+
constructor(leg) {
|
|
9419
|
+
super(`The ${leg} leg was removed before it finished connecting`);
|
|
9420
|
+
this.leg = leg;
|
|
9421
|
+
this.name = "AuxiliaryLegCancelledError";
|
|
9422
|
+
}
|
|
9423
|
+
};
|
|
9355
9424
|
var MediaTrackError = class extends Error {
|
|
9356
9425
|
constructor(operation, kind, originalError) {
|
|
9357
9426
|
super(`Media track ${operation} failed for ${kind}`, { cause: originalError instanceof Error ? originalError : void 0 });
|
|
@@ -9365,6 +9434,10 @@ var MediaTrackError = class extends Error {
|
|
|
9365
9434
|
function isMediaAccessDenial(originalError) {
|
|
9366
9435
|
return originalError instanceof Error && MEDIA_ACCESS_DENIAL_NAMES.includes(originalError.name);
|
|
9367
9436
|
}
|
|
9437
|
+
/** True when a `getUserMedia` rejection means the hardware is already held exclusively. */
|
|
9438
|
+
function isMediaDeviceInUse(originalError) {
|
|
9439
|
+
return originalError instanceof Error && MEDIA_DEVICE_IN_USE_NAMES.includes(originalError.name);
|
|
9440
|
+
}
|
|
9368
9441
|
/**
|
|
9369
9442
|
* Failure to acquire local media (camera, microphone, or screen capture)
|
|
9370
9443
|
* via `getUserMedia`/`getDisplayMedia`.
|
|
@@ -9388,6 +9461,22 @@ var MediaAccessError = class extends Error {
|
|
|
9388
9461
|
return isMediaAccessDenial(this.originalError);
|
|
9389
9462
|
}
|
|
9390
9463
|
};
|
|
9464
|
+
/**
|
|
9465
|
+
* Thrown by `startScreenShare()` when the call is already sharing a screen.
|
|
9466
|
+
*
|
|
9467
|
+
* A call carries at most one screen share. Accepting a second one would
|
|
9468
|
+
* overwrite the only reference the SDK holds to the first, leaving it
|
|
9469
|
+
* capturing and sending with no way to stop it — so the second request is
|
|
9470
|
+
* rejected and the live share is left untouched. Call `stopScreenShare()`
|
|
9471
|
+
* first to replace it.
|
|
9472
|
+
*/
|
|
9473
|
+
var ScreenShareAlreadyActiveError = class extends Error {
|
|
9474
|
+
constructor(screenShareId, options) {
|
|
9475
|
+
super(`A screen share is already active on this call (${screenShareId}). Call stopScreenShare() before starting another one.`, options);
|
|
9476
|
+
this.screenShareId = screenShareId;
|
|
9477
|
+
this.name = "ScreenShareAlreadyActiveError";
|
|
9478
|
+
}
|
|
9479
|
+
};
|
|
9391
9480
|
var DPoPInitError = class extends Error {
|
|
9392
9481
|
constructor(originalError, message = "Failed to initialize DPoP key pair") {
|
|
9393
9482
|
super(message, { cause: originalError instanceof Error ? originalError : void 0 });
|
|
@@ -9628,9 +9717,9 @@ var require_loglevel = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
9628
9717
|
defaultLogger$1 = new Logger();
|
|
9629
9718
|
defaultLogger$1.getLogger = function getLogger$1(name) {
|
|
9630
9719
|
if (typeof name !== "symbol" && typeof name !== "string" || name === "") throw new TypeError("You must supply a name when creating a logger.");
|
|
9631
|
-
var logger$
|
|
9632
|
-
if (!logger$
|
|
9633
|
-
return logger$
|
|
9720
|
+
var logger$34 = _loggersByName[name];
|
|
9721
|
+
if (!logger$34) logger$34 = _loggersByName[name] = new Logger(name, defaultLogger$1.methodFactory);
|
|
9722
|
+
return logger$34;
|
|
9634
9723
|
};
|
|
9635
9724
|
var _log = typeof window !== undefinedType ? window.log : void 0;
|
|
9636
9725
|
defaultLogger$1.noConflict = function() {
|
|
@@ -9666,8 +9755,8 @@ const defaultLoggerLevel = defaultLogger.levels.WARN;
|
|
|
9666
9755
|
defaultLogger.setLevel(defaultLoggerLevel);
|
|
9667
9756
|
let userLogger = null;
|
|
9668
9757
|
/** Replace the built-in logger with a custom implementation. Pass `null` to restore defaults. */
|
|
9669
|
-
const setLogger = (logger$
|
|
9670
|
-
userLogger = logger$
|
|
9758
|
+
const setLogger = (logger$34) => {
|
|
9759
|
+
userLogger = logger$34;
|
|
9671
9760
|
};
|
|
9672
9761
|
let debugOptions = {};
|
|
9673
9762
|
/** Configure debug options (e.g., `{ logWsTraffic: true }`). */
|
|
@@ -9711,8 +9800,8 @@ const wsTraffic = (options) => {
|
|
|
9711
9800
|
loggerInstance.debug(`${options.type.toUpperCase()}: \n`, msg, "\n");
|
|
9712
9801
|
};
|
|
9713
9802
|
const getLogger = () => {
|
|
9714
|
-
const logger$
|
|
9715
|
-
return new Proxy(logger$
|
|
9803
|
+
const logger$34 = getLoggerInstance();
|
|
9804
|
+
return new Proxy(logger$34, { get(_target, prop, _receiver) {
|
|
9716
9805
|
if (prop === "wsTraffic") return wsTraffic;
|
|
9717
9806
|
const instance = getLoggerInstance();
|
|
9718
9807
|
const value = Reflect.get(instance, prop);
|
|
@@ -9764,7 +9853,7 @@ const asyncRetry = async ({ asyncCallable, maxRetries: retries = DEFAULT_MAX_RET
|
|
|
9764
9853
|
|
|
9765
9854
|
//#endregion
|
|
9766
9855
|
//#region src/controllers/HTTPRequestController.ts
|
|
9767
|
-
const logger$
|
|
9856
|
+
const logger$33 = getLogger();
|
|
9768
9857
|
const GET_PARAMS = {
|
|
9769
9858
|
method: "GET",
|
|
9770
9859
|
headers: { Accept: "application/json" }
|
|
@@ -9828,7 +9917,7 @@ var HTTPRequestController = class HTTPRequestController extends Destroyable {
|
|
|
9828
9917
|
this._responses$.next(response);
|
|
9829
9918
|
return response;
|
|
9830
9919
|
} catch (error) {
|
|
9831
|
-
logger$
|
|
9920
|
+
logger$33.error("[HTTPRequestController] Request error:", error);
|
|
9832
9921
|
this._status$.next("error");
|
|
9833
9922
|
const err = error instanceof Error ? error : new Error("HTTP request failed", { cause: error });
|
|
9834
9923
|
this._errors$.next(err);
|
|
@@ -9855,7 +9944,7 @@ var HTTPRequestController = class HTTPRequestController extends Destroyable {
|
|
|
9855
9944
|
const url = this.buildURL(request.url);
|
|
9856
9945
|
const headers = this.buildHeaders(request.headers);
|
|
9857
9946
|
const timeout$5 = request.timeout ?? this.requestTimeout;
|
|
9858
|
-
logger$
|
|
9947
|
+
logger$33.debug("[HTTPRequestController] Executing request:", {
|
|
9859
9948
|
method: request.method,
|
|
9860
9949
|
url,
|
|
9861
9950
|
headers: Object.keys(headers).reduce((acc, key) => {
|
|
@@ -9875,7 +9964,7 @@ var HTTPRequestController = class HTTPRequestController extends Destroyable {
|
|
|
9875
9964
|
});
|
|
9876
9965
|
clearTimeout(timeoutId);
|
|
9877
9966
|
const httpResponse = await this.convertResponse(response);
|
|
9878
|
-
logger$
|
|
9967
|
+
logger$33.debug("[HTTPRequestController] Response received:", {
|
|
9879
9968
|
status: response.status,
|
|
9880
9969
|
statusText: response.statusText,
|
|
9881
9970
|
headers: [...response.headers.entries()],
|
|
@@ -9885,7 +9974,7 @@ var HTTPRequestController = class HTTPRequestController extends Destroyable {
|
|
|
9885
9974
|
} catch (error) {
|
|
9886
9975
|
clearTimeout(timeoutId);
|
|
9887
9976
|
if (error instanceof Error && error.name === "AbortError") throw new RequestTimeoutError(`Request timeout after ${timeout$5}ms`, { cause: error });
|
|
9888
|
-
logger$
|
|
9977
|
+
logger$33.error("[HTTPRequestController] Request failed:", error);
|
|
9889
9978
|
throw error;
|
|
9890
9979
|
}
|
|
9891
9980
|
}
|
|
@@ -9899,8 +9988,8 @@ var HTTPRequestController = class HTTPRequestController extends Destroyable {
|
|
|
9899
9988
|
const credential = this.getCredential();
|
|
9900
9989
|
if (credential.token) {
|
|
9901
9990
|
headers.Authorization = `Bearer ${credential.token}`;
|
|
9902
|
-
logger$
|
|
9903
|
-
} else logger$
|
|
9991
|
+
logger$33.debug("[HTTPRequestController] Using Bearer token auth, token length:", credential.token.length);
|
|
9992
|
+
} else logger$33.warn("[HTTPRequestController] No credentials available for authentication");
|
|
9904
9993
|
return headers;
|
|
9905
9994
|
}
|
|
9906
9995
|
/**
|
|
@@ -10048,7 +10137,7 @@ function fromMsToSec(milliseconds) {
|
|
|
10048
10137
|
|
|
10049
10138
|
//#endregion
|
|
10050
10139
|
//#region src/containers/PreferencesContainer.ts
|
|
10051
|
-
const logger$
|
|
10140
|
+
const logger$32 = getLogger();
|
|
10052
10141
|
var PreferencesContainer = class PreferencesContainer {
|
|
10053
10142
|
static get instance() {
|
|
10054
10143
|
this._instance ??= new PreferencesContainer();
|
|
@@ -10710,7 +10799,7 @@ var ClientPreferences = class {
|
|
|
10710
10799
|
if (!this._storage) return;
|
|
10711
10800
|
const data = collectStoredPreferences();
|
|
10712
10801
|
this._storage.setItem(PREFERENCES_STORAGE_KEY, data, "local").catch((error) => {
|
|
10713
|
-
logger$
|
|
10802
|
+
logger$32.error(`[ClientPreferences] Failed to save preferences: ${String(error)}`);
|
|
10714
10803
|
});
|
|
10715
10804
|
}
|
|
10716
10805
|
/** Loads preferences from storage and applies them to the container. */
|
|
@@ -10719,7 +10808,7 @@ var ClientPreferences = class {
|
|
|
10719
10808
|
this._storage.getItem(PREFERENCES_STORAGE_KEY, "local").then((stored) => {
|
|
10720
10809
|
if (stored) applyStoredPreferences(stored);
|
|
10721
10810
|
}).catch((error) => {
|
|
10722
|
-
logger$
|
|
10811
|
+
logger$32.error(`[ClientPreferences] Failed to load preferences: ${String(error)}`);
|
|
10723
10812
|
});
|
|
10724
10813
|
}
|
|
10725
10814
|
};
|
|
@@ -10741,7 +10830,7 @@ function toError(value) {
|
|
|
10741
10830
|
//#endregion
|
|
10742
10831
|
//#region src/controllers/NavigatorDeviceController.ts
|
|
10743
10832
|
var import_cjs$29 = require_cjs();
|
|
10744
|
-
const logger$
|
|
10833
|
+
const logger$31 = getLogger();
|
|
10745
10834
|
/** Maps a device kind to its storage key. */
|
|
10746
10835
|
const DEVICE_STORAGE_KEYS = {
|
|
10747
10836
|
audioinput: DEVICE_STORAGE_KEY_AUDIO_INPUT,
|
|
@@ -10763,7 +10852,7 @@ var NavigatorDeviceController = class extends Destroyable {
|
|
|
10763
10852
|
super();
|
|
10764
10853
|
this.webRTCApiProvider = webRTCApiProvider;
|
|
10765
10854
|
this.deviceChangeHandler = () => {
|
|
10766
|
-
logger$
|
|
10855
|
+
logger$31.debug("[DeviceController] Device change detected");
|
|
10767
10856
|
this.enumerateDevices();
|
|
10768
10857
|
};
|
|
10769
10858
|
this._devicesState$ = this.createBehaviorSubject(initialDevicesState);
|
|
@@ -10828,13 +10917,13 @@ var NavigatorDeviceController = class extends Destroyable {
|
|
|
10828
10917
|
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$)));
|
|
10829
10918
|
}
|
|
10830
10919
|
get selectedAudioInputDevice$() {
|
|
10831
|
-
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$
|
|
10920
|
+
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))));
|
|
10832
10921
|
}
|
|
10833
10922
|
get selectedAudioOutputDevice$() {
|
|
10834
|
-
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$
|
|
10923
|
+
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))));
|
|
10835
10924
|
}
|
|
10836
10925
|
get selectedVideoInputDevice$() {
|
|
10837
|
-
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$
|
|
10926
|
+
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))));
|
|
10838
10927
|
}
|
|
10839
10928
|
get selectedAudioInputDevice() {
|
|
10840
10929
|
if (this._audioInputDisabled$.value) return null;
|
|
@@ -10909,7 +10998,7 @@ var NavigatorDeviceController = class extends Destroyable {
|
|
|
10909
10998
|
if (device) this.persistDeviceSelection("audioinput", device);
|
|
10910
10999
|
}
|
|
10911
11000
|
selectVideoInputDevice(device) {
|
|
10912
|
-
logger$
|
|
11001
|
+
logger$31.debug("[DeviceController] Setting selected video input device:", device);
|
|
10913
11002
|
if (this._videoInputDisabled$.value && device) this._videoInputDisabled$.next(false);
|
|
10914
11003
|
const previous = this._selectedDevicesState$.value.videoinput;
|
|
10915
11004
|
if (previous && previous.deviceId !== device?.deviceId) this._deviceHistory.push("videoinput", previous);
|
|
@@ -10966,7 +11055,7 @@ var NavigatorDeviceController = class extends Destroyable {
|
|
|
10966
11055
|
}
|
|
10967
11056
|
const fromHistory = this._deviceHistory.findInHistory(kind, devices);
|
|
10968
11057
|
if (fromHistory) {
|
|
10969
|
-
logger$
|
|
11058
|
+
logger$31.debug(`[DeviceController] Device disappeared, falling back to history: ${fromHistory.label}`);
|
|
10970
11059
|
this.emitDeviceRecovered(kind, selected, fromHistory, "device_disconnected");
|
|
10971
11060
|
return fromHistory;
|
|
10972
11061
|
}
|
|
@@ -11019,7 +11108,7 @@ var NavigatorDeviceController = class extends Destroyable {
|
|
|
11019
11108
|
try {
|
|
11020
11109
|
await this._storageManager.setItem(DEVICE_STORAGE_KEYS[kind], stored, "local");
|
|
11021
11110
|
} catch (error) {
|
|
11022
|
-
logger$
|
|
11111
|
+
logger$31.error(`[DeviceController] Failed to persist device selection for ${kind}:`, error);
|
|
11023
11112
|
}
|
|
11024
11113
|
}
|
|
11025
11114
|
async loadPersistedDevices() {
|
|
@@ -11035,7 +11124,7 @@ var NavigatorDeviceController = class extends Destroyable {
|
|
|
11035
11124
|
[kind]: stored
|
|
11036
11125
|
};
|
|
11037
11126
|
} catch (error) {
|
|
11038
|
-
logger$
|
|
11127
|
+
logger$31.error(`[DeviceController] Failed to load persisted device for ${kind}:`, error);
|
|
11039
11128
|
}
|
|
11040
11129
|
}
|
|
11041
11130
|
/** Clears device history, persisted selections, and re-enumerates devices. */
|
|
@@ -11053,7 +11142,7 @@ var NavigatorDeviceController = class extends Destroyable {
|
|
|
11053
11142
|
this.disableDeviceMonitoring();
|
|
11054
11143
|
this.webRTCApiProvider.mediaDevices.addEventListener("devicechange", this.deviceChangeHandler);
|
|
11055
11144
|
if (PreferencesContainer.instance.devicePollingInterval > 0) this._devicesPoolingSubscription = (0, import_cjs$29.interval)(PreferencesContainer.instance.devicePollingInterval).subscribe(() => {
|
|
11056
|
-
logger$
|
|
11145
|
+
logger$31.debug("[DeviceController] Polling devices due to interval");
|
|
11057
11146
|
this.enumerateDevices();
|
|
11058
11147
|
});
|
|
11059
11148
|
this.enumerateDevices();
|
|
@@ -11079,13 +11168,13 @@ var NavigatorDeviceController = class extends Destroyable {
|
|
|
11079
11168
|
videoinput: []
|
|
11080
11169
|
});
|
|
11081
11170
|
this._devicesState$.next(devicesByKind);
|
|
11082
|
-
logger$
|
|
11171
|
+
logger$31.debug("[DeviceController] Devices enumerated:", {
|
|
11083
11172
|
audioInputs: devicesByKind.audioinput.length,
|
|
11084
11173
|
audioOutputs: devicesByKind.audiooutput.length,
|
|
11085
11174
|
videoInputs: devicesByKind.videoinput.length
|
|
11086
11175
|
});
|
|
11087
11176
|
} catch (error) {
|
|
11088
|
-
logger$
|
|
11177
|
+
logger$31.error("[DeviceController] Failed to enumerate devices:", error);
|
|
11089
11178
|
this._errors$.next(toError(error));
|
|
11090
11179
|
}
|
|
11091
11180
|
}
|
|
@@ -11101,7 +11190,7 @@ var NavigatorDeviceController = class extends Destroyable {
|
|
|
11101
11190
|
stream.getTracks().forEach((t) => t.stop());
|
|
11102
11191
|
return capabilities;
|
|
11103
11192
|
} catch (error) {
|
|
11104
|
-
logger$
|
|
11193
|
+
logger$31.error("[DeviceController] Failed to get device capabilities:", error);
|
|
11105
11194
|
this._errors$.next(toError(error));
|
|
11106
11195
|
throw error;
|
|
11107
11196
|
}
|
|
@@ -11352,7 +11441,7 @@ var DependencyContainer = class {
|
|
|
11352
11441
|
|
|
11353
11442
|
//#endregion
|
|
11354
11443
|
//#region src/controllers/CryptoController.ts
|
|
11355
|
-
const logger$
|
|
11444
|
+
const logger$30 = getLogger();
|
|
11356
11445
|
const DPOP_DB_NAME = "sw-dpop";
|
|
11357
11446
|
const DPOP_DB_VERSION = 1;
|
|
11358
11447
|
const DPOP_STORE_NAME = "keys";
|
|
@@ -11411,7 +11500,7 @@ async function loadKeyPairFromDB() {
|
|
|
11411
11500
|
tx.oncomplete = () => db.close();
|
|
11412
11501
|
});
|
|
11413
11502
|
} catch (error) {
|
|
11414
|
-
logger$
|
|
11503
|
+
logger$30.warn("[DPoP] Failed to load key pair from IndexedDB:", error);
|
|
11415
11504
|
return null;
|
|
11416
11505
|
}
|
|
11417
11506
|
}
|
|
@@ -11431,7 +11520,7 @@ async function saveKeyPairToDB(keyPair) {
|
|
|
11431
11520
|
};
|
|
11432
11521
|
});
|
|
11433
11522
|
} catch (error) {
|
|
11434
|
-
logger$
|
|
11523
|
+
logger$30.warn("[DPoP] Failed to save key pair to IndexedDB:", error);
|
|
11435
11524
|
}
|
|
11436
11525
|
}
|
|
11437
11526
|
async function deleteKeyPairFromDB() {
|
|
@@ -11450,7 +11539,7 @@ async function deleteKeyPairFromDB() {
|
|
|
11450
11539
|
};
|
|
11451
11540
|
});
|
|
11452
11541
|
} catch (error) {
|
|
11453
|
-
logger$
|
|
11542
|
+
logger$30.warn("[DPoP] Failed to delete key pair from IndexedDB:", error);
|
|
11454
11543
|
}
|
|
11455
11544
|
}
|
|
11456
11545
|
/**
|
|
@@ -11510,13 +11599,13 @@ var CryptoController = class {
|
|
|
11510
11599
|
this._publicJwk = await crypto.subtle.exportKey("jwk", stored.publicKey);
|
|
11511
11600
|
this._fingerprint = await computeJwkThumbprint(this._publicJwk);
|
|
11512
11601
|
this._initialized = true;
|
|
11513
|
-
logger$
|
|
11602
|
+
logger$30.debug("[DPoP] Key pair restored from IndexedDB, fingerprint:", this._fingerprint);
|
|
11514
11603
|
return this._fingerprint;
|
|
11515
11604
|
} catch (error) {
|
|
11516
|
-
logger$
|
|
11605
|
+
logger$30.warn("[DPoP] Stored key pair unusable, generating new one:", error);
|
|
11517
11606
|
await deleteKeyPairFromDB();
|
|
11518
11607
|
}
|
|
11519
|
-
logger$
|
|
11608
|
+
logger$30.debug("[DPoP] Generating RSA key pair");
|
|
11520
11609
|
this._keyPair = await crypto.subtle.generateKey({
|
|
11521
11610
|
name: "RSASSA-PKCS1-v1_5",
|
|
11522
11611
|
modulusLength: 2048,
|
|
@@ -11531,7 +11620,7 @@ var CryptoController = class {
|
|
|
11531
11620
|
this._fingerprint = await computeJwkThumbprint(this._publicJwk);
|
|
11532
11621
|
this._initialized = true;
|
|
11533
11622
|
await saveKeyPairToDB(this._keyPair);
|
|
11534
|
-
logger$
|
|
11623
|
+
logger$30.debug("[DPoP] Key pair generated and persisted, fingerprint:", this._fingerprint);
|
|
11535
11624
|
return this._fingerprint;
|
|
11536
11625
|
}
|
|
11537
11626
|
/**
|
|
@@ -11597,7 +11686,7 @@ var CryptoController = class {
|
|
|
11597
11686
|
this._fingerprint = null;
|
|
11598
11687
|
this._initialized = false;
|
|
11599
11688
|
deleteKeyPairFromDB();
|
|
11600
|
-
logger$
|
|
11689
|
+
logger$30.debug("[DPoP] Controller destroyed");
|
|
11601
11690
|
}
|
|
11602
11691
|
get publicJwk() {
|
|
11603
11692
|
if (!this._publicJwk) throw new DPoPInitError("CryptoController not initialized. Call init() first.");
|
|
@@ -11621,7 +11710,7 @@ var CryptoController = class {
|
|
|
11621
11710
|
//#endregion
|
|
11622
11711
|
//#region src/controllers/NetworkMonitor.ts
|
|
11623
11712
|
var import_cjs$28 = require_cjs();
|
|
11624
|
-
const logger$
|
|
11713
|
+
const logger$29 = getLogger();
|
|
11625
11714
|
/**
|
|
11626
11715
|
* Safely check whether we are running in a browser environment
|
|
11627
11716
|
* with `window` and the relevant event targets.
|
|
@@ -11678,7 +11767,7 @@ var NetworkMonitor = class extends Destroyable {
|
|
|
11678
11767
|
}
|
|
11679
11768
|
attachListeners() {
|
|
11680
11769
|
if (!hasBrowserNetworkEvents()) {
|
|
11681
|
-
logger$
|
|
11770
|
+
logger$29.debug("NetworkMonitor: no browser environment detected, skipping event listeners");
|
|
11682
11771
|
return;
|
|
11683
11772
|
}
|
|
11684
11773
|
window.addEventListener("online", this._onOnline);
|
|
@@ -11686,7 +11775,7 @@ var NetworkMonitor = class extends Destroyable {
|
|
|
11686
11775
|
const connection = getNetworkConnection();
|
|
11687
11776
|
if (connection) connection.addEventListener("change", this._onConnectionChange);
|
|
11688
11777
|
this._listenersAttached = true;
|
|
11689
|
-
logger$
|
|
11778
|
+
logger$29.debug("NetworkMonitor: event listeners attached");
|
|
11690
11779
|
}
|
|
11691
11780
|
removeListeners() {
|
|
11692
11781
|
if (!this._listenersAttached) return;
|
|
@@ -11697,10 +11786,10 @@ var NetworkMonitor = class extends Destroyable {
|
|
|
11697
11786
|
if (connection) connection.removeEventListener("change", this._onConnectionChange);
|
|
11698
11787
|
}
|
|
11699
11788
|
this._listenersAttached = false;
|
|
11700
|
-
logger$
|
|
11789
|
+
logger$29.debug("NetworkMonitor: event listeners removed");
|
|
11701
11790
|
}
|
|
11702
11791
|
handleOnline() {
|
|
11703
|
-
logger$
|
|
11792
|
+
logger$29.info("NetworkMonitor: browser went online");
|
|
11704
11793
|
this._isOnline$.next(true);
|
|
11705
11794
|
this._networkChange$.next({
|
|
11706
11795
|
type: "online",
|
|
@@ -11709,7 +11798,7 @@ var NetworkMonitor = class extends Destroyable {
|
|
|
11709
11798
|
});
|
|
11710
11799
|
}
|
|
11711
11800
|
handleOffline() {
|
|
11712
|
-
logger$
|
|
11801
|
+
logger$29.info("NetworkMonitor: browser went offline");
|
|
11713
11802
|
this._isOnline$.next(false);
|
|
11714
11803
|
this._networkChange$.next({
|
|
11715
11804
|
type: "offline",
|
|
@@ -11718,7 +11807,7 @@ var NetworkMonitor = class extends Destroyable {
|
|
|
11718
11807
|
}
|
|
11719
11808
|
handleConnectionChange() {
|
|
11720
11809
|
const networkType = getNetworkType();
|
|
11721
|
-
logger$
|
|
11810
|
+
logger$29.info(`NetworkMonitor: connection changed — effectiveType=${networkType ?? "unknown"}`);
|
|
11722
11811
|
this._networkChange$.next({
|
|
11723
11812
|
type: "connection_change",
|
|
11724
11813
|
timestamp: Date.now(),
|
|
@@ -11834,7 +11923,7 @@ function getNavigatorMediaDevices() {
|
|
|
11834
11923
|
//#endregion
|
|
11835
11924
|
//#region src/controllers/PreflightRunner.ts
|
|
11836
11925
|
var import_cjs$27 = require_cjs();
|
|
11837
|
-
const logger$
|
|
11926
|
+
const logger$28 = getLogger();
|
|
11838
11927
|
const DEFAULT_MEDIA_TEST_DURATION_S = 10;
|
|
11839
11928
|
const ICE_GATHERING_TIMEOUT_MS = 1e4;
|
|
11840
11929
|
const SIGNALING_RTT_TIMEOUT_MS = 5e3;
|
|
@@ -11883,7 +11972,7 @@ var PreflightRunner = class extends Destroyable {
|
|
|
11883
11972
|
if (!this._options.skipMediaTest) try {
|
|
11884
11973
|
bandwidth = await this.testMediaBandwidth(destination);
|
|
11885
11974
|
} catch (error) {
|
|
11886
|
-
logger$
|
|
11975
|
+
logger$28.warn("[PreflightRunner] Media bandwidth test failed:", error);
|
|
11887
11976
|
warnings.push("Media bandwidth test failed");
|
|
11888
11977
|
}
|
|
11889
11978
|
return {
|
|
@@ -11895,7 +11984,7 @@ var PreflightRunner = class extends Destroyable {
|
|
|
11895
11984
|
warnings
|
|
11896
11985
|
};
|
|
11897
11986
|
} catch (error) {
|
|
11898
|
-
logger$
|
|
11987
|
+
logger$28.error("[PreflightRunner] Preflight test failed:", error);
|
|
11899
11988
|
throw new PreflightError("preflight", error instanceof Error ? error : new Error(String(error)));
|
|
11900
11989
|
} finally {
|
|
11901
11990
|
this.destroy();
|
|
@@ -11926,7 +12015,7 @@ var PreflightRunner = class extends Destroyable {
|
|
|
11926
12015
|
if (track.kind === "video" && track.readyState === "live") videoWorking = true;
|
|
11927
12016
|
}
|
|
11928
12017
|
} catch (error) {
|
|
11929
|
-
logger$
|
|
12018
|
+
logger$28.warn("[PreflightRunner] Device test failed:", error);
|
|
11930
12019
|
} finally {
|
|
11931
12020
|
if (audioStream) audioStream.getTracks().forEach((t) => t.stop());
|
|
11932
12021
|
}
|
|
@@ -11984,7 +12073,7 @@ var PreflightRunner = class extends Destroyable {
|
|
|
11984
12073
|
rttMs
|
|
11985
12074
|
};
|
|
11986
12075
|
} catch (error) {
|
|
11987
|
-
logger$
|
|
12076
|
+
logger$28.warn("[PreflightRunner] ICE connectivity test failed:", error);
|
|
11988
12077
|
return {
|
|
11989
12078
|
type: "failed",
|
|
11990
12079
|
turnReachable: false,
|
|
@@ -12032,7 +12121,7 @@ var PreflightRunner = class extends Destroyable {
|
|
|
12032
12121
|
//#endregion
|
|
12033
12122
|
//#region src/controllers/VisibilityController.ts
|
|
12034
12123
|
var import_cjs$26 = require_cjs();
|
|
12035
|
-
const logger$
|
|
12124
|
+
const logger$27 = getLogger();
|
|
12036
12125
|
/**
|
|
12037
12126
|
* Checks whether the document visibility API is available.
|
|
12038
12127
|
*/
|
|
@@ -12069,8 +12158,8 @@ var VisibilityController = class extends Destroyable {
|
|
|
12069
12158
|
this._boundHandler = this._handleVisibilityChange.bind(this);
|
|
12070
12159
|
if (this._hasVisibilityApi) {
|
|
12071
12160
|
document.addEventListener("visibilitychange", this._boundHandler);
|
|
12072
|
-
logger$
|
|
12073
|
-
} else logger$
|
|
12161
|
+
logger$27.debug("VisibilityController: listening for visibilitychange events");
|
|
12162
|
+
} else logger$27.debug("VisibilityController: document visibility API not available, defaulting to visible");
|
|
12074
12163
|
}
|
|
12075
12164
|
/**
|
|
12076
12165
|
* Observable of the current visibility state.
|
|
@@ -12095,7 +12184,7 @@ var VisibilityController = class extends Destroyable {
|
|
|
12095
12184
|
destroy() {
|
|
12096
12185
|
if (this._hasVisibilityApi) {
|
|
12097
12186
|
document.removeEventListener("visibilitychange", this._boundHandler);
|
|
12098
|
-
logger$
|
|
12187
|
+
logger$27.debug("VisibilityController: removed visibilitychange listener");
|
|
12099
12188
|
}
|
|
12100
12189
|
super.destroy();
|
|
12101
12190
|
}
|
|
@@ -12113,7 +12202,7 @@ var VisibilityController = class extends Destroyable {
|
|
|
12113
12202
|
timestamp: Date.now()
|
|
12114
12203
|
};
|
|
12115
12204
|
this._visibilityChange$.next(changeEvent);
|
|
12116
|
-
logger$
|
|
12205
|
+
logger$27.debug("VisibilityController: visibility changed", {
|
|
12117
12206
|
from: previousState,
|
|
12118
12207
|
to: newState
|
|
12119
12208
|
});
|
|
@@ -12352,15 +12441,57 @@ const RPCEventAckResponse = (id) => makeRPCResponse({
|
|
|
12352
12441
|
result: {}
|
|
12353
12442
|
});
|
|
12354
12443
|
|
|
12444
|
+
//#endregion
|
|
12445
|
+
//#region src/utils/authRecovery.ts
|
|
12446
|
+
/**
|
|
12447
|
+
* Walk an error's `error`/`cause` chain looking for a {@link JSONRPCError}.
|
|
12448
|
+
* Errors thrown by call creation are wrapped (e.g. `CallCreateError`), so the
|
|
12449
|
+
* underlying signaling error is nested. Bounded by a visited set to guard
|
|
12450
|
+
* against cyclic causes.
|
|
12451
|
+
*/
|
|
12452
|
+
function findJSONRPCError(error) {
|
|
12453
|
+
const seen = /* @__PURE__ */ new Set();
|
|
12454
|
+
let current = error;
|
|
12455
|
+
while (current instanceof Error && !seen.has(current)) {
|
|
12456
|
+
seen.add(current);
|
|
12457
|
+
if (current instanceof JSONRPCError) return current;
|
|
12458
|
+
current = current.error ?? current.cause;
|
|
12459
|
+
}
|
|
12460
|
+
}
|
|
12461
|
+
/**
|
|
12462
|
+
* Whether an error is a session-recoverable authentication failure
|
|
12463
|
+
* (`-32002` authentication failed or `-32003` requester validation failed)
|
|
12464
|
+
* that a credential re-mint + retry can heal.
|
|
12465
|
+
*/
|
|
12466
|
+
function isRecoverableAuthError(error) {
|
|
12467
|
+
const rpcError = findJSONRPCError(error);
|
|
12468
|
+
return rpcError !== void 0 && (rpcError.code === RPC_ERROR_REQUESTER_VALIDATION_FAILED || rpcError.code === RPC_ERROR_AUTHENTICATION_FAILED);
|
|
12469
|
+
}
|
|
12470
|
+
/**
|
|
12471
|
+
* Whether an error is specifically a requester-validation rejection
|
|
12472
|
+
* (`-32003`) — the server refusing the session's credential.
|
|
12473
|
+
*
|
|
12474
|
+
* Narrower than {@link isRecoverableAuthError} on purpose. `-32002` is
|
|
12475
|
+
* overloaded server-side: a rejected reattach arrives as `-32002` with
|
|
12476
|
+
* `cause: INVALID_MSG_UNSPECIFIED` and message `CALL ERROR`, which is a
|
|
12477
|
+
* call-level rejection and says nothing about the credential. Use this where
|
|
12478
|
+
* the decision must not be fooled by that, such as deciding whether retrying
|
|
12479
|
+
* an operation could possibly succeed.
|
|
12480
|
+
*/
|
|
12481
|
+
function isRequesterValidationError(error) {
|
|
12482
|
+
return findJSONRPCError(error)?.code === RPC_ERROR_REQUESTER_VALIDATION_FAILED;
|
|
12483
|
+
}
|
|
12484
|
+
|
|
12355
12485
|
//#endregion
|
|
12356
12486
|
//#region src/managers/AttachManager.ts
|
|
12357
|
-
const logger$
|
|
12487
|
+
const logger$26 = getLogger();
|
|
12358
12488
|
var AttachManager = class {
|
|
12359
|
-
constructor(storage, deviceController, reconnectCallsTimeout, attachKey) {
|
|
12489
|
+
constructor(storage, deviceController, reconnectCallsTimeout, attachKey, credentialRecovered) {
|
|
12360
12490
|
this.storage = storage;
|
|
12361
12491
|
this.deviceController = deviceController;
|
|
12362
12492
|
this.reconnectCallsTimeout = reconnectCallsTimeout;
|
|
12363
12493
|
this.attachKey = attachKey;
|
|
12494
|
+
this.credentialRecovered = credentialRecovered;
|
|
12364
12495
|
this.writeQueue = Promise.resolve();
|
|
12365
12496
|
}
|
|
12366
12497
|
async detachAll() {
|
|
@@ -12375,7 +12506,7 @@ var AttachManager = class {
|
|
|
12375
12506
|
try {
|
|
12376
12507
|
return await this.storage.getItem(this.attachKey) ?? {};
|
|
12377
12508
|
} catch (error) {
|
|
12378
|
-
logger$
|
|
12509
|
+
logger$26.warn("[AttachManager] Failed to retrieve attached calls from storage", error);
|
|
12379
12510
|
return {};
|
|
12380
12511
|
}
|
|
12381
12512
|
}
|
|
@@ -12383,7 +12514,7 @@ var AttachManager = class {
|
|
|
12383
12514
|
try {
|
|
12384
12515
|
await this.storage.setItem(this.attachKey, attached);
|
|
12385
12516
|
} catch (error) {
|
|
12386
|
-
logger$
|
|
12517
|
+
logger$26.warn("[AttachManager] Failed to write attached calls to storage", error);
|
|
12387
12518
|
}
|
|
12388
12519
|
}
|
|
12389
12520
|
/**
|
|
@@ -12402,11 +12533,39 @@ var AttachManager = class {
|
|
|
12402
12533
|
}
|
|
12403
12534
|
async attach(call) {
|
|
12404
12535
|
if (!call.to) {
|
|
12405
|
-
logger$
|
|
12536
|
+
logger$26.warn("[AttachManager] Skip attach for calls with no destination");
|
|
12406
12537
|
return;
|
|
12407
12538
|
}
|
|
12539
|
+
const attachment = this.buildAttachment(call, call.to);
|
|
12540
|
+
await this.mutate((attached) => ({
|
|
12541
|
+
...attached,
|
|
12542
|
+
[call.id]: attachment
|
|
12543
|
+
}));
|
|
12544
|
+
}
|
|
12545
|
+
/**
|
|
12546
|
+
* Keep an already-stored call's reference alive and current — the periodic
|
|
12547
|
+
* refresh the `verto.ping` keepalive drives.
|
|
12548
|
+
*
|
|
12549
|
+
* Only ever updates: a call with no record is one nothing wants reattached,
|
|
12550
|
+
* and re-creating it here would undo a `detach`. That matters because a ping
|
|
12551
|
+
* can land in the window between `bye()` detaching and the call being torn
|
|
12552
|
+
* down, and a record revived there survives the hangup — so the next page
|
|
12553
|
+
* load dials a call nobody is on. The existence check and the write share
|
|
12554
|
+
* one {@link mutate} turn, so a concurrent detach cannot slip between them.
|
|
12555
|
+
*/
|
|
12556
|
+
async refresh(call) {
|
|
12557
|
+
if (!call.to) return;
|
|
12408
12558
|
const destination = call.to;
|
|
12409
|
-
|
|
12559
|
+
await this.mutate((attached) => {
|
|
12560
|
+
if (!Object.hasOwn(attached, call.id)) return attached;
|
|
12561
|
+
return {
|
|
12562
|
+
...attached,
|
|
12563
|
+
[call.id]: this.buildAttachment(call, destination)
|
|
12564
|
+
};
|
|
12565
|
+
});
|
|
12566
|
+
}
|
|
12567
|
+
buildAttachment(call, destination) {
|
|
12568
|
+
return {
|
|
12410
12569
|
nodeId: call.nodeId,
|
|
12411
12570
|
destination,
|
|
12412
12571
|
mediaDirections: call.mediaDirections,
|
|
@@ -12414,10 +12573,6 @@ var AttachManager = class {
|
|
|
12414
12573
|
videoInputDevice: call.mediaDirections.video !== "inactive" ? this.deviceController.selectedVideoInputDevice : null,
|
|
12415
12574
|
attachedAt: Date.now()
|
|
12416
12575
|
};
|
|
12417
|
-
await this.mutate((attached) => ({
|
|
12418
|
-
...attached,
|
|
12419
|
-
[call.id]: attachment
|
|
12420
|
-
}));
|
|
12421
12576
|
}
|
|
12422
12577
|
async detach(call) {
|
|
12423
12578
|
await this.mutate((attached) => {
|
|
@@ -12440,8 +12595,14 @@ var AttachManager = class {
|
|
|
12440
12595
|
* rejecting. Once that fix is deployed, this will work for both
|
|
12441
12596
|
* page reloads and WebSocket reconnects.
|
|
12442
12597
|
*
|
|
12443
|
-
*
|
|
12444
|
-
*
|
|
12598
|
+
* A failed reattach does NOT generally cost the stored reference. It is
|
|
12599
|
+
* discarded only when the server denied the reattach on a session whose
|
|
12600
|
+
* credential it had already accepted — a verified reauthentication followed
|
|
12601
|
+
* by a refusal is the server saying the call is gone, and that is the one
|
|
12602
|
+
* refusal worth acting on. Until then the credential may be what is being
|
|
12603
|
+
* refused, and the record is the only way a later reload can try again;
|
|
12604
|
+
* keeping it costs nothing, since `detachExpired` reaps it once it is older
|
|
12605
|
+
* than `reconnectCallsTimeout`.
|
|
12445
12606
|
*/
|
|
12446
12607
|
async reattachCalls() {
|
|
12447
12608
|
const attached = await this.readAttached();
|
|
@@ -12450,25 +12611,31 @@ var AttachManager = class {
|
|
|
12450
12611
|
const { destination } = attachment;
|
|
12451
12612
|
const options = this.buildCallOptions(attachment);
|
|
12452
12613
|
let succeeded = false;
|
|
12614
|
+
let refusedOnCredentials = false;
|
|
12453
12615
|
for (let attempt = 1; attempt <= 3; attempt++) try {
|
|
12454
12616
|
await this.session.createOutboundCall(destination, {
|
|
12455
12617
|
callId,
|
|
12456
12618
|
...options
|
|
12457
12619
|
});
|
|
12458
|
-
logger$
|
|
12620
|
+
logger$26.info(`[AttachManager] Reattached call ${callId} (attempt ${attempt})`);
|
|
12459
12621
|
succeeded = true;
|
|
12460
12622
|
break;
|
|
12461
12623
|
} catch (error) {
|
|
12462
|
-
logger$
|
|
12624
|
+
logger$26.warn(`[AttachManager] Reattach attempt ${attempt}/3 failed for call ${callId}:`, error);
|
|
12625
|
+
if (isRequesterValidationError(error)) {
|
|
12626
|
+
refusedOnCredentials = true;
|
|
12627
|
+
logger$26.warn(`[AttachManager] Reattach of ${callId} was refused on credentials; not retrying.`);
|
|
12628
|
+
break;
|
|
12629
|
+
}
|
|
12463
12630
|
if (attempt < 3) await new Promise((r) => setTimeout(r, (attempt + 1) * 1e3));
|
|
12464
12631
|
}
|
|
12465
|
-
if (!succeeded) {
|
|
12466
|
-
logger$
|
|
12632
|
+
if (!succeeded) if (this.credentialRecovered() && !refusedOnCredentials) {
|
|
12633
|
+
logger$26.warn(`[AttachManager] Reattach of ${callId} was denied after a verified reauthentication, removing reference`);
|
|
12467
12634
|
await this.detach({
|
|
12468
12635
|
id: callId,
|
|
12469
12636
|
mediaDirections: attachment.mediaDirections
|
|
12470
12637
|
});
|
|
12471
|
-
}
|
|
12638
|
+
} else logger$26.warn(`[AttachManager] Reattach failed for call ${callId}; keeping the reference (credential refused or never proven good)`);
|
|
12472
12639
|
}
|
|
12473
12640
|
}
|
|
12474
12641
|
/**
|
|
@@ -12569,12 +12736,12 @@ var require_race = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
12569
12736
|
exports.race = void 0;
|
|
12570
12737
|
var argsOrArgArray_1 = require_argsOrArgArray();
|
|
12571
12738
|
var raceWith_1$1 = require_raceWith();
|
|
12572
|
-
function race$
|
|
12739
|
+
function race$3() {
|
|
12573
12740
|
var args = [];
|
|
12574
12741
|
for (var _i = 0; _i < arguments.length; _i++) args[_i] = arguments[_i];
|
|
12575
12742
|
return raceWith_1$1.raceWith.apply(void 0, __spreadArray([], __read(argsOrArgArray_1.argsOrArgArray(args))));
|
|
12576
12743
|
}
|
|
12577
|
-
exports.race = race$
|
|
12744
|
+
exports.race = race$3;
|
|
12578
12745
|
}));
|
|
12579
12746
|
|
|
12580
12747
|
//#endregion
|
|
@@ -13634,7 +13801,7 @@ function toggleHandraiseMethod(is) {
|
|
|
13634
13801
|
|
|
13635
13802
|
//#endregion
|
|
13636
13803
|
//#region src/core/entities/Participant.ts
|
|
13637
|
-
const logger$
|
|
13804
|
+
const logger$25 = getLogger();
|
|
13638
13805
|
const initialState = {};
|
|
13639
13806
|
/**
|
|
13640
13807
|
* Represents a participant in a call.
|
|
@@ -13644,9 +13811,9 @@ const initialState = {};
|
|
|
13644
13811
|
* the local participant with additional device control.
|
|
13645
13812
|
*/
|
|
13646
13813
|
var Participant = class extends Destroyable {
|
|
13647
|
-
constructor(id,
|
|
13814
|
+
constructor(id, callExecuteMethod, deviceController) {
|
|
13648
13815
|
super();
|
|
13649
|
-
this.
|
|
13816
|
+
this.callExecuteMethod = callExecuteMethod;
|
|
13650
13817
|
this.deviceController = deviceController;
|
|
13651
13818
|
this._state$ = this.createBehaviorSubject(initialState);
|
|
13652
13819
|
this.id = id;
|
|
@@ -13868,22 +14035,55 @@ var Participant = class extends Destroyable {
|
|
|
13868
14035
|
get value() {
|
|
13869
14036
|
return this._state$.value;
|
|
13870
14037
|
}
|
|
14038
|
+
/**
|
|
14039
|
+
* Target triple for member RPCs, built from the participant's own state.
|
|
14040
|
+
* The backend locates the member's session by the target `call_id`/`node_id`,
|
|
14041
|
+
* so this must always be the participant's own call context — never the
|
|
14042
|
+
* local call's id (issue #19400).
|
|
14043
|
+
*
|
|
14044
|
+
* Reading it doubles as a readiness probe: it throws until the first full
|
|
14045
|
+
* member event (`member.joined`/`member.updated` or the `call.joined`
|
|
14046
|
+
* roster) arrives, and never regresses afterwards.
|
|
14047
|
+
*
|
|
14048
|
+
* @throws {ParticipantNotReadyError} If the member state has not been
|
|
14049
|
+
* received yet (e.g. a participant first seen via `member.talking`) — an
|
|
14050
|
+
* empty call context can never address the member, so fail fast instead of
|
|
14051
|
+
* sending a doomed RPC.
|
|
14052
|
+
*/
|
|
14053
|
+
get target() {
|
|
14054
|
+
const { call_id, node_id } = this._state$.value;
|
|
14055
|
+
if (!call_id || !node_id) throw new ParticipantNotReadyError(this.id);
|
|
14056
|
+
return {
|
|
14057
|
+
member_id: this.id,
|
|
14058
|
+
call_id,
|
|
14059
|
+
node_id
|
|
14060
|
+
};
|
|
14061
|
+
}
|
|
14062
|
+
/**
|
|
14063
|
+
* Executes a member RPC against this participant, injecting its own
|
|
14064
|
+
* {@link target} as the target.
|
|
14065
|
+
*
|
|
14066
|
+
* @throws {ParticipantNotReadyError} Via {@link target}, when the
|
|
14067
|
+
* member state has not been received yet.
|
|
14068
|
+
*/
|
|
14069
|
+
async executeMethod(method, args) {
|
|
14070
|
+
return this.callExecuteMethod(this.target, method, args);
|
|
14071
|
+
}
|
|
13871
14072
|
/** Toggles the deafened state (mutes/unmutes incoming audio). */
|
|
13872
14073
|
async toggleDeaf() {
|
|
13873
|
-
|
|
13874
|
-
await this.executeMethod(this.id, method, {});
|
|
14074
|
+
await this.executeMethod(toggleDeafMethod(this.deaf), {});
|
|
13875
14075
|
}
|
|
13876
14076
|
/** Toggles the hand-raised state. */
|
|
13877
14077
|
async toggleHandraise() {
|
|
13878
|
-
await this.executeMethod(
|
|
14078
|
+
await this.executeMethod(toggleHandraiseMethod(this.handraised), {});
|
|
13879
14079
|
}
|
|
13880
14080
|
/** Mutes the participant's audio. */
|
|
13881
14081
|
async mute() {
|
|
13882
|
-
await this.executeMethod(
|
|
14082
|
+
await this.executeMethod("call.mute", { channels: ["audio"] });
|
|
13883
14083
|
}
|
|
13884
14084
|
/** Unmutes the participant's audio. */
|
|
13885
14085
|
async unmute() {
|
|
13886
|
-
await this.executeMethod(
|
|
14086
|
+
await this.executeMethod("call.unmute", { channels: ["audio"] });
|
|
13887
14087
|
}
|
|
13888
14088
|
/** Toggles the participant's audio mute state. */
|
|
13889
14089
|
async toggleMute() {
|
|
@@ -13891,11 +14091,11 @@ var Participant = class extends Destroyable {
|
|
|
13891
14091
|
}
|
|
13892
14092
|
/** Mutes the participant's video. */
|
|
13893
14093
|
async muteVideo() {
|
|
13894
|
-
await this.executeMethod(
|
|
14094
|
+
await this.executeMethod("call.mute", { channels: ["video"] });
|
|
13895
14095
|
}
|
|
13896
14096
|
/** Unmutes the participant's video. */
|
|
13897
14097
|
async unmuteVideo() {
|
|
13898
|
-
await this.executeMethod(
|
|
14098
|
+
await this.executeMethod("call.unmute", { channels: ["video"] });
|
|
13899
14099
|
}
|
|
13900
14100
|
/** Toggles the participant's video mute state. */
|
|
13901
14101
|
async toggleMuteVideo() {
|
|
@@ -13903,7 +14103,7 @@ var Participant = class extends Destroyable {
|
|
|
13903
14103
|
}
|
|
13904
14104
|
/** Toggles echo cancellation on the audio input. */
|
|
13905
14105
|
async toggleEchoCancellation() {
|
|
13906
|
-
await this.executeMethod(
|
|
14106
|
+
await this.executeMethod("call.audioflags.set", {
|
|
13907
14107
|
echo_cancellation: !this.echoCancellation,
|
|
13908
14108
|
auto_gain: this.autoGain,
|
|
13909
14109
|
noise_suppression: this.noiseSuppression
|
|
@@ -13911,7 +14111,7 @@ var Participant = class extends Destroyable {
|
|
|
13911
14111
|
}
|
|
13912
14112
|
/** Toggles automatic gain control on the audio input. */
|
|
13913
14113
|
async toggleAudioInputAutoGain() {
|
|
13914
|
-
await this.executeMethod(
|
|
14114
|
+
await this.executeMethod("call.audioflags.set", {
|
|
13915
14115
|
echo_cancellation: this.echoCancellation,
|
|
13916
14116
|
auto_gain: !this.autoGain,
|
|
13917
14117
|
noise_suppression: this.noiseSuppression
|
|
@@ -13919,7 +14119,7 @@ var Participant = class extends Destroyable {
|
|
|
13919
14119
|
}
|
|
13920
14120
|
/** Toggles noise suppression on the audio input. */
|
|
13921
14121
|
async toggleNoiseSuppression() {
|
|
13922
|
-
await this.executeMethod(
|
|
14122
|
+
await this.executeMethod("call.audioflags.set", {
|
|
13923
14123
|
echo_cancellation: this.echoCancellation,
|
|
13924
14124
|
auto_gain: this.autoGain,
|
|
13925
14125
|
noise_suppression: !this.noiseSuppression
|
|
@@ -13927,7 +14127,7 @@ var Participant = class extends Destroyable {
|
|
|
13927
14127
|
}
|
|
13928
14128
|
/** Toggles low-bitrate mode for this participant's media. */
|
|
13929
14129
|
async toggleLowbitrate() {
|
|
13930
|
-
await this.executeMethod(
|
|
14130
|
+
await this.executeMethod("call.lowbitrate.set", { lowbitrate: !this.lowbitrate });
|
|
13931
14131
|
}
|
|
13932
14132
|
/**
|
|
13933
14133
|
* Adjusts the **conference-only** microphone energy gate / sensitivity level
|
|
@@ -13943,7 +14143,7 @@ var Participant = class extends Destroyable {
|
|
|
13943
14143
|
* (integer, larger values are more sensitive).
|
|
13944
14144
|
*/
|
|
13945
14145
|
async setAudioInputSensitivity(value) {
|
|
13946
|
-
await this.executeMethod(
|
|
14146
|
+
await this.executeMethod("call.microphone.sensitivity.set", { sensitivity: value });
|
|
13947
14147
|
}
|
|
13948
14148
|
/**
|
|
13949
14149
|
* Sets the **server-side** microphone volume on this participant's bridged
|
|
@@ -13956,7 +14156,7 @@ var Participant = class extends Destroyable {
|
|
|
13956
14156
|
* @param value - Volume level (0-100).
|
|
13957
14157
|
*/
|
|
13958
14158
|
async setAudioInputVolume(value) {
|
|
13959
|
-
await this.executeMethod(
|
|
14159
|
+
await this.executeMethod("call.microphone.volume.set", { volume: value });
|
|
13960
14160
|
}
|
|
13961
14161
|
/**
|
|
13962
14162
|
* Sets the **server-side** speaker volume on this participant's bridged call
|
|
@@ -13970,45 +14170,31 @@ var Participant = class extends Destroyable {
|
|
|
13970
14170
|
* @param value - Volume level (0-100).
|
|
13971
14171
|
*/
|
|
13972
14172
|
async setAudioOutputVolume(value) {
|
|
13973
|
-
await this.executeMethod(
|
|
14173
|
+
await this.executeMethod("call.speaker.volume.set", { volume: value });
|
|
13974
14174
|
}
|
|
13975
14175
|
/**
|
|
13976
14176
|
* Sets the participant's position in the video layout.
|
|
13977
14177
|
*
|
|
13978
|
-
* Requires the `member.position` capability. The gateway
|
|
13979
|
-
*
|
|
13980
|
-
*
|
|
13981
|
-
*
|
|
13982
|
-
* guarantee a visible change: the backend silently returns `200` (no-op) for
|
|
13983
|
-
* non-conference targets.
|
|
14178
|
+
* Requires the `member.position` capability. The gateway requires a
|
|
14179
|
+
* `targets` array of `{ target, position }` entries (issue #19400). A
|
|
14180
|
+
* resolved promise does not guarantee a visible change: the backend silently
|
|
14181
|
+
* returns `200` (no-op) for non-conference targets.
|
|
13984
14182
|
*
|
|
13985
14183
|
* @param value - The {@link VideoPosition} to assign (e.g. `'auto'`, `'reserved-0'`).
|
|
13986
14184
|
*/
|
|
13987
14185
|
async setPosition(value) {
|
|
13988
|
-
|
|
13989
|
-
|
|
13990
|
-
member_id: this.id,
|
|
13991
|
-
call_id: state.call_id ?? "",
|
|
13992
|
-
node_id: state.node_id ?? ""
|
|
13993
|
-
};
|
|
13994
|
-
await this.executeMethod(target, "call.member.position.set", { targets: [{
|
|
13995
|
-
target,
|
|
14186
|
+
await this.executeMethod("call.member.position.set", { targets: [{
|
|
14187
|
+
target: this.target,
|
|
13996
14188
|
position: value
|
|
13997
14189
|
}] });
|
|
13998
14190
|
}
|
|
13999
14191
|
/** Removes this participant from the call. */
|
|
14000
14192
|
async remove() {
|
|
14001
|
-
|
|
14002
|
-
const target = {
|
|
14003
|
-
member_id: this.id,
|
|
14004
|
-
call_id: state.call_id ?? "",
|
|
14005
|
-
node_id: state.node_id ?? ""
|
|
14006
|
-
};
|
|
14007
|
-
await this.executeMethod(target, "call.member.remove", {});
|
|
14193
|
+
await this.executeMethod("call.member.remove", { targets: [this.target] });
|
|
14008
14194
|
}
|
|
14009
14195
|
/** Ends the call for this participant. */
|
|
14010
14196
|
async end() {
|
|
14011
|
-
await this.executeMethod(
|
|
14197
|
+
await this.executeMethod("call.end", {});
|
|
14012
14198
|
}
|
|
14013
14199
|
/**
|
|
14014
14200
|
* Replaces custom metadata for this participant.
|
|
@@ -14028,7 +14214,7 @@ var Participant = class extends Destroyable {
|
|
|
14028
14214
|
}
|
|
14029
14215
|
/** Destroys the participant, releasing all subscriptions and references. */
|
|
14030
14216
|
destroy() {
|
|
14031
|
-
this.
|
|
14217
|
+
this.callExecuteMethod = void 0;
|
|
14032
14218
|
super.destroy();
|
|
14033
14219
|
}
|
|
14034
14220
|
};
|
|
@@ -14040,8 +14226,8 @@ var Participant = class extends Destroyable {
|
|
|
14040
14226
|
*/
|
|
14041
14227
|
var SelfParticipant = class extends Participant {
|
|
14042
14228
|
/** @internal */
|
|
14043
|
-
constructor(id,
|
|
14044
|
-
super(id,
|
|
14229
|
+
constructor(id, callExecuteMethod, vertoManager, deviceController) {
|
|
14230
|
+
super(id, callExecuteMethod, deviceController);
|
|
14045
14231
|
this.vertoManager = vertoManager;
|
|
14046
14232
|
this._studioAudio$ = this.createBehaviorSubject(false);
|
|
14047
14233
|
this.capabilities = new SelfCapabilities();
|
|
@@ -14065,7 +14251,7 @@ var SelfParticipant = class extends Participant {
|
|
|
14065
14251
|
async enableStudioAudio() {
|
|
14066
14252
|
if (this._studioAudio$.value) return;
|
|
14067
14253
|
this._studioAudio$.next(true);
|
|
14068
|
-
await this.executeMethod(
|
|
14254
|
+
await this.executeMethod("call.audioflags.set", {
|
|
14069
14255
|
echo_cancellation: false,
|
|
14070
14256
|
auto_gain: false,
|
|
14071
14257
|
noise_suppression: false
|
|
@@ -14078,7 +14264,7 @@ var SelfParticipant = class extends Participant {
|
|
|
14078
14264
|
async disableStudioAudio() {
|
|
14079
14265
|
if (!this._studioAudio$.value) return;
|
|
14080
14266
|
this._studioAudio$.next(false);
|
|
14081
|
-
await this.executeMethod(
|
|
14267
|
+
await this.executeMethod("call.audioflags.set", {
|
|
14082
14268
|
echo_cancellation: true,
|
|
14083
14269
|
auto_gain: true,
|
|
14084
14270
|
noise_suppression: true
|
|
@@ -14087,17 +14273,27 @@ var SelfParticipant = class extends Participant {
|
|
|
14087
14273
|
/**
|
|
14088
14274
|
* Starts sharing the local screen.
|
|
14089
14275
|
*
|
|
14276
|
+
* A call carries at most one screen share. Read `screenShareStatus` before
|
|
14277
|
+
* calling and treat `'starting'`/`'stopping'` as busy.
|
|
14278
|
+
*
|
|
14090
14279
|
* The call is unaffected when acquisition fails.
|
|
14091
14280
|
*
|
|
14281
|
+
* @param options - Pass `{ audio: true }` to also request the shared
|
|
14282
|
+
* surface's audio. Defaults to video only.
|
|
14283
|
+
* @throws {ScreenShareAlreadyActiveError} When this call is already
|
|
14284
|
+
* sharing a screen. Call {@link stopScreenShare} before starting another.
|
|
14285
|
+
* @throws {AuxiliaryLegCancelledError} When {@link stopScreenShare} removes
|
|
14286
|
+
* the share before its leg finishes connecting.
|
|
14092
14287
|
* @throws The raw `getDisplayMedia` error. A dismissed picker or a
|
|
14093
14288
|
* permission denial rejects with a `NotAllowedError` `DOMException` —
|
|
14094
14289
|
* inspect `error.name` to tell benign cancels apart from real failures.
|
|
14095
14290
|
*/
|
|
14096
|
-
async startScreenShare() {
|
|
14291
|
+
async startScreenShare(options) {
|
|
14097
14292
|
try {
|
|
14098
|
-
await this.vertoManager.addScreenMedia();
|
|
14293
|
+
await this.vertoManager.addScreenMedia(options);
|
|
14099
14294
|
} catch (error) {
|
|
14100
|
-
logger$
|
|
14295
|
+
if (error instanceof AuxiliaryLegCancelledError) logger$25.debug("[Participant.startScreenShare] Screen share cancelled before connecting.");
|
|
14296
|
+
else logger$25.error("[Participant.startScreenShare] Screen share error:", error);
|
|
14101
14297
|
throw error;
|
|
14102
14298
|
}
|
|
14103
14299
|
}
|
|
@@ -14118,14 +14314,18 @@ var SelfParticipant = class extends Participant {
|
|
|
14118
14314
|
*
|
|
14119
14315
|
* The call is unaffected when acquisition fails.
|
|
14120
14316
|
*
|
|
14317
|
+
* @throws {AuxiliaryLegCancelledError} When {@link removeAdditionalDevice}
|
|
14318
|
+
* removes the device before its leg finishes connecting.
|
|
14121
14319
|
* @throws The raw `getUserMedia` error (e.g. `NotAllowedError` on
|
|
14122
|
-
* permission denial) — inspect `error.name` to decide how to react
|
|
14320
|
+
* permission denial) — inspect `error.name` to decide how to react — or
|
|
14321
|
+
* `AuxiliaryLegTimeoutError` if the leg does not connect in time.
|
|
14123
14322
|
*/
|
|
14124
14323
|
async addAdditionalDevice(options) {
|
|
14125
14324
|
try {
|
|
14126
14325
|
await this.vertoManager.addInputDevice(options);
|
|
14127
14326
|
} catch (error) {
|
|
14128
|
-
logger$
|
|
14327
|
+
if (error instanceof AuxiliaryLegCancelledError) logger$25.debug("[Participant.addAdditionalDevice] Device removed before connecting.");
|
|
14328
|
+
else logger$25.error("[Participant.addAdditionalDevice] Additional device error:", error);
|
|
14129
14329
|
throw error;
|
|
14130
14330
|
}
|
|
14131
14331
|
}
|
|
@@ -14160,22 +14360,31 @@ var SelfParticipant = class extends Participant {
|
|
|
14160
14360
|
this.deviceController.selectAudioInputDevice(device);
|
|
14161
14361
|
if (options.savePreference) PreferencesContainer.instance.preferredAudioInput = device;
|
|
14162
14362
|
}
|
|
14163
|
-
/**
|
|
14363
|
+
/**
|
|
14364
|
+
* Updates the audio input track constraints for the active call.
|
|
14365
|
+
* @returns whether the constraints reached the media the call is sending.
|
|
14366
|
+
*/
|
|
14164
14367
|
async setAudioInputDeviceConstraints(constraints) {
|
|
14165
|
-
|
|
14368
|
+
return this.vertoManager.updateMediaConstraints({ audio: constraints });
|
|
14166
14369
|
}
|
|
14167
|
-
/**
|
|
14370
|
+
/**
|
|
14371
|
+
* Updates both audio and video input track constraints for the active call.
|
|
14372
|
+
* @returns whether both kinds took the constraints.
|
|
14373
|
+
*/
|
|
14168
14374
|
async setInputDevicesConstraints(constraints) {
|
|
14169
|
-
|
|
14375
|
+
return this.vertoManager.updateMediaConstraints(constraints);
|
|
14170
14376
|
}
|
|
14171
14377
|
/** Selects the video input device for future calls. Optionally saves as a preference. */
|
|
14172
14378
|
selectVideoInputDevice(device, options = {}) {
|
|
14173
14379
|
this.deviceController.selectVideoInputDevice(device);
|
|
14174
14380
|
if (options.savePreference) PreferencesContainer.instance.preferredVideoInput = device;
|
|
14175
14381
|
}
|
|
14176
|
-
/**
|
|
14382
|
+
/**
|
|
14383
|
+
* Updates the video input track constraints for the active call.
|
|
14384
|
+
* @returns whether the constraints reached the media the call is sending.
|
|
14385
|
+
*/
|
|
14177
14386
|
async setVideoInputDeviceConstraints(constraints) {
|
|
14178
|
-
|
|
14387
|
+
return this.vertoManager.updateMediaConstraints({ video: constraints });
|
|
14179
14388
|
}
|
|
14180
14389
|
/** Selects the audio output device. Optionally saves as a preference. */
|
|
14181
14390
|
selectAudioOutputDevice(device, options = {}) {
|
|
@@ -14188,7 +14397,7 @@ var SelfParticipant = class extends Participant {
|
|
|
14188
14397
|
*/
|
|
14189
14398
|
exitStudioModeIfActive() {
|
|
14190
14399
|
if (this._studioAudio$.value) {
|
|
14191
|
-
logger$
|
|
14400
|
+
logger$25.debug("[SelfParticipant] Exiting studio audio mode due to individual flag toggle");
|
|
14192
14401
|
this._studioAudio$.next(false);
|
|
14193
14402
|
}
|
|
14194
14403
|
}
|
|
@@ -14212,7 +14421,7 @@ var SelfParticipant = class extends Participant {
|
|
|
14212
14421
|
try {
|
|
14213
14422
|
await super.mute();
|
|
14214
14423
|
} catch (error) {
|
|
14215
|
-
logger$
|
|
14424
|
+
logger$25.warn("[Participant.toggleAudioInput] Server Error while muting audio input, proceeding with local toggle anyway", error);
|
|
14216
14425
|
} finally {
|
|
14217
14426
|
this.vertoManager.muteMainAudioInputDevice();
|
|
14218
14427
|
}
|
|
@@ -14222,7 +14431,7 @@ var SelfParticipant = class extends Participant {
|
|
|
14222
14431
|
try {
|
|
14223
14432
|
await super.unmute();
|
|
14224
14433
|
} catch (error) {
|
|
14225
|
-
logger$
|
|
14434
|
+
logger$25.warn("[Participant.toggleAudioInput] Server Error while unmuting audio input, proceeding with local toggle anyway", error);
|
|
14226
14435
|
} finally {
|
|
14227
14436
|
await this.vertoManager.unmuteMainAudioInputDevice();
|
|
14228
14437
|
}
|
|
@@ -14232,7 +14441,7 @@ var SelfParticipant = class extends Participant {
|
|
|
14232
14441
|
try {
|
|
14233
14442
|
await super.muteVideo();
|
|
14234
14443
|
} catch (error) {
|
|
14235
|
-
logger$
|
|
14444
|
+
logger$25.warn("[Participant.toggleVideoInput] Server Error while muting video input, proceeding with local toggle anyway", error);
|
|
14236
14445
|
} finally {
|
|
14237
14446
|
this.vertoManager.muteMainVideoInputDevice();
|
|
14238
14447
|
}
|
|
@@ -14242,7 +14451,7 @@ var SelfParticipant = class extends Participant {
|
|
|
14242
14451
|
try {
|
|
14243
14452
|
await super.unmuteVideo();
|
|
14244
14453
|
} catch (error) {
|
|
14245
|
-
logger$
|
|
14454
|
+
logger$25.warn("[Participant.toggleVideoInput] Server Error while unmuting video input, proceeding with local toggle anyway", error);
|
|
14246
14455
|
} finally {
|
|
14247
14456
|
await this.vertoManager.unmuteMainVideoInputDevice();
|
|
14248
14457
|
}
|
|
@@ -14447,7 +14656,7 @@ function filterAs(predicate, resultPath) {
|
|
|
14447
14656
|
//#endregion
|
|
14448
14657
|
//#region src/operators/throwOnRPCError.ts
|
|
14449
14658
|
var import_cjs$21 = require_cjs();
|
|
14450
|
-
const logger$
|
|
14659
|
+
const logger$24 = getLogger();
|
|
14451
14660
|
/**
|
|
14452
14661
|
* RxJS operator that throws a {@link JSONRPCError} when the RPC response contains an error.
|
|
14453
14662
|
* Passes successful responses through unchanged.
|
|
@@ -14455,14 +14664,14 @@ const logger$23 = getLogger();
|
|
|
14455
14664
|
function throwOnRPCError() {
|
|
14456
14665
|
return (0, import_cjs$21.map)((response) => {
|
|
14457
14666
|
if (response.error) {
|
|
14458
|
-
logger$
|
|
14667
|
+
logger$24.error("[throwOnRPCError] RPC error response:", {
|
|
14459
14668
|
code: response.error.code,
|
|
14460
14669
|
message: response.error.message,
|
|
14461
14670
|
data: response.error.data
|
|
14462
14671
|
});
|
|
14463
14672
|
throw new JSONRPCError(response.error.code, response.error.message, response.error.data);
|
|
14464
14673
|
}
|
|
14465
|
-
logger$
|
|
14674
|
+
logger$24.debug("[throwOnRPCError] RPC successful response:", response);
|
|
14466
14675
|
return response;
|
|
14467
14676
|
});
|
|
14468
14677
|
}
|
|
@@ -14470,7 +14679,7 @@ function throwOnRPCError() {
|
|
|
14470
14679
|
//#endregion
|
|
14471
14680
|
//#region src/managers/CallEventsManager.ts
|
|
14472
14681
|
var import_cjs$20 = require_cjs();
|
|
14473
|
-
const logger$
|
|
14682
|
+
const logger$23 = getLogger();
|
|
14474
14683
|
const initialSessionState = {};
|
|
14475
14684
|
/** @internal */
|
|
14476
14685
|
var CallEventsManager = class extends Destroyable {
|
|
@@ -14574,7 +14783,7 @@ var CallEventsManager = class extends Destroyable {
|
|
|
14574
14783
|
}
|
|
14575
14784
|
initSubscriptions() {
|
|
14576
14785
|
this.subscribeTo(this.callJoinedEvent$, (callJoinedEvent) => {
|
|
14577
|
-
logger$
|
|
14786
|
+
logger$23.debug("[CallEventsManager] Handling call.joined event for call/session IDs:", {
|
|
14578
14787
|
callId: callJoinedEvent.call_id,
|
|
14579
14788
|
roomSessionId: callJoinedEvent.room_session_id
|
|
14580
14789
|
});
|
|
@@ -14601,19 +14810,19 @@ var CallEventsManager = class extends Destroyable {
|
|
|
14601
14810
|
if (this._self$.value?.capabilities.setLayout) this.updateLayouts();
|
|
14602
14811
|
});
|
|
14603
14812
|
this.subscribeTo(this.memberUpdates$, (member) => {
|
|
14604
|
-
logger$
|
|
14813
|
+
logger$23.debug("[CallEventsManager] Handling member update event for member ID:", member);
|
|
14605
14814
|
this.upsertParticipant(member);
|
|
14606
14815
|
});
|
|
14607
14816
|
this.subscribeTo(this.webRtcCallSession.memberLeft$, (memberLeftEvent) => {
|
|
14608
|
-
logger$
|
|
14817
|
+
logger$23.debug("[CallEventsManager] Handling member.left event for member ID:", memberLeftEvent.member.member_id);
|
|
14609
14818
|
const participants = { ...this._participants$.value };
|
|
14610
14819
|
if (memberLeftEvent.member.member_id in participants) {
|
|
14611
14820
|
delete participants[memberLeftEvent.member.member_id];
|
|
14612
14821
|
this._participants$.next(participants);
|
|
14613
|
-
} else logger$
|
|
14822
|
+
} else logger$23.warn(`[CallEventsManager] Received member.left event for unknown member ID: ${memberLeftEvent.member.member_id}`);
|
|
14614
14823
|
});
|
|
14615
14824
|
this.subscribeTo(this.webRtcCallSession.callUpdated$, (callUpdatedEvent) => {
|
|
14616
|
-
logger$
|
|
14825
|
+
logger$23.debug("[CallEventsManager] Handling call.updated event:", callUpdatedEvent);
|
|
14617
14826
|
const roomSession = callUpdatedEvent.room_session;
|
|
14618
14827
|
this._sessionState$.next({
|
|
14619
14828
|
...this._sessionState$.value,
|
|
@@ -14628,7 +14837,7 @@ var CallEventsManager = class extends Destroyable {
|
|
|
14628
14837
|
});
|
|
14629
14838
|
});
|
|
14630
14839
|
this.subscribeTo(this.layoutChangedEvent$, (layoutChangedEvent) => {
|
|
14631
|
-
logger$
|
|
14840
|
+
logger$23.debug("[CallEventsManager] Handling layout.changed event:", layoutChangedEvent);
|
|
14632
14841
|
this._sessionState$.next({
|
|
14633
14842
|
...this._sessionState$.value,
|
|
14634
14843
|
layout_name: layoutChangedEvent.id,
|
|
@@ -14638,10 +14847,10 @@ var CallEventsManager = class extends Destroyable {
|
|
|
14638
14847
|
});
|
|
14639
14848
|
}
|
|
14640
14849
|
updateParticipantPositions(layoutChangedEvent) {
|
|
14641
|
-
if (Object.keys(this._participants$.value).length > 0 && !layoutChangedEvent.layers.some((layer) => !!layer.member_id)) logger$
|
|
14850
|
+
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.");
|
|
14642
14851
|
layoutChangedEvent.layers.filter((layer) => !!layer.member_id).filter((layer) => {
|
|
14643
14852
|
if (!(layer.member_id in this._participants$.value)) {
|
|
14644
|
-
logger$
|
|
14853
|
+
logger$23.warn(`[CallEventsManager] Skipping layout layer for unknown member_id: ${layer.member_id}`);
|
|
14645
14854
|
return false;
|
|
14646
14855
|
}
|
|
14647
14856
|
return true;
|
|
@@ -14659,12 +14868,17 @@ var CallEventsManager = class extends Destroyable {
|
|
|
14659
14868
|
updateLayouts() {
|
|
14660
14869
|
if (!this.selfId) return;
|
|
14661
14870
|
this.webRtcCallSession.executeMethod(this.selfId, "call.layout.list", {}).then((response) => {
|
|
14871
|
+
const layouts = response.result?.layouts;
|
|
14872
|
+
if (!layouts) {
|
|
14873
|
+
logger$23.warn("[CallEventsManager] Layout list response carried no layouts; keeping current layouts");
|
|
14874
|
+
return;
|
|
14875
|
+
}
|
|
14662
14876
|
this._sessionState$.next({
|
|
14663
14877
|
...this._sessionState$.value,
|
|
14664
|
-
layouts
|
|
14878
|
+
layouts
|
|
14665
14879
|
});
|
|
14666
14880
|
}).catch((error) => {
|
|
14667
|
-
logger$
|
|
14881
|
+
logger$23.error("[CallEventsManager] Error fetching layouts:", error);
|
|
14668
14882
|
});
|
|
14669
14883
|
}
|
|
14670
14884
|
updateParticipants(members) {
|
|
@@ -14680,7 +14894,7 @@ var CallEventsManager = class extends Destroyable {
|
|
|
14680
14894
|
}
|
|
14681
14895
|
const participant = this._participants$.value[member.member_id];
|
|
14682
14896
|
const oldValue = participant.value;
|
|
14683
|
-
logger$
|
|
14897
|
+
logger$23.debug("[CallEventsManager] Updating participant:", member.member_id, {
|
|
14684
14898
|
oldValue,
|
|
14685
14899
|
newValue: member
|
|
14686
14900
|
});
|
|
@@ -14693,17 +14907,17 @@ var CallEventsManager = class extends Destroyable {
|
|
|
14693
14907
|
}
|
|
14694
14908
|
get callJoinedEvent$() {
|
|
14695
14909
|
return this.cachedObservable("callJoinedEvent$", () => this.webRtcCallSession.callEvent$.pipe((0, import_cjs$20.filter)(isCallJoinedPayload), (0, import_cjs$20.tap)((event) => {
|
|
14696
|
-
logger$
|
|
14910
|
+
logger$23.debug("[CallEventsManager] Call joined event:", event);
|
|
14697
14911
|
})));
|
|
14698
14912
|
}
|
|
14699
14913
|
get layoutChangedEvent$() {
|
|
14700
14914
|
return this.cachedObservable("layoutChangedEvent$", () => this.webRtcCallSession.callEvent$.pipe(filterAs(isLayoutChangedPayload, "layout"), (0, import_cjs$20.tap)((event) => {
|
|
14701
|
-
logger$
|
|
14915
|
+
logger$23.debug("[CallEventsManager] Layout changed event:", event);
|
|
14702
14916
|
})));
|
|
14703
14917
|
}
|
|
14704
14918
|
get memberUpdates$() {
|
|
14705
14919
|
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) => {
|
|
14706
|
-
logger$
|
|
14920
|
+
logger$23.debug("[CallEventsManager] Member update event:", event);
|
|
14707
14921
|
})));
|
|
14708
14922
|
}
|
|
14709
14923
|
destroy() {
|
|
@@ -14722,6 +14936,111 @@ var CallEventsManager = class extends Destroyable {
|
|
|
14722
14936
|
}
|
|
14723
14937
|
};
|
|
14724
14938
|
|
|
14939
|
+
//#endregion
|
|
14940
|
+
//#region src/controllers/ConstraintFallbackHelper.ts
|
|
14941
|
+
/**
|
|
14942
|
+
* ConstraintFallbackHelper - Provides getUserMedia with automatic constraint
|
|
14943
|
+
* fallback on OverconstrainedError.
|
|
14944
|
+
*
|
|
14945
|
+
* When a specific device ID is requested, the helper tries progressively
|
|
14946
|
+
* looser constraints:
|
|
14947
|
+
* 1. `{ deviceId: { exact: deviceId } }` -- exact match
|
|
14948
|
+
* 2. `{ deviceId: deviceId }` -- preferred (browser may pick another)
|
|
14949
|
+
* 3. `{}` -- no constraint, browser default
|
|
14950
|
+
*
|
|
14951
|
+
* This prevents stale device IDs from blocking call setup.
|
|
14952
|
+
*
|
|
14953
|
+
* @see Section 5.8 and Section 11 of the Implementation Guide
|
|
14954
|
+
*/
|
|
14955
|
+
const logger$22 = getLogger();
|
|
14956
|
+
/**
|
|
14957
|
+
* Attempts getUserMedia with progressively looser constraints.
|
|
14958
|
+
*
|
|
14959
|
+
* The function tries three levels of constraint specificity for the given
|
|
14960
|
+
* device kind. Each level is only attempted if the previous one fails with
|
|
14961
|
+
* an OverconstrainedError. Non-OverconstrainedError failures (e.g.,
|
|
14962
|
+
* NotAllowedError) are thrown immediately without fallback.
|
|
14963
|
+
*
|
|
14964
|
+
* @param mediaDevices - Anything exposing `getUserMedia` (a full
|
|
14965
|
+
* `WebRTCMediaDevices`, or a shim wrapping one)
|
|
14966
|
+
* @param constraints - The full MediaStreamConstraints to use as a base
|
|
14967
|
+
* @param kind - Which track kind to apply fallback to ('audio' | 'video')
|
|
14968
|
+
* @param deviceId - The device ID to try (if undefined, calls getUserMedia as-is)
|
|
14969
|
+
* @returns The stream and the fallback level that succeeded
|
|
14970
|
+
* @throws When all fallback levels fail, or when a non-OverconstrainedError occurs
|
|
14971
|
+
*/
|
|
14972
|
+
async function getUserMediaWithFallback(mediaDevices, constraints, kind, deviceId) {
|
|
14973
|
+
if (!deviceId) return {
|
|
14974
|
+
stream: await mediaDevices.getUserMedia(constraints),
|
|
14975
|
+
fallbackLevel: "default"
|
|
14976
|
+
};
|
|
14977
|
+
const baseConstraints = typeof constraints[kind] === "object" ? constraints[kind] : {};
|
|
14978
|
+
try {
|
|
14979
|
+
const exactConstraints = {
|
|
14980
|
+
...constraints,
|
|
14981
|
+
[kind]: {
|
|
14982
|
+
...baseConstraints,
|
|
14983
|
+
deviceId: { exact: deviceId }
|
|
14984
|
+
}
|
|
14985
|
+
};
|
|
14986
|
+
return {
|
|
14987
|
+
stream: await mediaDevices.getUserMedia(exactConstraints),
|
|
14988
|
+
fallbackLevel: "exact"
|
|
14989
|
+
};
|
|
14990
|
+
} catch (error) {
|
|
14991
|
+
if (!isOverconstrainedError(error)) throw error;
|
|
14992
|
+
logger$22.debug(`[ConstraintFallbackHelper] Exact constraint failed for ${kind}, trying preferred`, { deviceId });
|
|
14993
|
+
}
|
|
14994
|
+
try {
|
|
14995
|
+
const preferredConstraints = {
|
|
14996
|
+
...constraints,
|
|
14997
|
+
[kind]: {
|
|
14998
|
+
...baseConstraints,
|
|
14999
|
+
deviceId
|
|
15000
|
+
}
|
|
15001
|
+
};
|
|
15002
|
+
return {
|
|
15003
|
+
stream: await mediaDevices.getUserMedia(preferredConstraints),
|
|
15004
|
+
fallbackLevel: "preferred"
|
|
15005
|
+
};
|
|
15006
|
+
} catch (error) {
|
|
15007
|
+
if (!isOverconstrainedError(error)) throw error;
|
|
15008
|
+
logger$22.debug(`[ConstraintFallbackHelper] Preferred constraint failed for ${kind}, trying default`, { deviceId });
|
|
15009
|
+
}
|
|
15010
|
+
try {
|
|
15011
|
+
const defaultConstraints = {
|
|
15012
|
+
...constraints,
|
|
15013
|
+
[kind]: { ...baseConstraints }
|
|
15014
|
+
};
|
|
15015
|
+
if (typeof defaultConstraints[kind] === "object") {
|
|
15016
|
+
const { deviceId: _removed, ...rest } = defaultConstraints[kind];
|
|
15017
|
+
defaultConstraints[kind] = rest;
|
|
15018
|
+
}
|
|
15019
|
+
const stream = await mediaDevices.getUserMedia(defaultConstraints);
|
|
15020
|
+
logger$22.warn(`[ConstraintFallbackHelper] Fell back to browser default for ${kind}`, { requestedDeviceId: deviceId });
|
|
15021
|
+
return {
|
|
15022
|
+
stream,
|
|
15023
|
+
fallbackLevel: "default"
|
|
15024
|
+
};
|
|
15025
|
+
} catch (error) {
|
|
15026
|
+
logger$22.error(`[ConstraintFallbackHelper] All fallback levels exhausted for ${kind}`, {
|
|
15027
|
+
deviceId,
|
|
15028
|
+
error
|
|
15029
|
+
});
|
|
15030
|
+
throw error;
|
|
15031
|
+
}
|
|
15032
|
+
}
|
|
15033
|
+
/**
|
|
15034
|
+
* Checks whether an error is an OverconstrainedError.
|
|
15035
|
+
*
|
|
15036
|
+
* Browsers may throw either a native OverconstrainedError or a DOMException
|
|
15037
|
+
* with a specific name.
|
|
15038
|
+
*/
|
|
15039
|
+
function isOverconstrainedError(error) {
|
|
15040
|
+
if (error instanceof Error) return error.name === "OverconstrainedError" || error.name === "ConstraintNotSatisfiedError";
|
|
15041
|
+
return false;
|
|
15042
|
+
}
|
|
15043
|
+
|
|
14725
15044
|
//#endregion
|
|
14726
15045
|
//#region src/helpers/SDPHelper.ts
|
|
14727
15046
|
/**
|
|
@@ -15280,6 +15599,7 @@ var LocalStreamController = class extends Destroyable {
|
|
|
15280
15599
|
this._localAudioTracks$ = this.createBehaviorSubject([]);
|
|
15281
15600
|
this._localVideoTracks$ = this.createBehaviorSubject([]);
|
|
15282
15601
|
this._mediaTrackEnded$ = this.createSubject();
|
|
15602
|
+
this._trackOrigins = /* @__PURE__ */ new WeakMap();
|
|
15283
15603
|
}
|
|
15284
15604
|
get localStream$() {
|
|
15285
15605
|
return this._localStream$.asObservable().pipe((0, import_cjs$17.takeUntil)(this.destroyed$));
|
|
@@ -15302,6 +15622,22 @@ var LocalStreamController = class extends Destroyable {
|
|
|
15302
15622
|
get localVideoTracks() {
|
|
15303
15623
|
return this._localVideoTracks$.value;
|
|
15304
15624
|
}
|
|
15625
|
+
tagTracks(tracks, origin) {
|
|
15626
|
+
for (const track of tracks) this._trackOrigins.set(track, origin);
|
|
15627
|
+
}
|
|
15628
|
+
setTrackOrigin(track, origin) {
|
|
15629
|
+
this._trackOrigins.set(track, origin);
|
|
15630
|
+
}
|
|
15631
|
+
getTrackOrigin(track) {
|
|
15632
|
+
return this._trackOrigins.get(track);
|
|
15633
|
+
}
|
|
15634
|
+
/**
|
|
15635
|
+
* Fail-safe: an unrecorded track reads as not-a-device-capture, so a missed
|
|
15636
|
+
* tagging site leaves media alone rather than destroying it.
|
|
15637
|
+
*/
|
|
15638
|
+
isDeviceCapture(track) {
|
|
15639
|
+
return this._trackOrigins.get(track) === "device";
|
|
15640
|
+
}
|
|
15305
15641
|
/**
|
|
15306
15642
|
* Build the local media stream based on the provided options.
|
|
15307
15643
|
*/
|
|
@@ -15311,13 +15647,16 @@ var LocalStreamController = class extends Destroyable {
|
|
|
15311
15647
|
if (this.options.inputAudioStream ?? this.options.inputVideoStream) {
|
|
15312
15648
|
const tracks = [...this.options.inputAudioStream?.getTracks() ?? [], ...this.options.inputVideoStream?.getTracks() ?? []];
|
|
15313
15649
|
stream = new MediaStream(tracks);
|
|
15650
|
+
this.tagTracks(tracks, "application");
|
|
15314
15651
|
} else if (this.options.propose === "screenshare") {
|
|
15315
|
-
|
|
15652
|
+
const audio = this.options.screenShareAudio ?? false;
|
|
15653
|
+
logger$19.debug("[LocalStreamController] Requesting display media for screen sharing with audio:", audio);
|
|
15316
15654
|
stream = await this.options.getDisplayMedia({
|
|
15317
15655
|
video: true,
|
|
15318
|
-
audio
|
|
15656
|
+
audio
|
|
15319
15657
|
});
|
|
15320
15658
|
logger$19.debug("[LocalStreamController] Screen share media obtained:", stream);
|
|
15659
|
+
this.tagTracks(stream.getTracks(), "display");
|
|
15321
15660
|
} else {
|
|
15322
15661
|
const constraints = {
|
|
15323
15662
|
audio: this.options.inputAudioDeviceConstraints,
|
|
@@ -15326,6 +15665,7 @@ var LocalStreamController = class extends Destroyable {
|
|
|
15326
15665
|
logger$19.debug("[LocalStreamController] Requesting user media with constraints:", constraints);
|
|
15327
15666
|
stream = await this.options.getUserMedia(constraints);
|
|
15328
15667
|
logger$19.debug("[LocalStreamController] User media obtained:", stream);
|
|
15668
|
+
this.tagTracks(stream.getTracks(), "device");
|
|
15329
15669
|
}
|
|
15330
15670
|
this._localStream$.next(stream);
|
|
15331
15671
|
this._localAudioTracks$.next(stream.getAudioTracks());
|
|
@@ -15335,10 +15675,13 @@ var LocalStreamController = class extends Destroyable {
|
|
|
15335
15675
|
/**
|
|
15336
15676
|
* Add a local media track to the local stream.
|
|
15337
15677
|
* @param track - The MediaStreamTrack to add
|
|
15678
|
+
* @param origin - Defaults to `'device'`; every internal caller passes a
|
|
15679
|
+
* fresh `getUserMedia` capture.
|
|
15338
15680
|
* @returns The MediaStream (either existing or newly created)
|
|
15339
15681
|
*/
|
|
15340
|
-
addTrack(track) {
|
|
15682
|
+
addTrack(track, origin = "device") {
|
|
15341
15683
|
const localStream = this._localStream$.value ?? new MediaStream();
|
|
15684
|
+
this._trackOrigins.set(track, origin);
|
|
15342
15685
|
track.addEventListener("ended", this.mediaTrackEndedHandler);
|
|
15343
15686
|
localStream.addTrack(track);
|
|
15344
15687
|
this._localStream$.next(localStream);
|
|
@@ -15608,15 +15951,27 @@ var TransceiverController = class extends Destroyable {
|
|
|
15608
15951
|
for (let i = 0; i < Number(msStreamsNumber); i++) this.peerConnection.addTransceiver("video", { direction: "recvonly" });
|
|
15609
15952
|
}
|
|
15610
15953
|
}
|
|
15954
|
+
/**
|
|
15955
|
+
* @returns whether every live sender of the kind took the constraints. A
|
|
15956
|
+
* skipped non-device sender, an exhausted fallback, and having no live sender
|
|
15957
|
+
* at all all report `false` — `mediaParamsUpdated.applied` is built from this,
|
|
15958
|
+
* and an application told `true` cannot tell a working push from a no-op.
|
|
15959
|
+
*/
|
|
15611
15960
|
async updateSendersConstraints(kind, constraints) {
|
|
15612
15961
|
if (!constraints) {
|
|
15613
15962
|
this.stopTrackSender(kind);
|
|
15614
|
-
return
|
|
15963
|
+
return false;
|
|
15615
15964
|
}
|
|
15616
15965
|
const senders = this.peerConnection.getSenders().filter((sender) => sender.track?.kind === kind && sender.track.readyState === "live");
|
|
15966
|
+
let applied = senders.length > 0;
|
|
15617
15967
|
for (const sender of senders) {
|
|
15618
15968
|
const { track } = sender;
|
|
15619
15969
|
if (track) {
|
|
15970
|
+
if (!this.options.localStreamController.isDeviceCapture(track)) {
|
|
15971
|
+
logger$18.debug(`[TransceiverController] Skipping ${kind} constraints for a non-device track (origin: ${this.options.localStreamController.getTrackOrigin(track) ?? "unrecorded"}), track ${track.id}`);
|
|
15972
|
+
applied = false;
|
|
15973
|
+
continue;
|
|
15974
|
+
}
|
|
15620
15975
|
const constraintsToApply = {
|
|
15621
15976
|
...track.getConstraints(),
|
|
15622
15977
|
...constraints
|
|
@@ -15632,33 +15987,39 @@ var TransceiverController = class extends Destroyable {
|
|
|
15632
15987
|
} catch (fallbackError) {
|
|
15633
15988
|
logger$18.warn(`[TransceiverController] Track replacement fallback also failed for ${kind} track:`, fallbackError);
|
|
15634
15989
|
this.options.onError?.(new MediaTrackError("updateSendersConstraints", kind, fallbackError));
|
|
15990
|
+
applied = false;
|
|
15635
15991
|
}
|
|
15636
15992
|
}
|
|
15637
15993
|
}
|
|
15638
15994
|
}
|
|
15995
|
+
return applied;
|
|
15639
15996
|
}
|
|
15640
15997
|
/**
|
|
15641
|
-
* Fallback when applyConstraints fails
|
|
15642
|
-
* one via getUserMedia with the merged constraints (preserving the current
|
|
15643
|
-
* deviceId), replace the sender track, and update the localStream.
|
|
15998
|
+
* Fallback when applyConstraints fails, which on iOS Safari it silently does.
|
|
15644
15999
|
*
|
|
15645
|
-
*
|
|
15646
|
-
*
|
|
16000
|
+
* Order matters: acquiring before stopping means a failed acquisition leaves
|
|
16001
|
+
* the existing media playing. The deviceId goes through the fallback ladder
|
|
16002
|
+
* rather than pinned `{ exact }`, so a stale id degrades instead of failing.
|
|
15647
16003
|
*/
|
|
15648
16004
|
async replaceTrackFallback(sender, oldTrack, kind, mergedConstraints) {
|
|
15649
16005
|
const { deviceId } = oldTrack.getSettings();
|
|
15650
|
-
const
|
|
15651
|
-
|
|
15652
|
-
|
|
15653
|
-
|
|
15654
|
-
|
|
16006
|
+
const { stream, fallbackLevel } = await getUserMediaWithFallback({ getUserMedia: this.options.getUserMedia }, { [kind]: mergedConstraints }, kind, deviceId);
|
|
16007
|
+
const newTrack = stream.getTracks().find((t) => t.kind === kind);
|
|
16008
|
+
if (!newTrack) {
|
|
16009
|
+
stream.getTracks().forEach((t) => t.stop());
|
|
16010
|
+
throw new MediaTrackError("replaceTrackFallback", kind, /* @__PURE__ */ new Error("getUserMedia returned no track of the requested kind"));
|
|
16011
|
+
}
|
|
16012
|
+
try {
|
|
16013
|
+
await sender.replaceTrack(newTrack);
|
|
16014
|
+
} catch (error) {
|
|
16015
|
+
stream.getTracks().forEach((t) => t.stop());
|
|
16016
|
+
throw error;
|
|
16017
|
+
}
|
|
16018
|
+
const oldTrackId = oldTrack.id;
|
|
16019
|
+
this.options.localStreamController.removeTrack(oldTrackId);
|
|
15655
16020
|
oldTrack.stop();
|
|
15656
|
-
this.options.localStreamController.removeTrack(trackId);
|
|
15657
|
-
const newTrack = (await this.options.getUserMedia({ [kind]: constraintsWithDevice })).getTracks().find((t) => t.kind === kind);
|
|
15658
|
-
if (!newTrack) throw new MediaTrackError("replaceTrackFallback", kind, /* @__PURE__ */ new Error("getUserMedia returned no track of the requested kind"));
|
|
15659
|
-
await sender.replaceTrack(newTrack);
|
|
15660
16021
|
this.options.localStreamController.addTrack(newTrack);
|
|
15661
|
-
logger$18.debug(`[TransceiverController] Track replacement fallback succeeded for ${kind}. New track: ${newTrack.id}`);
|
|
16022
|
+
logger$18.debug(`[TransceiverController] Track replacement fallback succeeded for ${kind} (deviceId fallback level: ${fallbackLevel}). New track: ${newTrack.id}`);
|
|
15662
16023
|
}
|
|
15663
16024
|
getMediaDirections() {
|
|
15664
16025
|
if (this.peerConnection.connectionState === "connected") return this.peerConnection.getTransceivers().reduce((acc, transceiver) => {
|
|
@@ -15728,48 +16089,44 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
15728
16089
|
this.negotiationNeeded$.next();
|
|
15729
16090
|
};
|
|
15730
16091
|
this.updateSelectedInputDevice = async (kind, deviceInfo) => {
|
|
16092
|
+
const { localStream } = this;
|
|
16093
|
+
if (!localStream) {
|
|
16094
|
+
logger$17.warn("[RTCPeerConnectionController] No local stream available to update input device.");
|
|
16095
|
+
return;
|
|
16096
|
+
}
|
|
16097
|
+
const currentTrack = localStream.getTracks().find((track) => track.kind === kind);
|
|
16098
|
+
if (!currentTrack) {
|
|
16099
|
+
logger$17.debug(`[RTCPeerConnectionController] No ${kind} track to switch.`);
|
|
16100
|
+
return;
|
|
16101
|
+
}
|
|
16102
|
+
if (!deviceInfo) {
|
|
16103
|
+
logger$17.debug(`[RTCPeerConnectionController] ${kind} input device selected: none`);
|
|
16104
|
+
this.stopTrackSender(kind);
|
|
16105
|
+
return;
|
|
16106
|
+
}
|
|
16107
|
+
const constraints = {
|
|
16108
|
+
...currentTrack.getConstraints(),
|
|
16109
|
+
...this.deviceController.deviceInfoToConstraints(deviceInfo)
|
|
16110
|
+
};
|
|
15731
16111
|
try {
|
|
15732
|
-
const
|
|
15733
|
-
|
|
15734
|
-
|
|
15735
|
-
return;
|
|
15736
|
-
}
|
|
15737
|
-
logger$17.debug(`[RTCPeerConnectionController] Updating selected ${kind} input device:`, localStream.getTracks());
|
|
15738
|
-
const track = localStream.getTracks().find((track$1) => track$1.kind === kind);
|
|
15739
|
-
if (track) {
|
|
15740
|
-
this.transceiverController?.stopTrackSender(kind);
|
|
15741
|
-
this.localStreamController.removeTrack(track.id);
|
|
15742
|
-
logger$17.debug(`[RTCPeerConnectionController] Stopped existing ${kind} track: ${track.id}`, localStream.getTracks());
|
|
15743
|
-
if (!deviceInfo) {
|
|
15744
|
-
logger$17.debug(`[RTCPeerConnectionController] ${kind} input device selected: none`);
|
|
15745
|
-
return;
|
|
15746
|
-
}
|
|
15747
|
-
const streamTrack = (await this.getUserMedia({ [kind]: {
|
|
15748
|
-
...track.getConstraints(),
|
|
15749
|
-
...this.deviceController.deviceInfoToConstraints(deviceInfo)
|
|
15750
|
-
} })).getTracks().find((t) => t.kind === kind);
|
|
15751
|
-
if (streamTrack) {
|
|
15752
|
-
logger$17.debug(`[RTCPeerConnectionController] Adding new ${kind} track: ${streamTrack.id}`);
|
|
15753
|
-
this.localStreamController.addTrack(streamTrack);
|
|
15754
|
-
await this.transceiverController?.replaceSenderTrack(kind, streamTrack);
|
|
15755
|
-
logger$17.debug(`[RTCPeerConnectionController] Added new ${kind} track: ${streamTrack.id}`, this.localStream?.getTracks());
|
|
15756
|
-
}
|
|
15757
|
-
}
|
|
15758
|
-
logger$17.debug(`[RTCPeerConnectionController] ${kind} input device selected:`, deviceInfo?.label);
|
|
16112
|
+
const newTrack = await this.acquireInputTrack(kind, constraints, deviceInfo, currentTrack);
|
|
16113
|
+
await this.attachInputTrack(kind, newTrack, currentTrack);
|
|
16114
|
+
logger$17.debug(`[RTCPeerConnectionController] ${kind} input device selected:`, deviceInfo.label, newTrack.id);
|
|
15759
16115
|
} catch (error) {
|
|
15760
16116
|
logger$17.error(`[RTCPeerConnectionController] Failed to select ${kind} input device:`, error);
|
|
15761
16117
|
this._errors$.next(new MediaTrackError("updateSelectedInputDevice", kind, error));
|
|
15762
|
-
throw error;
|
|
15763
16118
|
}
|
|
15764
16119
|
};
|
|
15765
16120
|
this._isNegotiating$ = this.createBehaviorSubject(false);
|
|
15766
16121
|
this._memberId = null;
|
|
16122
|
+
this._nodeId = null;
|
|
15767
16123
|
this._iceConnectionState$ = this.createReplaySubject(1);
|
|
15768
16124
|
this._connectionState$ = this.createReplaySubject(1);
|
|
15769
16125
|
this._signalingState$ = this.createReplaySubject(1);
|
|
15770
16126
|
this._iceGatheringState$ = this.createReplaySubject(1);
|
|
15771
16127
|
this._errors$ = this.createReplaySubject(1);
|
|
15772
16128
|
this._iceCandidates$ = this.createReplaySubject(1);
|
|
16129
|
+
this._localMediaSettled$ = this.createReplaySubject(1);
|
|
15773
16130
|
this._initialized$ = this.createReplaySubject(1);
|
|
15774
16131
|
this._remoteDescription$ = this.createReplaySubject(1);
|
|
15775
16132
|
this._remoteStream$ = this.createBehaviorSubject(null);
|
|
@@ -15802,6 +16159,7 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
15802
16159
|
inputVideoStream: this.options.inputVideoStream,
|
|
15803
16160
|
inputAudioDeviceConstraints: this.inputAudioDeviceConstraints,
|
|
15804
16161
|
inputVideoDeviceConstraints: this.inputVideoDeviceConstraints,
|
|
16162
|
+
screenShareAudio: this.options.screenShareAudio,
|
|
15805
16163
|
getUserMedia: async (constraints) => this.getUserMedia(constraints),
|
|
15806
16164
|
getDisplayMedia: async (options$1) => this.getDisplayMedia(options$1)
|
|
15807
16165
|
});
|
|
@@ -15828,6 +16186,13 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
15828
16186
|
get memberId() {
|
|
15829
16187
|
return this._memberId;
|
|
15830
16188
|
}
|
|
16189
|
+
/** The node this leg's invite landed on — auxiliary legs are placed independently. */
|
|
16190
|
+
setNodeId(nodeId) {
|
|
16191
|
+
this._nodeId = nodeId;
|
|
16192
|
+
}
|
|
16193
|
+
get nodeId() {
|
|
16194
|
+
return this._nodeId;
|
|
16195
|
+
}
|
|
15831
16196
|
stopTrackSender(kind, options = { updateTransceiverDirection: false }) {
|
|
15832
16197
|
const audioCovered = kind === "audio" || kind === "both";
|
|
15833
16198
|
if (audioCovered && this._localAudioPipeline) this.stopRawAudioInputForPipeline();
|
|
@@ -15837,10 +16202,7 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
15837
16202
|
}
|
|
15838
16203
|
stopRawAudioInputForPipeline() {
|
|
15839
16204
|
const rawTracks = this.localStreamController.localAudioTracks;
|
|
15840
|
-
for (const track of rawTracks) if (track.readyState === "live")
|
|
15841
|
-
track.stop();
|
|
15842
|
-
this.localStreamController.removeTrack(track.id);
|
|
15843
|
-
}
|
|
16205
|
+
for (const track of rawTracks) if (track.readyState === "live") this.localStreamController.removeTrack(track.id);
|
|
15844
16206
|
this._localAudioPipeline?.setInputTrack(null);
|
|
15845
16207
|
}
|
|
15846
16208
|
get isNegotiating$() {
|
|
@@ -15873,6 +16235,10 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
15873
16235
|
get remoteDescription$() {
|
|
15874
16236
|
return this.cachedObservable("remoteDescription$", () => this._remoteDescription$.asObservable().pipe((0, import_cjs$16.takeUntil)(this.destroyed$)));
|
|
15875
16237
|
}
|
|
16238
|
+
/** Emits once local media is settled — acquired, or knowingly receive-only. */
|
|
16239
|
+
get localMediaSettled$() {
|
|
16240
|
+
return this.cachedObservable("localMediaSettled$", () => this._localMediaSettled$.asObservable().pipe((0, import_cjs$16.takeUntil)(this.destroyed$)));
|
|
16241
|
+
}
|
|
15876
16242
|
get localStream$() {
|
|
15877
16243
|
return this.cachedObservable("localStream$", () => this.localStreamController.localStream$.pipe((0, import_cjs$16.takeUntil)(this.destroyed$)));
|
|
15878
16244
|
}
|
|
@@ -15900,6 +16266,9 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
15900
16266
|
get propose() {
|
|
15901
16267
|
return this.options.propose ?? "main";
|
|
15902
16268
|
}
|
|
16269
|
+
get connectionState() {
|
|
16270
|
+
return this.peerConnection?.connectionState;
|
|
16271
|
+
}
|
|
15903
16272
|
get isAdditionalDevice() {
|
|
15904
16273
|
return this.propose === "additional-device";
|
|
15905
16274
|
}
|
|
@@ -16028,7 +16397,7 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
16028
16397
|
this._isNegotiating$.next(true);
|
|
16029
16398
|
await this._setRemoteDescription(this.sdpInit);
|
|
16030
16399
|
} else {
|
|
16031
|
-
await this.setupTrackHandling();
|
|
16400
|
+
if (!await this.setupTrackHandling()) return;
|
|
16032
16401
|
this._initialized$.next(true);
|
|
16033
16402
|
}
|
|
16034
16403
|
} catch (error) {
|
|
@@ -16152,7 +16521,10 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
16152
16521
|
inputVideoDeviceConstraints: this.inputVideoDeviceConstraints
|
|
16153
16522
|
});
|
|
16154
16523
|
}
|
|
16155
|
-
await this.setupLocalTracks()
|
|
16524
|
+
if (!await this.setupLocalTracks()) {
|
|
16525
|
+
logger$17.debug("[RTCPeerConnectionController] Inbound answer abandoned; the connection went away.");
|
|
16526
|
+
return;
|
|
16527
|
+
}
|
|
16156
16528
|
const { answerOptions } = this;
|
|
16157
16529
|
logger$17.debug("[RTCPeerConnectionController] Creating inbound answer with options:", answerOptions);
|
|
16158
16530
|
await this.createAnswer(answerOptions);
|
|
@@ -16290,35 +16662,52 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
16290
16662
|
}
|
|
16291
16663
|
/**
|
|
16292
16664
|
* Setup track handling for remote tracks.
|
|
16665
|
+
*
|
|
16666
|
+
* @returns `false` when the connection went away while local media was being
|
|
16667
|
+
* acquired — see {@link setupLocalTracks}.
|
|
16293
16668
|
*/
|
|
16294
16669
|
async setupTrackHandling() {
|
|
16295
16670
|
if (!this.peerConnection) throw new DependencyError("RTCPeerConnection is not initialized");
|
|
16296
|
-
await this.setupLocalTracks();
|
|
16671
|
+
if (!await this.setupLocalTracks()) return false;
|
|
16297
16672
|
await this.setupRemoteTracks();
|
|
16673
|
+
return true;
|
|
16298
16674
|
}
|
|
16675
|
+
/**
|
|
16676
|
+
* @returns `false` when the connection was torn down while getUserMedia was
|
|
16677
|
+
* in flight. The acquisition is not cancellable, so the caller must stop
|
|
16678
|
+
* rather than go on to touch a peer connection that is closed or gone.
|
|
16679
|
+
*/
|
|
16299
16680
|
async setupLocalTracks() {
|
|
16300
16681
|
logger$17.debug("[RTCPeerConnectionController] Setting up local tracks/transceivers.");
|
|
16301
16682
|
if (this.hasNoLocalMediaToSend()) {
|
|
16302
16683
|
if (!this.receiveAudio && !this.receiveVideo) throw new InvalidParams("Call requests no media: enable audio/video or receiveAudio/receiveVideo");
|
|
16303
16684
|
logger$17.debug("[RTCPeerConnectionController] No local media requested; negotiating receive-only.");
|
|
16304
16685
|
this.setupReceiveOnlyTransceivers();
|
|
16305
|
-
|
|
16686
|
+
this._localMediaSettled$.next();
|
|
16687
|
+
return true;
|
|
16306
16688
|
}
|
|
16307
16689
|
let localStream;
|
|
16308
16690
|
try {
|
|
16309
16691
|
localStream = this.localStream ?? await this.localStreamController.buildLocalStream();
|
|
16310
16692
|
} catch (error) {
|
|
16311
16693
|
this.handleLocalMediaFailure(error);
|
|
16312
|
-
|
|
16694
|
+
this._localMediaSettled$.next();
|
|
16695
|
+
return true;
|
|
16313
16696
|
}
|
|
16697
|
+
if (!this.peerConnection || this.peerConnection.signalingState === "closed") {
|
|
16698
|
+
logger$17.debug("[RTCPeerConnectionController] Local media arrived after teardown; releasing it.");
|
|
16699
|
+
localStream.getTracks().forEach((track) => track.stop());
|
|
16700
|
+
return false;
|
|
16701
|
+
}
|
|
16702
|
+
this._localMediaSettled$.next();
|
|
16314
16703
|
if (this.transceiverController?.useAddStream ?? false) {
|
|
16315
16704
|
logger$17.warn("[RTCPeerConnectionController] Using deprecated addStream API to add local stream.");
|
|
16316
|
-
this.peerConnection
|
|
16705
|
+
this.peerConnection.addStream(localStream);
|
|
16317
16706
|
if (!this.isNegotiating) {
|
|
16318
16707
|
logger$17.debug("[RTCPeerConnectionController] Forcing negotiationneeded after local tracks setup.");
|
|
16319
16708
|
this.negotiationNeeded$.next();
|
|
16320
16709
|
}
|
|
16321
|
-
return;
|
|
16710
|
+
return true;
|
|
16322
16711
|
}
|
|
16323
16712
|
for (const kind of ["audio", "video"]) {
|
|
16324
16713
|
const tracks = (kind === "audio" ? localStream.getAudioTracks() : localStream.getVideoTracks()).map((track, index) => ({
|
|
@@ -16332,10 +16721,11 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
16332
16721
|
await this.transceiverController?.setupTransceiverSender(track, localStream, transceivers[index]);
|
|
16333
16722
|
} else {
|
|
16334
16723
|
logger$17.debug(`[RTCPeerConnectionController] Using addTrack for local ${kind} track:`, track.id);
|
|
16335
|
-
this.peerConnection
|
|
16724
|
+
this.peerConnection.addTrack(track, localStream);
|
|
16336
16725
|
}
|
|
16337
16726
|
}
|
|
16338
16727
|
}
|
|
16728
|
+
return true;
|
|
16339
16729
|
}
|
|
16340
16730
|
/** True for a main connection with no local media to send. */
|
|
16341
16731
|
hasNoLocalMediaToSend() {
|
|
@@ -16424,6 +16814,61 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
16424
16814
|
this._localAudioPipeline.setInputTrack(newTrack);
|
|
16425
16815
|
}
|
|
16426
16816
|
/**
|
|
16817
|
+
* Capture the newly selected device, leaving the current capture running.
|
|
16818
|
+
*
|
|
16819
|
+
* A rejection must leave the current track sending, so nothing is released
|
|
16820
|
+
* until the replacement is in hand. The one exception is hardware that admits
|
|
16821
|
+
* a single opener — a phone's front and back cameras, typically — which
|
|
16822
|
+
* rejects the second capture until the first is closed.
|
|
16823
|
+
*/
|
|
16824
|
+
async acquireInputTrack(kind, constraints, deviceInfo, currentTrack) {
|
|
16825
|
+
try {
|
|
16826
|
+
return await this.captureTrack(kind, constraints, deviceInfo.deviceId);
|
|
16827
|
+
} catch (error) {
|
|
16828
|
+
if (!isMediaDeviceInUse(error)) throw error;
|
|
16829
|
+
logger$17.warn(`[RTCPeerConnectionController] ${kind} device is held exclusively; releasing the current capture to retry:`, error);
|
|
16830
|
+
const previousDeviceId = currentTrack.getSettings().deviceId;
|
|
16831
|
+
this.stopTrackSender(kind);
|
|
16832
|
+
try {
|
|
16833
|
+
return await this.captureTrack(kind, constraints, deviceInfo.deviceId);
|
|
16834
|
+
} catch (retryError) {
|
|
16835
|
+
await this.restorePreviousInputTrack(kind, constraints, previousDeviceId, currentTrack);
|
|
16836
|
+
throw retryError;
|
|
16837
|
+
}
|
|
16838
|
+
}
|
|
16839
|
+
}
|
|
16840
|
+
async captureTrack(kind, constraints, deviceId) {
|
|
16841
|
+
const { stream, fallbackLevel } = await getUserMediaWithFallback({ getUserMedia: async (c) => this.getUserMedia(c) }, { [kind]: constraints }, kind, deviceId);
|
|
16842
|
+
const track = stream.getTracks().find((t) => t.kind === kind);
|
|
16843
|
+
if (!track) {
|
|
16844
|
+
stream.getTracks().forEach((t) => t.stop());
|
|
16845
|
+
throw new MediaTrackError("updateSelectedInputDevice", kind, /* @__PURE__ */ new Error("getUserMedia returned no track of the requested kind"));
|
|
16846
|
+
}
|
|
16847
|
+
if (fallbackLevel !== "exact") logger$17.warn(`[RTCPeerConnectionController] ${kind} device acquired at fallback level '${fallbackLevel}'; the capture may not be the requested device.`);
|
|
16848
|
+
return track;
|
|
16849
|
+
}
|
|
16850
|
+
/** Best-effort return to the device that was released for an exclusive retry. */
|
|
16851
|
+
async restorePreviousInputTrack(kind, constraints, previousDeviceId, releasedTrack) {
|
|
16852
|
+
try {
|
|
16853
|
+
const restored = await this.captureTrack(kind, constraints, previousDeviceId);
|
|
16854
|
+
await this.attachInputTrack(kind, restored, releasedTrack);
|
|
16855
|
+
} catch (error) {
|
|
16856
|
+
logger$17.error(`[RTCPeerConnectionController] Failed to restore the previous ${kind} device:`, error);
|
|
16857
|
+
}
|
|
16858
|
+
}
|
|
16859
|
+
async attachInputTrack(kind, newTrack, oldTrack) {
|
|
16860
|
+
const pipelineOwnsAudio = kind === "audio" && this._localAudioPipeline;
|
|
16861
|
+
if (!pipelineOwnsAudio) try {
|
|
16862
|
+
await this.transceiverController?.replaceSenderTrack(kind, newTrack);
|
|
16863
|
+
} catch (error) {
|
|
16864
|
+
newTrack.stop();
|
|
16865
|
+
throw error;
|
|
16866
|
+
}
|
|
16867
|
+
this.localStreamController.removeTrack(oldTrack.id);
|
|
16868
|
+
this.localStreamController.addTrack(newTrack);
|
|
16869
|
+
if (pipelineOwnsAudio) this._localAudioPipeline?.setInputTrack(newTrack);
|
|
16870
|
+
}
|
|
16871
|
+
/**
|
|
16427
16872
|
* Return the lazily-created {@link LocalAudioPipeline}, constructing it on
|
|
16428
16873
|
* first access. On creation the current audio sender's track is routed
|
|
16429
16874
|
* through the pipeline (input → gain → analyser → destination) and the
|
|
@@ -16456,6 +16901,7 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
16456
16901
|
const sender = (this.transceiverController?.audioTransceivers.at(0))?.sender ?? this.peerConnection.getSenders().find((s) => s.track?.kind === "audio");
|
|
16457
16902
|
if (!sender || !raw) return;
|
|
16458
16903
|
try {
|
|
16904
|
+
this.localStreamController.setTrackOrigin(this._localAudioPipeline.outputTrack, "processed");
|
|
16459
16905
|
await sender.replaceTrack(this._localAudioPipeline.outputTrack);
|
|
16460
16906
|
} catch (error) {
|
|
16461
16907
|
logger$17.warn("[RTCPeerConnectionController] Failed to route audio sender through pipeline:", error);
|
|
@@ -16516,42 +16962,73 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
16516
16962
|
for (const existingTrack of existingTracks) this.removeLocalTrack(existingTrack.id);
|
|
16517
16963
|
this.addLocalTrack(track);
|
|
16518
16964
|
}
|
|
16519
|
-
async updateSendersConstraints(kind, constraints) {
|
|
16520
|
-
await this.transceiverController?.updateSendersConstraints(kind, constraints);
|
|
16521
|
-
}
|
|
16522
16965
|
/**
|
|
16523
|
-
*
|
|
16524
|
-
*
|
|
16525
|
-
*
|
|
16526
|
-
*
|
|
16966
|
+
* @returns whether the constraints reached the media the leg is sending.
|
|
16967
|
+
*
|
|
16968
|
+
* With the pipeline engaged the audio sender carries the processed
|
|
16969
|
+
* destination track, so the sender scan would find nothing it may touch and
|
|
16970
|
+
* every audio constraint API would silently no-op. The constraints belong to
|
|
16971
|
+
* the pipeline's device source, which is the capture that sender ultimately
|
|
16972
|
+
* carries.
|
|
16527
16973
|
*/
|
|
16528
|
-
async
|
|
16529
|
-
|
|
16530
|
-
|
|
16531
|
-
|
|
16532
|
-
|
|
16533
|
-
|
|
16534
|
-
|
|
16535
|
-
const oldTrack = sender.track;
|
|
16536
|
-
if (!oldTrack) continue;
|
|
16537
|
-
const { deviceId } = oldTrack.getSettings();
|
|
16538
|
-
const mergedConstraints = {
|
|
16539
|
-
...oldTrack.getConstraints(),
|
|
16540
|
-
...constraints,
|
|
16541
|
-
...deviceId ? { deviceId: { exact: deviceId } } : {}
|
|
16542
|
-
};
|
|
16543
|
-
const trackId = oldTrack.id;
|
|
16544
|
-
oldTrack.stop();
|
|
16545
|
-
this.localStreamController.removeTrack(trackId);
|
|
16546
|
-
const newTrack = (await this.getUserMedia({ audio: mergedConstraints })).getAudioTracks()[0];
|
|
16547
|
-
await sender.replaceTrack(newTrack);
|
|
16548
|
-
this.localStreamController.addTrack(newTrack);
|
|
16549
|
-
logger$17.debug(`[RTCPeerConnectionController] Audio track replaced for server-pushed params. New track: ${newTrack.id}`);
|
|
16974
|
+
async updateSendersConstraints(kind, constraints) {
|
|
16975
|
+
if (kind === "audio" && this._localAudioPipeline) {
|
|
16976
|
+
if (!constraints) {
|
|
16977
|
+
this.stopTrackSender("audio");
|
|
16978
|
+
return false;
|
|
16979
|
+
}
|
|
16980
|
+
return this.applyPipelineSourceConstraints(constraints);
|
|
16550
16981
|
}
|
|
16982
|
+
return await this.transceiverController?.updateSendersConstraints(kind, constraints) ?? false;
|
|
16551
16983
|
}
|
|
16552
16984
|
/**
|
|
16553
|
-
*
|
|
16554
|
-
*
|
|
16985
|
+
* Mirror of the sender path for a piped audio leg: same merge, same fallback
|
|
16986
|
+
* ladder, same device-capture invariant — but the swap target is the pipeline
|
|
16987
|
+
* input, so the sender keeps emitting the pipeline's output track and its
|
|
16988
|
+
* identity survives the change.
|
|
16989
|
+
*/
|
|
16990
|
+
async applyPipelineSourceConstraints(constraints) {
|
|
16991
|
+
const pipeline = this._localAudioPipeline;
|
|
16992
|
+
const source = this.localStreamController.localAudioTracks.at(0);
|
|
16993
|
+
if (!pipeline || !source) {
|
|
16994
|
+
logger$17.debug("[RTCPeerConnectionController] No pipeline input to constrain.");
|
|
16995
|
+
return false;
|
|
16996
|
+
}
|
|
16997
|
+
if (!this.localStreamController.isDeviceCapture(source)) {
|
|
16998
|
+
logger$17.debug(`[RTCPeerConnectionController] Skipping audio constraints for a non-device pipeline input (origin: ${this.localStreamController.getTrackOrigin(source) ?? "unrecorded"}).`);
|
|
16999
|
+
return false;
|
|
17000
|
+
}
|
|
17001
|
+
const merged = {
|
|
17002
|
+
...source.getConstraints(),
|
|
17003
|
+
...constraints
|
|
17004
|
+
};
|
|
17005
|
+
try {
|
|
17006
|
+
await source.applyConstraints(merged);
|
|
17007
|
+
logger$17.debug("[RTCPeerConnectionController] Pipeline input constraints updated:", merged);
|
|
17008
|
+
return true;
|
|
17009
|
+
} catch (error) {
|
|
17010
|
+
logger$17.warn("[RTCPeerConnectionController] applyConstraints failed on the pipeline input, re-acquiring:", error);
|
|
17011
|
+
}
|
|
17012
|
+
try {
|
|
17013
|
+
const { stream } = await getUserMediaWithFallback({ getUserMedia: async (c) => this.getUserMedia(c) }, { audio: merged }, "audio", source.getSettings().deviceId);
|
|
17014
|
+
const newTrack = stream.getAudioTracks().at(0);
|
|
17015
|
+
if (!newTrack) {
|
|
17016
|
+
stream.getTracks().forEach((track) => track.stop());
|
|
17017
|
+
throw new Error("getUserMedia returned no audio track");
|
|
17018
|
+
}
|
|
17019
|
+
this.localStreamController.removeTrack(source.id);
|
|
17020
|
+
this.localStreamController.addTrack(newTrack);
|
|
17021
|
+
pipeline.setInputTrack(newTrack);
|
|
17022
|
+
return true;
|
|
17023
|
+
} catch (error) {
|
|
17024
|
+
logger$17.warn("[RTCPeerConnectionController] Failed to re-acquire the pipeline input for constraints:", error);
|
|
17025
|
+
this._errors$.next(new MediaTrackError("updateSendersConstraints", "audio", error));
|
|
17026
|
+
return false;
|
|
17027
|
+
}
|
|
17028
|
+
}
|
|
17029
|
+
/**
|
|
17030
|
+
* Clean up resources and close the peer connection.
|
|
17031
|
+
* Completes all observables to prevent memory leaks.
|
|
16555
17032
|
*/
|
|
16556
17033
|
destroy() {
|
|
16557
17034
|
logger$17.debug(`[RTCPeerConnectionController] Destroying RTCPeerConnectionController. ${this.propose}`);
|
|
@@ -16666,6 +17143,48 @@ const logger$16 = getLogger();
|
|
|
16666
17143
|
function resolveInviteNodeId(args) {
|
|
16667
17144
|
return args.isInvite && !args.reattach && !args.explicitNodeId ? "" : args.currentNodeId ?? "";
|
|
16668
17145
|
}
|
|
17146
|
+
/**
|
|
17147
|
+
* Surface the real outcome of a `webrtc.verto` reply.
|
|
17148
|
+
*
|
|
17149
|
+
* A webrtc.verto response nests several envelopes, each keyed by a verto-style
|
|
17150
|
+
* string `code` ("200" ok, "400"/etc. fail) rather than a JSON-RPC `error`. An outer
|
|
17151
|
+
* layer reports only whether the frame was delivered; an inner layer carries the op's
|
|
17152
|
+
* own outcome:
|
|
17153
|
+
*
|
|
17154
|
+
* response.result = { code:"200", result:{…} } ← delivery acknowledgement
|
|
17155
|
+
* .result = { jsonrpc, id, result:{…} } ← the reply payload
|
|
17156
|
+
* .result = { code:"400", message:"Bad request" } ← the actual op outcome
|
|
17157
|
+
*
|
|
17158
|
+
* A failure can appear at any layer (delivery refused, or the op itself rejected
|
|
17159
|
+
* deeper down), so walk every nested `.result` object and return the FIRST non-2xx
|
|
17160
|
+
* `code` with its message. Returns null when every `code` seen is 2xx or absent —
|
|
17161
|
+
* i.e. the op succeeded. This is the only way to detect that e.g. a mute/kick was
|
|
17162
|
+
* rejected, since the outer delivery `code` is "200" (delivered) even then.
|
|
17163
|
+
*
|
|
17164
|
+
* Pure function — exported for unit testing.
|
|
17165
|
+
*/
|
|
17166
|
+
function findNestedVertoFailure(response) {
|
|
17167
|
+
let node = response;
|
|
17168
|
+
while (node !== null && typeof node === "object") {
|
|
17169
|
+
const obj = node;
|
|
17170
|
+
const err = obj.error;
|
|
17171
|
+
if (err !== null && typeof err === "object") {
|
|
17172
|
+
const e = err;
|
|
17173
|
+
const errCode = typeof e.code === "string" || typeof e.code === "number" ? String(e.code) : void 0;
|
|
17174
|
+
if (errCode !== void 0 && !/^2\d\d$/.test(errCode)) return {
|
|
17175
|
+
code: errCode,
|
|
17176
|
+
message: typeof e.message === "string" ? e.message : void 0
|
|
17177
|
+
};
|
|
17178
|
+
}
|
|
17179
|
+
const code = typeof obj.code === "string" || typeof obj.code === "number" ? String(obj.code) : void 0;
|
|
17180
|
+
if (code !== void 0 && !/^2\d\d$/.test(code)) return {
|
|
17181
|
+
code,
|
|
17182
|
+
message: typeof obj.message === "string" ? obj.message : void 0
|
|
17183
|
+
};
|
|
17184
|
+
node = obj.result !== null && typeof obj.result === "object" ? obj.result : null;
|
|
17185
|
+
}
|
|
17186
|
+
return null;
|
|
17187
|
+
}
|
|
16669
17188
|
var VertoManager = class extends Destroyable {
|
|
16670
17189
|
constructor(callSession) {
|
|
16671
17190
|
super();
|
|
@@ -16688,7 +17207,7 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
16688
17207
|
this._signalingStatus$ = this.createReplaySubject(1);
|
|
16689
17208
|
this._screenShareStatus$ = this.createBehaviorSubject("none");
|
|
16690
17209
|
this._rtcPeerConnectionsMap = /* @__PURE__ */ new Map();
|
|
16691
|
-
this.
|
|
17210
|
+
this._legErrors$ = this.createSubject();
|
|
16692
17211
|
this._nodeId$ = this.createBehaviorSubject(options.nodeId ?? null);
|
|
16693
17212
|
this.onError = options.onError;
|
|
16694
17213
|
this.onModifyFailed = options.onModifyFailed;
|
|
@@ -16736,6 +17255,10 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
16736
17255
|
get selfId$() {
|
|
16737
17256
|
return this._selfId$.asObservable();
|
|
16738
17257
|
}
|
|
17258
|
+
/** Separates the media phase of call creation from the signalling phase. */
|
|
17259
|
+
get localMediaSettled$() {
|
|
17260
|
+
return this.mainPeerConnection.localMediaSettled$;
|
|
17261
|
+
}
|
|
16739
17262
|
get localStream() {
|
|
16740
17263
|
return this._rtcPeerConnectionsMap.get(this.webRtcCallSession.id)?.localStream ?? null;
|
|
16741
17264
|
}
|
|
@@ -16792,35 +17315,95 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
16792
17315
|
const { mediaParams, callID } = event;
|
|
16793
17316
|
const rtcPeerConnController = this._rtcPeerConnectionsMap.get(callID);
|
|
16794
17317
|
const { audio, video } = mediaParams;
|
|
16795
|
-
|
|
16796
|
-
|
|
16797
|
-
|
|
16798
|
-
|
|
16799
|
-
|
|
16800
|
-
audio,
|
|
16801
|
-
video,
|
|
16802
|
-
timestamp: Date.now()
|
|
16803
|
-
});
|
|
16804
|
-
} catch (error) {
|
|
16805
|
-
logger$16.warn("[WebRTCManager] Error applying server-pushed media params:", error);
|
|
16806
|
-
this.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
16807
|
-
}
|
|
16808
|
-
})();
|
|
17318
|
+
if (!rtcPeerConnController) {
|
|
17319
|
+
logger$16.warn(`[WebRTCManager] Ignoring server-pushed media params for unknown leg ${callID}`);
|
|
17320
|
+
return;
|
|
17321
|
+
}
|
|
17322
|
+
this.applyServerMediaParams(rtcPeerConnController, audio, video);
|
|
16809
17323
|
});
|
|
16810
17324
|
this.subscribeTo(this.vertoPing$, (vertoPing) => {
|
|
16811
|
-
this.attachManager.
|
|
17325
|
+
this.attachManager.refresh(this.buildAttachableCall());
|
|
16812
17326
|
this.sendVertoPong(vertoPing);
|
|
16813
17327
|
});
|
|
16814
17328
|
}
|
|
16815
17329
|
/**
|
|
17330
|
+
* An auxiliary-leg failure must never destroy the call; main-leg and
|
|
17331
|
+
* call-level errors keep `CallFactory.isFatalError`'s classification.
|
|
17332
|
+
*
|
|
17333
|
+
* Every site holding a peer connection reports through here, so the invariant
|
|
17334
|
+
* is structural rather than per-call-site — which is how cloud-product#20523
|
|
17335
|
+
* happened, with only one of fourteen sites passing `{ fatal: false }`.
|
|
17336
|
+
*
|
|
17337
|
+
* `override` composes rather than replaces: a caller may force non-fatal for a
|
|
17338
|
+
* reason of its own (a `verto.info` frame is best-effort whichever leg carries
|
|
17339
|
+
* it), and an auxiliary leg stays non-fatal regardless.
|
|
17340
|
+
*/
|
|
17341
|
+
reportLegError(error, rtcPeerConnController, override) {
|
|
17342
|
+
const leg = rtcPeerConnController?.propose;
|
|
17343
|
+
const legId = rtcPeerConnController?.id;
|
|
17344
|
+
const auxiliary = Boolean(rtcPeerConnController) && !rtcPeerConnController?.isMainDevice;
|
|
17345
|
+
this.onError?.(error, {
|
|
17346
|
+
...override?.fatal === false || auxiliary ? { fatal: false } : {},
|
|
17347
|
+
...leg ? { leg } : {},
|
|
17348
|
+
...legId ? { legId } : {}
|
|
17349
|
+
});
|
|
17350
|
+
if (legId) this._legErrors$.next({
|
|
17351
|
+
legId,
|
|
17352
|
+
error
|
|
17353
|
+
});
|
|
17354
|
+
}
|
|
17355
|
+
/**
|
|
17356
|
+
* Errors reported for one leg, as a stream that fails with them.
|
|
17357
|
+
*
|
|
17358
|
+
* Signaling failures are reported, never thrown — so nothing that waits on a
|
|
17359
|
+
* leg's progress would otherwise learn of a rejected invite. Merging this in
|
|
17360
|
+
* lets the wait end with the reason the server gave.
|
|
17361
|
+
*/
|
|
17362
|
+
legError$(legId) {
|
|
17363
|
+
return this._legErrors$.pipe((0, import_cjs$15.filter)((report) => report.legId === legId), (0, import_cjs$15.map)((report) => {
|
|
17364
|
+
throw report.error;
|
|
17365
|
+
}));
|
|
17366
|
+
}
|
|
17367
|
+
/**
|
|
17368
|
+
* Audio and video are applied independently so a failure in one cannot
|
|
17369
|
+
* suppress the other, and `mediaParamsUpdated` is emitted whatever happens —
|
|
17370
|
+
* an application should not be starved of the params by a constraint failure.
|
|
17371
|
+
*/
|
|
17372
|
+
async applyServerMediaParams(rtcPeerConnController, audio, video) {
|
|
17373
|
+
const failures = [];
|
|
17374
|
+
let applied = true;
|
|
17375
|
+
if (audio) try {
|
|
17376
|
+
applied = await rtcPeerConnController.updateSendersConstraints("audio", audio) && applied;
|
|
17377
|
+
} catch (error) {
|
|
17378
|
+
applied = false;
|
|
17379
|
+
failures.push(toError(error));
|
|
17380
|
+
}
|
|
17381
|
+
if (video) try {
|
|
17382
|
+
applied = await rtcPeerConnController.updateSendersConstraints("video", video) && applied;
|
|
17383
|
+
} catch (error) {
|
|
17384
|
+
applied = false;
|
|
17385
|
+
failures.push(toError(error));
|
|
17386
|
+
}
|
|
17387
|
+
this.webRtcCallSession.emitMediaParamsUpdated({
|
|
17388
|
+
audio,
|
|
17389
|
+
video,
|
|
17390
|
+
timestamp: Date.now(),
|
|
17391
|
+
applied
|
|
17392
|
+
});
|
|
17393
|
+
for (const failure of failures) {
|
|
17394
|
+
logger$16.warn("[WebRTCManager] Error applying server-pushed media params:", failure);
|
|
17395
|
+
this.reportLegError(failure, rtcPeerConnController, { fatal: false });
|
|
17396
|
+
}
|
|
17397
|
+
}
|
|
17398
|
+
/**
|
|
16816
17399
|
* Set node_id/selfId only when the current value is null.
|
|
16817
17400
|
*
|
|
16818
17401
|
* During reattach, `call.joined` and `verto.answer` events can deliver
|
|
16819
17402
|
* these identifiers before the `verto.invite` RPC response (`CALL CREATED`)
|
|
16820
17403
|
* arrives. These methods let early events populate them eagerly so that
|
|
16821
17404
|
* downstream RPC calls (e.g. `call.layout.list`) don't fail with empty
|
|
16822
|
-
* identifiers. `processInviteResponse()` remains the authoritative source
|
|
16823
|
-
*
|
|
17405
|
+
* identifiers. `processInviteResponse()` remains the authoritative source and
|
|
17406
|
+
* overwrites unconditionally — for selfId, on the main leg only.
|
|
16824
17407
|
*/
|
|
16825
17408
|
setNodeIdIfNull(nodeId) {
|
|
16826
17409
|
if (!this._nodeId$.value && nodeId) {
|
|
@@ -16843,24 +17426,33 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
16843
17426
|
this.onError?.(new VertoPongError(error));
|
|
16844
17427
|
}
|
|
16845
17428
|
}
|
|
17429
|
+
/**
|
|
17430
|
+
* @returns whether the constraints reached the media the call is sending.
|
|
17431
|
+
* `false` is an outcome, not a failure: the leg may have no live sender of
|
|
17432
|
+
* the kind, or carry media the SDK did not capture and may not replace.
|
|
17433
|
+
* A failure behind it still reaches the call's `errors$`, so a caller that
|
|
17434
|
+
* ignores this value learns of it there.
|
|
17435
|
+
*/
|
|
16846
17436
|
async updateMediaConstraints(options = {}) {
|
|
16847
17437
|
const { audio, video } = options;
|
|
17438
|
+
let applied = true;
|
|
16848
17439
|
try {
|
|
16849
|
-
if (audio) await this.mainPeerConnection.updateSendersConstraints("audio", audio);
|
|
16850
|
-
if (video) await this.mainPeerConnection.updateSendersConstraints("video", video);
|
|
17440
|
+
if (audio) applied = await this.mainPeerConnection.updateSendersConstraints("audio", audio) && applied;
|
|
17441
|
+
if (video) applied = await this.mainPeerConnection.updateSendersConstraints("video", video) && applied;
|
|
16851
17442
|
} catch (error) {
|
|
16852
17443
|
logger$16.warn("[WebRTCManager] Error updating media constraints:", error);
|
|
16853
|
-
this.
|
|
17444
|
+
this.reportLegError(toError(error), this.mainPeerConnection);
|
|
16854
17445
|
throw error;
|
|
16855
17446
|
}
|
|
17447
|
+
return applied;
|
|
16856
17448
|
}
|
|
16857
17449
|
get selfId() {
|
|
16858
17450
|
return this._selfId$.value;
|
|
16859
17451
|
}
|
|
16860
17452
|
/** Build an AttachableCall from the current call state. */
|
|
16861
|
-
buildAttachableCall(idOverride) {
|
|
17453
|
+
buildAttachableCall(idOverride, nodeIdOverride) {
|
|
16862
17454
|
return {
|
|
16863
|
-
nodeId: this.nodeId ?? void 0,
|
|
17455
|
+
nodeId: nodeIdOverride ?? this.nodeId ?? void 0,
|
|
16864
17456
|
id: idOverride ?? this.webRtcCallSession.id,
|
|
16865
17457
|
to: this.webRtcCallSession.to,
|
|
16866
17458
|
mediaDirections: this.webRtcCallSession.mediaDirections
|
|
@@ -16990,24 +17582,49 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
16990
17582
|
get vertoPing$() {
|
|
16991
17583
|
return this.cachedObservable("vertoPing$", () => this.webRtcCallSession.webrtcMessages$.pipe(filterAs(isVertoPingInnerParams, "params"), (0, import_cjs$15.takeUntil)(this.destroyed$)));
|
|
16992
17584
|
}
|
|
17585
|
+
/**
|
|
17586
|
+
* Send a member-control op in-dialog via verto.info.
|
|
17587
|
+
*
|
|
17588
|
+
* The control payload rides in `params.command` — a sibling of `dialogParams`,
|
|
17589
|
+
* at the same level as `dtmf` in {@link sendDigits} — and the inner verto.info
|
|
17590
|
+
* is matched to this call's channel by `dialogParams.callID`, the in-dialog
|
|
17591
|
+
* convention for member-scoped frames. Because it is delivered on the dialog
|
|
17592
|
+
* itself, control lands on the call's own channel with no {node_id,call_id,member_id}
|
|
17593
|
+
* "self" tuple to get wrong. The outer webrtc.verto envelope (added by executeVerto)
|
|
17594
|
+
* still carries the own-leg callID + node_id for session routing.
|
|
17595
|
+
*
|
|
17596
|
+
* Keep `command` OUT of `dialogParams`: it is read at the params level, and
|
|
17597
|
+
* filterVertoParams rewrites/filters dialogParams keys but passes params-level
|
|
17598
|
+
* keys through verbatim.
|
|
17599
|
+
*/
|
|
17600
|
+
async sendCallControl(method, params) {
|
|
17601
|
+
const response = await this.executeVerto(VertoInfo({
|
|
17602
|
+
dialogParams: { callID: this.webRtcCallSession.id },
|
|
17603
|
+
command: {
|
|
17604
|
+
method,
|
|
17605
|
+
params
|
|
17606
|
+
}
|
|
17607
|
+
}));
|
|
17608
|
+
const failure = findNestedVertoFailure(response);
|
|
17609
|
+
if (failure) throw new JSONRPCError(Number.parseInt(failure.code, 10) || 0, `Call control "${method}" failed (code ${failure.code})${failure.message ? `: ${failure.message}` : ""}`, void 0);
|
|
17610
|
+
return response;
|
|
17611
|
+
}
|
|
16993
17612
|
async executeVerto(message, optionals = {}) {
|
|
16994
|
-
const
|
|
17613
|
+
const params = {
|
|
16995
17614
|
callID: optionals.callID ?? this.webRtcCallSession.id,
|
|
16996
17615
|
node_id: optionals.node_id ?? this._nodeId$.value ?? "",
|
|
16997
17616
|
message,
|
|
16998
17617
|
subscribe: optionals.subscribe
|
|
16999
|
-
}
|
|
17618
|
+
};
|
|
17619
|
+
const webrtcVertoMessage = WebrtcVerto(params);
|
|
17000
17620
|
const response = await this.webRtcCallSession.execute(webrtcVertoMessage);
|
|
17001
|
-
|
|
17002
|
-
const error = new JSONRPCError(response.error.code, response.error.message, response.error.data);
|
|
17003
|
-
this.onError?.(error);
|
|
17004
|
-
return response;
|
|
17005
|
-
}
|
|
17621
|
+
const nonFatal = message.method === "verto.info" ? { fatal: false } : void 0;
|
|
17006
17622
|
const innerResult = getValueFrom(response, "result.result");
|
|
17007
|
-
|
|
17008
|
-
|
|
17009
|
-
|
|
17010
|
-
|
|
17623
|
+
const failure = response.error ?? innerResult?.error;
|
|
17624
|
+
if (failure) {
|
|
17625
|
+
const error = new JSONRPCError(failure.code, failure.message, failure.data);
|
|
17626
|
+
if (message.method === "verto.invite" || message.method === "verto.answer") throw error;
|
|
17627
|
+
this.reportLegError(error, this._rtcPeerConnectionsMap.get(params.callID), nonFatal);
|
|
17011
17628
|
}
|
|
17012
17629
|
return response;
|
|
17013
17630
|
}
|
|
@@ -17026,8 +17643,9 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
17026
17643
|
default:
|
|
17027
17644
|
}
|
|
17028
17645
|
} catch (error) {
|
|
17646
|
+
if (vertoMethod === "verto.answer") throw error;
|
|
17029
17647
|
logger$16.error(`[WebRTCManager] Error sending Verto ${vertoMethod}:`, error);
|
|
17030
|
-
this.
|
|
17648
|
+
this.reportLegError(toError(error), rtcPeerConnController);
|
|
17031
17649
|
if (vertoMethod === "verto.modify") this.onModifyFailed?.();
|
|
17032
17650
|
}
|
|
17033
17651
|
}
|
|
@@ -17043,7 +17661,7 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
17043
17661
|
} catch (error) {
|
|
17044
17662
|
logger$16.warn("[WebRTCManager] Error processing modify response:", error);
|
|
17045
17663
|
const modifyError = error instanceof Error ? error : new Error(String(error), { cause: error });
|
|
17046
|
-
this.
|
|
17664
|
+
this.reportLegError(modifyError, rtcPeerConnController);
|
|
17047
17665
|
}
|
|
17048
17666
|
}
|
|
17049
17667
|
}
|
|
@@ -17056,37 +17674,35 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
17056
17674
|
status,
|
|
17057
17675
|
signalingError
|
|
17058
17676
|
});
|
|
17059
|
-
this.
|
|
17677
|
+
this.reportLegError(signalingError, null, { fatal: false });
|
|
17060
17678
|
return;
|
|
17061
17679
|
}
|
|
17062
17680
|
if (rtcPeerConnController.isMainDevice) this._signalingStatus$.next(status);
|
|
17063
17681
|
}
|
|
17064
17682
|
processInviteResponse(response, rtcPeerConnController) {
|
|
17065
|
-
if (
|
|
17683
|
+
if (getValueFrom(response, "result.result.result.message") === "CALL CREATED") {
|
|
17066
17684
|
this.emitMainSignalingStatus(rtcPeerConnController.id, "trying");
|
|
17067
|
-
|
|
17685
|
+
const nodeId = getValueFrom(response, "result.node_id") ?? null;
|
|
17068
17686
|
const memberId = getValueFrom(response, "result.result.result.memberID") ?? null;
|
|
17069
|
-
const callId = getValueFrom(response, "result.result.result.callID")
|
|
17687
|
+
const callId = getValueFrom(response, "result.result.result.callID");
|
|
17070
17688
|
logger$16.debug("[WebRTCManager] Verto invite response:", {
|
|
17071
17689
|
callId,
|
|
17072
17690
|
memberId,
|
|
17073
17691
|
response
|
|
17074
17692
|
});
|
|
17075
|
-
this._selfId$.next(memberId);
|
|
17076
17693
|
rtcPeerConnController.setMemberId(memberId);
|
|
17077
|
-
|
|
17078
|
-
|
|
17079
|
-
this.
|
|
17080
|
-
|
|
17081
|
-
|
|
17082
|
-
|
|
17083
|
-
|
|
17694
|
+
rtcPeerConnController.setNodeId(nodeId);
|
|
17695
|
+
if (rtcPeerConnController.isMainDevice) {
|
|
17696
|
+
this._selfId$.next(memberId);
|
|
17697
|
+
this._nodeId$.next(nodeId);
|
|
17698
|
+
this.attachManager.attach(this.buildAttachableCall(callId, nodeId ?? void 0));
|
|
17699
|
+
}
|
|
17700
|
+
if (callId) this.webRtcCallSession.addCallId(callId);
|
|
17084
17701
|
logger$16.info("[WebRTCManager] Verto invite successful");
|
|
17085
17702
|
logger$16.debug(`[WebRTCManager] nodeid: ${this._nodeId$.value}, selfId: ${this._selfId$.value}`);
|
|
17086
17703
|
} else {
|
|
17087
17704
|
logger$16.error("[WebRTCManager] Verto invite failed:", response);
|
|
17088
|
-
|
|
17089
|
-
this.onError?.(inviteError);
|
|
17705
|
+
this.reportLegError(/* @__PURE__ */ new Error("Verto invite failed: unexpected response"), rtcPeerConnController);
|
|
17090
17706
|
}
|
|
17091
17707
|
}
|
|
17092
17708
|
get RTCPeerConnectionConfig() {
|
|
@@ -17125,7 +17741,7 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
17125
17741
|
this._rtcPeerConnectionsMap.set(rtcPeerConnController.id, rtcPeerConnController);
|
|
17126
17742
|
this._rtcPeerConnections$.next(Array.from(this._rtcPeerConnectionsMap.values()));
|
|
17127
17743
|
this.subscribeTo(rtcPeerConnController.errors$, (error) => {
|
|
17128
|
-
this.
|
|
17744
|
+
this.reportLegError(error, rtcPeerConnController);
|
|
17129
17745
|
});
|
|
17130
17746
|
if (options.initOffer) this.handleInboundAnswer(rtcPeerConnController);
|
|
17131
17747
|
}
|
|
@@ -17154,7 +17770,7 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
17154
17770
|
await rtcPeerConnController.acceptInbound(answerOptions);
|
|
17155
17771
|
} catch (error) {
|
|
17156
17772
|
logger$16.error("[WebRTCManager] Error creating inbound answer:", error);
|
|
17157
|
-
this.
|
|
17773
|
+
this.reportLegError(toError(error), rtcPeerConnController);
|
|
17158
17774
|
}
|
|
17159
17775
|
}
|
|
17160
17776
|
}
|
|
@@ -17226,7 +17842,7 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
17226
17842
|
isInvite: isVertoInviteMessage(vertoMessage),
|
|
17227
17843
|
reattach: this.webRtcCallSession.options.reattach === true,
|
|
17228
17844
|
explicitNodeId: this.webRtcCallSession.options.nodeId,
|
|
17229
|
-
currentNodeId: this._nodeId$.value
|
|
17845
|
+
currentNodeId: rtcPeerConnController.nodeId ?? this._nodeId$.value
|
|
17230
17846
|
}),
|
|
17231
17847
|
subscribe
|
|
17232
17848
|
};
|
|
@@ -17258,7 +17874,7 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
17258
17874
|
await this.attachManager.attach(this.buildAttachableCall());
|
|
17259
17875
|
} catch (error) {
|
|
17260
17876
|
logger$16.error("[WebRTCManager] Error sending Verto answer:", error);
|
|
17261
|
-
this.
|
|
17877
|
+
this.reportLegError(toError(error), rtcPeerConnectionController);
|
|
17262
17878
|
await rtcPeerConnectionController.updateAnswerStatus({ status: "failed" });
|
|
17263
17879
|
}
|
|
17264
17880
|
}
|
|
@@ -17330,15 +17946,19 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
17330
17946
|
await this.mainPeerConnection.restoreTrackSender(deviceKind);
|
|
17331
17947
|
} else {
|
|
17332
17948
|
const error = new InvalidParams("No valid device to be added");
|
|
17333
|
-
this.
|
|
17949
|
+
this.reportLegError(error, this.mainPeerConnection);
|
|
17334
17950
|
throw error;
|
|
17335
17951
|
}
|
|
17336
17952
|
}
|
|
17337
|
-
async addScreenMedia(options = {
|
|
17338
|
-
await this.initAdditionalPeerConnection("screenshare",
|
|
17953
|
+
async addScreenMedia(options = {}) {
|
|
17954
|
+
await this.initAdditionalPeerConnection("screenshare", {
|
|
17955
|
+
audio: false,
|
|
17956
|
+
screenShareAudio: options.audio ?? false
|
|
17957
|
+
});
|
|
17339
17958
|
}
|
|
17340
17959
|
async initAdditionalPeerConnection(propose, options) {
|
|
17341
17960
|
const isScreenShare = propose === "screenshare";
|
|
17961
|
+
if (isScreenShare && this._screenShareId && this._rtcPeerConnectionsMap.has(this._screenShareId)) throw new ScreenShareAlreadyActiveError(this._screenShareId);
|
|
17342
17962
|
let firstPeerConnectionError;
|
|
17343
17963
|
let rtcPeerConnController = null;
|
|
17344
17964
|
try {
|
|
@@ -17355,21 +17975,36 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
17355
17975
|
this._rtcPeerConnections$.next(Array.from(this._rtcPeerConnectionsMap.values()));
|
|
17356
17976
|
this.subscribeTo(rtcPeerConnController.errors$, (error) => {
|
|
17357
17977
|
firstPeerConnectionError ??= error;
|
|
17358
|
-
this.
|
|
17978
|
+
this.reportLegError(error, rtcPeerConnController);
|
|
17359
17979
|
});
|
|
17360
|
-
|
|
17980
|
+
const pc = rtcPeerConnController;
|
|
17981
|
+
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$))));
|
|
17361
17982
|
if (isScreenShare) this._screenShareStatus$.next("started");
|
|
17362
17983
|
logger$16.info(`[WebRTCManager] Additional peer connection connected (${propose}).`);
|
|
17363
17984
|
return rtcPeerConnController.id;
|
|
17364
17985
|
} catch (error) {
|
|
17365
|
-
|
|
17366
|
-
|
|
17367
|
-
if (
|
|
17986
|
+
const cancelled = error instanceof AuxiliaryLegCancelledError;
|
|
17987
|
+
const aborted = error instanceof import_cjs$15.EmptyError && !firstPeerConnectionError;
|
|
17988
|
+
if (!cancelled && !aborted) logger$16.warn("[WebRTCManager] Error initializing additional peer connection:", error);
|
|
17989
|
+
if (rtcPeerConnController && this._rtcPeerConnectionsMap.has(rtcPeerConnController.id)) {
|
|
17990
|
+
rtcPeerConnController.destroy();
|
|
17991
|
+
this._rtcPeerConnectionsMap.delete(rtcPeerConnController.id);
|
|
17992
|
+
this._rtcPeerConnections$.next(Array.from(this._rtcPeerConnectionsMap.values()));
|
|
17993
|
+
}
|
|
17994
|
+
if (isScreenShare) {
|
|
17995
|
+
this._screenShareId = void 0;
|
|
17996
|
+
this._screenShareStatus$.next("none");
|
|
17997
|
+
}
|
|
17998
|
+
if (cancelled) {
|
|
17999
|
+
logger$16.debug("[WebRTCManager] Additional peer connection removed before connecting.");
|
|
18000
|
+
throw error;
|
|
18001
|
+
}
|
|
17368
18002
|
if (firstPeerConnectionError) throw firstPeerConnectionError instanceof MediaAccessError && firstPeerConnectionError.originalError instanceof Error ? firstPeerConnectionError.originalError : firstPeerConnectionError;
|
|
17369
18003
|
if (error instanceof import_cjs$15.EmptyError) {
|
|
17370
18004
|
logger$16.debug("[WebRTCManager] Additional peer connection aborted before connecting.");
|
|
17371
18005
|
return;
|
|
17372
18006
|
}
|
|
18007
|
+
if (error instanceof import_cjs$15.TimeoutError) throw new AuxiliaryLegTimeoutError(propose, error);
|
|
17373
18008
|
throw error instanceof Error ? error : new Error(String(error), { cause: error });
|
|
17374
18009
|
}
|
|
17375
18010
|
}
|
|
@@ -17386,7 +18021,10 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
17386
18021
|
if (removeTrack) return this.mainPeerConnection.stopTrackSender(removeTrack, { updateTransceiverDirection: true });
|
|
17387
18022
|
}
|
|
17388
18023
|
async removeScreenMedia() {
|
|
17389
|
-
if (!["starting", "started"].includes(this._screenShareStatus$.value))
|
|
18024
|
+
if (!["starting", "started"].includes(this._screenShareStatus$.value)) {
|
|
18025
|
+
logger$16.warn("[WebRTCManager] No active screen share to stop.");
|
|
18026
|
+
return;
|
|
18027
|
+
}
|
|
17390
18028
|
if (!this._screenShareId) {
|
|
17391
18029
|
logger$16.debug("[WebRTCManager] No screen share peer connection found.");
|
|
17392
18030
|
return;
|
|
@@ -17401,6 +18039,10 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
17401
18039
|
try {
|
|
17402
18040
|
if (rtcPeerConnController) await this.executeVertoBye(rtcPeerConnController);
|
|
17403
18041
|
} finally {
|
|
18042
|
+
if (rtcPeerConnController && rtcPeerConnController.connectionState !== "connected") this._legErrors$.next({
|
|
18043
|
+
legId: id,
|
|
18044
|
+
error: new AuxiliaryLegCancelledError(rtcPeerConnController.propose)
|
|
18045
|
+
});
|
|
17404
18046
|
rtcPeerConnController?.destroy();
|
|
17405
18047
|
this._rtcPeerConnectionsMap.delete(id);
|
|
17406
18048
|
this._rtcPeerConnections$.next(Array.from(this._rtcPeerConnectionsMap.values()));
|
|
@@ -17415,7 +18057,10 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
17415
18057
|
await this.executeVerto(VertoBye({
|
|
17416
18058
|
...causeParams,
|
|
17417
18059
|
dialogParams: this.dialogParams(rtcPeerConnController)
|
|
17418
|
-
})
|
|
18060
|
+
}), {
|
|
18061
|
+
callID: rtcPeerConnController.id,
|
|
18062
|
+
node_id: rtcPeerConnController.nodeId ?? void 0
|
|
18063
|
+
});
|
|
17419
18064
|
} catch (error) {
|
|
17420
18065
|
logger$16.warn("[WebRTCManager] Call might already be disconnected, error sending Verto bye:", error);
|
|
17421
18066
|
throw error;
|
|
@@ -18294,17 +18939,38 @@ function mosToQualityLevel(mos) {
|
|
|
18294
18939
|
return "critical";
|
|
18295
18940
|
}
|
|
18296
18941
|
|
|
18942
|
+
//#endregion
|
|
18943
|
+
//#region src/utils/unwrapVertoReply.ts
|
|
18944
|
+
/**
|
|
18945
|
+
* Unwrap a method's own reply from a `webrtc.verto` envelope.
|
|
18946
|
+
*
|
|
18947
|
+
* A control verb sent in-dialog comes back nested two levels deep:
|
|
18948
|
+
*
|
|
18949
|
+
* ```
|
|
18950
|
+
* { result: { node_id, code, result: { jsonrpc, id, result: <method payload> } } }
|
|
18951
|
+
* ```
|
|
18952
|
+
*
|
|
18953
|
+
* whereas the routed transport resolves the method payload directly under
|
|
18954
|
+
* `.result`. Readers want the latter shape, and the difference is silent when it
|
|
18955
|
+
* is wrong — `response.result.layouts` simply evaluates to `undefined` against the
|
|
18956
|
+
* envelope, so the data arrives, nothing throws, and the caller sees an empty
|
|
18957
|
+
* value. That exact failure produced an empty layout dropdown with a successful
|
|
18958
|
+
* request behind it.
|
|
18959
|
+
*
|
|
18960
|
+
* Accepting both shapes here keeps every reader indifferent to which transport
|
|
18961
|
+
* produced the response. Anything that is not a nested envelope (a plain ack, or
|
|
18962
|
+
* an already-unwrapped reply) passes through untouched.
|
|
18963
|
+
*/
|
|
18964
|
+
function unwrapVertoReply(response) {
|
|
18965
|
+
const inner = getValueFrom(response, "result.result");
|
|
18966
|
+
return inner && typeof inner === "object" && "result" in inner ? inner : response;
|
|
18967
|
+
}
|
|
18968
|
+
|
|
18297
18969
|
//#endregion
|
|
18298
18970
|
//#region src/core/entities/Call.ts
|
|
18299
18971
|
var import_cjs$11 = require_cjs();
|
|
18300
18972
|
const logger$12 = getLogger();
|
|
18301
18973
|
/**
|
|
18302
|
-
* Verto method for setting member layout positions. Its gateway DTO requires a
|
|
18303
|
-
* `targets` array whose entries are `{ target, position }` (NOT bare targets),
|
|
18304
|
-
* so {@link WebRTCCall.buildMethodParams} special-cases it. See issue #19400.
|
|
18305
|
-
*/
|
|
18306
|
-
const POSITION_SET_METHOD = "call.member.position.set";
|
|
18307
|
-
/**
|
|
18308
18974
|
* Ratio between the critical and warning RTT spike multipliers.
|
|
18309
18975
|
* Warning threshold = baseline * warningMultiplier (default 3x)
|
|
18310
18976
|
* Critical threshold = baseline * warningMultiplier * RTT_CRITICAL_TO_WARNING_RATIO
|
|
@@ -18399,8 +19065,11 @@ var WebRTCCall = class extends Destroyable {
|
|
|
18399
19065
|
emitError(callError) {
|
|
18400
19066
|
if (this._status$.value === "destroyed" || this._status$.value === "failed") return;
|
|
18401
19067
|
this._errors$.next(callError);
|
|
18402
|
-
if (callError.fatal) {
|
|
19068
|
+
if (callError.fatal && this._status$.value !== "disconnecting") {
|
|
18403
19069
|
this._status$.next("failed");
|
|
19070
|
+
this.vertoManager.bye().catch((error) => {
|
|
19071
|
+
logger$12.debug("[Call] fatal-teardown bye failed (signaling likely already dead):", error);
|
|
19072
|
+
});
|
|
18404
19073
|
this.destroy();
|
|
18405
19074
|
}
|
|
18406
19075
|
}
|
|
@@ -18455,7 +19124,7 @@ var WebRTCCall = class extends Destroyable {
|
|
|
18455
19124
|
/** Toggles the call lock state, preventing or allowing new participants from joining. */
|
|
18456
19125
|
async toggleLock() {
|
|
18457
19126
|
const method = this.locked ? "call.unlock" : "call.lock";
|
|
18458
|
-
await this.executeMethod(this.
|
|
19127
|
+
await this.executeMethod(this.callSelf, method, {});
|
|
18459
19128
|
}
|
|
18460
19129
|
/**
|
|
18461
19130
|
* Toggles the hold state of the call (pauses/resumes local media transmission).
|
|
@@ -18504,14 +19173,25 @@ var WebRTCCall = class extends Destroyable {
|
|
|
18504
19173
|
*
|
|
18505
19174
|
* Constructs call context (node_id, call_id, member_id) and sends the RPC request.
|
|
18506
19175
|
*
|
|
18507
|
-
* @param target - Target
|
|
19176
|
+
* @param target - Target {@link MemberTarget} triple, or the local member's
|
|
19177
|
+
* ID string for self-operations (any other string is rejected — a bare
|
|
19178
|
+
* member id cannot carry the remote member's own call context).
|
|
18508
19179
|
* @param method - Verto method name (e.g. `'call.mute'`, `'call.member.remove'`).
|
|
18509
19180
|
* @param args - Parameters for the RPC method.
|
|
18510
19181
|
* @returns The RPC response.
|
|
19182
|
+
* @throws {CallNotReadyError} If the call has no self member context yet.
|
|
19183
|
+
* @throws {InvalidParams} If a string target is not the local member's ID.
|
|
18511
19184
|
* @throws {JSONRPCError} If the RPC call returns an error.
|
|
18512
19185
|
*/
|
|
18513
19186
|
async executeMethod(target, method, args) {
|
|
18514
|
-
const
|
|
19187
|
+
const self = this.callSelf;
|
|
19188
|
+
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}`);
|
|
19189
|
+
if (this.clientSession.callControl === "in-dialog") return this.executeMethodInDialog(target, method, args);
|
|
19190
|
+
const params = {
|
|
19191
|
+
...args,
|
|
19192
|
+
self,
|
|
19193
|
+
target: typeof target === "string" ? self : target
|
|
19194
|
+
};
|
|
18515
19195
|
const request = buildRPCRequest({
|
|
18516
19196
|
method,
|
|
18517
19197
|
params
|
|
@@ -18525,29 +19205,98 @@ var WebRTCCall = class extends Destroyable {
|
|
|
18525
19205
|
throw error;
|
|
18526
19206
|
}
|
|
18527
19207
|
}
|
|
18528
|
-
|
|
18529
|
-
|
|
18530
|
-
|
|
18531
|
-
|
|
18532
|
-
|
|
18533
|
-
|
|
18534
|
-
|
|
18535
|
-
|
|
18536
|
-
|
|
18537
|
-
|
|
18538
|
-
|
|
18539
|
-
|
|
18540
|
-
|
|
18541
|
-
|
|
18542
|
-
};
|
|
18543
|
-
|
|
18544
|
-
|
|
18545
|
-
|
|
18546
|
-
|
|
18547
|
-
|
|
19208
|
+
/**
|
|
19209
|
+
* `executeMethod` for a call opened with `callControl: 'in-dialog'`.
|
|
19210
|
+
*
|
|
19211
|
+
* Translates the routed transport's calling convention into the in-dialog one. No
|
|
19212
|
+
* `self` tuple is sent, but a `target` is — the same {call_id, member_id} the routed
|
|
19213
|
+
* transport puts in `target` (minus node_id), for self-ops and cross-member ops alike.
|
|
19214
|
+
*
|
|
19215
|
+
* Target shapes are per-verb and irregular, so they are centralised here rather
|
|
19216
|
+
* than left to callers: most verbs take a singular `target`, `call.member.remove`
|
|
19217
|
+
* takes a plural `targets` array, and `call.member.position.set` takes a flat
|
|
19218
|
+
* `targets` of `{call_id, position}` — the one verb keyed on call_id rather than
|
|
19219
|
+
* member_id, so the member triple `Participant.setPosition` built is unwrapped.
|
|
19220
|
+
*/
|
|
19221
|
+
async executeMethodInDialog(target, method, args) {
|
|
19222
|
+
const control = { ...args };
|
|
19223
|
+
if (method === "call.member.position.set") control.targets = (args.targets ?? []).map((entry) => ({
|
|
19224
|
+
call_id: entry.target?.call_id ?? entry.call_id,
|
|
19225
|
+
position: entry.position
|
|
19226
|
+
}));
|
|
19227
|
+
else {
|
|
19228
|
+
const member = typeof target === "object" ? {
|
|
19229
|
+
call_id: target.call_id,
|
|
19230
|
+
member_id: target.member_id
|
|
19231
|
+
} : {
|
|
18548
19232
|
call_id: this.id,
|
|
18549
19233
|
member_id: target
|
|
18550
|
-
}
|
|
19234
|
+
};
|
|
19235
|
+
if (method === "call.member.remove") control.targets = [member];
|
|
19236
|
+
else control.target = member;
|
|
19237
|
+
}
|
|
19238
|
+
return this.sendCommand(method, control);
|
|
19239
|
+
}
|
|
19240
|
+
/**
|
|
19241
|
+
* Sends a `call.*` control verb **in-dialog** via `verto.info`, as an alternative
|
|
19242
|
+
* to the routed {@link executeMethod} transport.
|
|
19243
|
+
*
|
|
19244
|
+
* Why both exist: `executeMethod` addresses the member with an explicit
|
|
19245
|
+
* `{node_id, call_id, member_id}` tuple, which does not resolve for every conference,
|
|
19246
|
+
* so the op can fail. An in-dialog frame carries the verb on the member's own
|
|
19247
|
+
* signaling channel instead, so control works without the client needing to know how
|
|
19248
|
+
* the conference is hosted.
|
|
19249
|
+
*
|
|
19250
|
+
* The trade-off is reach: the in-dialog transport is only accepted for calls that
|
|
19251
|
+
* join a conference over SWML (e.g. an SWML `join_conference`); use the routed
|
|
19252
|
+
* default otherwise.
|
|
19253
|
+
*
|
|
19254
|
+
* `params` are sent verbatim — nothing is built for you, which includes the target.
|
|
19255
|
+
* **A self-directed op still needs one**, or it is refused; name yourself explicitly:
|
|
19256
|
+
*
|
|
19257
|
+
* ```ts
|
|
19258
|
+
* const { call_id, member_id } = call.self.target;
|
|
19259
|
+
* await call.sendCommand('call.mute', { channels: ['audio'], target: { call_id, member_id } });
|
|
19260
|
+
* ```
|
|
19261
|
+
*
|
|
19262
|
+
* Never include `node_id` — only the two ids. The shapes are per-verb: most take a
|
|
19263
|
+
* singular `target`, `call.member.remove` takes a plural `targets` array, and
|
|
19264
|
+
* `call.member.position.set` takes a flat `targets: [{call_id, position}]` (the one
|
|
19265
|
+
* verb keyed on `call_id` rather than `member_id`). Verbs that act on the call as a
|
|
19266
|
+
* whole, or that the SDK does not wrap at all, take no target.
|
|
19267
|
+
*
|
|
19268
|
+
* For the typed alternative that handles all of this, create the client with
|
|
19269
|
+
* `callControl: 'in-dialog'` and use the ordinary `Call`/`Participant` methods.
|
|
19270
|
+
*
|
|
19271
|
+
* @internal Not part of the supported surface while the in-dialog transport is still
|
|
19272
|
+
* rolling out. `WebRTCCall` is exported from the package entry, so without this tag
|
|
19273
|
+
* TypeDoc publishes the method — and the example above — as public API.
|
|
19274
|
+
*
|
|
19275
|
+
* @param method - A `call.*` method name (e.g. `'call.mute'`).
|
|
19276
|
+
* @param params - Method parameters, sent verbatim.
|
|
19277
|
+
* @returns The method's own reply, unwrapped from the `verto.info` envelope.
|
|
19278
|
+
* @throws {JSONRPCError} If the control op fails.
|
|
19279
|
+
*/
|
|
19280
|
+
async sendCommand(method, params = {}) {
|
|
19281
|
+
return unwrapVertoReply(await this.vertoManager.sendCallControl(method, params));
|
|
19282
|
+
}
|
|
19283
|
+
/**
|
|
19284
|
+
* The local leg's member triple — sent as `self` in every member RPC
|
|
19285
|
+
* envelope, and as the `target` of call-scoped self-operations (e.g. lock,
|
|
19286
|
+
* layout).
|
|
19287
|
+
*
|
|
19288
|
+
* @throws {CallNotReadyError} Before `call.joined` delivers the self member
|
|
19289
|
+
* context (`selfId`/`nodeId`) — an RPC without it cannot be routed, so fail
|
|
19290
|
+
* fast instead of sending a doomed request.
|
|
19291
|
+
*/
|
|
19292
|
+
get callSelf() {
|
|
19293
|
+
const node_id = this.nodeId;
|
|
19294
|
+
const member_id = this.vertoManager.selfId;
|
|
19295
|
+
if (!node_id || !member_id) throw new CallNotReadyError(this.id);
|
|
19296
|
+
return {
|
|
19297
|
+
node_id,
|
|
19298
|
+
call_id: this.id,
|
|
19299
|
+
member_id
|
|
18551
19300
|
};
|
|
18552
19301
|
}
|
|
18553
19302
|
/** Observable of the current call status (e.g. `'ringing'`, `'connected'`). */
|
|
@@ -18703,13 +19452,13 @@ var WebRTCCall = class extends Destroyable {
|
|
|
18703
19452
|
get bandwidthConstrained$() {
|
|
18704
19453
|
return this.deferEmission(this._bandwidthConstrained$.asObservable());
|
|
18705
19454
|
}
|
|
18706
|
-
/** Observable that emits when server
|
|
19455
|
+
/** Observable that emits when the server pushes media params. */
|
|
18707
19456
|
get mediaParamsUpdated$() {
|
|
18708
19457
|
return this.deferEmission(this._mediaParamsUpdated$.asObservable());
|
|
18709
19458
|
}
|
|
18710
19459
|
/**
|
|
18711
19460
|
* @internal Emit a media params update event.
|
|
18712
|
-
* Called by the VertoManager when server
|
|
19461
|
+
* Called by the VertoManager when the server pushes media params.
|
|
18713
19462
|
*/
|
|
18714
19463
|
emitMediaParamsUpdated(event) {
|
|
18715
19464
|
this._mediaParamsUpdated$.next(event);
|
|
@@ -18917,6 +19666,10 @@ var WebRTCCall = class extends Destroyable {
|
|
|
18917
19666
|
get selfId$() {
|
|
18918
19667
|
return this.vertoManager.selfId$;
|
|
18919
19668
|
}
|
|
19669
|
+
/** @internal Lets call creation bound the media and signalling phases apart. */
|
|
19670
|
+
get localMediaSettled$() {
|
|
19671
|
+
return this.vertoManager.localMediaSettled$;
|
|
19672
|
+
}
|
|
18920
19673
|
/** Local participant's member ID, or `null` if not joined. */
|
|
18921
19674
|
get selfId() {
|
|
18922
19675
|
return this.vertoManager.selfId;
|
|
@@ -19110,12 +19863,17 @@ var WebRTCCall = class extends Destroyable {
|
|
|
19110
19863
|
*
|
|
19111
19864
|
* **These operations are NOT atomic.** The layout is applied first, then each
|
|
19112
19865
|
* member position sequentially, so members may briefly flash into their
|
|
19113
|
-
* default slots before being moved to the requested positions.
|
|
19866
|
+
* default slots before being moved to the requested positions. Targeted
|
|
19867
|
+
* members are validated upfront, though: when any of them has no
|
|
19868
|
+
* {@link Participant.target | member call context} yet, the whole call
|
|
19869
|
+
* rejects before any request is sent and the layout is left unchanged.
|
|
19114
19870
|
*
|
|
19115
19871
|
* @param layout - Layout name (must be one of {@link layouts}).
|
|
19116
19872
|
* @param positions - Optional map of member IDs to {@link VideoPosition} values.
|
|
19117
19873
|
* When omitted or empty, only the layout is changed.
|
|
19118
19874
|
* @throws {InvalidParams} If the layout is not in the available {@link layouts}.
|
|
19875
|
+
* @throws {ParticipantNotReadyError} If a targeted member's call context has
|
|
19876
|
+
* not been received yet — thrown before any request is sent.
|
|
19119
19877
|
*
|
|
19120
19878
|
* @example
|
|
19121
19879
|
* ```ts
|
|
@@ -19126,18 +19884,19 @@ var WebRTCCall = class extends Destroyable {
|
|
|
19126
19884
|
*/
|
|
19127
19885
|
async setLayout(layout, positions) {
|
|
19128
19886
|
if (!this.layouts.includes(layout)) throw new InvalidParams(`Layout ${layout} is not available in the current call layouts: ${this.layouts.join(", ")}`);
|
|
19129
|
-
const
|
|
19130
|
-
|
|
19131
|
-
const positionEntries = Object.entries(positions ?? {});
|
|
19132
|
-
if (positionEntries.length === 0) return;
|
|
19133
|
-
for (const [memberId, position] of positionEntries) {
|
|
19887
|
+
const targets = [];
|
|
19888
|
+
for (const [memberId, position] of Object.entries(positions ?? {})) {
|
|
19134
19889
|
const participant = this.participants.find((p) => p.id === memberId);
|
|
19135
19890
|
if (!participant) {
|
|
19136
19891
|
logger$12.warn(`[Call] setLayout: member ${memberId} not found in participants; skipping position ${position}`);
|
|
19137
19892
|
continue;
|
|
19138
19893
|
}
|
|
19139
|
-
|
|
19894
|
+
participant.target;
|
|
19895
|
+
targets.push([participant, position]);
|
|
19140
19896
|
}
|
|
19897
|
+
const selfId = await (0, import_cjs$11.firstValueFrom)(this.selfId$.pipe((0, import_cjs$11.filter)((id) => id !== null)));
|
|
19898
|
+
await this.executeMethod(selfId, "call.layout.set", { layout });
|
|
19899
|
+
for (const [participant, position] of targets) await participant.setPosition(position);
|
|
19141
19900
|
}
|
|
19142
19901
|
/**
|
|
19143
19902
|
* Transfers the call to another destination.
|
|
@@ -19239,17 +19998,28 @@ var WebRTCCall = class extends Destroyable {
|
|
|
19239
19998
|
* (notably iOS Safari) fall back to re-acquiring the track with the new
|
|
19240
19999
|
* constraint set and plumbing the replacement through the local audio
|
|
19241
20000
|
* pipeline if one is active.
|
|
20001
|
+
*
|
|
20002
|
+
* @returns whether the constraint reached the microphone. `false` is an
|
|
20003
|
+
* outcome rather than an error — a leg sending media the SDK did not capture
|
|
20004
|
+
* is left alone — so a UI that reflects the toggle must read it. Any failure
|
|
20005
|
+
* behind a `false` is also reported on {@link errors$}.
|
|
19242
20006
|
*/
|
|
19243
20007
|
async setEchoCancellation(enabled) {
|
|
19244
|
-
|
|
20008
|
+
return this.vertoManager.updateMediaConstraints({ audio: { echoCancellation: enabled } });
|
|
19245
20009
|
}
|
|
19246
|
-
/**
|
|
20010
|
+
/**
|
|
20011
|
+
* Toggle browser noise suppression on the local mic at runtime.
|
|
20012
|
+
* @returns whether the constraint reached the microphone.
|
|
20013
|
+
*/
|
|
19247
20014
|
async setNoiseSuppression(enabled) {
|
|
19248
|
-
|
|
20015
|
+
return this.vertoManager.updateMediaConstraints({ audio: { noiseSuppression: enabled } });
|
|
19249
20016
|
}
|
|
19250
|
-
/**
|
|
20017
|
+
/**
|
|
20018
|
+
* Toggle browser automatic gain control on the local mic at runtime.
|
|
20019
|
+
* @returns whether the constraint reached the microphone.
|
|
20020
|
+
*/
|
|
19251
20021
|
async setAutoGainControl(enabled) {
|
|
19252
|
-
|
|
20022
|
+
return this.vertoManager.updateMediaConstraints({ audio: { autoGainControl: enabled } });
|
|
19253
20023
|
}
|
|
19254
20024
|
/**
|
|
19255
20025
|
* Observable of the aggregate remote audio level, 0..1 RMS. The server
|
|
@@ -19306,6 +20076,9 @@ var WebRTCCall = class extends Destroyable {
|
|
|
19306
20076
|
/**
|
|
19307
20077
|
* Infers the semantic error category from a raw Error thrown by VertoManager
|
|
19308
20078
|
* or an RTCPeerConnection layer.
|
|
20079
|
+
*
|
|
20080
|
+
* Pure function — exported for unit testing.
|
|
20081
|
+
* @internal
|
|
19309
20082
|
*/
|
|
19310
20083
|
function inferCallErrorKind(error) {
|
|
19311
20084
|
if (error instanceof RPCTimeoutError) return "timeout";
|
|
@@ -19324,7 +20097,14 @@ const RECOVERABLE_RPC_CODES = new Set([
|
|
|
19324
20097
|
RPC_ERROR_AUTHENTICATION_FAILED,
|
|
19325
20098
|
RPC_ERROR_INVALID_PARAMS
|
|
19326
20099
|
]);
|
|
19327
|
-
/**
|
|
20100
|
+
/**
|
|
20101
|
+
* A *fallback*: callers knowing which leg failed pass an explicit `fatal` and
|
|
20102
|
+
* never reach here, so the default-fatal branch only sees call- and main-leg
|
|
20103
|
+
* errors. Auxiliary legs go through `WebRTCVertoManager.reportLegError`.
|
|
20104
|
+
*
|
|
20105
|
+
* Pure function — exported for unit testing.
|
|
20106
|
+
* @internal
|
|
20107
|
+
*/
|
|
19328
20108
|
function isFatalError(error) {
|
|
19329
20109
|
if (error instanceof VertoPongError) return false;
|
|
19330
20110
|
if (error instanceof MediaTrackError) return false;
|
|
@@ -19359,7 +20139,9 @@ var CallFactory = class {
|
|
|
19359
20139
|
kind: inferCallErrorKind(error),
|
|
19360
20140
|
fatal: options$1?.fatal ?? isFatalError(error),
|
|
19361
20141
|
error,
|
|
19362
|
-
callId: callInstance.id
|
|
20142
|
+
callId: callInstance.id,
|
|
20143
|
+
...options$1?.leg ? { leg: options$1.leg } : {},
|
|
20144
|
+
...options$1?.legId ? { legId: options$1.legId } : {}
|
|
19363
20145
|
};
|
|
19364
20146
|
callInstance.emitError(callError);
|
|
19365
20147
|
},
|
|
@@ -19831,6 +20613,30 @@ const logger$9 = getLogger();
|
|
|
19831
20613
|
function shouldAbortDial(callError) {
|
|
19832
20614
|
return callError.fatal || !(callError.error instanceof MediaAccessError);
|
|
19833
20615
|
}
|
|
20616
|
+
/**
|
|
20617
|
+
* Wait for a dialed call to be ready, or for the failure that stops it.
|
|
20618
|
+
*
|
|
20619
|
+
* Local media acquisition is deliberately unbounded: a permission prompt or a
|
|
20620
|
+
* device picker is human time, and `getUserMedia` cannot be cancelled anyway.
|
|
20621
|
+
* The clock starts only once acquisition settles, so a slow human never spends
|
|
20622
|
+
* the server's budget.
|
|
20623
|
+
*
|
|
20624
|
+
* `merge` rather than `race`, because the two legs settle asymmetrically. A
|
|
20625
|
+
* fatal acquisition failure reports the error and then destroys the call in the
|
|
20626
|
+
* same synchronous step; `errors$` defers delivery by a microtask while
|
|
20627
|
+
* `localMediaSettled$` completes immediately. Under `race` that bare completion
|
|
20628
|
+
* ended the wait first and `dial()` rejected with an RxJS `EmptyError`, burying
|
|
20629
|
+
* the `NotAllowedError` applications are told to inspect. Under `merge` the
|
|
20630
|
+
* completed leg is simply spent, and the queued error — enqueued before the
|
|
20631
|
+
* completion, so delivered before it — arrives to reject the wait. A dial
|
|
20632
|
+
* abandoned with no error at all still ends both legs, and the resulting
|
|
20633
|
+
* `EmptyError` remains the benign-cancel signal.
|
|
20634
|
+
*
|
|
20635
|
+
* Exported for unit testing.
|
|
20636
|
+
*/
|
|
20637
|
+
async function awaitDialReady(session, signalingTimeoutMs) {
|
|
20638
|
+
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)))));
|
|
20639
|
+
}
|
|
19834
20640
|
const getAddressSearchURI = (options) => {
|
|
19835
20641
|
const to = options.to?.split("?")[0];
|
|
19836
20642
|
const from$9 = options.from?.startsWith("subscriber://") ? options.from.replace("subscriber://", "") : options.from;
|
|
@@ -19847,7 +20653,6 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
19847
20653
|
this.authorizationStateKey = authorizationStateKey;
|
|
19848
20654
|
this.attachManager = attachManager;
|
|
19849
20655
|
this.dpopManager = dpopManager;
|
|
19850
|
-
this.callCreateTimeout = 6e3;
|
|
19851
20656
|
this.agent = `signalwire-js/4.0.0`;
|
|
19852
20657
|
this.eventAcks = true;
|
|
19853
20658
|
this.authorizationState$ = this.createReplaySubject(1);
|
|
@@ -19856,6 +20661,7 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
19856
20661
|
minor: 0,
|
|
19857
20662
|
revision: 0
|
|
19858
20663
|
};
|
|
20664
|
+
this.callControl = "routed";
|
|
19859
20665
|
this._authorization$ = this.createBehaviorSubject(void 0);
|
|
19860
20666
|
this._errors$ = this.createReplaySubject(1);
|
|
19861
20667
|
this._authState$ = this.createBehaviorSubject({ kind: "unauthenticated" });
|
|
@@ -20056,21 +20862,18 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
20056
20862
|
}
|
|
20057
20863
|
async handleAuthenticationError(error) {
|
|
20058
20864
|
logger$9.error("Authentication error:", error);
|
|
20059
|
-
const isRecoverableAuthError = error instanceof JSONRPCError && (error.code === RPC_ERROR_REQUESTER_VALIDATION_FAILED || error.code === RPC_ERROR_INVALID_PARAMS || error.code === RPC_ERROR_AUTHENTICATION_FAILED);
|
|
20865
|
+
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);
|
|
20060
20866
|
const hasStoredState = await (0, import_cjs$7.firstValueFrom)(this.authorizationState$.pipe((0, import_cjs$7.take)(1))) !== void 0;
|
|
20061
|
-
if (isRecoverableAuthError && hasStoredState) {
|
|
20867
|
+
if (isRecoverableAuthError$1 && hasStoredState) {
|
|
20062
20868
|
logger$9.debug("[Session] Recoverable auth error — cleaning up stored state and reconnecting fresh");
|
|
20063
|
-
|
|
20064
|
-
await this.cleanupStoredConnectionParams();
|
|
20065
|
-
} catch (cleanupError) {
|
|
20066
|
-
logger$9.error("Failed to cleanup stored connection params:", cleanupError);
|
|
20067
|
-
} finally {
|
|
20068
|
-
this.transport.reconnect();
|
|
20069
|
-
}
|
|
20869
|
+
await this.discardResumeStateAndReconnect();
|
|
20070
20870
|
} else this._errors$.next(error);
|
|
20071
20871
|
}
|
|
20072
20872
|
/**
|
|
20073
|
-
* Clear the resume state (authorization_state + protocol)
|
|
20873
|
+
* Clear the resume state (authorization_state + protocol) and ask the
|
|
20874
|
+
* transport to reconnect. The `connected` event re-triggers
|
|
20875
|
+
* `authenticate()`, which now has no stored state and so performs a fresh
|
|
20876
|
+
* connect.
|
|
20074
20877
|
*
|
|
20075
20878
|
* This is the stale-auth-state recovery helper used by handleAuthError:
|
|
20076
20879
|
* the server rejected a reconnect, so the resume state is discarded and a
|
|
@@ -20078,9 +20881,24 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
20078
20881
|
* session lives on through the reconnect and reattachCalls() needs the
|
|
20079
20882
|
* stored call references afterwards. Do NOT add detachAll() here.
|
|
20080
20883
|
*
|
|
20884
|
+
* Connect-time recovery only. A *request* refused on an already
|
|
20885
|
+
* authenticated session is never healed here: dropping the resume state
|
|
20886
|
+
* destroys the association between the socket and the previous session,
|
|
20887
|
+
* which is what reattach depends on. That path mints a fresh credential and
|
|
20888
|
+
* reauthenticates instead (see `SignalWire.recoverAndRetry`).
|
|
20889
|
+
*
|
|
20081
20890
|
* For public teardown (disconnect/destroy), use {@link teardownSessionState}
|
|
20082
20891
|
* instead, which clears the attach records as well.
|
|
20083
20892
|
*/
|
|
20893
|
+
async discardResumeStateAndReconnect() {
|
|
20894
|
+
try {
|
|
20895
|
+
await this.cleanupStoredConnectionParams();
|
|
20896
|
+
} catch (cleanupError) {
|
|
20897
|
+
logger$9.error("Failed to cleanup stored connection params:", cleanupError);
|
|
20898
|
+
} finally {
|
|
20899
|
+
this.transport.reconnect();
|
|
20900
|
+
}
|
|
20901
|
+
}
|
|
20084
20902
|
async cleanupStoredConnectionParams() {
|
|
20085
20903
|
await this.transport.setProtocol(void 0);
|
|
20086
20904
|
await this.updateAuthorizationStateInStorage(void 0);
|
|
@@ -20154,11 +20972,15 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
20154
20972
|
const isReconnect = hasReconnectState && storedToken;
|
|
20155
20973
|
let dpopToken;
|
|
20156
20974
|
if (isReconnect) logger$9.debug("[Session] Reconnecting with stored jwt_token + authorization_state");
|
|
20157
|
-
else
|
|
20158
|
-
|
|
20159
|
-
|
|
20975
|
+
else {
|
|
20976
|
+
const credential = this.getCredential();
|
|
20977
|
+
const credentialExpired = credential.expiry_at !== void 0 && credential.expiry_at <= Date.now() + CREDENTIAL_EXPIRY_SKEW_MS;
|
|
20978
|
+
if (this.onBeforeReconnect && (this.clientBound || credentialExpired)) {
|
|
20979
|
+
logger$9.debug("[Session] Refreshing credentials before fresh connect");
|
|
20980
|
+
await this.onBeforeReconnect();
|
|
20981
|
+
}
|
|
20160
20982
|
}
|
|
20161
|
-
if (
|
|
20983
|
+
if (this.dpopManager?.initialized) try {
|
|
20162
20984
|
dpopToken = await this.dpopManager.createRpcProof({ method: "signalwire.connect" });
|
|
20163
20985
|
} catch (error) {
|
|
20164
20986
|
if (this.clientBound) throw error;
|
|
@@ -20191,6 +21013,7 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
20191
21013
|
});
|
|
20192
21014
|
if (response.protocol) await this.transport.setProtocol(response.protocol);
|
|
20193
21015
|
this._authorization$.next(response.authorization);
|
|
21016
|
+
if (response.authorization.cnf?.jkt) this._wasClientBound = true;
|
|
20194
21017
|
this._iceServers$.next(response.ice_servers ?? []);
|
|
20195
21018
|
this._authState$.next({ kind: "authenticated" });
|
|
20196
21019
|
logger$9.debug("[Session] Authentication completed successfully");
|
|
@@ -20257,7 +21080,7 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
20257
21080
|
to: destinationURI,
|
|
20258
21081
|
...options
|
|
20259
21082
|
});
|
|
20260
|
-
await (
|
|
21083
|
+
await awaitDialReady(callSession, DEFAULT_CALL_SIGNALING_TIMEOUT_MS);
|
|
20261
21084
|
this._calls$.next({
|
|
20262
21085
|
[`${callSession.id}`]: callSession,
|
|
20263
21086
|
...this._calls$.value
|
|
@@ -20310,12 +21133,23 @@ var ClientSessionWrapper = class {
|
|
|
20310
21133
|
get authenticated() {
|
|
20311
21134
|
return this.clientSessionManager.authenticated;
|
|
20312
21135
|
}
|
|
21136
|
+
/**
|
|
21137
|
+
* Whether the session is using a Client Bound SAT (DPoP). Sticky — set
|
|
21138
|
+
* when the binding is established or restored from a resumed session's
|
|
21139
|
+
* server authorization.
|
|
21140
|
+
*/
|
|
21141
|
+
get clientBound() {
|
|
21142
|
+
return this.clientSessionManager.clientBound;
|
|
21143
|
+
}
|
|
20313
21144
|
get signalingEvent$() {
|
|
20314
21145
|
return this.clientSessionManager.signalingEvent$;
|
|
20315
21146
|
}
|
|
20316
21147
|
get iceServers() {
|
|
20317
21148
|
return this.clientSessionManager.iceServers;
|
|
20318
21149
|
}
|
|
21150
|
+
get callControl() {
|
|
21151
|
+
return this.clientSessionManager.callControl;
|
|
21152
|
+
}
|
|
20319
21153
|
async execute(request, options) {
|
|
20320
21154
|
return this.clientSessionManager.execute(request, options);
|
|
20321
21155
|
}
|
|
@@ -20512,7 +21346,7 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
20512
21346
|
await session.reauthenticate(tokenData.token, rpcProof, { clientBound: true });
|
|
20513
21347
|
updateCredential({ token: tokenData.token });
|
|
20514
21348
|
logger$7.info("[DeviceToken] Client Bound SAT activated successfully");
|
|
20515
|
-
this.
|
|
21349
|
+
this.emitCurrentToken(tokenData);
|
|
20516
21350
|
return { activated: true };
|
|
20517
21351
|
} catch (error) {
|
|
20518
21352
|
logger$7.error("[DeviceToken] Failed to activate Client Bound SAT:", error);
|
|
@@ -20524,6 +21358,21 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
20524
21358
|
}
|
|
20525
21359
|
}
|
|
20526
21360
|
/**
|
|
21361
|
+
* Emit a freshly received token to the reactive pipeline, stamping an
|
|
21362
|
+
* absolute `expires_at` when the response carried only `expires_in`.
|
|
21363
|
+
* Resolving the expiry at RECEIVE time (not at read time) is what lets
|
|
21364
|
+
* {@link refreshNowIfDue} detect due-ness on resume: a bare `expires_in`
|
|
21365
|
+
* re-resolved later would always compute a full TTL from "now" and never
|
|
21366
|
+
* cross the refresh buffer.
|
|
21367
|
+
*/
|
|
21368
|
+
emitCurrentToken(token) {
|
|
21369
|
+
const stamped = token.expires_at ? token : {
|
|
21370
|
+
...token,
|
|
21371
|
+
expires_at: resolveExpiresAt(token)
|
|
21372
|
+
};
|
|
21373
|
+
this._currentToken$.next(stamped);
|
|
21374
|
+
}
|
|
21375
|
+
/**
|
|
20527
21376
|
* Returns true when the cached token has enough headroom before expiry to
|
|
20528
21377
|
* be safely reused on reactivation. The headroom matches the refresh
|
|
20529
21378
|
* buffer, so a token within the refresh window is treated as stale (the
|
|
@@ -20541,7 +21390,7 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
20541
21390
|
method: "POST",
|
|
20542
21391
|
uri: DEVICE_TOKEN_ENDPOINT
|
|
20543
21392
|
});
|
|
20544
|
-
const response = await this.http.request({
|
|
21393
|
+
const response = await this.http().request({
|
|
20545
21394
|
url: DEVICE_TOKEN_ENDPOINT,
|
|
20546
21395
|
...POST_PARAMS,
|
|
20547
21396
|
body: JSON.stringify({
|
|
@@ -20568,7 +21417,7 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
20568
21417
|
uri: DEVICE_REFRESH_ENDPOINT,
|
|
20569
21418
|
accessToken: currentToken
|
|
20570
21419
|
});
|
|
20571
|
-
const response = await this.http.request({
|
|
21420
|
+
const response = await this.http().request({
|
|
20572
21421
|
url: DEVICE_REFRESH_ENDPOINT,
|
|
20573
21422
|
...POST_PARAMS,
|
|
20574
21423
|
body: JSON.stringify({
|
|
@@ -20616,7 +21465,7 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
20616
21465
|
const currentToken = this.getCredential().token;
|
|
20617
21466
|
if (!currentToken) throw new TokenRefreshError("No current token available for refresh");
|
|
20618
21467
|
const newTokenData = await this.retryRefresh(session, currentToken, updateCredential);
|
|
20619
|
-
this.
|
|
21468
|
+
this.emitCurrentToken(newTokenData);
|
|
20620
21469
|
} catch (error) {
|
|
20621
21470
|
logger$7.error("[DeviceToken] Automatic Client Bound SAT refresh failed:", error);
|
|
20622
21471
|
this.errorHandler(error instanceof TokenRefreshError ? error : new TokenRefreshError("Automatic token refresh failed", error));
|
|
@@ -20643,6 +21492,22 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
20643
21492
|
throw lastError instanceof Error ? lastError : new TokenRefreshError("All refresh retries exhausted", lastError);
|
|
20644
21493
|
}
|
|
20645
21494
|
/**
|
|
21495
|
+
* Force an immediate refresh when the cached Client Bound SAT is already
|
|
21496
|
+
* past its refresh window. Called on resume from suspension where
|
|
21497
|
+
* background-tab throttling can delay the reactive timer past the buffer.
|
|
21498
|
+
* A no-op when no token is cached or it still has headroom; the normal
|
|
21499
|
+
* {@link executeRefresh} guards (paused / in-progress / unauthenticated)
|
|
21500
|
+
* still apply.
|
|
21501
|
+
*/
|
|
21502
|
+
refreshNowIfDue() {
|
|
21503
|
+
const token = this._currentToken$.value;
|
|
21504
|
+
if (!token) return;
|
|
21505
|
+
if (resolveExpiresAt(token) * 1e3 - Date.now() <= DEVICE_TOKEN_REFRESH_BUFFER_MS) {
|
|
21506
|
+
logger$7.debug("[DeviceToken] Resume: cached SAT past refresh window; refreshing now");
|
|
21507
|
+
this.executeRefresh();
|
|
21508
|
+
}
|
|
21509
|
+
}
|
|
21510
|
+
/**
|
|
20646
21511
|
* Stops the reactive refresh pipeline from firing. Use when the underlying
|
|
20647
21512
|
* session is being torn down (e.g., during {@link SignalWire.disconnect})
|
|
20648
21513
|
* so a scheduled refresh cannot fire against a destroyed session.
|
|
@@ -20693,6 +21558,7 @@ var CredentialRefreshCoordinator = class extends Destroyable {
|
|
|
20693
21558
|
this.deps = deps;
|
|
20694
21559
|
this._activating = false;
|
|
20695
21560
|
this._activationGeneration = 0;
|
|
21561
|
+
this._developerRefreshInProgress = false;
|
|
20696
21562
|
if (dpopManager?.initialized) this._deviceTokenManager = (deps.deviceTokenManagerFactory ?? defaultDeviceTokenManagerFactory)(dpopManager, deps.http, (error) => deps.notifier.onError(error), () => deps.store.read());
|
|
20697
21563
|
}
|
|
20698
21564
|
/** True when the Client Bound SAT path is available (DPoP initialized). */
|
|
@@ -20711,28 +21577,120 @@ var CredentialRefreshCoordinator = class extends Destroyable {
|
|
|
20711
21577
|
* invokes `deps.onRefreshExhausted` so the orchestrator can disconnect.
|
|
20712
21578
|
*/
|
|
20713
21579
|
scheduleDeveloperRefresh(provider, expiresAt, attempt = 0) {
|
|
21580
|
+
this._activeProvider = provider;
|
|
20714
21581
|
if (this._developerTimerId !== void 0) clearTimeout(this._developerTimerId);
|
|
20715
21582
|
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);
|
|
20716
|
-
this._developerTimerId = setTimeout(
|
|
21583
|
+
this._developerTimerId = setTimeout(() => {
|
|
21584
|
+
this._developerTimerId = void 0;
|
|
21585
|
+
this.executeDeveloperRefresh(provider, expiresAt, attempt);
|
|
21586
|
+
}, refreshInterval);
|
|
21587
|
+
}
|
|
21588
|
+
/**
|
|
21589
|
+
* Runs the developer-provided refresh once: mints a new credential, stores
|
|
21590
|
+
* and persists it, reauthenticates the live session (via the notifier), and
|
|
21591
|
+
* reschedules against the new expiry. On failure retries with backoff up to
|
|
21592
|
+
* {@link CREDENTIAL_REFRESH_MAX_RETRIES}, then signals exhaustion.
|
|
21593
|
+
*
|
|
21594
|
+
* Shared by the scheduled timer tick and {@link forceRefreshIfDue}. The
|
|
21595
|
+
* `_developerRefreshInProgress` guard prevents the two from overlapping.
|
|
21596
|
+
*/
|
|
21597
|
+
async executeDeveloperRefresh(provider, expiresAt, attempt) {
|
|
21598
|
+
if (this._developerRefreshInProgress) {
|
|
21599
|
+
logger$6.debug("[Coordinator] Developer refresh already in progress; skipping");
|
|
21600
|
+
return;
|
|
21601
|
+
}
|
|
21602
|
+
this._developerRefreshInProgress = true;
|
|
21603
|
+
try {
|
|
21604
|
+
const newCredentials = await this.refreshCredential(provider);
|
|
21605
|
+
this.deps.store.write(newCredentials);
|
|
21606
|
+
this.deps.store.persist(newCredentials);
|
|
20717
21607
|
try {
|
|
20718
|
-
|
|
20719
|
-
|
|
20720
|
-
|
|
20721
|
-
this.deps.store.persist(newCredentials);
|
|
20722
|
-
logger$6.info("[Coordinator] Credentials refreshed successfully.");
|
|
20723
|
-
if (newCredentials.expiry_at) this.scheduleDeveloperRefresh(provider, newCredentials.expiry_at, 0);
|
|
20724
|
-
} catch (error) {
|
|
20725
|
-
const nextAttempt = attempt + 1;
|
|
20726
|
-
logger$6.error(`[Coordinator] Credential refresh failed (attempt ${nextAttempt}/${CREDENTIAL_REFRESH_MAX_RETRIES}):`, error);
|
|
20727
|
-
this.deps.notifier.onError(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
20728
|
-
if (nextAttempt < CREDENTIAL_REFRESH_MAX_RETRIES) this.scheduleDeveloperRefresh(provider, expiresAt, nextAttempt);
|
|
20729
|
-
else {
|
|
20730
|
-
logger$6.error("[Coordinator] Credential refresh exhausted all retries. Disconnecting.");
|
|
20731
|
-
this.deps.notifier.onError(new TokenRefreshError("Credential refresh failed after max retries"));
|
|
20732
|
-
this.deps.notifier.onRefreshExhausted();
|
|
20733
|
-
}
|
|
21608
|
+
await this.deps.notifier.onCredentialRefreshed(newCredentials);
|
|
21609
|
+
} catch (reauthError) {
|
|
21610
|
+
logger$6.warn("[Coordinator] onCredentialRefreshed rejected (non-fatal):", reauthError);
|
|
20734
21611
|
}
|
|
20735
|
-
|
|
21612
|
+
logger$6.info("[Coordinator] Credentials refreshed successfully.");
|
|
21613
|
+
if (newCredentials.expiry_at) this.scheduleDeveloperRefresh(provider, newCredentials.expiry_at, 0);
|
|
21614
|
+
} catch (error) {
|
|
21615
|
+
const nextAttempt = attempt + 1;
|
|
21616
|
+
logger$6.error(`[Coordinator] Credential refresh failed (attempt ${nextAttempt}/${CREDENTIAL_REFRESH_MAX_RETRIES}):`, error);
|
|
21617
|
+
this.deps.notifier.onError(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
21618
|
+
if (nextAttempt < CREDENTIAL_REFRESH_MAX_RETRIES) this.scheduleDeveloperRefresh(provider, expiresAt, nextAttempt);
|
|
21619
|
+
else {
|
|
21620
|
+
logger$6.error("[Coordinator] Credential refresh exhausted all retries. Disconnecting.");
|
|
21621
|
+
this.deps.notifier.onError(new TokenRefreshError("Credential refresh failed after max retries"));
|
|
21622
|
+
this.deps.notifier.onRefreshExhausted();
|
|
21623
|
+
}
|
|
21624
|
+
} finally {
|
|
21625
|
+
this._developerRefreshInProgress = false;
|
|
21626
|
+
}
|
|
21627
|
+
}
|
|
21628
|
+
/**
|
|
21629
|
+
* Force an immediate refresh when the current credential is already past its
|
|
21630
|
+
* scheduled refresh window. Called on resume from suspension, where
|
|
21631
|
+
* background-tab timer throttling can delay the armed refresh well past
|
|
21632
|
+
* expiry, leaving the live session stale.
|
|
21633
|
+
*
|
|
21634
|
+
* Routes to whichever mechanism is armed: the developer timer if armed,
|
|
21635
|
+
* otherwise the Client Bound SAT pipeline. A no-op when nothing is due.
|
|
21636
|
+
*/
|
|
21637
|
+
forceRefreshIfDue() {
|
|
21638
|
+
if (this._developerTimerId !== void 0 && this._activeProvider) {
|
|
21639
|
+
const expiry = this.deps.store.read().expiry_at;
|
|
21640
|
+
if (expiry !== void 0 && Date.now() >= expiry - CREDENTIAL_REFRESH_BUFFER_MS) {
|
|
21641
|
+
logger$6.debug("[Coordinator] Resume: credential past refresh window; forcing refresh");
|
|
21642
|
+
clearTimeout(this._developerTimerId);
|
|
21643
|
+
this._developerTimerId = void 0;
|
|
21644
|
+
this.executeDeveloperRefresh(this._activeProvider, expiry, 0);
|
|
21645
|
+
}
|
|
21646
|
+
return;
|
|
21647
|
+
}
|
|
21648
|
+
this._deviceTokenManager?.refreshNowIfDue();
|
|
21649
|
+
}
|
|
21650
|
+
/**
|
|
21651
|
+
* Sync the credential's expiry from the server-provided authorization (the
|
|
21652
|
+
* `signalwire.connect` result). SATs are opaque JWE, so
|
|
21653
|
+
* `fabric_subscriber.expires_at` is the authoritative expiry of the token
|
|
21654
|
+
* the session actually connected with — the provider-reported `expiry_at`
|
|
21655
|
+
* is only a hint (and may be wrong or absent). Corrects the stored
|
|
21656
|
+
* credential and re-arms the developer refresh timer against the real
|
|
21657
|
+
* deadline when the provider supports `refresh()`.
|
|
21658
|
+
*/
|
|
21659
|
+
syncExpiryFromAuthorization(authorization, provider) {
|
|
21660
|
+
const expiresAtSec = authorization?.fabric_subscriber?.expires_at;
|
|
21661
|
+
if (!expiresAtSec) return;
|
|
21662
|
+
const expiryAt = expiresAtSec * 1e3;
|
|
21663
|
+
const credential = this.deps.store.read();
|
|
21664
|
+
if (credential.expiry_at === expiryAt) return;
|
|
21665
|
+
logger$6.debug(`[Coordinator] Correcting credential expiry from server authorization: ${new Date(expiryAt).toISOString()}`);
|
|
21666
|
+
const updated = {
|
|
21667
|
+
...credential,
|
|
21668
|
+
expiry_at: expiryAt
|
|
21669
|
+
};
|
|
21670
|
+
this.deps.store.write(updated);
|
|
21671
|
+
this.deps.store.persist(updated);
|
|
21672
|
+
if (provider?.refresh) this.scheduleDeveloperRefresh(provider, expiryAt);
|
|
21673
|
+
}
|
|
21674
|
+
/**
|
|
21675
|
+
* Invoke `provider.refresh()` deduped against any concurrent developer
|
|
21676
|
+
* refresh. Concurrent callers — the scheduled tick, a resume-forced refresh,
|
|
21677
|
+
* and the orchestrator's -32003 recovery / reconnect re-mint — share one
|
|
21678
|
+
* in-flight promise, so a provider backed by one-time-use rotating refresh
|
|
21679
|
+
* tokens is never invoked twice in parallel.
|
|
21680
|
+
*
|
|
21681
|
+
* The caller owns applying the returned credential (store write, session
|
|
21682
|
+
* reauth, rescheduling); this method only serializes the network call.
|
|
21683
|
+
*/
|
|
21684
|
+
async refreshCredential(provider) {
|
|
21685
|
+
if (this._refreshInFlight) return this._refreshInFlight;
|
|
21686
|
+
if (!provider.refresh) throw new InvalidCredentialsError("Credential provider does not support refresh");
|
|
21687
|
+
const run = provider.refresh();
|
|
21688
|
+
this._refreshInFlight = run;
|
|
21689
|
+
const clear = () => {
|
|
21690
|
+
if (this._refreshInFlight === run) this._refreshInFlight = void 0;
|
|
21691
|
+
};
|
|
21692
|
+
run.then(clear, clear);
|
|
21693
|
+
return run;
|
|
20736
21694
|
}
|
|
20737
21695
|
/**
|
|
20738
21696
|
* Cancels any scheduled developer-provided refresh. Idempotent.
|
|
@@ -21516,6 +22474,13 @@ var TransportManager = class extends Destroyable {
|
|
|
21516
22474
|
//#region src/clients/SignalWire.ts
|
|
21517
22475
|
var import_cjs$1 = require_cjs();
|
|
21518
22476
|
const logger$1 = getLogger();
|
|
22477
|
+
/**
|
|
22478
|
+
* Storage key for the client-bound marker. The SAT and authorization_state are
|
|
22479
|
+
* both opaque to the SDK, so on a page reload the preflight recovery — which
|
|
22480
|
+
* runs before any session exists — has no other way to know the session was
|
|
22481
|
+
* client-bound. See {@link SignalWire.persistClientBoundMarker}.
|
|
22482
|
+
*/
|
|
22483
|
+
const CLIENT_BOUND_STORAGE_KEY = "sw:client_bound";
|
|
21519
22484
|
const buildOptionsFromDestination = (destination) => {
|
|
21520
22485
|
if (typeof destination === "string") {
|
|
21521
22486
|
const queryStartIndex = destination.indexOf("?");
|
|
@@ -21559,6 +22524,7 @@ var SignalWire = class extends Destroyable {
|
|
|
21559
22524
|
this.preferences = new ClientPreferences();
|
|
21560
22525
|
this._user$ = this.createBehaviorSubject(void 0);
|
|
21561
22526
|
this._directory$ = this.createBehaviorSubject(void 0);
|
|
22527
|
+
this._credentialRecovered = false;
|
|
21562
22528
|
this._isConnected$ = this.createBehaviorSubject(false);
|
|
21563
22529
|
this._isRegistered$ = this.createBehaviorSubject(false);
|
|
21564
22530
|
this._errors$ = this.createReplaySubject(1);
|
|
@@ -21597,6 +22563,16 @@ var SignalWire = class extends Destroyable {
|
|
|
21597
22563
|
});
|
|
21598
22564
|
}
|
|
21599
22565
|
/**
|
|
22566
|
+
* Build the refresh path's own HTTP controller, against whatever host is current.
|
|
22567
|
+
*
|
|
22568
|
+
* Called on first use rather than up front, so `apiHost` already reflects the
|
|
22569
|
+
* token's `ch` claim. Same credential source as the container's controller — only
|
|
22570
|
+
* the instance, and therefore its observable streams, is separate.
|
|
22571
|
+
*/
|
|
22572
|
+
createRefreshHttpController() {
|
|
22573
|
+
return new HTTPRequestController(this._deps.apiHost, () => this._deps.credential);
|
|
22574
|
+
}
|
|
22575
|
+
/**
|
|
21600
22576
|
* Initializes DPoP if not already set up. Returns the fingerprint on success.
|
|
21601
22577
|
*/
|
|
21602
22578
|
async initDPoP() {
|
|
@@ -21623,11 +22599,19 @@ var SignalWire = class extends Destroyable {
|
|
|
21623
22599
|
async resolveCredentials() {
|
|
21624
22600
|
const fingerprint = await this.initDPoP();
|
|
21625
22601
|
this._refreshCoordinator = new CredentialRefreshCoordinator(this._dpopManager, {
|
|
21626
|
-
http:
|
|
22602
|
+
http: () => {
|
|
22603
|
+
if (!this._refreshHttp || this._refreshHttpHost !== this._deps.apiHost) {
|
|
22604
|
+
this._refreshHttp?.destroy();
|
|
22605
|
+
this._refreshHttp = this.createRefreshHttpController();
|
|
22606
|
+
this._refreshHttpHost = this._deps.apiHost;
|
|
22607
|
+
}
|
|
22608
|
+
return this._refreshHttp;
|
|
22609
|
+
},
|
|
21627
22610
|
notifier: {
|
|
21628
22611
|
onError: (error) => this._errors$.next(error),
|
|
21629
22612
|
onWarning: (warning) => this._warnings$.next(warning),
|
|
21630
|
-
onRefreshExhausted: () => void this.disconnect()
|
|
22613
|
+
onRefreshExhausted: () => void this.disconnect(),
|
|
22614
|
+
onCredentialRefreshed: async (credential) => this.reauthenticateLiveSession(credential)
|
|
21631
22615
|
},
|
|
21632
22616
|
store: {
|
|
21633
22617
|
read: () => this._deps.credential,
|
|
@@ -21639,6 +22623,7 @@ var SignalWire = class extends Destroyable {
|
|
|
21639
22623
|
...this._deps.credential,
|
|
21640
22624
|
...partial
|
|
21641
22625
|
};
|
|
22626
|
+
this.persistCredential(this._deps.credential);
|
|
21642
22627
|
},
|
|
21643
22628
|
persist: (credential) => this.persistCredential(credential)
|
|
21644
22629
|
}
|
|
@@ -21684,20 +22669,181 @@ var SignalWire = class extends Destroyable {
|
|
|
21684
22669
|
}
|
|
21685
22670
|
this._deps.credential = _credentials;
|
|
21686
22671
|
this.persistCredential(_credentials);
|
|
21687
|
-
|
|
21688
|
-
|
|
22672
|
+
await this.reauthenticateLiveSession(_credentials);
|
|
22673
|
+
}
|
|
22674
|
+
/**
|
|
22675
|
+
* Reauthenticate the currently-open session with a freshly obtained
|
|
22676
|
+
* credential so the new token takes effect on the live socket immediately —
|
|
22677
|
+
* not just on the next reconnect. No-op when the session is not
|
|
22678
|
+
* connected/authenticated or the credential carries no token (e.g. an
|
|
22679
|
+
* authorization-state-only refresh). Non-fatal: reauth failures surface on
|
|
22680
|
+
* `errors$` without aborting the refresh that triggered this.
|
|
22681
|
+
*/
|
|
22682
|
+
async reauthenticateLiveSession(credential) {
|
|
22683
|
+
if (!this.isConnected || !this._clientSession.authenticated || !credential.token) return;
|
|
22684
|
+
try {
|
|
22685
|
+
await this._clientSession.reauthenticate(credential.token);
|
|
21689
22686
|
logger$1.info("[SignalWire] Session refreshed with new credentials.");
|
|
21690
22687
|
} catch (error) {
|
|
21691
22688
|
logger$1.error("[SignalWire] Failed to refresh session with new credentials:", error);
|
|
21692
22689
|
this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
21693
22690
|
}
|
|
21694
22691
|
}
|
|
22692
|
+
/**
|
|
22693
|
+
* Recover a session the server is refusing: mint a fresh credential,
|
|
22694
|
+
* reauthenticate the live session with it, and retry the operation.
|
|
22695
|
+
*
|
|
22696
|
+
* The connection is deliberately kept. A reload authenticates the new socket
|
|
22697
|
+
* against the persisted `authorization_state`, and that handshake is what
|
|
22698
|
+
* associates the socket with the previous session — the association reattach
|
|
22699
|
+
* depends on. `signalwire.reauthenticate` swaps the credential *on that same
|
|
22700
|
+
* session*, so recovery never touches the resume state. Discarding it would
|
|
22701
|
+
* heal the credential by destroying the very thing the caller is trying to
|
|
22702
|
+
* get back to.
|
|
22703
|
+
*
|
|
22704
|
+
* The operation is still the verdict, never the RPC. Reauthenticating with
|
|
22705
|
+
* the in-memory token is accepted by a resume even while requests stay
|
|
22706
|
+
* refused, because the persisted `authorization_state` short-circuits token
|
|
22707
|
+
* validation — and `signalwire.reauthenticate` with a *freshly minted* token
|
|
22708
|
+
* has also been observed accepted while `subscriber.online` keeps being
|
|
22709
|
+
* refused (staging run 33826974634). Both look like success and are not.
|
|
22710
|
+
*
|
|
22711
|
+
* @returns the operation's value, or the reason recovery could not deliver
|
|
22712
|
+
* one. `error` is undefined when there was no way to mint at all.
|
|
22713
|
+
*/
|
|
22714
|
+
async recoverAndRetry(operation) {
|
|
22715
|
+
if (!await this.remintAndReauthenticate()) return { ok: false };
|
|
22716
|
+
try {
|
|
22717
|
+
const value = await operation();
|
|
22718
|
+
this._credentialRecovered = true;
|
|
22719
|
+
return {
|
|
22720
|
+
ok: true,
|
|
22721
|
+
value
|
|
22722
|
+
};
|
|
22723
|
+
} catch (error) {
|
|
22724
|
+
logger$1.warn("[SignalWire] Reauthentication was accepted but the operation is still refused:", error);
|
|
22725
|
+
return {
|
|
22726
|
+
ok: false,
|
|
22727
|
+
error
|
|
22728
|
+
};
|
|
22729
|
+
}
|
|
22730
|
+
}
|
|
22731
|
+
/**
|
|
22732
|
+
* Re-mint a credential and adopt it only if the live session accepts it.
|
|
22733
|
+
*
|
|
22734
|
+
* The mechanism follows the binding: a client-bound session re-mints a bound
|
|
22735
|
+
* base SAT through `authenticate()` with the DPoP fingerprint, because the
|
|
22736
|
+
* developer refresh handler would hand back an unbound token and silently
|
|
22737
|
+
* degrade the session. An unbound session uses the refresh handler. Rotation
|
|
22738
|
+
* cost is not a reason to skip this — the only reason is having no mechanism.
|
|
22739
|
+
*
|
|
22740
|
+
* @returns whether the session is now running on a freshly accepted credential.
|
|
22741
|
+
*/
|
|
22742
|
+
async remintAndReauthenticate() {
|
|
22743
|
+
const provider = this._credentialProvider;
|
|
22744
|
+
if (!provider) return false;
|
|
22745
|
+
const { clientBound } = this._clientSession;
|
|
22746
|
+
if (!clientBound && !provider.refresh) {
|
|
22747
|
+
logger$1.debug("[SignalWire] [SW-NO-REFRESH-HANDLER] Unbound session with no refresh handler; cannot re-mint.");
|
|
22748
|
+
return false;
|
|
22749
|
+
}
|
|
22750
|
+
try {
|
|
22751
|
+
const newCredentials = clientBound ? await provider.authenticate(this._dpopManager?.initialized ? { fingerprint: this._dpopManager.fingerprint } : void 0) : await this.remintCredential(provider);
|
|
22752
|
+
if (!newCredentials.token) {
|
|
22753
|
+
logger$1.warn("[SignalWire] Re-minted credential has no token; keeping the current one.");
|
|
22754
|
+
return false;
|
|
22755
|
+
}
|
|
22756
|
+
await this._clientSession.reauthenticate(newCredentials.token);
|
|
22757
|
+
this._deps.credential = newCredentials;
|
|
22758
|
+
this.persistCredential(newCredentials);
|
|
22759
|
+
if (newCredentials.expiry_at && provider.refresh) this._refreshCoordinator?.scheduleDeveloperRefresh(provider, newCredentials.expiry_at);
|
|
22760
|
+
return true;
|
|
22761
|
+
} catch (error) {
|
|
22762
|
+
logger$1.warn("[SignalWire] Re-mint recovery failed:", error);
|
|
22763
|
+
return false;
|
|
22764
|
+
}
|
|
22765
|
+
}
|
|
22766
|
+
/**
|
|
22767
|
+
* Re-mint a credential via `provider.refresh()`, routed through the
|
|
22768
|
+
* coordinator's shared in-flight guard so concurrent re-mint paths (a
|
|
22769
|
+
* scheduled/resume refresh, -32003 recovery, and reconnect) never fire a
|
|
22770
|
+
* second `provider.refresh()` in parallel — which rotating one-time-use
|
|
22771
|
+
* refresh tokens reject. Falls back to a direct call only if the coordinator
|
|
22772
|
+
* has not been constructed yet.
|
|
22773
|
+
*/
|
|
22774
|
+
async remintCredential(provider) {
|
|
22775
|
+
if (this._refreshCoordinator) return this._refreshCoordinator.refreshCredential(provider);
|
|
22776
|
+
if (!provider.refresh) throw new InvalidCredentialsError("Credential provider does not support refresh");
|
|
22777
|
+
return provider.refresh();
|
|
22778
|
+
}
|
|
22779
|
+
/**
|
|
22780
|
+
* Re-mint credentials before a fresh (re)connect (`onBeforeReconnect` hook).
|
|
22781
|
+
* The session invokes this only when it is client-bound OR the in-memory
|
|
22782
|
+
* token is expired. The re-mint mechanism depends on the binding:
|
|
22783
|
+
* - Client-bound: `authenticate()` with the DPoP fingerprint to obtain a
|
|
22784
|
+
* fresh base SAT the upcoming reconnect can re-bind (the
|
|
22785
|
+
* DeviceTokenManager re-activates afterwards).
|
|
22786
|
+
* - Unbound: the developer's non-interactive `refresh()` handler.
|
|
22787
|
+
* `authenticate()` is deliberately NOT used here — it may be interactive
|
|
22788
|
+
* (a login prompt) and must not fire on a background reconnect.
|
|
22789
|
+
*
|
|
22790
|
+
* Rejects on failure so the session aborts the reconnect rather than
|
|
22791
|
+
* replaying a stale token.
|
|
22792
|
+
*/
|
|
22793
|
+
async refreshCredentialForReconnect() {
|
|
22794
|
+
if (!this._credentialProvider) return;
|
|
22795
|
+
try {
|
|
22796
|
+
let newCredentials;
|
|
22797
|
+
if (this._clientSession?.clientBound ?? await this.wasClientBound()) {
|
|
22798
|
+
logger$1.debug("[SignalWire] Re-minting client-bound base SAT before reconnect");
|
|
22799
|
+
newCredentials = await this._credentialProvider.authenticate(this._dpopManager?.initialized ? { fingerprint: this._dpopManager.fingerprint } : void 0);
|
|
22800
|
+
} else if (this._credentialProvider.refresh) {
|
|
22801
|
+
logger$1.debug("[SignalWire] Refreshing unbound credential before reconnect");
|
|
22802
|
+
newCredentials = await this.remintCredential(this._credentialProvider);
|
|
22803
|
+
} else {
|
|
22804
|
+
logger$1.warn("[SignalWire] [SW-NO-REFRESH-HANDLER] Token expired on reconnect but no refresh handler; reconnecting with the existing token.");
|
|
22805
|
+
return;
|
|
22806
|
+
}
|
|
22807
|
+
if (!newCredentials.token) {
|
|
22808
|
+
logger$1.warn("[SignalWire] Re-minted credential has no token; keeping the existing credential for reconnect.");
|
|
22809
|
+
return;
|
|
22810
|
+
}
|
|
22811
|
+
this._deps.credential = newCredentials;
|
|
22812
|
+
this.persistCredential(newCredentials);
|
|
22813
|
+
if (newCredentials.expiry_at && this._credentialProvider.refresh) this._refreshCoordinator?.scheduleDeveloperRefresh(this._credentialProvider, newCredentials.expiry_at);
|
|
22814
|
+
logger$1.debug("[SignalWire] Credential refreshed successfully for reconnect");
|
|
22815
|
+
} catch (error) {
|
|
22816
|
+
logger$1.error("[SignalWire] Failed to refresh credentials for reconnect:", error);
|
|
22817
|
+
this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
22818
|
+
throw error;
|
|
22819
|
+
}
|
|
22820
|
+
}
|
|
21695
22821
|
/** Persist credential to localStorage when persistSession is enabled. */
|
|
21696
22822
|
persistCredential(credential) {
|
|
21697
22823
|
if (!credential.token) return;
|
|
21698
22824
|
this._deps.storage.setItem("sw:cached_credential", credential);
|
|
21699
22825
|
if (this._deps.persistSession) this._deps.storage.setItem("sw:cached_credential", credential, "local");
|
|
21700
22826
|
}
|
|
22827
|
+
/**
|
|
22828
|
+
* Persist whether the session is client-bound, mirroring the credential's
|
|
22829
|
+
* storage scopes so it survives a reload. The preflight recovery reads it
|
|
22830
|
+
* before any session exists to decide whether to re-bind via `authenticate()`
|
|
22831
|
+
* or refresh an unbound token; the marker tracks the latest binding, so an
|
|
22832
|
+
* unbound reconnect clears a stale marker from an earlier client-bound login.
|
|
22833
|
+
*/
|
|
22834
|
+
persistClientBoundMarker(bound) {
|
|
22835
|
+
const scopes = this._deps.persistSession ? ["session", "local"] : ["session"];
|
|
22836
|
+
for (const scope of scopes) if (bound) this._deps.storage.setItem(CLIENT_BOUND_STORAGE_KEY, true, scope);
|
|
22837
|
+
else this._deps.storage.removeItem(CLIENT_BOUND_STORAGE_KEY, scope);
|
|
22838
|
+
}
|
|
22839
|
+
/** Read the persisted client-bound marker (see {@link persistClientBoundMarker}). */
|
|
22840
|
+
async wasClientBound() {
|
|
22841
|
+
const scopes = this._deps.persistSession ? ["local", "session"] : ["session"];
|
|
22842
|
+
for (const scope of scopes) try {
|
|
22843
|
+
if (await this._deps.storage.getItem(CLIENT_BOUND_STORAGE_KEY, scope)) return true;
|
|
22844
|
+
} catch {}
|
|
22845
|
+
return false;
|
|
22846
|
+
}
|
|
21701
22847
|
async init() {
|
|
21702
22848
|
this._user$.next(new User(this._deps.http));
|
|
21703
22849
|
if (!this._options.skipConnection) await this.connect();
|
|
@@ -21721,6 +22867,42 @@ var SignalWire = class extends Destroyable {
|
|
|
21721
22867
|
} catch (error) {
|
|
21722
22868
|
logger$1.error("[SignalWire] Failed to reattach calls:", error);
|
|
21723
22869
|
this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
22870
|
+
} finally {
|
|
22871
|
+
this._credentialRecovered = false;
|
|
22872
|
+
}
|
|
22873
|
+
}
|
|
22874
|
+
/**
|
|
22875
|
+
* Fetch the authenticated user profile, recovering a stale credential.
|
|
22876
|
+
*
|
|
22877
|
+
* On a reload the persisted credential can be expired. Unlike the WS resume —
|
|
22878
|
+
* which the server accepts against the persisted `authorization_state` even
|
|
22879
|
+
* with an expired token — this REST preflight has no such short-circuit and is
|
|
22880
|
+
* refused (401). There is no session yet to reauthenticate, so recovery
|
|
22881
|
+
* re-mints the credential through the provider ({@link refreshCredentialForReconnect})
|
|
22882
|
+
* and retries with a FRESH {@link User}: Fetchable memoizes its result
|
|
22883
|
+
* (shareReplay), so reusing the instance would replay the 401 instead of
|
|
22884
|
+
* re-fetching with the new token. Without the user id the transport/session —
|
|
22885
|
+
* and the reattach a reload is trying to preserve — cannot even be addressed.
|
|
22886
|
+
*/
|
|
22887
|
+
async fetchUserOrRecover() {
|
|
22888
|
+
const fetchUser = async (user$1) => {
|
|
22889
|
+
if (!await (0, import_cjs$1.firstValueFrom)(user$1.fetched$)) throw new UnexpectedError("Failed to fetch user information - fetched$ emitted false");
|
|
22890
|
+
this._deps.user = user$1;
|
|
22891
|
+
};
|
|
22892
|
+
const user = this._user$.value;
|
|
22893
|
+
if (!user) throw new UnexpectedError("User not initialized before connect");
|
|
22894
|
+
try {
|
|
22895
|
+
await fetchUser(user);
|
|
22896
|
+
} catch (firstError) {
|
|
22897
|
+
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.`);
|
|
22898
|
+
try {
|
|
22899
|
+
await this.refreshCredentialForReconnect();
|
|
22900
|
+
const refetched = new User(this._deps.http);
|
|
22901
|
+
await fetchUser(refetched);
|
|
22902
|
+
this._user$.next(refetched);
|
|
22903
|
+
} catch (retryError) {
|
|
22904
|
+
throw new UnexpectedError("Error fetching user information", { cause: retryError });
|
|
22905
|
+
}
|
|
21724
22906
|
}
|
|
21725
22907
|
}
|
|
21726
22908
|
/**
|
|
@@ -21762,40 +22944,23 @@ var SignalWire = class extends Destroyable {
|
|
|
21762
22944
|
*/
|
|
21763
22945
|
async connect() {
|
|
21764
22946
|
await this.teardownTransportAndSession();
|
|
21765
|
-
|
|
21766
|
-
const user = this._user$.value;
|
|
21767
|
-
if (!user) throw new UnexpectedError("User not initialized before connect");
|
|
21768
|
-
if (!await (0, import_cjs$1.firstValueFrom)(user.fetched$)) throw new UnexpectedError("Failed to fetch user information - fetched$ emitted false");
|
|
21769
|
-
this._deps.user = user;
|
|
21770
|
-
} catch (error) {
|
|
21771
|
-
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.`);
|
|
21772
|
-
throw new UnexpectedError("Error fetching user information", { cause: error });
|
|
21773
|
-
}
|
|
22947
|
+
await this.fetchUserOrRecover();
|
|
21774
22948
|
const errorHandler = (error) => {
|
|
21775
22949
|
this._errors$.next(error);
|
|
21776
22950
|
};
|
|
21777
22951
|
this._transport = new TransportManager(this._deps.storage, this._deps.protocolKey, this._deps.WebSocket, PreferencesContainer.instance.relayHost ?? this._deps.relayHost, errorHandler);
|
|
21778
|
-
this._attachManager = new AttachManager(this._deps.storage, this._deps.deviceController, PreferencesContainer.instance.reconnectCallsTimeout, this._deps.attachedCallsKey);
|
|
22952
|
+
this._attachManager = new AttachManager(this._deps.storage, this._deps.deviceController, PreferencesContainer.instance.reconnectCallsTimeout, this._deps.attachedCallsKey, () => this._credentialRecovered);
|
|
21779
22953
|
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$);
|
|
21780
22954
|
this._publicSession = new ClientSessionWrapper(this._clientSession);
|
|
21781
|
-
this._clientSession.
|
|
21782
|
-
|
|
21783
|
-
try {
|
|
21784
|
-
const fingerprint = this._dpopManager?.initialized ? this._dpopManager.fingerprint : void 0;
|
|
21785
|
-
logger$1.debug("[SignalWire] Credential expired, refreshing before reconnect");
|
|
21786
|
-
const newCredentials = await this._credentialProvider.authenticate(fingerprint ? { fingerprint } : void 0);
|
|
21787
|
-
this._deps.credential = newCredentials;
|
|
21788
|
-
if (newCredentials.expiry_at && this._credentialProvider.refresh) this._refreshCoordinator?.scheduleDeveloperRefresh(this._credentialProvider, newCredentials.expiry_at);
|
|
21789
|
-
logger$1.debug("[SignalWire] Credential refreshed successfully for reconnect");
|
|
21790
|
-
} catch (error) {
|
|
21791
|
-
logger$1.error("[SignalWire] Failed to refresh credentials for reconnect:", error);
|
|
21792
|
-
this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
21793
|
-
throw error;
|
|
21794
|
-
}
|
|
21795
|
-
};
|
|
22955
|
+
this._clientSession.callControl = this._options.callControl ?? "routed";
|
|
22956
|
+
this._clientSession.onBeforeReconnect = async () => this.refreshCredentialForReconnect();
|
|
21796
22957
|
this.subscribeTo(this._clientSession.errors$, (error) => {
|
|
21797
22958
|
this._errors$.next(error);
|
|
21798
22959
|
});
|
|
22960
|
+
this.subscribeTo(this._clientSession.authorization$, (authorization) => {
|
|
22961
|
+
this._refreshCoordinator?.syncExpiryFromAuthorization(authorization, this._credentialProvider);
|
|
22962
|
+
this.persistClientBoundMarker(Boolean(authorization?.cnf?.jkt));
|
|
22963
|
+
});
|
|
21799
22964
|
await this._clientSession.connect();
|
|
21800
22965
|
await this._refreshCoordinator?.activate(this._deps.user, this._clientSession);
|
|
21801
22966
|
this.subscribeTo(this._clientSession.authenticated$.pipe((0, import_cjs$1.skip)(1), (0, import_cjs$1.filter)(Boolean)), async () => {
|
|
@@ -21958,6 +23123,13 @@ var SignalWire = class extends Destroyable {
|
|
|
21958
23123
|
}
|
|
21959
23124
|
try {
|
|
21960
23125
|
this._visibilityController = new VisibilityController();
|
|
23126
|
+
this.subscribeTo(this._visibilityController.visibilityChange$.pipe((0, import_cjs$1.filter)((event) => event.to === "visible")), () => {
|
|
23127
|
+
try {
|
|
23128
|
+
this._refreshCoordinator?.forceRefreshIfDue();
|
|
23129
|
+
} catch (error) {
|
|
23130
|
+
logger$1.warn("[SignalWire] Resume credential revalidation failed (non-fatal):", error);
|
|
23131
|
+
}
|
|
23132
|
+
});
|
|
21961
23133
|
this.subscribeTo(this._visibilityController.visibilityChange$.pipe((0, import_cjs$1.filter)((event) => event.to === "visible" && PreferencesContainer.instance.refreshDevicesOnVisible)), () => {
|
|
21962
23134
|
logger$1.debug("[SignalWire] Page visible, re-enumerating devices");
|
|
21963
23135
|
try {
|
|
@@ -21969,7 +23141,7 @@ var SignalWire = class extends Destroyable {
|
|
|
21969
23141
|
logger$1.warn("[SignalWire] Failed to initialize VisibilityController:", error);
|
|
21970
23142
|
}
|
|
21971
23143
|
try {
|
|
21972
|
-
this._diagnosticsCollector = new DiagnosticsCollector({ sdkVersion: "4.0.0-rc.
|
|
23144
|
+
this._diagnosticsCollector = new DiagnosticsCollector({ sdkVersion: "4.0.0-rc.3" });
|
|
21973
23145
|
} catch (error) {
|
|
21974
23146
|
logger$1.warn("[SignalWire] Failed to initialize DiagnosticsCollector:", error);
|
|
21975
23147
|
}
|
|
@@ -22040,21 +23212,22 @@ var SignalWire = class extends Destroyable {
|
|
|
22040
23212
|
this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
22041
23213
|
throw error;
|
|
22042
23214
|
}
|
|
22043
|
-
logger$1.debug("[SignalWire] Failed to register user,
|
|
22044
|
-
|
|
22045
|
-
await this._clientSession.reauthenticate(this._deps.credential.token);
|
|
22046
|
-
logger$1.debug("[SignalWire] Reauthentication successful, retrying register()");
|
|
23215
|
+
logger$1.debug("[SignalWire] Failed to register user, attempting credential recovery...");
|
|
23216
|
+
const outcome = await this.recoverAndRetry(async () => {
|
|
22047
23217
|
await this._transport.execute(RPCExecute({
|
|
22048
23218
|
method: "subscriber.online",
|
|
22049
23219
|
params: {}
|
|
22050
23220
|
}));
|
|
23221
|
+
});
|
|
23222
|
+
if (outcome.ok) {
|
|
23223
|
+
logger$1.debug("[SignalWire] Recovery restored registration");
|
|
22051
23224
|
this._isRegistered$.next(true);
|
|
22052
|
-
|
|
22053
|
-
logger$1.error("[SignalWire] Reauthentication failed during register():", reauthError);
|
|
22054
|
-
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 }) });
|
|
22055
|
-
this._errors$.next(registerError);
|
|
22056
|
-
throw registerError;
|
|
23225
|
+
return;
|
|
22057
23226
|
}
|
|
23227
|
+
const failureCause = outcome.error ?? error;
|
|
23228
|
+
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 }) });
|
|
23229
|
+
this._errors$.next(registerError);
|
|
23230
|
+
throw registerError;
|
|
22058
23231
|
}
|
|
22059
23232
|
}
|
|
22060
23233
|
/**
|
|
@@ -22085,6 +23258,11 @@ var SignalWire = class extends Destroyable {
|
|
|
22085
23258
|
* Returns a {@link Call} in `'ringing'` state. Subscribe to {@link Call.status$}
|
|
22086
23259
|
* to track progression through `'connected'` → `'disconnected'`.
|
|
22087
23260
|
*
|
|
23261
|
+
* Local media acquisition is deliberately unbounded: an unanswered permission
|
|
23262
|
+
* prompt leaves this promise pending indefinitely, so apply your own bound if
|
|
23263
|
+
* your UI needs one. The 12 s signaling budget starts only once acquisition
|
|
23264
|
+
* settles.
|
|
23265
|
+
*
|
|
22088
23266
|
* @param destination - Address URI string (e.g. `'/public/my-room'`) or {@link Address} instance.
|
|
22089
23267
|
* @param options - Media and dial options (audio/video, device constraints). Overrides defaults.
|
|
22090
23268
|
* @returns The created {@link Call} instance.
|
|
@@ -22107,7 +23285,18 @@ var SignalWire = class extends Destroyable {
|
|
|
22107
23285
|
};
|
|
22108
23286
|
await this.waitAuthentication();
|
|
22109
23287
|
logger$1.debug("[SignalWire] Dialing with options:", computed_options);
|
|
22110
|
-
|
|
23288
|
+
try {
|
|
23289
|
+
return await this._clientSession.createOutboundCall(destination, computed_options);
|
|
23290
|
+
} catch (error) {
|
|
23291
|
+
if (!isRecoverableAuthError(error)) throw error;
|
|
23292
|
+
logger$1.debug("[SignalWire] Dial hit a recoverable auth error; recovering the session and retrying");
|
|
23293
|
+
const outcome = await this.recoverAndRetry(async () => {
|
|
23294
|
+
await this.waitAuthentication();
|
|
23295
|
+
return this._clientSession.createOutboundCall(destination, computed_options);
|
|
23296
|
+
});
|
|
23297
|
+
if (outcome.ok) return outcome.value;
|
|
23298
|
+
throw outcome.error ?? error;
|
|
23299
|
+
}
|
|
22111
23300
|
}
|
|
22112
23301
|
/**
|
|
22113
23302
|
* Runs a multi-phase connectivity test against the given destination.
|
|
@@ -22388,10 +23577,13 @@ var SignalWire = class extends Destroyable {
|
|
|
22388
23577
|
destroy() {
|
|
22389
23578
|
this._refreshCoordinator?.destroy();
|
|
22390
23579
|
this._refreshCoordinator = void 0;
|
|
23580
|
+
this._refreshHttp?.destroy();
|
|
23581
|
+
this._refreshHttp = void 0;
|
|
22391
23582
|
this._dpopManager?.destroy();
|
|
22392
|
-
this._clientSession
|
|
22393
|
-
|
|
22394
|
-
this.
|
|
23583
|
+
const session = this._clientSession;
|
|
23584
|
+
session?.teardownSessionState();
|
|
23585
|
+
this._transport?.destroy();
|
|
23586
|
+
session?.destroy();
|
|
22395
23587
|
try {
|
|
22396
23588
|
this._networkMonitor?.destroy();
|
|
22397
23589
|
} catch {}
|
|
@@ -22504,7 +23696,7 @@ var StaticCredentialProvider = class {
|
|
|
22504
23696
|
/**
|
|
22505
23697
|
* Library version from package.json, injected at build time.
|
|
22506
23698
|
*/
|
|
22507
|
-
const version = "4.0.0-rc.
|
|
23699
|
+
const version = "4.0.0-rc.3";
|
|
22508
23700
|
/**
|
|
22509
23701
|
* Flag indicating the library has been loaded and is ready to use.
|
|
22510
23702
|
* For UMD builds: `window.SignalWire.ready`
|
|
@@ -22526,7 +23718,7 @@ const ready = true;
|
|
|
22526
23718
|
*/
|
|
22527
23719
|
const emitReadyEvent = () => {
|
|
22528
23720
|
if (typeof window !== "undefined") {
|
|
22529
|
-
const event = new CustomEvent("signalwire:js:ready", { detail: { version: "4.0.0-rc.
|
|
23721
|
+
const event = new CustomEvent("signalwire:js:ready", { detail: { version: "4.0.0-rc.3" } });
|
|
22530
23722
|
window.dispatchEvent(event);
|
|
22531
23723
|
}
|
|
22532
23724
|
};
|
|
@@ -22550,5 +23742,5 @@ emitReadyEvent();
|
|
|
22550
23742
|
if (typeof process === "undefined") globalThis.process = { env: { NODE_ENV: "production" } };
|
|
22551
23743
|
|
|
22552
23744
|
//#endregion
|
|
22553
|
-
export { Address, CallCreateError, ClientPreferences, CollectionFetchError, DPoPInitError, DeviceTokenError, EmbedTokenCredentialProvider, InvalidCredentialsError, MediaAccessError, MediaTrackError, MessageParseError, OverconstrainedFallbackError, Participant, PreflightError, RecoveryError, SelfCapabilities, SelfParticipant, SignalWire, StaticCredentialProvider, TokenRefreshError, UnexpectedError, User, VertoPongError, WebRTCCall, embeddableCall, getLogger, isSelfParticipant, ready, setDebugOptions, setLogLevel, setLogger, version };
|
|
23745
|
+
export { Address, AuxiliaryLegCancelledError, AuxiliaryLegTimeoutError, CallCreateError, CallNotReadyError, ClientPreferences, CollectionFetchError, DPoPInitError, DeviceTokenError, EmbedTokenCredentialProvider, InvalidCredentialsError, MediaAccessError, MediaTrackError, MessageParseError, OverconstrainedFallbackError, Participant, ParticipantNotReadyError, PreflightError, RecoveryError, ScreenShareAlreadyActiveError, SelfCapabilities, SelfParticipant, SignalWire, StaticCredentialProvider, TokenRefreshError, UnexpectedError, User, VertoPongError, WebRTCCall, embeddableCall, getLogger, isSelfParticipant, ready, setDebugOptions, setLogLevel, setLogger, version };
|
|
22554
23746
|
//# sourceMappingURL=browser.mjs.map
|