@signalwire/js 4.0.0-rc.2 → 4.0.0-rc.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser.mjs +1733 -440
- package/dist/browser.mjs.map +1 -1
- package/dist/browser.umd.js +1737 -439
- package/dist/browser.umd.js.map +1 -1
- package/dist/index.cjs +1627 -430
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2327 -1835
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +2327 -1835
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1623 -431
- package/dist/index.mjs.map +1 -1
- package/dist/operators/index.cjs +1 -1
- package/dist/operators/index.mjs +1 -1
- package/dist/{operators-BlUtq-t0.mjs → operators-CTNnvDcG.mjs} +103 -2
- package/dist/operators-CTNnvDcG.mjs.map +1 -0
- package/dist/{operators-B8ipd4Xl.cjs → operators-IoitK-As.cjs} +168 -1
- package/dist/operators-IoitK-As.cjs.map +1 -0
- package/package.json +1 -1
- package/dist/operators-B8ipd4Xl.cjs.map +0 -1
- package/dist/operators-BlUtq-t0.mjs.map +0 -1
package/dist/browser.umd.js
CHANGED
|
@@ -4192,7 +4192,7 @@ var require_merge$1 = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
4192
4192
|
var empty_1$7 = require_empty();
|
|
4193
4193
|
var args_1$8 = require_args();
|
|
4194
4194
|
var from_1$4 = require_from();
|
|
4195
|
-
function merge$
|
|
4195
|
+
function merge$7() {
|
|
4196
4196
|
var args = [];
|
|
4197
4197
|
for (var _i = 0; _i < arguments.length; _i++) args[_i] = arguments[_i];
|
|
4198
4198
|
var scheduler = args_1$8.popScheduler(args);
|
|
@@ -4200,7 +4200,7 @@ var require_merge$1 = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
4200
4200
|
var sources = args;
|
|
4201
4201
|
return !sources.length ? empty_1$7.EMPTY : sources.length === 1 ? innerFrom_1$31.innerFrom(sources[0]) : mergeAll_1$3.mergeAll(concurrent)(from_1$4.from(sources, scheduler));
|
|
4202
4202
|
}
|
|
4203
|
-
exports.merge = merge$
|
|
4203
|
+
exports.merge = merge$7;
|
|
4204
4204
|
}));
|
|
4205
4205
|
|
|
4206
4206
|
//#endregion
|
|
@@ -6082,7 +6082,7 @@ var require_merge = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
6082
6082
|
var mergeAll_1$2 = require_mergeAll();
|
|
6083
6083
|
var args_1$3 = require_args();
|
|
6084
6084
|
var from_1$1 = require_from();
|
|
6085
|
-
function merge$
|
|
6085
|
+
function merge$6() {
|
|
6086
6086
|
var args = [];
|
|
6087
6087
|
for (var _i = 0; _i < arguments.length; _i++) args[_i] = arguments[_i];
|
|
6088
6088
|
var scheduler = args_1$3.popScheduler(args);
|
|
@@ -6091,7 +6091,7 @@ var require_merge = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
6091
6091
|
mergeAll_1$2.mergeAll(concurrent)(from_1$1.from(__spreadArray$8([source], __read$8(args)), scheduler)).subscribe(subscriber);
|
|
6092
6092
|
});
|
|
6093
6093
|
}
|
|
6094
|
-
exports.merge = merge$
|
|
6094
|
+
exports.merge = merge$6;
|
|
6095
6095
|
}));
|
|
6096
6096
|
|
|
6097
6097
|
//#endregion
|
|
@@ -6829,12 +6829,12 @@ var require_skip = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
6829
6829
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6830
6830
|
exports.skip = void 0;
|
|
6831
6831
|
var filter_1$3 = require_filter();
|
|
6832
|
-
function skip$
|
|
6832
|
+
function skip$5(count$1) {
|
|
6833
6833
|
return filter_1$3.filter(function(_, index) {
|
|
6834
6834
|
return count$1 <= index;
|
|
6835
6835
|
});
|
|
6836
6836
|
}
|
|
6837
|
-
exports.skip = skip$
|
|
6837
|
+
exports.skip = skip$5;
|
|
6838
6838
|
}));
|
|
6839
6839
|
|
|
6840
6840
|
//#endregion
|
|
@@ -6938,7 +6938,7 @@ var require_switchMap = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
6938
6938
|
var innerFrom_1$6 = require_innerFrom();
|
|
6939
6939
|
var lift_1$13 = require_lift();
|
|
6940
6940
|
var OperatorSubscriber_1$11 = require_OperatorSubscriber();
|
|
6941
|
-
function switchMap$
|
|
6941
|
+
function switchMap$8(project, resultSelector) {
|
|
6942
6942
|
return lift_1$13.operate(function(source, subscriber) {
|
|
6943
6943
|
var innerSubscriber = null;
|
|
6944
6944
|
var index = 0;
|
|
@@ -6962,7 +6962,7 @@ var require_switchMap = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
6962
6962
|
}));
|
|
6963
6963
|
});
|
|
6964
6964
|
}
|
|
6965
|
-
exports.switchMap = switchMap$
|
|
6965
|
+
exports.switchMap = switchMap$8;
|
|
6966
6966
|
}));
|
|
6967
6967
|
|
|
6968
6968
|
//#endregion
|
|
@@ -9028,6 +9028,37 @@ const DEFAULT_ICE_CANDIDATE_TIMEOUT_MS = 600;
|
|
|
9028
9028
|
const DEFAULT_ICE_GATHERING_TIMEOUT_MS = 6e3;
|
|
9029
9029
|
const DEFAULT_RECONNECT_CALLS_TIMEOUT_MS = 300 * 1e3;
|
|
9030
9030
|
const DEFAULT_CONNECTION_TIMEOUT_MS = 1e4;
|
|
9031
|
+
/**
|
|
9032
|
+
* The server sends `signalwire.ping` every 10s. A socket that stays silent
|
|
9033
|
+
* longer than this is half-open: the server has dropped the session but the
|
|
9034
|
+
* browser never fired `close`.
|
|
9035
|
+
*/
|
|
9036
|
+
const SERVER_PING_TIMEOUT_MS = 15e3;
|
|
9037
|
+
/**
|
|
9038
|
+
* How long a `signalwire.ping` the client sends after the server's pings
|
|
9039
|
+
* stopped may take to be answered. The server defers its own pings while its
|
|
9040
|
+
* threadpool is congested, so silence alone does not mean the socket is dead.
|
|
9041
|
+
*/
|
|
9042
|
+
const SERVER_PING_PROBE_TIMEOUT_MS = 1e4;
|
|
9043
|
+
/**
|
|
9044
|
+
* How long call setup may wait for the network, measured from local media
|
|
9045
|
+
* settling until the member id arrives.
|
|
9046
|
+
*
|
|
9047
|
+
* Local media acquisition is deliberately NOT inside this: a permission prompt
|
|
9048
|
+
* or a device picker is human time, and a human may take as long as they like
|
|
9049
|
+
* without spending the server's budget. The clock starts when acquisition ends.
|
|
9050
|
+
*
|
|
9051
|
+
* Must exceed `iceGatheringTimeout + the RPC timeout`, which both run inside it.
|
|
9052
|
+
*/
|
|
9053
|
+
const DEFAULT_CALL_SIGNALING_TIMEOUT_MS = 12e3;
|
|
9054
|
+
/**
|
|
9055
|
+
* How long an auxiliary leg (screen share, additional device) may take to
|
|
9056
|
+
* connect once its media is in hand.
|
|
9057
|
+
*
|
|
9058
|
+
* One bound for both: the picker is human time and sits outside it. What remains
|
|
9059
|
+
* — offer, ICE, invite, answer, DTLS — is the same work for either leg kind.
|
|
9060
|
+
*/
|
|
9061
|
+
const DEFAULT_AUX_LEG_CONNECT_TIMEOUT_MS = 15e3;
|
|
9031
9062
|
const DEFAULT_RECONNECT_DELAY_MIN_MS = 100;
|
|
9032
9063
|
const DEFAULT_RECONNECT_DELAY_MAX_MS = 3e3;
|
|
9033
9064
|
const DEFAULT_DEVICE_DEBOUNCE_TIME_MS = 1500;
|
|
@@ -9055,6 +9086,14 @@ const CREDENTIAL_REFRESH_MAX_DELAY_MS = 3e4;
|
|
|
9055
9086
|
/** Buffer in milliseconds before token expiry to trigger refresh. */
|
|
9056
9087
|
const CREDENTIAL_REFRESH_BUFFER_MS = 5e3;
|
|
9057
9088
|
/**
|
|
9089
|
+
* Clock-skew allowance (ms) for treating an in-memory credential as expired
|
|
9090
|
+
* when deciding whether a fresh (re)connect must re-mint the token before
|
|
9091
|
+
* authenticating. A token within this window of its `expiry_at` is treated as
|
|
9092
|
+
* stale so the reconnect re-mints via the credential provider instead of
|
|
9093
|
+
* replaying a dead token (which the server rejects with -32003).
|
|
9094
|
+
*/
|
|
9095
|
+
const CREDENTIAL_EXPIRY_SKEW_MS = 3e4;
|
|
9096
|
+
/**
|
|
9058
9097
|
* Maximum time the coordinator will wait for `DeviceTokenManager.activate()`
|
|
9059
9098
|
* to resolve before treating the activation as failed and falling back to
|
|
9060
9099
|
* the developer-provided refresh path. Prevents a wedged HTTP layer from
|
|
@@ -9073,6 +9112,8 @@ const MEDIA_ACCESS_DENIAL_NAMES = [
|
|
|
9073
9112
|
"SecurityError",
|
|
9074
9113
|
"PermissionDeniedError"
|
|
9075
9114
|
];
|
|
9115
|
+
/** Error names browsers use when the capture hardware is already held exclusively. */
|
|
9116
|
+
const MEDIA_DEVICE_IN_USE_NAMES = ["NotReadableError", "TrackStartError"];
|
|
9076
9117
|
/** Default polling interval for RTCPeerConnection.getStats() in milliseconds. */
|
|
9077
9118
|
const DEFAULT_STATS_POLLING_INTERVAL_MS = 1e3;
|
|
9078
9119
|
/** Number of initial samples used to build a baseline for spike detection. */
|
|
@@ -9287,6 +9328,20 @@ var CallCreateError = class extends Error {
|
|
|
9287
9328
|
this.name = "CallCreateError";
|
|
9288
9329
|
}
|
|
9289
9330
|
};
|
|
9331
|
+
var CallNotReadyError = class extends Error {
|
|
9332
|
+
constructor(callId, options) {
|
|
9333
|
+
super(`Call "${callId}" has no self member context yet: selfId/nodeId have not been received from the server`, options);
|
|
9334
|
+
this.callId = callId;
|
|
9335
|
+
this.name = "CallNotReadyError";
|
|
9336
|
+
}
|
|
9337
|
+
};
|
|
9338
|
+
var ParticipantNotReadyError = class extends Error {
|
|
9339
|
+
constructor(memberId, options) {
|
|
9340
|
+
super(`Participant "${memberId}" has no call context yet: its member state (call_id/node_id) has not been received from the server`, options);
|
|
9341
|
+
this.memberId = memberId;
|
|
9342
|
+
this.name = "ParticipantNotReadyError";
|
|
9343
|
+
}
|
|
9344
|
+
};
|
|
9290
9345
|
var JSONRPCError = class extends Error {
|
|
9291
9346
|
constructor(code, message, data, options, requestId) {
|
|
9292
9347
|
super(message, options);
|
|
@@ -9358,6 +9413,32 @@ var CollectionFetchError = class extends Error {
|
|
|
9358
9413
|
this.name = "CollectionFetchError";
|
|
9359
9414
|
}
|
|
9360
9415
|
};
|
|
9416
|
+
/**
|
|
9417
|
+
* An auxiliary leg did not connect within its budget. Typed rather than a bare
|
|
9418
|
+
* RxJS `TimeoutError` so the leg and cause survive.
|
|
9419
|
+
*/
|
|
9420
|
+
var AuxiliaryLegTimeoutError = class extends Error {
|
|
9421
|
+
constructor(leg, originalError) {
|
|
9422
|
+
super(`Timed out waiting for the ${leg} connection to be established`, { cause: originalError });
|
|
9423
|
+
this.leg = leg;
|
|
9424
|
+
this.originalError = originalError;
|
|
9425
|
+
this.name = "AuxiliaryLegTimeoutError";
|
|
9426
|
+
}
|
|
9427
|
+
};
|
|
9428
|
+
/**
|
|
9429
|
+
* An auxiliary leg was removed before it finished connecting.
|
|
9430
|
+
*
|
|
9431
|
+
* Typed rather than a bare resolve so a caller awaiting the start can tell a
|
|
9432
|
+
* cancel apart from a share that actually came up — the public methods return
|
|
9433
|
+
* `void`, so the promise is the only signal they have.
|
|
9434
|
+
*/
|
|
9435
|
+
var AuxiliaryLegCancelledError = class extends Error {
|
|
9436
|
+
constructor(leg) {
|
|
9437
|
+
super(`The ${leg} leg was removed before it finished connecting`);
|
|
9438
|
+
this.leg = leg;
|
|
9439
|
+
this.name = "AuxiliaryLegCancelledError";
|
|
9440
|
+
}
|
|
9441
|
+
};
|
|
9361
9442
|
var MediaTrackError = class extends Error {
|
|
9362
9443
|
constructor(operation, kind, originalError) {
|
|
9363
9444
|
super(`Media track ${operation} failed for ${kind}`, { cause: originalError instanceof Error ? originalError : void 0 });
|
|
@@ -9371,6 +9452,10 @@ var MediaTrackError = class extends Error {
|
|
|
9371
9452
|
function isMediaAccessDenial(originalError) {
|
|
9372
9453
|
return originalError instanceof Error && MEDIA_ACCESS_DENIAL_NAMES.includes(originalError.name);
|
|
9373
9454
|
}
|
|
9455
|
+
/** True when a `getUserMedia` rejection means the hardware is already held exclusively. */
|
|
9456
|
+
function isMediaDeviceInUse(originalError) {
|
|
9457
|
+
return originalError instanceof Error && MEDIA_DEVICE_IN_USE_NAMES.includes(originalError.name);
|
|
9458
|
+
}
|
|
9374
9459
|
/**
|
|
9375
9460
|
* Failure to acquire local media (camera, microphone, or screen capture)
|
|
9376
9461
|
* via `getUserMedia`/`getDisplayMedia`.
|
|
@@ -9394,6 +9479,22 @@ var MediaAccessError = class extends Error {
|
|
|
9394
9479
|
return isMediaAccessDenial(this.originalError);
|
|
9395
9480
|
}
|
|
9396
9481
|
};
|
|
9482
|
+
/**
|
|
9483
|
+
* Thrown by `startScreenShare()` when the call is already sharing a screen.
|
|
9484
|
+
*
|
|
9485
|
+
* A call carries at most one screen share. Accepting a second one would
|
|
9486
|
+
* overwrite the only reference the SDK holds to the first, leaving it
|
|
9487
|
+
* capturing and sending with no way to stop it — so the second request is
|
|
9488
|
+
* rejected and the live share is left untouched. Call `stopScreenShare()`
|
|
9489
|
+
* first to replace it.
|
|
9490
|
+
*/
|
|
9491
|
+
var ScreenShareAlreadyActiveError = class extends Error {
|
|
9492
|
+
constructor(screenShareId, options) {
|
|
9493
|
+
super(`A screen share is already active on this call (${screenShareId}). Call stopScreenShare() before starting another one.`, options);
|
|
9494
|
+
this.screenShareId = screenShareId;
|
|
9495
|
+
this.name = "ScreenShareAlreadyActiveError";
|
|
9496
|
+
}
|
|
9497
|
+
};
|
|
9397
9498
|
var DPoPInitError = class extends Error {
|
|
9398
9499
|
constructor(originalError, message = "Failed to initialize DPoP key pair") {
|
|
9399
9500
|
super(message, { cause: originalError instanceof Error ? originalError : void 0 });
|
|
@@ -9634,9 +9735,9 @@ var require_loglevel = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
9634
9735
|
defaultLogger$1 = new Logger();
|
|
9635
9736
|
defaultLogger$1.getLogger = function getLogger$1(name) {
|
|
9636
9737
|
if (typeof name !== "symbol" && typeof name !== "string" || name === "") throw new TypeError("You must supply a name when creating a logger.");
|
|
9637
|
-
var logger$
|
|
9638
|
-
if (!logger$
|
|
9639
|
-
return logger$
|
|
9738
|
+
var logger$34 = _loggersByName[name];
|
|
9739
|
+
if (!logger$34) logger$34 = _loggersByName[name] = new Logger(name, defaultLogger$1.methodFactory);
|
|
9740
|
+
return logger$34;
|
|
9640
9741
|
};
|
|
9641
9742
|
var _log = typeof window !== undefinedType ? window.log : void 0;
|
|
9642
9743
|
defaultLogger$1.noConflict = function() {
|
|
@@ -9672,8 +9773,8 @@ const defaultLoggerLevel = defaultLogger.levels.WARN;
|
|
|
9672
9773
|
defaultLogger.setLevel(defaultLoggerLevel);
|
|
9673
9774
|
let userLogger = null;
|
|
9674
9775
|
/** Replace the built-in logger with a custom implementation. Pass `null` to restore defaults. */
|
|
9675
|
-
const setLogger = (logger$
|
|
9676
|
-
userLogger = logger$
|
|
9776
|
+
const setLogger = (logger$34) => {
|
|
9777
|
+
userLogger = logger$34;
|
|
9677
9778
|
};
|
|
9678
9779
|
let debugOptions = {};
|
|
9679
9780
|
/** Configure debug options (e.g., `{ logWsTraffic: true }`). */
|
|
@@ -9717,8 +9818,8 @@ const wsTraffic = (options) => {
|
|
|
9717
9818
|
loggerInstance.debug(`${options.type.toUpperCase()}: \n`, msg, "\n");
|
|
9718
9819
|
};
|
|
9719
9820
|
const getLogger = () => {
|
|
9720
|
-
const logger$
|
|
9721
|
-
return new Proxy(logger$
|
|
9821
|
+
const logger$34 = getLoggerInstance();
|
|
9822
|
+
return new Proxy(logger$34, { get(_target, prop, _receiver) {
|
|
9722
9823
|
if (prop === "wsTraffic") return wsTraffic;
|
|
9723
9824
|
const instance = getLoggerInstance();
|
|
9724
9825
|
const value = Reflect.get(instance, prop);
|
|
@@ -9770,7 +9871,7 @@ const asyncRetry = async ({ asyncCallable, maxRetries: retries = DEFAULT_MAX_RET
|
|
|
9770
9871
|
|
|
9771
9872
|
//#endregion
|
|
9772
9873
|
//#region src/controllers/HTTPRequestController.ts
|
|
9773
|
-
const logger$
|
|
9874
|
+
const logger$33 = getLogger();
|
|
9774
9875
|
const GET_PARAMS = {
|
|
9775
9876
|
method: "GET",
|
|
9776
9877
|
headers: { Accept: "application/json" }
|
|
@@ -9834,7 +9935,7 @@ var HTTPRequestController = class HTTPRequestController extends Destroyable {
|
|
|
9834
9935
|
this._responses$.next(response);
|
|
9835
9936
|
return response;
|
|
9836
9937
|
} catch (error) {
|
|
9837
|
-
logger$
|
|
9938
|
+
logger$33.error("[HTTPRequestController] Request error:", error);
|
|
9838
9939
|
this._status$.next("error");
|
|
9839
9940
|
const err = error instanceof Error ? error : new Error("HTTP request failed", { cause: error });
|
|
9840
9941
|
this._errors$.next(err);
|
|
@@ -9861,7 +9962,7 @@ var HTTPRequestController = class HTTPRequestController extends Destroyable {
|
|
|
9861
9962
|
const url = this.buildURL(request.url);
|
|
9862
9963
|
const headers = this.buildHeaders(request.headers);
|
|
9863
9964
|
const timeout$5 = request.timeout ?? this.requestTimeout;
|
|
9864
|
-
logger$
|
|
9965
|
+
logger$33.debug("[HTTPRequestController] Executing request:", {
|
|
9865
9966
|
method: request.method,
|
|
9866
9967
|
url,
|
|
9867
9968
|
headers: Object.keys(headers).reduce((acc, key) => {
|
|
@@ -9881,7 +9982,7 @@ var HTTPRequestController = class HTTPRequestController extends Destroyable {
|
|
|
9881
9982
|
});
|
|
9882
9983
|
clearTimeout(timeoutId);
|
|
9883
9984
|
const httpResponse = await this.convertResponse(response);
|
|
9884
|
-
logger$
|
|
9985
|
+
logger$33.debug("[HTTPRequestController] Response received:", {
|
|
9885
9986
|
status: response.status,
|
|
9886
9987
|
statusText: response.statusText,
|
|
9887
9988
|
headers: [...response.headers.entries()],
|
|
@@ -9891,7 +9992,7 @@ var HTTPRequestController = class HTTPRequestController extends Destroyable {
|
|
|
9891
9992
|
} catch (error) {
|
|
9892
9993
|
clearTimeout(timeoutId);
|
|
9893
9994
|
if (error instanceof Error && error.name === "AbortError") throw new RequestTimeoutError(`Request timeout after ${timeout$5}ms`, { cause: error });
|
|
9894
|
-
logger$
|
|
9995
|
+
logger$33.error("[HTTPRequestController] Request failed:", error);
|
|
9895
9996
|
throw error;
|
|
9896
9997
|
}
|
|
9897
9998
|
}
|
|
@@ -9905,8 +10006,8 @@ var HTTPRequestController = class HTTPRequestController extends Destroyable {
|
|
|
9905
10006
|
const credential = this.getCredential();
|
|
9906
10007
|
if (credential.token) {
|
|
9907
10008
|
headers.Authorization = `Bearer ${credential.token}`;
|
|
9908
|
-
logger$
|
|
9909
|
-
} else logger$
|
|
10009
|
+
logger$33.debug("[HTTPRequestController] Using Bearer token auth, token length:", credential.token.length);
|
|
10010
|
+
} else logger$33.warn("[HTTPRequestController] No credentials available for authentication");
|
|
9910
10011
|
return headers;
|
|
9911
10012
|
}
|
|
9912
10013
|
/**
|
|
@@ -10054,7 +10155,7 @@ function fromMsToSec(milliseconds) {
|
|
|
10054
10155
|
|
|
10055
10156
|
//#endregion
|
|
10056
10157
|
//#region src/containers/PreferencesContainer.ts
|
|
10057
|
-
const logger$
|
|
10158
|
+
const logger$32 = getLogger();
|
|
10058
10159
|
var PreferencesContainer = class PreferencesContainer {
|
|
10059
10160
|
static get instance() {
|
|
10060
10161
|
this._instance ??= new PreferencesContainer();
|
|
@@ -10716,7 +10817,7 @@ var ClientPreferences = class {
|
|
|
10716
10817
|
if (!this._storage) return;
|
|
10717
10818
|
const data = collectStoredPreferences();
|
|
10718
10819
|
this._storage.setItem(PREFERENCES_STORAGE_KEY, data, "local").catch((error) => {
|
|
10719
|
-
logger$
|
|
10820
|
+
logger$32.error(`[ClientPreferences] Failed to save preferences: ${String(error)}`);
|
|
10720
10821
|
});
|
|
10721
10822
|
}
|
|
10722
10823
|
/** Loads preferences from storage and applies them to the container. */
|
|
@@ -10725,7 +10826,7 @@ var ClientPreferences = class {
|
|
|
10725
10826
|
this._storage.getItem(PREFERENCES_STORAGE_KEY, "local").then((stored) => {
|
|
10726
10827
|
if (stored) applyStoredPreferences(stored);
|
|
10727
10828
|
}).catch((error) => {
|
|
10728
|
-
logger$
|
|
10829
|
+
logger$32.error(`[ClientPreferences] Failed to load preferences: ${String(error)}`);
|
|
10729
10830
|
});
|
|
10730
10831
|
}
|
|
10731
10832
|
};
|
|
@@ -10747,7 +10848,7 @@ function toError(value) {
|
|
|
10747
10848
|
//#endregion
|
|
10748
10849
|
//#region src/controllers/NavigatorDeviceController.ts
|
|
10749
10850
|
var import_cjs$29 = require_cjs();
|
|
10750
|
-
const logger$
|
|
10851
|
+
const logger$31 = getLogger();
|
|
10751
10852
|
/** Maps a device kind to its storage key. */
|
|
10752
10853
|
const DEVICE_STORAGE_KEYS = {
|
|
10753
10854
|
audioinput: DEVICE_STORAGE_KEY_AUDIO_INPUT,
|
|
@@ -10769,7 +10870,7 @@ var NavigatorDeviceController = class extends Destroyable {
|
|
|
10769
10870
|
super();
|
|
10770
10871
|
this.webRTCApiProvider = webRTCApiProvider;
|
|
10771
10872
|
this.deviceChangeHandler = () => {
|
|
10772
|
-
logger$
|
|
10873
|
+
logger$31.debug("[DeviceController] Device change detected");
|
|
10773
10874
|
this.enumerateDevices();
|
|
10774
10875
|
};
|
|
10775
10876
|
this._devicesState$ = this.createBehaviorSubject(initialDevicesState);
|
|
@@ -10834,13 +10935,13 @@ var NavigatorDeviceController = class extends Destroyable {
|
|
|
10834
10935
|
return this.cachedObservable("videoInputDevices$", () => this._devicesState$.pipe((0, import_cjs$29.map)((state) => state.videoinput), (0, import_cjs$29.distinctUntilChanged)(), (0, import_cjs$29.takeUntil)(this.destroyed$)));
|
|
10835
10936
|
}
|
|
10836
10937
|
get selectedAudioInputDevice$() {
|
|
10837
|
-
return this.cachedObservable("selectedAudioInputDevice$", () => this._selectedDevicesState$.asObservable().pipe((0, import_cjs$29.map)((state) => state.audioinput), (0, import_cjs$29.distinctUntilChanged)(), (0, import_cjs$29.takeUntil)(this.destroyed$), (0, import_cjs$29.tap)((info) => logger$
|
|
10938
|
+
return this.cachedObservable("selectedAudioInputDevice$", () => this._selectedDevicesState$.asObservable().pipe((0, import_cjs$29.map)((state) => state.audioinput), (0, import_cjs$29.distinctUntilChanged)(), (0, import_cjs$29.takeUntil)(this.destroyed$), (0, import_cjs$29.tap)((info) => logger$31.debug("[DeviceController] Selected audio input device changed:", info))));
|
|
10838
10939
|
}
|
|
10839
10940
|
get selectedAudioOutputDevice$() {
|
|
10840
|
-
return this.cachedObservable("selectedAudioOutputDevice$", () => this._selectedDevicesState$.asObservable().pipe((0, import_cjs$29.map)((state) => state.audiooutput), (0, import_cjs$29.distinctUntilChanged)(), (0, import_cjs$29.takeUntil)(this.destroyed$), (0, import_cjs$29.tap)((info) => logger$
|
|
10941
|
+
return this.cachedObservable("selectedAudioOutputDevice$", () => this._selectedDevicesState$.asObservable().pipe((0, import_cjs$29.map)((state) => state.audiooutput), (0, import_cjs$29.distinctUntilChanged)(), (0, import_cjs$29.takeUntil)(this.destroyed$), (0, import_cjs$29.tap)((info) => logger$31.debug("[DeviceController] Selected audio output device changed:", info))));
|
|
10841
10942
|
}
|
|
10842
10943
|
get selectedVideoInputDevice$() {
|
|
10843
|
-
return this.cachedObservable("selectedVideoInputDevice$", () => this._selectedDevicesState$.asObservable().pipe((0, import_cjs$29.map)((state) => state.videoinput), (0, import_cjs$29.distinctUntilChanged)(), (0, import_cjs$29.takeUntil)(this.destroyed$), (0, import_cjs$29.tap)((info) => logger$
|
|
10944
|
+
return this.cachedObservable("selectedVideoInputDevice$", () => this._selectedDevicesState$.asObservable().pipe((0, import_cjs$29.map)((state) => state.videoinput), (0, import_cjs$29.distinctUntilChanged)(), (0, import_cjs$29.takeUntil)(this.destroyed$), (0, import_cjs$29.tap)((info) => logger$31.debug("[DeviceController] Selected video input device changed:", info))));
|
|
10844
10945
|
}
|
|
10845
10946
|
get selectedAudioInputDevice() {
|
|
10846
10947
|
if (this._audioInputDisabled$.value) return null;
|
|
@@ -10915,7 +11016,7 @@ var NavigatorDeviceController = class extends Destroyable {
|
|
|
10915
11016
|
if (device) this.persistDeviceSelection("audioinput", device);
|
|
10916
11017
|
}
|
|
10917
11018
|
selectVideoInputDevice(device) {
|
|
10918
|
-
logger$
|
|
11019
|
+
logger$31.debug("[DeviceController] Setting selected video input device:", device);
|
|
10919
11020
|
if (this._videoInputDisabled$.value && device) this._videoInputDisabled$.next(false);
|
|
10920
11021
|
const previous = this._selectedDevicesState$.value.videoinput;
|
|
10921
11022
|
if (previous && previous.deviceId !== device?.deviceId) this._deviceHistory.push("videoinput", previous);
|
|
@@ -10972,7 +11073,7 @@ var NavigatorDeviceController = class extends Destroyable {
|
|
|
10972
11073
|
}
|
|
10973
11074
|
const fromHistory = this._deviceHistory.findInHistory(kind, devices);
|
|
10974
11075
|
if (fromHistory) {
|
|
10975
|
-
logger$
|
|
11076
|
+
logger$31.debug(`[DeviceController] Device disappeared, falling back to history: ${fromHistory.label}`);
|
|
10976
11077
|
this.emitDeviceRecovered(kind, selected, fromHistory, "device_disconnected");
|
|
10977
11078
|
return fromHistory;
|
|
10978
11079
|
}
|
|
@@ -11025,7 +11126,7 @@ var NavigatorDeviceController = class extends Destroyable {
|
|
|
11025
11126
|
try {
|
|
11026
11127
|
await this._storageManager.setItem(DEVICE_STORAGE_KEYS[kind], stored, "local");
|
|
11027
11128
|
} catch (error) {
|
|
11028
|
-
logger$
|
|
11129
|
+
logger$31.error(`[DeviceController] Failed to persist device selection for ${kind}:`, error);
|
|
11029
11130
|
}
|
|
11030
11131
|
}
|
|
11031
11132
|
async loadPersistedDevices() {
|
|
@@ -11041,7 +11142,7 @@ var NavigatorDeviceController = class extends Destroyable {
|
|
|
11041
11142
|
[kind]: stored
|
|
11042
11143
|
};
|
|
11043
11144
|
} catch (error) {
|
|
11044
|
-
logger$
|
|
11145
|
+
logger$31.error(`[DeviceController] Failed to load persisted device for ${kind}:`, error);
|
|
11045
11146
|
}
|
|
11046
11147
|
}
|
|
11047
11148
|
/** Clears device history, persisted selections, and re-enumerates devices. */
|
|
@@ -11059,7 +11160,7 @@ var NavigatorDeviceController = class extends Destroyable {
|
|
|
11059
11160
|
this.disableDeviceMonitoring();
|
|
11060
11161
|
this.webRTCApiProvider.mediaDevices.addEventListener("devicechange", this.deviceChangeHandler);
|
|
11061
11162
|
if (PreferencesContainer.instance.devicePollingInterval > 0) this._devicesPoolingSubscription = (0, import_cjs$29.interval)(PreferencesContainer.instance.devicePollingInterval).subscribe(() => {
|
|
11062
|
-
logger$
|
|
11163
|
+
logger$31.debug("[DeviceController] Polling devices due to interval");
|
|
11063
11164
|
this.enumerateDevices();
|
|
11064
11165
|
});
|
|
11065
11166
|
this.enumerateDevices();
|
|
@@ -11085,13 +11186,13 @@ var NavigatorDeviceController = class extends Destroyable {
|
|
|
11085
11186
|
videoinput: []
|
|
11086
11187
|
});
|
|
11087
11188
|
this._devicesState$.next(devicesByKind);
|
|
11088
|
-
logger$
|
|
11189
|
+
logger$31.debug("[DeviceController] Devices enumerated:", {
|
|
11089
11190
|
audioInputs: devicesByKind.audioinput.length,
|
|
11090
11191
|
audioOutputs: devicesByKind.audiooutput.length,
|
|
11091
11192
|
videoInputs: devicesByKind.videoinput.length
|
|
11092
11193
|
});
|
|
11093
11194
|
} catch (error) {
|
|
11094
|
-
logger$
|
|
11195
|
+
logger$31.error("[DeviceController] Failed to enumerate devices:", error);
|
|
11095
11196
|
this._errors$.next(toError(error));
|
|
11096
11197
|
}
|
|
11097
11198
|
}
|
|
@@ -11107,7 +11208,7 @@ var NavigatorDeviceController = class extends Destroyable {
|
|
|
11107
11208
|
stream.getTracks().forEach((t) => t.stop());
|
|
11108
11209
|
return capabilities;
|
|
11109
11210
|
} catch (error) {
|
|
11110
|
-
logger$
|
|
11211
|
+
logger$31.error("[DeviceController] Failed to get device capabilities:", error);
|
|
11111
11212
|
this._errors$.next(toError(error));
|
|
11112
11213
|
throw error;
|
|
11113
11214
|
}
|
|
@@ -11358,7 +11459,7 @@ var DependencyContainer = class {
|
|
|
11358
11459
|
|
|
11359
11460
|
//#endregion
|
|
11360
11461
|
//#region src/controllers/CryptoController.ts
|
|
11361
|
-
const logger$
|
|
11462
|
+
const logger$30 = getLogger();
|
|
11362
11463
|
const DPOP_DB_NAME = "sw-dpop";
|
|
11363
11464
|
const DPOP_DB_VERSION = 1;
|
|
11364
11465
|
const DPOP_STORE_NAME = "keys";
|
|
@@ -11417,7 +11518,7 @@ async function loadKeyPairFromDB() {
|
|
|
11417
11518
|
tx.oncomplete = () => db.close();
|
|
11418
11519
|
});
|
|
11419
11520
|
} catch (error) {
|
|
11420
|
-
logger$
|
|
11521
|
+
logger$30.warn("[DPoP] Failed to load key pair from IndexedDB:", error);
|
|
11421
11522
|
return null;
|
|
11422
11523
|
}
|
|
11423
11524
|
}
|
|
@@ -11437,7 +11538,7 @@ async function saveKeyPairToDB(keyPair) {
|
|
|
11437
11538
|
};
|
|
11438
11539
|
});
|
|
11439
11540
|
} catch (error) {
|
|
11440
|
-
logger$
|
|
11541
|
+
logger$30.warn("[DPoP] Failed to save key pair to IndexedDB:", error);
|
|
11441
11542
|
}
|
|
11442
11543
|
}
|
|
11443
11544
|
async function deleteKeyPairFromDB() {
|
|
@@ -11456,7 +11557,7 @@ async function deleteKeyPairFromDB() {
|
|
|
11456
11557
|
};
|
|
11457
11558
|
});
|
|
11458
11559
|
} catch (error) {
|
|
11459
|
-
logger$
|
|
11560
|
+
logger$30.warn("[DPoP] Failed to delete key pair from IndexedDB:", error);
|
|
11460
11561
|
}
|
|
11461
11562
|
}
|
|
11462
11563
|
/**
|
|
@@ -11516,13 +11617,13 @@ var CryptoController = class {
|
|
|
11516
11617
|
this._publicJwk = await crypto.subtle.exportKey("jwk", stored.publicKey);
|
|
11517
11618
|
this._fingerprint = await computeJwkThumbprint(this._publicJwk);
|
|
11518
11619
|
this._initialized = true;
|
|
11519
|
-
logger$
|
|
11620
|
+
logger$30.debug("[DPoP] Key pair restored from IndexedDB, fingerprint:", this._fingerprint);
|
|
11520
11621
|
return this._fingerprint;
|
|
11521
11622
|
} catch (error) {
|
|
11522
|
-
logger$
|
|
11623
|
+
logger$30.warn("[DPoP] Stored key pair unusable, generating new one:", error);
|
|
11523
11624
|
await deleteKeyPairFromDB();
|
|
11524
11625
|
}
|
|
11525
|
-
logger$
|
|
11626
|
+
logger$30.debug("[DPoP] Generating RSA key pair");
|
|
11526
11627
|
this._keyPair = await crypto.subtle.generateKey({
|
|
11527
11628
|
name: "RSASSA-PKCS1-v1_5",
|
|
11528
11629
|
modulusLength: 2048,
|
|
@@ -11537,7 +11638,7 @@ var CryptoController = class {
|
|
|
11537
11638
|
this._fingerprint = await computeJwkThumbprint(this._publicJwk);
|
|
11538
11639
|
this._initialized = true;
|
|
11539
11640
|
await saveKeyPairToDB(this._keyPair);
|
|
11540
|
-
logger$
|
|
11641
|
+
logger$30.debug("[DPoP] Key pair generated and persisted, fingerprint:", this._fingerprint);
|
|
11541
11642
|
return this._fingerprint;
|
|
11542
11643
|
}
|
|
11543
11644
|
/**
|
|
@@ -11603,7 +11704,7 @@ var CryptoController = class {
|
|
|
11603
11704
|
this._fingerprint = null;
|
|
11604
11705
|
this._initialized = false;
|
|
11605
11706
|
deleteKeyPairFromDB();
|
|
11606
|
-
logger$
|
|
11707
|
+
logger$30.debug("[DPoP] Controller destroyed");
|
|
11607
11708
|
}
|
|
11608
11709
|
get publicJwk() {
|
|
11609
11710
|
if (!this._publicJwk) throw new DPoPInitError("CryptoController not initialized. Call init() first.");
|
|
@@ -11627,7 +11728,7 @@ var CryptoController = class {
|
|
|
11627
11728
|
//#endregion
|
|
11628
11729
|
//#region src/controllers/NetworkMonitor.ts
|
|
11629
11730
|
var import_cjs$28 = require_cjs();
|
|
11630
|
-
const logger$
|
|
11731
|
+
const logger$29 = getLogger();
|
|
11631
11732
|
/**
|
|
11632
11733
|
* Safely check whether we are running in a browser environment
|
|
11633
11734
|
* with `window` and the relevant event targets.
|
|
@@ -11684,7 +11785,7 @@ var NetworkMonitor = class extends Destroyable {
|
|
|
11684
11785
|
}
|
|
11685
11786
|
attachListeners() {
|
|
11686
11787
|
if (!hasBrowserNetworkEvents()) {
|
|
11687
|
-
logger$
|
|
11788
|
+
logger$29.debug("NetworkMonitor: no browser environment detected, skipping event listeners");
|
|
11688
11789
|
return;
|
|
11689
11790
|
}
|
|
11690
11791
|
window.addEventListener("online", this._onOnline);
|
|
@@ -11692,7 +11793,7 @@ var NetworkMonitor = class extends Destroyable {
|
|
|
11692
11793
|
const connection = getNetworkConnection();
|
|
11693
11794
|
if (connection) connection.addEventListener("change", this._onConnectionChange);
|
|
11694
11795
|
this._listenersAttached = true;
|
|
11695
|
-
logger$
|
|
11796
|
+
logger$29.debug("NetworkMonitor: event listeners attached");
|
|
11696
11797
|
}
|
|
11697
11798
|
removeListeners() {
|
|
11698
11799
|
if (!this._listenersAttached) return;
|
|
@@ -11703,10 +11804,10 @@ var NetworkMonitor = class extends Destroyable {
|
|
|
11703
11804
|
if (connection) connection.removeEventListener("change", this._onConnectionChange);
|
|
11704
11805
|
}
|
|
11705
11806
|
this._listenersAttached = false;
|
|
11706
|
-
logger$
|
|
11807
|
+
logger$29.debug("NetworkMonitor: event listeners removed");
|
|
11707
11808
|
}
|
|
11708
11809
|
handleOnline() {
|
|
11709
|
-
logger$
|
|
11810
|
+
logger$29.info("NetworkMonitor: browser went online");
|
|
11710
11811
|
this._isOnline$.next(true);
|
|
11711
11812
|
this._networkChange$.next({
|
|
11712
11813
|
type: "online",
|
|
@@ -11715,7 +11816,7 @@ var NetworkMonitor = class extends Destroyable {
|
|
|
11715
11816
|
});
|
|
11716
11817
|
}
|
|
11717
11818
|
handleOffline() {
|
|
11718
|
-
logger$
|
|
11819
|
+
logger$29.info("NetworkMonitor: browser went offline");
|
|
11719
11820
|
this._isOnline$.next(false);
|
|
11720
11821
|
this._networkChange$.next({
|
|
11721
11822
|
type: "offline",
|
|
@@ -11724,7 +11825,7 @@ var NetworkMonitor = class extends Destroyable {
|
|
|
11724
11825
|
}
|
|
11725
11826
|
handleConnectionChange() {
|
|
11726
11827
|
const networkType = getNetworkType();
|
|
11727
|
-
logger$
|
|
11828
|
+
logger$29.info(`NetworkMonitor: connection changed — effectiveType=${networkType ?? "unknown"}`);
|
|
11728
11829
|
this._networkChange$.next({
|
|
11729
11830
|
type: "connection_change",
|
|
11730
11831
|
timestamp: Date.now(),
|
|
@@ -11840,7 +11941,7 @@ function getNavigatorMediaDevices() {
|
|
|
11840
11941
|
//#endregion
|
|
11841
11942
|
//#region src/controllers/PreflightRunner.ts
|
|
11842
11943
|
var import_cjs$27 = require_cjs();
|
|
11843
|
-
const logger$
|
|
11944
|
+
const logger$28 = getLogger();
|
|
11844
11945
|
const DEFAULT_MEDIA_TEST_DURATION_S = 10;
|
|
11845
11946
|
const ICE_GATHERING_TIMEOUT_MS = 1e4;
|
|
11846
11947
|
const SIGNALING_RTT_TIMEOUT_MS = 5e3;
|
|
@@ -11889,7 +11990,7 @@ var PreflightRunner = class extends Destroyable {
|
|
|
11889
11990
|
if (!this._options.skipMediaTest) try {
|
|
11890
11991
|
bandwidth = await this.testMediaBandwidth(destination);
|
|
11891
11992
|
} catch (error) {
|
|
11892
|
-
logger$
|
|
11993
|
+
logger$28.warn("[PreflightRunner] Media bandwidth test failed:", error);
|
|
11893
11994
|
warnings.push("Media bandwidth test failed");
|
|
11894
11995
|
}
|
|
11895
11996
|
return {
|
|
@@ -11901,7 +12002,7 @@ var PreflightRunner = class extends Destroyable {
|
|
|
11901
12002
|
warnings
|
|
11902
12003
|
};
|
|
11903
12004
|
} catch (error) {
|
|
11904
|
-
logger$
|
|
12005
|
+
logger$28.error("[PreflightRunner] Preflight test failed:", error);
|
|
11905
12006
|
throw new PreflightError("preflight", error instanceof Error ? error : new Error(String(error)));
|
|
11906
12007
|
} finally {
|
|
11907
12008
|
this.destroy();
|
|
@@ -11932,7 +12033,7 @@ var PreflightRunner = class extends Destroyable {
|
|
|
11932
12033
|
if (track.kind === "video" && track.readyState === "live") videoWorking = true;
|
|
11933
12034
|
}
|
|
11934
12035
|
} catch (error) {
|
|
11935
|
-
logger$
|
|
12036
|
+
logger$28.warn("[PreflightRunner] Device test failed:", error);
|
|
11936
12037
|
} finally {
|
|
11937
12038
|
if (audioStream) audioStream.getTracks().forEach((t) => t.stop());
|
|
11938
12039
|
}
|
|
@@ -11990,7 +12091,7 @@ var PreflightRunner = class extends Destroyable {
|
|
|
11990
12091
|
rttMs
|
|
11991
12092
|
};
|
|
11992
12093
|
} catch (error) {
|
|
11993
|
-
logger$
|
|
12094
|
+
logger$28.warn("[PreflightRunner] ICE connectivity test failed:", error);
|
|
11994
12095
|
return {
|
|
11995
12096
|
type: "failed",
|
|
11996
12097
|
turnReachable: false,
|
|
@@ -12038,7 +12139,7 @@ var PreflightRunner = class extends Destroyable {
|
|
|
12038
12139
|
//#endregion
|
|
12039
12140
|
//#region src/controllers/VisibilityController.ts
|
|
12040
12141
|
var import_cjs$26 = require_cjs();
|
|
12041
|
-
const logger$
|
|
12142
|
+
const logger$27 = getLogger();
|
|
12042
12143
|
/**
|
|
12043
12144
|
* Checks whether the document visibility API is available.
|
|
12044
12145
|
*/
|
|
@@ -12075,8 +12176,8 @@ var VisibilityController = class extends Destroyable {
|
|
|
12075
12176
|
this._boundHandler = this._handleVisibilityChange.bind(this);
|
|
12076
12177
|
if (this._hasVisibilityApi) {
|
|
12077
12178
|
document.addEventListener("visibilitychange", this._boundHandler);
|
|
12078
|
-
logger$
|
|
12079
|
-
} else logger$
|
|
12179
|
+
logger$27.debug("VisibilityController: listening for visibilitychange events");
|
|
12180
|
+
} else logger$27.debug("VisibilityController: document visibility API not available, defaulting to visible");
|
|
12080
12181
|
}
|
|
12081
12182
|
/**
|
|
12082
12183
|
* Observable of the current visibility state.
|
|
@@ -12101,7 +12202,7 @@ var VisibilityController = class extends Destroyable {
|
|
|
12101
12202
|
destroy() {
|
|
12102
12203
|
if (this._hasVisibilityApi) {
|
|
12103
12204
|
document.removeEventListener("visibilitychange", this._boundHandler);
|
|
12104
|
-
logger$
|
|
12205
|
+
logger$27.debug("VisibilityController: removed visibilitychange listener");
|
|
12105
12206
|
}
|
|
12106
12207
|
super.destroy();
|
|
12107
12208
|
}
|
|
@@ -12119,7 +12220,7 @@ var VisibilityController = class extends Destroyable {
|
|
|
12119
12220
|
timestamp: Date.now()
|
|
12120
12221
|
};
|
|
12121
12222
|
this._visibilityChange$.next(changeEvent);
|
|
12122
|
-
logger$
|
|
12223
|
+
logger$27.debug("VisibilityController: visibility changed", {
|
|
12123
12224
|
from: previousState,
|
|
12124
12225
|
to: newState
|
|
12125
12226
|
});
|
|
@@ -12270,6 +12371,12 @@ const RPCReauthenticate = (params) => {
|
|
|
12270
12371
|
|
|
12271
12372
|
//#endregion
|
|
12272
12373
|
//#region src/core/RPCMessages/RPCPing.ts
|
|
12374
|
+
const RPCPing = () => {
|
|
12375
|
+
return buildRPCRequest({
|
|
12376
|
+
method: "signalwire.ping",
|
|
12377
|
+
params: { timestamp: Date.now() / 1e3 }
|
|
12378
|
+
});
|
|
12379
|
+
};
|
|
12273
12380
|
const RPCPingResponse = (id, timestamp$1) => {
|
|
12274
12381
|
return makeRPCResponse({
|
|
12275
12382
|
id,
|
|
@@ -12358,15 +12465,57 @@ const RPCEventAckResponse = (id) => makeRPCResponse({
|
|
|
12358
12465
|
result: {}
|
|
12359
12466
|
});
|
|
12360
12467
|
|
|
12468
|
+
//#endregion
|
|
12469
|
+
//#region src/utils/authRecovery.ts
|
|
12470
|
+
/**
|
|
12471
|
+
* Walk an error's `error`/`cause` chain looking for a {@link JSONRPCError}.
|
|
12472
|
+
* Errors thrown by call creation are wrapped (e.g. `CallCreateError`), so the
|
|
12473
|
+
* underlying signaling error is nested. Bounded by a visited set to guard
|
|
12474
|
+
* against cyclic causes.
|
|
12475
|
+
*/
|
|
12476
|
+
function findJSONRPCError(error) {
|
|
12477
|
+
const seen = /* @__PURE__ */ new Set();
|
|
12478
|
+
let current = error;
|
|
12479
|
+
while (current instanceof Error && !seen.has(current)) {
|
|
12480
|
+
seen.add(current);
|
|
12481
|
+
if (current instanceof JSONRPCError) return current;
|
|
12482
|
+
current = current.error ?? current.cause;
|
|
12483
|
+
}
|
|
12484
|
+
}
|
|
12485
|
+
/**
|
|
12486
|
+
* Whether an error is a session-recoverable authentication failure
|
|
12487
|
+
* (`-32002` authentication failed or `-32003` requester validation failed)
|
|
12488
|
+
* that a credential re-mint + retry can heal.
|
|
12489
|
+
*/
|
|
12490
|
+
function isRecoverableAuthError(error) {
|
|
12491
|
+
const rpcError = findJSONRPCError(error);
|
|
12492
|
+
return rpcError !== void 0 && (rpcError.code === RPC_ERROR_REQUESTER_VALIDATION_FAILED || rpcError.code === RPC_ERROR_AUTHENTICATION_FAILED);
|
|
12493
|
+
}
|
|
12494
|
+
/**
|
|
12495
|
+
* Whether an error is specifically a requester-validation rejection
|
|
12496
|
+
* (`-32003`) — the server refusing the session's credential.
|
|
12497
|
+
*
|
|
12498
|
+
* Narrower than {@link isRecoverableAuthError} on purpose. `-32002` is
|
|
12499
|
+
* overloaded server-side: a rejected reattach arrives as `-32002` with
|
|
12500
|
+
* `cause: INVALID_MSG_UNSPECIFIED` and message `CALL ERROR`, which is a
|
|
12501
|
+
* call-level rejection and says nothing about the credential. Use this where
|
|
12502
|
+
* the decision must not be fooled by that, such as deciding whether retrying
|
|
12503
|
+
* an operation could possibly succeed.
|
|
12504
|
+
*/
|
|
12505
|
+
function isRequesterValidationError(error) {
|
|
12506
|
+
return findJSONRPCError(error)?.code === RPC_ERROR_REQUESTER_VALIDATION_FAILED;
|
|
12507
|
+
}
|
|
12508
|
+
|
|
12361
12509
|
//#endregion
|
|
12362
12510
|
//#region src/managers/AttachManager.ts
|
|
12363
|
-
const logger$
|
|
12511
|
+
const logger$26 = getLogger();
|
|
12364
12512
|
var AttachManager = class {
|
|
12365
|
-
constructor(storage, deviceController, reconnectCallsTimeout, attachKey) {
|
|
12513
|
+
constructor(storage, deviceController, reconnectCallsTimeout, attachKey, credentialRecovered) {
|
|
12366
12514
|
this.storage = storage;
|
|
12367
12515
|
this.deviceController = deviceController;
|
|
12368
12516
|
this.reconnectCallsTimeout = reconnectCallsTimeout;
|
|
12369
12517
|
this.attachKey = attachKey;
|
|
12518
|
+
this.credentialRecovered = credentialRecovered;
|
|
12370
12519
|
this.writeQueue = Promise.resolve();
|
|
12371
12520
|
}
|
|
12372
12521
|
async detachAll() {
|
|
@@ -12381,7 +12530,7 @@ var AttachManager = class {
|
|
|
12381
12530
|
try {
|
|
12382
12531
|
return await this.storage.getItem(this.attachKey) ?? {};
|
|
12383
12532
|
} catch (error) {
|
|
12384
|
-
logger$
|
|
12533
|
+
logger$26.warn("[AttachManager] Failed to retrieve attached calls from storage", error);
|
|
12385
12534
|
return {};
|
|
12386
12535
|
}
|
|
12387
12536
|
}
|
|
@@ -12389,7 +12538,7 @@ var AttachManager = class {
|
|
|
12389
12538
|
try {
|
|
12390
12539
|
await this.storage.setItem(this.attachKey, attached);
|
|
12391
12540
|
} catch (error) {
|
|
12392
|
-
logger$
|
|
12541
|
+
logger$26.warn("[AttachManager] Failed to write attached calls to storage", error);
|
|
12393
12542
|
}
|
|
12394
12543
|
}
|
|
12395
12544
|
/**
|
|
@@ -12408,11 +12557,39 @@ var AttachManager = class {
|
|
|
12408
12557
|
}
|
|
12409
12558
|
async attach(call) {
|
|
12410
12559
|
if (!call.to) {
|
|
12411
|
-
logger$
|
|
12560
|
+
logger$26.warn("[AttachManager] Skip attach for calls with no destination");
|
|
12412
12561
|
return;
|
|
12413
12562
|
}
|
|
12563
|
+
const attachment = this.buildAttachment(call, call.to);
|
|
12564
|
+
await this.mutate((attached) => ({
|
|
12565
|
+
...attached,
|
|
12566
|
+
[call.id]: attachment
|
|
12567
|
+
}));
|
|
12568
|
+
}
|
|
12569
|
+
/**
|
|
12570
|
+
* Keep an already-stored call's reference alive and current — the periodic
|
|
12571
|
+
* refresh the `verto.ping` keepalive drives.
|
|
12572
|
+
*
|
|
12573
|
+
* Only ever updates: a call with no record is one nothing wants reattached,
|
|
12574
|
+
* and re-creating it here would undo a `detach`. That matters because a ping
|
|
12575
|
+
* can land in the window between `bye()` detaching and the call being torn
|
|
12576
|
+
* down, and a record revived there survives the hangup — so the next page
|
|
12577
|
+
* load dials a call nobody is on. The existence check and the write share
|
|
12578
|
+
* one {@link mutate} turn, so a concurrent detach cannot slip between them.
|
|
12579
|
+
*/
|
|
12580
|
+
async refresh(call) {
|
|
12581
|
+
if (!call.to) return;
|
|
12414
12582
|
const destination = call.to;
|
|
12415
|
-
|
|
12583
|
+
await this.mutate((attached) => {
|
|
12584
|
+
if (!Object.hasOwn(attached, call.id)) return attached;
|
|
12585
|
+
return {
|
|
12586
|
+
...attached,
|
|
12587
|
+
[call.id]: this.buildAttachment(call, destination)
|
|
12588
|
+
};
|
|
12589
|
+
});
|
|
12590
|
+
}
|
|
12591
|
+
buildAttachment(call, destination) {
|
|
12592
|
+
return {
|
|
12416
12593
|
nodeId: call.nodeId,
|
|
12417
12594
|
destination,
|
|
12418
12595
|
mediaDirections: call.mediaDirections,
|
|
@@ -12420,10 +12597,6 @@ var AttachManager = class {
|
|
|
12420
12597
|
videoInputDevice: call.mediaDirections.video !== "inactive" ? this.deviceController.selectedVideoInputDevice : null,
|
|
12421
12598
|
attachedAt: Date.now()
|
|
12422
12599
|
};
|
|
12423
|
-
await this.mutate((attached) => ({
|
|
12424
|
-
...attached,
|
|
12425
|
-
[call.id]: attachment
|
|
12426
|
-
}));
|
|
12427
12600
|
}
|
|
12428
12601
|
async detach(call) {
|
|
12429
12602
|
await this.mutate((attached) => {
|
|
@@ -12446,8 +12619,14 @@ var AttachManager = class {
|
|
|
12446
12619
|
* rejecting. Once that fix is deployed, this will work for both
|
|
12447
12620
|
* page reloads and WebSocket reconnects.
|
|
12448
12621
|
*
|
|
12449
|
-
*
|
|
12450
|
-
*
|
|
12622
|
+
* A failed reattach does NOT generally cost the stored reference. It is
|
|
12623
|
+
* discarded only when the server denied the reattach on a session whose
|
|
12624
|
+
* credential it had already accepted — a verified reauthentication followed
|
|
12625
|
+
* by a refusal is the server saying the call is gone, and that is the one
|
|
12626
|
+
* refusal worth acting on. Until then the credential may be what is being
|
|
12627
|
+
* refused, and the record is the only way a later reload can try again;
|
|
12628
|
+
* keeping it costs nothing, since `detachExpired` reaps it once it is older
|
|
12629
|
+
* than `reconnectCallsTimeout`.
|
|
12451
12630
|
*/
|
|
12452
12631
|
async reattachCalls() {
|
|
12453
12632
|
const attached = await this.readAttached();
|
|
@@ -12456,25 +12635,31 @@ var AttachManager = class {
|
|
|
12456
12635
|
const { destination } = attachment;
|
|
12457
12636
|
const options = this.buildCallOptions(attachment);
|
|
12458
12637
|
let succeeded = false;
|
|
12638
|
+
let refusedOnCredentials = false;
|
|
12459
12639
|
for (let attempt = 1; attempt <= 3; attempt++) try {
|
|
12460
12640
|
await this.session.createOutboundCall(destination, {
|
|
12461
12641
|
callId,
|
|
12462
12642
|
...options
|
|
12463
12643
|
});
|
|
12464
|
-
logger$
|
|
12644
|
+
logger$26.info(`[AttachManager] Reattached call ${callId} (attempt ${attempt})`);
|
|
12465
12645
|
succeeded = true;
|
|
12466
12646
|
break;
|
|
12467
12647
|
} catch (error) {
|
|
12468
|
-
logger$
|
|
12648
|
+
logger$26.warn(`[AttachManager] Reattach attempt ${attempt}/3 failed for call ${callId}:`, error);
|
|
12649
|
+
if (isRequesterValidationError(error)) {
|
|
12650
|
+
refusedOnCredentials = true;
|
|
12651
|
+
logger$26.warn(`[AttachManager] Reattach of ${callId} was refused on credentials; not retrying.`);
|
|
12652
|
+
break;
|
|
12653
|
+
}
|
|
12469
12654
|
if (attempt < 3) await new Promise((r) => setTimeout(r, (attempt + 1) * 1e3));
|
|
12470
12655
|
}
|
|
12471
|
-
if (!succeeded) {
|
|
12472
|
-
logger$
|
|
12656
|
+
if (!succeeded) if (this.credentialRecovered() && !refusedOnCredentials) {
|
|
12657
|
+
logger$26.warn(`[AttachManager] Reattach of ${callId} was denied after a verified reauthentication, removing reference`);
|
|
12473
12658
|
await this.detach({
|
|
12474
12659
|
id: callId,
|
|
12475
12660
|
mediaDirections: attachment.mediaDirections
|
|
12476
12661
|
});
|
|
12477
|
-
}
|
|
12662
|
+
} else logger$26.warn(`[AttachManager] Reattach failed for call ${callId}; keeping the reference (credential refused or never proven good)`);
|
|
12478
12663
|
}
|
|
12479
12664
|
}
|
|
12480
12665
|
/**
|
|
@@ -13640,7 +13825,7 @@ function toggleHandraiseMethod(is) {
|
|
|
13640
13825
|
|
|
13641
13826
|
//#endregion
|
|
13642
13827
|
//#region src/core/entities/Participant.ts
|
|
13643
|
-
const logger$
|
|
13828
|
+
const logger$25 = getLogger();
|
|
13644
13829
|
const initialState = {};
|
|
13645
13830
|
/**
|
|
13646
13831
|
* Represents a participant in a call.
|
|
@@ -13650,9 +13835,9 @@ const initialState = {};
|
|
|
13650
13835
|
* the local participant with additional device control.
|
|
13651
13836
|
*/
|
|
13652
13837
|
var Participant = class extends Destroyable {
|
|
13653
|
-
constructor(id,
|
|
13838
|
+
constructor(id, callExecuteMethod, deviceController) {
|
|
13654
13839
|
super();
|
|
13655
|
-
this.
|
|
13840
|
+
this.callExecuteMethod = callExecuteMethod;
|
|
13656
13841
|
this.deviceController = deviceController;
|
|
13657
13842
|
this._state$ = this.createBehaviorSubject(initialState);
|
|
13658
13843
|
this.id = id;
|
|
@@ -13874,22 +14059,55 @@ var Participant = class extends Destroyable {
|
|
|
13874
14059
|
get value() {
|
|
13875
14060
|
return this._state$.value;
|
|
13876
14061
|
}
|
|
14062
|
+
/**
|
|
14063
|
+
* Target triple for member RPCs, built from the participant's own state.
|
|
14064
|
+
* The backend locates the member's session by the target `call_id`/`node_id`,
|
|
14065
|
+
* so this must always be the participant's own call context — never the
|
|
14066
|
+
* local call's id (issue #19400).
|
|
14067
|
+
*
|
|
14068
|
+
* Reading it doubles as a readiness probe: it throws until the first full
|
|
14069
|
+
* member event (`member.joined`/`member.updated` or the `call.joined`
|
|
14070
|
+
* roster) arrives, and never regresses afterwards.
|
|
14071
|
+
*
|
|
14072
|
+
* @throws {ParticipantNotReadyError} If the member state has not been
|
|
14073
|
+
* received yet (e.g. a participant first seen via `member.talking`) — an
|
|
14074
|
+
* empty call context can never address the member, so fail fast instead of
|
|
14075
|
+
* sending a doomed RPC.
|
|
14076
|
+
*/
|
|
14077
|
+
get target() {
|
|
14078
|
+
const { call_id, node_id } = this._state$.value;
|
|
14079
|
+
if (!call_id || !node_id) throw new ParticipantNotReadyError(this.id);
|
|
14080
|
+
return {
|
|
14081
|
+
member_id: this.id,
|
|
14082
|
+
call_id,
|
|
14083
|
+
node_id
|
|
14084
|
+
};
|
|
14085
|
+
}
|
|
14086
|
+
/**
|
|
14087
|
+
* Executes a member RPC against this participant, injecting its own
|
|
14088
|
+
* {@link target} as the target.
|
|
14089
|
+
*
|
|
14090
|
+
* @throws {ParticipantNotReadyError} Via {@link target}, when the
|
|
14091
|
+
* member state has not been received yet.
|
|
14092
|
+
*/
|
|
14093
|
+
async executeMethod(method, args) {
|
|
14094
|
+
return this.callExecuteMethod(this.target, method, args);
|
|
14095
|
+
}
|
|
13877
14096
|
/** Toggles the deafened state (mutes/unmutes incoming audio). */
|
|
13878
14097
|
async toggleDeaf() {
|
|
13879
|
-
|
|
13880
|
-
await this.executeMethod(this.id, method, {});
|
|
14098
|
+
await this.executeMethod(toggleDeafMethod(this.deaf), {});
|
|
13881
14099
|
}
|
|
13882
14100
|
/** Toggles the hand-raised state. */
|
|
13883
14101
|
async toggleHandraise() {
|
|
13884
|
-
await this.executeMethod(
|
|
14102
|
+
await this.executeMethod(toggleHandraiseMethod(this.handraised), {});
|
|
13885
14103
|
}
|
|
13886
14104
|
/** Mutes the participant's audio. */
|
|
13887
14105
|
async mute() {
|
|
13888
|
-
await this.executeMethod(
|
|
14106
|
+
await this.executeMethod("call.mute", { channels: ["audio"] });
|
|
13889
14107
|
}
|
|
13890
14108
|
/** Unmutes the participant's audio. */
|
|
13891
14109
|
async unmute() {
|
|
13892
|
-
await this.executeMethod(
|
|
14110
|
+
await this.executeMethod("call.unmute", { channels: ["audio"] });
|
|
13893
14111
|
}
|
|
13894
14112
|
/** Toggles the participant's audio mute state. */
|
|
13895
14113
|
async toggleMute() {
|
|
@@ -13897,11 +14115,11 @@ var Participant = class extends Destroyable {
|
|
|
13897
14115
|
}
|
|
13898
14116
|
/** Mutes the participant's video. */
|
|
13899
14117
|
async muteVideo() {
|
|
13900
|
-
await this.executeMethod(
|
|
14118
|
+
await this.executeMethod("call.mute", { channels: ["video"] });
|
|
13901
14119
|
}
|
|
13902
14120
|
/** Unmutes the participant's video. */
|
|
13903
14121
|
async unmuteVideo() {
|
|
13904
|
-
await this.executeMethod(
|
|
14122
|
+
await this.executeMethod("call.unmute", { channels: ["video"] });
|
|
13905
14123
|
}
|
|
13906
14124
|
/** Toggles the participant's video mute state. */
|
|
13907
14125
|
async toggleMuteVideo() {
|
|
@@ -13909,7 +14127,7 @@ var Participant = class extends Destroyable {
|
|
|
13909
14127
|
}
|
|
13910
14128
|
/** Toggles echo cancellation on the audio input. */
|
|
13911
14129
|
async toggleEchoCancellation() {
|
|
13912
|
-
await this.executeMethod(
|
|
14130
|
+
await this.executeMethod("call.audioflags.set", {
|
|
13913
14131
|
echo_cancellation: !this.echoCancellation,
|
|
13914
14132
|
auto_gain: this.autoGain,
|
|
13915
14133
|
noise_suppression: this.noiseSuppression
|
|
@@ -13917,7 +14135,7 @@ var Participant = class extends Destroyable {
|
|
|
13917
14135
|
}
|
|
13918
14136
|
/** Toggles automatic gain control on the audio input. */
|
|
13919
14137
|
async toggleAudioInputAutoGain() {
|
|
13920
|
-
await this.executeMethod(
|
|
14138
|
+
await this.executeMethod("call.audioflags.set", {
|
|
13921
14139
|
echo_cancellation: this.echoCancellation,
|
|
13922
14140
|
auto_gain: !this.autoGain,
|
|
13923
14141
|
noise_suppression: this.noiseSuppression
|
|
@@ -13925,7 +14143,7 @@ var Participant = class extends Destroyable {
|
|
|
13925
14143
|
}
|
|
13926
14144
|
/** Toggles noise suppression on the audio input. */
|
|
13927
14145
|
async toggleNoiseSuppression() {
|
|
13928
|
-
await this.executeMethod(
|
|
14146
|
+
await this.executeMethod("call.audioflags.set", {
|
|
13929
14147
|
echo_cancellation: this.echoCancellation,
|
|
13930
14148
|
auto_gain: this.autoGain,
|
|
13931
14149
|
noise_suppression: !this.noiseSuppression
|
|
@@ -13933,7 +14151,7 @@ var Participant = class extends Destroyable {
|
|
|
13933
14151
|
}
|
|
13934
14152
|
/** Toggles low-bitrate mode for this participant's media. */
|
|
13935
14153
|
async toggleLowbitrate() {
|
|
13936
|
-
await this.executeMethod(
|
|
14154
|
+
await this.executeMethod("call.lowbitrate.set", { lowbitrate: !this.lowbitrate });
|
|
13937
14155
|
}
|
|
13938
14156
|
/**
|
|
13939
14157
|
* Adjusts the **conference-only** microphone energy gate / sensitivity level
|
|
@@ -13949,7 +14167,7 @@ var Participant = class extends Destroyable {
|
|
|
13949
14167
|
* (integer, larger values are more sensitive).
|
|
13950
14168
|
*/
|
|
13951
14169
|
async setAudioInputSensitivity(value) {
|
|
13952
|
-
await this.executeMethod(
|
|
14170
|
+
await this.executeMethod("call.microphone.sensitivity.set", { sensitivity: value });
|
|
13953
14171
|
}
|
|
13954
14172
|
/**
|
|
13955
14173
|
* Sets the **server-side** microphone volume on this participant's bridged
|
|
@@ -13962,7 +14180,7 @@ var Participant = class extends Destroyable {
|
|
|
13962
14180
|
* @param value - Volume level (0-100).
|
|
13963
14181
|
*/
|
|
13964
14182
|
async setAudioInputVolume(value) {
|
|
13965
|
-
await this.executeMethod(
|
|
14183
|
+
await this.executeMethod("call.microphone.volume.set", { volume: value });
|
|
13966
14184
|
}
|
|
13967
14185
|
/**
|
|
13968
14186
|
* Sets the **server-side** speaker volume on this participant's bridged call
|
|
@@ -13976,45 +14194,31 @@ var Participant = class extends Destroyable {
|
|
|
13976
14194
|
* @param value - Volume level (0-100).
|
|
13977
14195
|
*/
|
|
13978
14196
|
async setAudioOutputVolume(value) {
|
|
13979
|
-
await this.executeMethod(
|
|
14197
|
+
await this.executeMethod("call.speaker.volume.set", { volume: value });
|
|
13980
14198
|
}
|
|
13981
14199
|
/**
|
|
13982
14200
|
* Sets the participant's position in the video layout.
|
|
13983
14201
|
*
|
|
13984
|
-
* Requires the `member.position` capability. The gateway
|
|
13985
|
-
*
|
|
13986
|
-
*
|
|
13987
|
-
*
|
|
13988
|
-
* guarantee a visible change: the backend silently returns `200` (no-op) for
|
|
13989
|
-
* non-conference targets.
|
|
14202
|
+
* Requires the `member.position` capability. The gateway requires a
|
|
14203
|
+
* `targets` array of `{ target, position }` entries (issue #19400). A
|
|
14204
|
+
* resolved promise does not guarantee a visible change: the backend silently
|
|
14205
|
+
* returns `200` (no-op) for non-conference targets.
|
|
13990
14206
|
*
|
|
13991
14207
|
* @param value - The {@link VideoPosition} to assign (e.g. `'auto'`, `'reserved-0'`).
|
|
13992
14208
|
*/
|
|
13993
14209
|
async setPosition(value) {
|
|
13994
|
-
|
|
13995
|
-
|
|
13996
|
-
member_id: this.id,
|
|
13997
|
-
call_id: state.call_id ?? "",
|
|
13998
|
-
node_id: state.node_id ?? ""
|
|
13999
|
-
};
|
|
14000
|
-
await this.executeMethod(target, "call.member.position.set", { targets: [{
|
|
14001
|
-
target,
|
|
14210
|
+
await this.executeMethod("call.member.position.set", { targets: [{
|
|
14211
|
+
target: this.target,
|
|
14002
14212
|
position: value
|
|
14003
14213
|
}] });
|
|
14004
14214
|
}
|
|
14005
14215
|
/** Removes this participant from the call. */
|
|
14006
14216
|
async remove() {
|
|
14007
|
-
|
|
14008
|
-
const target = {
|
|
14009
|
-
member_id: this.id,
|
|
14010
|
-
call_id: state.call_id ?? "",
|
|
14011
|
-
node_id: state.node_id ?? ""
|
|
14012
|
-
};
|
|
14013
|
-
await this.executeMethod(target, "call.member.remove", {});
|
|
14217
|
+
await this.executeMethod("call.member.remove", { targets: [this.target] });
|
|
14014
14218
|
}
|
|
14015
14219
|
/** Ends the call for this participant. */
|
|
14016
14220
|
async end() {
|
|
14017
|
-
await this.executeMethod(
|
|
14221
|
+
await this.executeMethod("call.end", {});
|
|
14018
14222
|
}
|
|
14019
14223
|
/**
|
|
14020
14224
|
* Replaces custom metadata for this participant.
|
|
@@ -14034,7 +14238,7 @@ var Participant = class extends Destroyable {
|
|
|
14034
14238
|
}
|
|
14035
14239
|
/** Destroys the participant, releasing all subscriptions and references. */
|
|
14036
14240
|
destroy() {
|
|
14037
|
-
this.
|
|
14241
|
+
this.callExecuteMethod = void 0;
|
|
14038
14242
|
super.destroy();
|
|
14039
14243
|
}
|
|
14040
14244
|
};
|
|
@@ -14046,8 +14250,8 @@ var Participant = class extends Destroyable {
|
|
|
14046
14250
|
*/
|
|
14047
14251
|
var SelfParticipant = class extends Participant {
|
|
14048
14252
|
/** @internal */
|
|
14049
|
-
constructor(id,
|
|
14050
|
-
super(id,
|
|
14253
|
+
constructor(id, callExecuteMethod, vertoManager, deviceController) {
|
|
14254
|
+
super(id, callExecuteMethod, deviceController);
|
|
14051
14255
|
this.vertoManager = vertoManager;
|
|
14052
14256
|
this._studioAudio$ = this.createBehaviorSubject(false);
|
|
14053
14257
|
this.capabilities = new SelfCapabilities();
|
|
@@ -14071,7 +14275,7 @@ var SelfParticipant = class extends Participant {
|
|
|
14071
14275
|
async enableStudioAudio() {
|
|
14072
14276
|
if (this._studioAudio$.value) return;
|
|
14073
14277
|
this._studioAudio$.next(true);
|
|
14074
|
-
await this.executeMethod(
|
|
14278
|
+
await this.executeMethod("call.audioflags.set", {
|
|
14075
14279
|
echo_cancellation: false,
|
|
14076
14280
|
auto_gain: false,
|
|
14077
14281
|
noise_suppression: false
|
|
@@ -14084,7 +14288,7 @@ var SelfParticipant = class extends Participant {
|
|
|
14084
14288
|
async disableStudioAudio() {
|
|
14085
14289
|
if (!this._studioAudio$.value) return;
|
|
14086
14290
|
this._studioAudio$.next(false);
|
|
14087
|
-
await this.executeMethod(
|
|
14291
|
+
await this.executeMethod("call.audioflags.set", {
|
|
14088
14292
|
echo_cancellation: true,
|
|
14089
14293
|
auto_gain: true,
|
|
14090
14294
|
noise_suppression: true
|
|
@@ -14093,17 +14297,27 @@ var SelfParticipant = class extends Participant {
|
|
|
14093
14297
|
/**
|
|
14094
14298
|
* Starts sharing the local screen.
|
|
14095
14299
|
*
|
|
14300
|
+
* A call carries at most one screen share. Read `screenShareStatus` before
|
|
14301
|
+
* calling and treat `'starting'`/`'stopping'` as busy.
|
|
14302
|
+
*
|
|
14096
14303
|
* The call is unaffected when acquisition fails.
|
|
14097
14304
|
*
|
|
14305
|
+
* @param options - Pass `{ audio: true }` to also request the shared
|
|
14306
|
+
* surface's audio. Defaults to video only.
|
|
14307
|
+
* @throws {ScreenShareAlreadyActiveError} When this call is already
|
|
14308
|
+
* sharing a screen. Call {@link stopScreenShare} before starting another.
|
|
14309
|
+
* @throws {AuxiliaryLegCancelledError} When {@link stopScreenShare} removes
|
|
14310
|
+
* the share before its leg finishes connecting.
|
|
14098
14311
|
* @throws The raw `getDisplayMedia` error. A dismissed picker or a
|
|
14099
14312
|
* permission denial rejects with a `NotAllowedError` `DOMException` —
|
|
14100
14313
|
* inspect `error.name` to tell benign cancels apart from real failures.
|
|
14101
14314
|
*/
|
|
14102
|
-
async startScreenShare() {
|
|
14315
|
+
async startScreenShare(options) {
|
|
14103
14316
|
try {
|
|
14104
|
-
await this.vertoManager.addScreenMedia();
|
|
14317
|
+
await this.vertoManager.addScreenMedia(options);
|
|
14105
14318
|
} catch (error) {
|
|
14106
|
-
logger$
|
|
14319
|
+
if (error instanceof AuxiliaryLegCancelledError) logger$25.debug("[Participant.startScreenShare] Screen share cancelled before connecting.");
|
|
14320
|
+
else logger$25.error("[Participant.startScreenShare] Screen share error:", error);
|
|
14107
14321
|
throw error;
|
|
14108
14322
|
}
|
|
14109
14323
|
}
|
|
@@ -14124,14 +14338,18 @@ var SelfParticipant = class extends Participant {
|
|
|
14124
14338
|
*
|
|
14125
14339
|
* The call is unaffected when acquisition fails.
|
|
14126
14340
|
*
|
|
14341
|
+
* @throws {AuxiliaryLegCancelledError} When {@link removeAdditionalDevice}
|
|
14342
|
+
* removes the device before its leg finishes connecting.
|
|
14127
14343
|
* @throws The raw `getUserMedia` error (e.g. `NotAllowedError` on
|
|
14128
|
-
* permission denial) — inspect `error.name` to decide how to react
|
|
14344
|
+
* permission denial) — inspect `error.name` to decide how to react — or
|
|
14345
|
+
* `AuxiliaryLegTimeoutError` if the leg does not connect in time.
|
|
14129
14346
|
*/
|
|
14130
14347
|
async addAdditionalDevice(options) {
|
|
14131
14348
|
try {
|
|
14132
14349
|
await this.vertoManager.addInputDevice(options);
|
|
14133
14350
|
} catch (error) {
|
|
14134
|
-
logger$
|
|
14351
|
+
if (error instanceof AuxiliaryLegCancelledError) logger$25.debug("[Participant.addAdditionalDevice] Device removed before connecting.");
|
|
14352
|
+
else logger$25.error("[Participant.addAdditionalDevice] Additional device error:", error);
|
|
14135
14353
|
throw error;
|
|
14136
14354
|
}
|
|
14137
14355
|
}
|
|
@@ -14166,22 +14384,31 @@ var SelfParticipant = class extends Participant {
|
|
|
14166
14384
|
this.deviceController.selectAudioInputDevice(device);
|
|
14167
14385
|
if (options.savePreference) PreferencesContainer.instance.preferredAudioInput = device;
|
|
14168
14386
|
}
|
|
14169
|
-
/**
|
|
14387
|
+
/**
|
|
14388
|
+
* Updates the audio input track constraints for the active call.
|
|
14389
|
+
* @returns whether the constraints reached the media the call is sending.
|
|
14390
|
+
*/
|
|
14170
14391
|
async setAudioInputDeviceConstraints(constraints) {
|
|
14171
|
-
|
|
14392
|
+
return this.vertoManager.updateMediaConstraints({ audio: constraints });
|
|
14172
14393
|
}
|
|
14173
|
-
/**
|
|
14394
|
+
/**
|
|
14395
|
+
* Updates both audio and video input track constraints for the active call.
|
|
14396
|
+
* @returns whether both kinds took the constraints.
|
|
14397
|
+
*/
|
|
14174
14398
|
async setInputDevicesConstraints(constraints) {
|
|
14175
|
-
|
|
14399
|
+
return this.vertoManager.updateMediaConstraints(constraints);
|
|
14176
14400
|
}
|
|
14177
14401
|
/** Selects the video input device for future calls. Optionally saves as a preference. */
|
|
14178
14402
|
selectVideoInputDevice(device, options = {}) {
|
|
14179
14403
|
this.deviceController.selectVideoInputDevice(device);
|
|
14180
14404
|
if (options.savePreference) PreferencesContainer.instance.preferredVideoInput = device;
|
|
14181
14405
|
}
|
|
14182
|
-
/**
|
|
14406
|
+
/**
|
|
14407
|
+
* Updates the video input track constraints for the active call.
|
|
14408
|
+
* @returns whether the constraints reached the media the call is sending.
|
|
14409
|
+
*/
|
|
14183
14410
|
async setVideoInputDeviceConstraints(constraints) {
|
|
14184
|
-
|
|
14411
|
+
return this.vertoManager.updateMediaConstraints({ video: constraints });
|
|
14185
14412
|
}
|
|
14186
14413
|
/** Selects the audio output device. Optionally saves as a preference. */
|
|
14187
14414
|
selectAudioOutputDevice(device, options = {}) {
|
|
@@ -14194,7 +14421,7 @@ var SelfParticipant = class extends Participant {
|
|
|
14194
14421
|
*/
|
|
14195
14422
|
exitStudioModeIfActive() {
|
|
14196
14423
|
if (this._studioAudio$.value) {
|
|
14197
|
-
logger$
|
|
14424
|
+
logger$25.debug("[SelfParticipant] Exiting studio audio mode due to individual flag toggle");
|
|
14198
14425
|
this._studioAudio$.next(false);
|
|
14199
14426
|
}
|
|
14200
14427
|
}
|
|
@@ -14218,7 +14445,7 @@ var SelfParticipant = class extends Participant {
|
|
|
14218
14445
|
try {
|
|
14219
14446
|
await super.mute();
|
|
14220
14447
|
} catch (error) {
|
|
14221
|
-
logger$
|
|
14448
|
+
logger$25.warn("[Participant.toggleAudioInput] Server Error while muting audio input, proceeding with local toggle anyway", error);
|
|
14222
14449
|
} finally {
|
|
14223
14450
|
this.vertoManager.muteMainAudioInputDevice();
|
|
14224
14451
|
}
|
|
@@ -14228,7 +14455,7 @@ var SelfParticipant = class extends Participant {
|
|
|
14228
14455
|
try {
|
|
14229
14456
|
await super.unmute();
|
|
14230
14457
|
} catch (error) {
|
|
14231
|
-
logger$
|
|
14458
|
+
logger$25.warn("[Participant.toggleAudioInput] Server Error while unmuting audio input, proceeding with local toggle anyway", error);
|
|
14232
14459
|
} finally {
|
|
14233
14460
|
await this.vertoManager.unmuteMainAudioInputDevice();
|
|
14234
14461
|
}
|
|
@@ -14238,7 +14465,7 @@ var SelfParticipant = class extends Participant {
|
|
|
14238
14465
|
try {
|
|
14239
14466
|
await super.muteVideo();
|
|
14240
14467
|
} catch (error) {
|
|
14241
|
-
logger$
|
|
14468
|
+
logger$25.warn("[Participant.toggleVideoInput] Server Error while muting video input, proceeding with local toggle anyway", error);
|
|
14242
14469
|
} finally {
|
|
14243
14470
|
this.vertoManager.muteMainVideoInputDevice();
|
|
14244
14471
|
}
|
|
@@ -14248,7 +14475,7 @@ var SelfParticipant = class extends Participant {
|
|
|
14248
14475
|
try {
|
|
14249
14476
|
await super.unmuteVideo();
|
|
14250
14477
|
} catch (error) {
|
|
14251
|
-
logger$
|
|
14478
|
+
logger$25.warn("[Participant.toggleVideoInput] Server Error while unmuting video input, proceeding with local toggle anyway", error);
|
|
14252
14479
|
} finally {
|
|
14253
14480
|
await this.vertoManager.unmuteMainVideoInputDevice();
|
|
14254
14481
|
}
|
|
@@ -14453,7 +14680,7 @@ function filterAs(predicate, resultPath) {
|
|
|
14453
14680
|
//#endregion
|
|
14454
14681
|
//#region src/operators/throwOnRPCError.ts
|
|
14455
14682
|
var import_cjs$21 = require_cjs();
|
|
14456
|
-
const logger$
|
|
14683
|
+
const logger$24 = getLogger();
|
|
14457
14684
|
/**
|
|
14458
14685
|
* RxJS operator that throws a {@link JSONRPCError} when the RPC response contains an error.
|
|
14459
14686
|
* Passes successful responses through unchanged.
|
|
@@ -14461,14 +14688,14 @@ const logger$23 = getLogger();
|
|
|
14461
14688
|
function throwOnRPCError() {
|
|
14462
14689
|
return (0, import_cjs$21.map)((response) => {
|
|
14463
14690
|
if (response.error) {
|
|
14464
|
-
logger$
|
|
14691
|
+
logger$24.error("[throwOnRPCError] RPC error response:", {
|
|
14465
14692
|
code: response.error.code,
|
|
14466
14693
|
message: response.error.message,
|
|
14467
14694
|
data: response.error.data
|
|
14468
14695
|
});
|
|
14469
14696
|
throw new JSONRPCError(response.error.code, response.error.message, response.error.data);
|
|
14470
14697
|
}
|
|
14471
|
-
logger$
|
|
14698
|
+
logger$24.debug("[throwOnRPCError] RPC successful response:", response);
|
|
14472
14699
|
return response;
|
|
14473
14700
|
});
|
|
14474
14701
|
}
|
|
@@ -14476,7 +14703,7 @@ function throwOnRPCError() {
|
|
|
14476
14703
|
//#endregion
|
|
14477
14704
|
//#region src/managers/CallEventsManager.ts
|
|
14478
14705
|
var import_cjs$20 = require_cjs();
|
|
14479
|
-
const logger$
|
|
14706
|
+
const logger$23 = getLogger();
|
|
14480
14707
|
const initialSessionState = {};
|
|
14481
14708
|
/** @internal */
|
|
14482
14709
|
var CallEventsManager = class extends Destroyable {
|
|
@@ -14580,7 +14807,7 @@ var CallEventsManager = class extends Destroyable {
|
|
|
14580
14807
|
}
|
|
14581
14808
|
initSubscriptions() {
|
|
14582
14809
|
this.subscribeTo(this.callJoinedEvent$, (callJoinedEvent) => {
|
|
14583
|
-
logger$
|
|
14810
|
+
logger$23.debug("[CallEventsManager] Handling call.joined event for call/session IDs:", {
|
|
14584
14811
|
callId: callJoinedEvent.call_id,
|
|
14585
14812
|
roomSessionId: callJoinedEvent.room_session_id
|
|
14586
14813
|
});
|
|
@@ -14607,19 +14834,19 @@ var CallEventsManager = class extends Destroyable {
|
|
|
14607
14834
|
if (this._self$.value?.capabilities.setLayout) this.updateLayouts();
|
|
14608
14835
|
});
|
|
14609
14836
|
this.subscribeTo(this.memberUpdates$, (member) => {
|
|
14610
|
-
logger$
|
|
14837
|
+
logger$23.debug("[CallEventsManager] Handling member update event for member ID:", member);
|
|
14611
14838
|
this.upsertParticipant(member);
|
|
14612
14839
|
});
|
|
14613
14840
|
this.subscribeTo(this.webRtcCallSession.memberLeft$, (memberLeftEvent) => {
|
|
14614
|
-
logger$
|
|
14841
|
+
logger$23.debug("[CallEventsManager] Handling member.left event for member ID:", memberLeftEvent.member.member_id);
|
|
14615
14842
|
const participants = { ...this._participants$.value };
|
|
14616
14843
|
if (memberLeftEvent.member.member_id in participants) {
|
|
14617
14844
|
delete participants[memberLeftEvent.member.member_id];
|
|
14618
14845
|
this._participants$.next(participants);
|
|
14619
|
-
} else logger$
|
|
14846
|
+
} else logger$23.warn(`[CallEventsManager] Received member.left event for unknown member ID: ${memberLeftEvent.member.member_id}`);
|
|
14620
14847
|
});
|
|
14621
14848
|
this.subscribeTo(this.webRtcCallSession.callUpdated$, (callUpdatedEvent) => {
|
|
14622
|
-
logger$
|
|
14849
|
+
logger$23.debug("[CallEventsManager] Handling call.updated event:", callUpdatedEvent);
|
|
14623
14850
|
const roomSession = callUpdatedEvent.room_session;
|
|
14624
14851
|
this._sessionState$.next({
|
|
14625
14852
|
...this._sessionState$.value,
|
|
@@ -14634,7 +14861,7 @@ var CallEventsManager = class extends Destroyable {
|
|
|
14634
14861
|
});
|
|
14635
14862
|
});
|
|
14636
14863
|
this.subscribeTo(this.layoutChangedEvent$, (layoutChangedEvent) => {
|
|
14637
|
-
logger$
|
|
14864
|
+
logger$23.debug("[CallEventsManager] Handling layout.changed event:", layoutChangedEvent);
|
|
14638
14865
|
this._sessionState$.next({
|
|
14639
14866
|
...this._sessionState$.value,
|
|
14640
14867
|
layout_name: layoutChangedEvent.id,
|
|
@@ -14644,10 +14871,10 @@ var CallEventsManager = class extends Destroyable {
|
|
|
14644
14871
|
});
|
|
14645
14872
|
}
|
|
14646
14873
|
updateParticipantPositions(layoutChangedEvent) {
|
|
14647
|
-
if (Object.keys(this._participants$.value).length > 0 && !layoutChangedEvent.layers.some((layer) => !!layer.member_id)) logger$
|
|
14874
|
+
if (Object.keys(this._participants$.value).length > 0 && !layoutChangedEvent.layers.some((layer) => !!layer.member_id)) logger$23.warn("[CallEventsManager] No layers with member_id found in layout.changed event. Nothing to update.");
|
|
14648
14875
|
layoutChangedEvent.layers.filter((layer) => !!layer.member_id).filter((layer) => {
|
|
14649
14876
|
if (!(layer.member_id in this._participants$.value)) {
|
|
14650
|
-
logger$
|
|
14877
|
+
logger$23.warn(`[CallEventsManager] Skipping layout layer for unknown member_id: ${layer.member_id}`);
|
|
14651
14878
|
return false;
|
|
14652
14879
|
}
|
|
14653
14880
|
return true;
|
|
@@ -14665,12 +14892,17 @@ var CallEventsManager = class extends Destroyable {
|
|
|
14665
14892
|
updateLayouts() {
|
|
14666
14893
|
if (!this.selfId) return;
|
|
14667
14894
|
this.webRtcCallSession.executeMethod(this.selfId, "call.layout.list", {}).then((response) => {
|
|
14895
|
+
const layouts = response.result?.layouts;
|
|
14896
|
+
if (!layouts) {
|
|
14897
|
+
logger$23.warn("[CallEventsManager] Layout list response carried no layouts; keeping current layouts");
|
|
14898
|
+
return;
|
|
14899
|
+
}
|
|
14668
14900
|
this._sessionState$.next({
|
|
14669
14901
|
...this._sessionState$.value,
|
|
14670
|
-
layouts
|
|
14902
|
+
layouts
|
|
14671
14903
|
});
|
|
14672
14904
|
}).catch((error) => {
|
|
14673
|
-
logger$
|
|
14905
|
+
logger$23.error("[CallEventsManager] Error fetching layouts:", error);
|
|
14674
14906
|
});
|
|
14675
14907
|
}
|
|
14676
14908
|
updateParticipants(members) {
|
|
@@ -14686,7 +14918,7 @@ var CallEventsManager = class extends Destroyable {
|
|
|
14686
14918
|
}
|
|
14687
14919
|
const participant = this._participants$.value[member.member_id];
|
|
14688
14920
|
const oldValue = participant.value;
|
|
14689
|
-
logger$
|
|
14921
|
+
logger$23.debug("[CallEventsManager] Updating participant:", member.member_id, {
|
|
14690
14922
|
oldValue,
|
|
14691
14923
|
newValue: member
|
|
14692
14924
|
});
|
|
@@ -14699,17 +14931,17 @@ var CallEventsManager = class extends Destroyable {
|
|
|
14699
14931
|
}
|
|
14700
14932
|
get callJoinedEvent$() {
|
|
14701
14933
|
return this.cachedObservable("callJoinedEvent$", () => this.webRtcCallSession.callEvent$.pipe((0, import_cjs$20.filter)(isCallJoinedPayload), (0, import_cjs$20.tap)((event) => {
|
|
14702
|
-
logger$
|
|
14934
|
+
logger$23.debug("[CallEventsManager] Call joined event:", event);
|
|
14703
14935
|
})));
|
|
14704
14936
|
}
|
|
14705
14937
|
get layoutChangedEvent$() {
|
|
14706
14938
|
return this.cachedObservable("layoutChangedEvent$", () => this.webRtcCallSession.callEvent$.pipe(filterAs(isLayoutChangedPayload, "layout"), (0, import_cjs$20.tap)((event) => {
|
|
14707
|
-
logger$
|
|
14939
|
+
logger$23.debug("[CallEventsManager] Layout changed event:", event);
|
|
14708
14940
|
})));
|
|
14709
14941
|
}
|
|
14710
14942
|
get memberUpdates$() {
|
|
14711
14943
|
return this.cachedObservable("memberUpdates$", () => (0, import_cjs$20.merge)(this.webRtcCallSession.memberJoined$, this.webRtcCallSession.memberUpdated$, this.webRtcCallSession.memberTalking$).pipe((0, import_cjs$20.map)((event) => event.member), (0, import_cjs$20.tap)((event) => {
|
|
14712
|
-
logger$
|
|
14944
|
+
logger$23.debug("[CallEventsManager] Member update event:", event);
|
|
14713
14945
|
})));
|
|
14714
14946
|
}
|
|
14715
14947
|
destroy() {
|
|
@@ -14728,6 +14960,111 @@ var CallEventsManager = class extends Destroyable {
|
|
|
14728
14960
|
}
|
|
14729
14961
|
};
|
|
14730
14962
|
|
|
14963
|
+
//#endregion
|
|
14964
|
+
//#region src/controllers/ConstraintFallbackHelper.ts
|
|
14965
|
+
/**
|
|
14966
|
+
* ConstraintFallbackHelper - Provides getUserMedia with automatic constraint
|
|
14967
|
+
* fallback on OverconstrainedError.
|
|
14968
|
+
*
|
|
14969
|
+
* When a specific device ID is requested, the helper tries progressively
|
|
14970
|
+
* looser constraints:
|
|
14971
|
+
* 1. `{ deviceId: { exact: deviceId } }` -- exact match
|
|
14972
|
+
* 2. `{ deviceId: deviceId }` -- preferred (browser may pick another)
|
|
14973
|
+
* 3. `{}` -- no constraint, browser default
|
|
14974
|
+
*
|
|
14975
|
+
* This prevents stale device IDs from blocking call setup.
|
|
14976
|
+
*
|
|
14977
|
+
* @see Section 5.8 and Section 11 of the Implementation Guide
|
|
14978
|
+
*/
|
|
14979
|
+
const logger$22 = getLogger();
|
|
14980
|
+
/**
|
|
14981
|
+
* Attempts getUserMedia with progressively looser constraints.
|
|
14982
|
+
*
|
|
14983
|
+
* The function tries three levels of constraint specificity for the given
|
|
14984
|
+
* device kind. Each level is only attempted if the previous one fails with
|
|
14985
|
+
* an OverconstrainedError. Non-OverconstrainedError failures (e.g.,
|
|
14986
|
+
* NotAllowedError) are thrown immediately without fallback.
|
|
14987
|
+
*
|
|
14988
|
+
* @param mediaDevices - Anything exposing `getUserMedia` (a full
|
|
14989
|
+
* `WebRTCMediaDevices`, or a shim wrapping one)
|
|
14990
|
+
* @param constraints - The full MediaStreamConstraints to use as a base
|
|
14991
|
+
* @param kind - Which track kind to apply fallback to ('audio' | 'video')
|
|
14992
|
+
* @param deviceId - The device ID to try (if undefined, calls getUserMedia as-is)
|
|
14993
|
+
* @returns The stream and the fallback level that succeeded
|
|
14994
|
+
* @throws When all fallback levels fail, or when a non-OverconstrainedError occurs
|
|
14995
|
+
*/
|
|
14996
|
+
async function getUserMediaWithFallback(mediaDevices, constraints, kind, deviceId) {
|
|
14997
|
+
if (!deviceId) return {
|
|
14998
|
+
stream: await mediaDevices.getUserMedia(constraints),
|
|
14999
|
+
fallbackLevel: "default"
|
|
15000
|
+
};
|
|
15001
|
+
const baseConstraints = typeof constraints[kind] === "object" ? constraints[kind] : {};
|
|
15002
|
+
try {
|
|
15003
|
+
const exactConstraints = {
|
|
15004
|
+
...constraints,
|
|
15005
|
+
[kind]: {
|
|
15006
|
+
...baseConstraints,
|
|
15007
|
+
deviceId: { exact: deviceId }
|
|
15008
|
+
}
|
|
15009
|
+
};
|
|
15010
|
+
return {
|
|
15011
|
+
stream: await mediaDevices.getUserMedia(exactConstraints),
|
|
15012
|
+
fallbackLevel: "exact"
|
|
15013
|
+
};
|
|
15014
|
+
} catch (error) {
|
|
15015
|
+
if (!isOverconstrainedError(error)) throw error;
|
|
15016
|
+
logger$22.debug(`[ConstraintFallbackHelper] Exact constraint failed for ${kind}, trying preferred`, { deviceId });
|
|
15017
|
+
}
|
|
15018
|
+
try {
|
|
15019
|
+
const preferredConstraints = {
|
|
15020
|
+
...constraints,
|
|
15021
|
+
[kind]: {
|
|
15022
|
+
...baseConstraints,
|
|
15023
|
+
deviceId
|
|
15024
|
+
}
|
|
15025
|
+
};
|
|
15026
|
+
return {
|
|
15027
|
+
stream: await mediaDevices.getUserMedia(preferredConstraints),
|
|
15028
|
+
fallbackLevel: "preferred"
|
|
15029
|
+
};
|
|
15030
|
+
} catch (error) {
|
|
15031
|
+
if (!isOverconstrainedError(error)) throw error;
|
|
15032
|
+
logger$22.debug(`[ConstraintFallbackHelper] Preferred constraint failed for ${kind}, trying default`, { deviceId });
|
|
15033
|
+
}
|
|
15034
|
+
try {
|
|
15035
|
+
const defaultConstraints = {
|
|
15036
|
+
...constraints,
|
|
15037
|
+
[kind]: { ...baseConstraints }
|
|
15038
|
+
};
|
|
15039
|
+
if (typeof defaultConstraints[kind] === "object") {
|
|
15040
|
+
const { deviceId: _removed, ...rest } = defaultConstraints[kind];
|
|
15041
|
+
defaultConstraints[kind] = rest;
|
|
15042
|
+
}
|
|
15043
|
+
const stream = await mediaDevices.getUserMedia(defaultConstraints);
|
|
15044
|
+
logger$22.warn(`[ConstraintFallbackHelper] Fell back to browser default for ${kind}`, { requestedDeviceId: deviceId });
|
|
15045
|
+
return {
|
|
15046
|
+
stream,
|
|
15047
|
+
fallbackLevel: "default"
|
|
15048
|
+
};
|
|
15049
|
+
} catch (error) {
|
|
15050
|
+
logger$22.error(`[ConstraintFallbackHelper] All fallback levels exhausted for ${kind}`, {
|
|
15051
|
+
deviceId,
|
|
15052
|
+
error
|
|
15053
|
+
});
|
|
15054
|
+
throw error;
|
|
15055
|
+
}
|
|
15056
|
+
}
|
|
15057
|
+
/**
|
|
15058
|
+
* Checks whether an error is an OverconstrainedError.
|
|
15059
|
+
*
|
|
15060
|
+
* Browsers may throw either a native OverconstrainedError or a DOMException
|
|
15061
|
+
* with a specific name.
|
|
15062
|
+
*/
|
|
15063
|
+
function isOverconstrainedError(error) {
|
|
15064
|
+
if (error instanceof Error) return error.name === "OverconstrainedError" || error.name === "ConstraintNotSatisfiedError";
|
|
15065
|
+
return false;
|
|
15066
|
+
}
|
|
15067
|
+
|
|
14731
15068
|
//#endregion
|
|
14732
15069
|
//#region src/helpers/SDPHelper.ts
|
|
14733
15070
|
/**
|
|
@@ -15286,6 +15623,7 @@ var LocalStreamController = class extends Destroyable {
|
|
|
15286
15623
|
this._localAudioTracks$ = this.createBehaviorSubject([]);
|
|
15287
15624
|
this._localVideoTracks$ = this.createBehaviorSubject([]);
|
|
15288
15625
|
this._mediaTrackEnded$ = this.createSubject();
|
|
15626
|
+
this._trackOrigins = /* @__PURE__ */ new WeakMap();
|
|
15289
15627
|
}
|
|
15290
15628
|
get localStream$() {
|
|
15291
15629
|
return this._localStream$.asObservable().pipe((0, import_cjs$17.takeUntil)(this.destroyed$));
|
|
@@ -15308,6 +15646,22 @@ var LocalStreamController = class extends Destroyable {
|
|
|
15308
15646
|
get localVideoTracks() {
|
|
15309
15647
|
return this._localVideoTracks$.value;
|
|
15310
15648
|
}
|
|
15649
|
+
tagTracks(tracks, origin) {
|
|
15650
|
+
for (const track of tracks) this._trackOrigins.set(track, origin);
|
|
15651
|
+
}
|
|
15652
|
+
setTrackOrigin(track, origin) {
|
|
15653
|
+
this._trackOrigins.set(track, origin);
|
|
15654
|
+
}
|
|
15655
|
+
getTrackOrigin(track) {
|
|
15656
|
+
return this._trackOrigins.get(track);
|
|
15657
|
+
}
|
|
15658
|
+
/**
|
|
15659
|
+
* Fail-safe: an unrecorded track reads as not-a-device-capture, so a missed
|
|
15660
|
+
* tagging site leaves media alone rather than destroying it.
|
|
15661
|
+
*/
|
|
15662
|
+
isDeviceCapture(track) {
|
|
15663
|
+
return this._trackOrigins.get(track) === "device";
|
|
15664
|
+
}
|
|
15311
15665
|
/**
|
|
15312
15666
|
* Build the local media stream based on the provided options.
|
|
15313
15667
|
*/
|
|
@@ -15317,13 +15671,16 @@ var LocalStreamController = class extends Destroyable {
|
|
|
15317
15671
|
if (this.options.inputAudioStream ?? this.options.inputVideoStream) {
|
|
15318
15672
|
const tracks = [...this.options.inputAudioStream?.getTracks() ?? [], ...this.options.inputVideoStream?.getTracks() ?? []];
|
|
15319
15673
|
stream = new MediaStream(tracks);
|
|
15674
|
+
this.tagTracks(tracks, "application");
|
|
15320
15675
|
} else if (this.options.propose === "screenshare") {
|
|
15321
|
-
|
|
15676
|
+
const audio = this.options.screenShareAudio ?? false;
|
|
15677
|
+
logger$19.debug("[LocalStreamController] Requesting display media for screen sharing with audio:", audio);
|
|
15322
15678
|
stream = await this.options.getDisplayMedia({
|
|
15323
15679
|
video: true,
|
|
15324
|
-
audio
|
|
15680
|
+
audio
|
|
15325
15681
|
});
|
|
15326
15682
|
logger$19.debug("[LocalStreamController] Screen share media obtained:", stream);
|
|
15683
|
+
this.tagTracks(stream.getTracks(), "display");
|
|
15327
15684
|
} else {
|
|
15328
15685
|
const constraints = {
|
|
15329
15686
|
audio: this.options.inputAudioDeviceConstraints,
|
|
@@ -15332,6 +15689,7 @@ var LocalStreamController = class extends Destroyable {
|
|
|
15332
15689
|
logger$19.debug("[LocalStreamController] Requesting user media with constraints:", constraints);
|
|
15333
15690
|
stream = await this.options.getUserMedia(constraints);
|
|
15334
15691
|
logger$19.debug("[LocalStreamController] User media obtained:", stream);
|
|
15692
|
+
this.tagTracks(stream.getTracks(), "device");
|
|
15335
15693
|
}
|
|
15336
15694
|
this._localStream$.next(stream);
|
|
15337
15695
|
this._localAudioTracks$.next(stream.getAudioTracks());
|
|
@@ -15341,10 +15699,13 @@ var LocalStreamController = class extends Destroyable {
|
|
|
15341
15699
|
/**
|
|
15342
15700
|
* Add a local media track to the local stream.
|
|
15343
15701
|
* @param track - The MediaStreamTrack to add
|
|
15702
|
+
* @param origin - Defaults to `'device'`; every internal caller passes a
|
|
15703
|
+
* fresh `getUserMedia` capture.
|
|
15344
15704
|
* @returns The MediaStream (either existing or newly created)
|
|
15345
15705
|
*/
|
|
15346
|
-
addTrack(track) {
|
|
15706
|
+
addTrack(track, origin = "device") {
|
|
15347
15707
|
const localStream = this._localStream$.value ?? new MediaStream();
|
|
15708
|
+
this._trackOrigins.set(track, origin);
|
|
15348
15709
|
track.addEventListener("ended", this.mediaTrackEndedHandler);
|
|
15349
15710
|
localStream.addTrack(track);
|
|
15350
15711
|
this._localStream$.next(localStream);
|
|
@@ -15614,15 +15975,27 @@ var TransceiverController = class extends Destroyable {
|
|
|
15614
15975
|
for (let i = 0; i < Number(msStreamsNumber); i++) this.peerConnection.addTransceiver("video", { direction: "recvonly" });
|
|
15615
15976
|
}
|
|
15616
15977
|
}
|
|
15978
|
+
/**
|
|
15979
|
+
* @returns whether every live sender of the kind took the constraints. A
|
|
15980
|
+
* skipped non-device sender, an exhausted fallback, and having no live sender
|
|
15981
|
+
* at all all report `false` — `mediaParamsUpdated.applied` is built from this,
|
|
15982
|
+
* and an application told `true` cannot tell a working push from a no-op.
|
|
15983
|
+
*/
|
|
15617
15984
|
async updateSendersConstraints(kind, constraints) {
|
|
15618
15985
|
if (!constraints) {
|
|
15619
15986
|
this.stopTrackSender(kind);
|
|
15620
|
-
return
|
|
15987
|
+
return false;
|
|
15621
15988
|
}
|
|
15622
15989
|
const senders = this.peerConnection.getSenders().filter((sender) => sender.track?.kind === kind && sender.track.readyState === "live");
|
|
15990
|
+
let applied = senders.length > 0;
|
|
15623
15991
|
for (const sender of senders) {
|
|
15624
15992
|
const { track } = sender;
|
|
15625
15993
|
if (track) {
|
|
15994
|
+
if (!this.options.localStreamController.isDeviceCapture(track)) {
|
|
15995
|
+
logger$18.debug(`[TransceiverController] Skipping ${kind} constraints for a non-device track (origin: ${this.options.localStreamController.getTrackOrigin(track) ?? "unrecorded"}), track ${track.id}`);
|
|
15996
|
+
applied = false;
|
|
15997
|
+
continue;
|
|
15998
|
+
}
|
|
15626
15999
|
const constraintsToApply = {
|
|
15627
16000
|
...track.getConstraints(),
|
|
15628
16001
|
...constraints
|
|
@@ -15638,33 +16011,39 @@ var TransceiverController = class extends Destroyable {
|
|
|
15638
16011
|
} catch (fallbackError) {
|
|
15639
16012
|
logger$18.warn(`[TransceiverController] Track replacement fallback also failed for ${kind} track:`, fallbackError);
|
|
15640
16013
|
this.options.onError?.(new MediaTrackError("updateSendersConstraints", kind, fallbackError));
|
|
16014
|
+
applied = false;
|
|
15641
16015
|
}
|
|
15642
16016
|
}
|
|
15643
16017
|
}
|
|
15644
16018
|
}
|
|
16019
|
+
return applied;
|
|
15645
16020
|
}
|
|
15646
16021
|
/**
|
|
15647
|
-
* Fallback when applyConstraints fails
|
|
15648
|
-
* one via getUserMedia with the merged constraints (preserving the current
|
|
15649
|
-
* deviceId), replace the sender track, and update the localStream.
|
|
16022
|
+
* Fallback when applyConstraints fails, which on iOS Safari it silently does.
|
|
15650
16023
|
*
|
|
15651
|
-
*
|
|
15652
|
-
*
|
|
16024
|
+
* Order matters: acquiring before stopping means a failed acquisition leaves
|
|
16025
|
+
* the existing media playing. The deviceId goes through the fallback ladder
|
|
16026
|
+
* rather than pinned `{ exact }`, so a stale id degrades instead of failing.
|
|
15653
16027
|
*/
|
|
15654
16028
|
async replaceTrackFallback(sender, oldTrack, kind, mergedConstraints) {
|
|
15655
16029
|
const { deviceId } = oldTrack.getSettings();
|
|
15656
|
-
const
|
|
15657
|
-
|
|
15658
|
-
|
|
15659
|
-
|
|
15660
|
-
|
|
16030
|
+
const { stream, fallbackLevel } = await getUserMediaWithFallback({ getUserMedia: this.options.getUserMedia }, { [kind]: mergedConstraints }, kind, deviceId);
|
|
16031
|
+
const newTrack = stream.getTracks().find((t) => t.kind === kind);
|
|
16032
|
+
if (!newTrack) {
|
|
16033
|
+
stream.getTracks().forEach((t) => t.stop());
|
|
16034
|
+
throw new MediaTrackError("replaceTrackFallback", kind, /* @__PURE__ */ new Error("getUserMedia returned no track of the requested kind"));
|
|
16035
|
+
}
|
|
16036
|
+
try {
|
|
16037
|
+
await sender.replaceTrack(newTrack);
|
|
16038
|
+
} catch (error) {
|
|
16039
|
+
stream.getTracks().forEach((t) => t.stop());
|
|
16040
|
+
throw error;
|
|
16041
|
+
}
|
|
16042
|
+
const oldTrackId = oldTrack.id;
|
|
16043
|
+
this.options.localStreamController.removeTrack(oldTrackId);
|
|
15661
16044
|
oldTrack.stop();
|
|
15662
|
-
this.options.localStreamController.removeTrack(trackId);
|
|
15663
|
-
const newTrack = (await this.options.getUserMedia({ [kind]: constraintsWithDevice })).getTracks().find((t) => t.kind === kind);
|
|
15664
|
-
if (!newTrack) throw new MediaTrackError("replaceTrackFallback", kind, /* @__PURE__ */ new Error("getUserMedia returned no track of the requested kind"));
|
|
15665
|
-
await sender.replaceTrack(newTrack);
|
|
15666
16045
|
this.options.localStreamController.addTrack(newTrack);
|
|
15667
|
-
logger$18.debug(`[TransceiverController] Track replacement fallback succeeded for ${kind}. New track: ${newTrack.id}`);
|
|
16046
|
+
logger$18.debug(`[TransceiverController] Track replacement fallback succeeded for ${kind} (deviceId fallback level: ${fallbackLevel}). New track: ${newTrack.id}`);
|
|
15668
16047
|
}
|
|
15669
16048
|
getMediaDirections() {
|
|
15670
16049
|
if (this.peerConnection.connectionState === "connected") return this.peerConnection.getTransceivers().reduce((acc, transceiver) => {
|
|
@@ -15734,48 +16113,44 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
15734
16113
|
this.negotiationNeeded$.next();
|
|
15735
16114
|
};
|
|
15736
16115
|
this.updateSelectedInputDevice = async (kind, deviceInfo) => {
|
|
16116
|
+
const { localStream } = this;
|
|
16117
|
+
if (!localStream) {
|
|
16118
|
+
logger$17.warn("[RTCPeerConnectionController] No local stream available to update input device.");
|
|
16119
|
+
return;
|
|
16120
|
+
}
|
|
16121
|
+
const currentTrack = localStream.getTracks().find((track) => track.kind === kind);
|
|
16122
|
+
if (!currentTrack) {
|
|
16123
|
+
logger$17.debug(`[RTCPeerConnectionController] No ${kind} track to switch.`);
|
|
16124
|
+
return;
|
|
16125
|
+
}
|
|
16126
|
+
if (!deviceInfo) {
|
|
16127
|
+
logger$17.debug(`[RTCPeerConnectionController] ${kind} input device selected: none`);
|
|
16128
|
+
this.stopTrackSender(kind);
|
|
16129
|
+
return;
|
|
16130
|
+
}
|
|
16131
|
+
const constraints = {
|
|
16132
|
+
...currentTrack.getConstraints(),
|
|
16133
|
+
...this.deviceController.deviceInfoToConstraints(deviceInfo)
|
|
16134
|
+
};
|
|
15737
16135
|
try {
|
|
15738
|
-
const
|
|
15739
|
-
|
|
15740
|
-
|
|
15741
|
-
return;
|
|
15742
|
-
}
|
|
15743
|
-
logger$17.debug(`[RTCPeerConnectionController] Updating selected ${kind} input device:`, localStream.getTracks());
|
|
15744
|
-
const track = localStream.getTracks().find((track$1) => track$1.kind === kind);
|
|
15745
|
-
if (track) {
|
|
15746
|
-
this.transceiverController?.stopTrackSender(kind);
|
|
15747
|
-
this.localStreamController.removeTrack(track.id);
|
|
15748
|
-
logger$17.debug(`[RTCPeerConnectionController] Stopped existing ${kind} track: ${track.id}`, localStream.getTracks());
|
|
15749
|
-
if (!deviceInfo) {
|
|
15750
|
-
logger$17.debug(`[RTCPeerConnectionController] ${kind} input device selected: none`);
|
|
15751
|
-
return;
|
|
15752
|
-
}
|
|
15753
|
-
const streamTrack = (await this.getUserMedia({ [kind]: {
|
|
15754
|
-
...track.getConstraints(),
|
|
15755
|
-
...this.deviceController.deviceInfoToConstraints(deviceInfo)
|
|
15756
|
-
} })).getTracks().find((t) => t.kind === kind);
|
|
15757
|
-
if (streamTrack) {
|
|
15758
|
-
logger$17.debug(`[RTCPeerConnectionController] Adding new ${kind} track: ${streamTrack.id}`);
|
|
15759
|
-
this.localStreamController.addTrack(streamTrack);
|
|
15760
|
-
await this.transceiverController?.replaceSenderTrack(kind, streamTrack);
|
|
15761
|
-
logger$17.debug(`[RTCPeerConnectionController] Added new ${kind} track: ${streamTrack.id}`, this.localStream?.getTracks());
|
|
15762
|
-
}
|
|
15763
|
-
}
|
|
15764
|
-
logger$17.debug(`[RTCPeerConnectionController] ${kind} input device selected:`, deviceInfo?.label);
|
|
16136
|
+
const newTrack = await this.acquireInputTrack(kind, constraints, deviceInfo, currentTrack);
|
|
16137
|
+
await this.attachInputTrack(kind, newTrack, currentTrack);
|
|
16138
|
+
logger$17.debug(`[RTCPeerConnectionController] ${kind} input device selected:`, deviceInfo.label, newTrack.id);
|
|
15765
16139
|
} catch (error) {
|
|
15766
16140
|
logger$17.error(`[RTCPeerConnectionController] Failed to select ${kind} input device:`, error);
|
|
15767
16141
|
this._errors$.next(new MediaTrackError("updateSelectedInputDevice", kind, error));
|
|
15768
|
-
throw error;
|
|
15769
16142
|
}
|
|
15770
16143
|
};
|
|
15771
16144
|
this._isNegotiating$ = this.createBehaviorSubject(false);
|
|
15772
16145
|
this._memberId = null;
|
|
16146
|
+
this._nodeId = null;
|
|
15773
16147
|
this._iceConnectionState$ = this.createReplaySubject(1);
|
|
15774
16148
|
this._connectionState$ = this.createReplaySubject(1);
|
|
15775
16149
|
this._signalingState$ = this.createReplaySubject(1);
|
|
15776
16150
|
this._iceGatheringState$ = this.createReplaySubject(1);
|
|
15777
16151
|
this._errors$ = this.createReplaySubject(1);
|
|
15778
16152
|
this._iceCandidates$ = this.createReplaySubject(1);
|
|
16153
|
+
this._localMediaSettled$ = this.createReplaySubject(1);
|
|
15779
16154
|
this._initialized$ = this.createReplaySubject(1);
|
|
15780
16155
|
this._remoteDescription$ = this.createReplaySubject(1);
|
|
15781
16156
|
this._remoteStream$ = this.createBehaviorSubject(null);
|
|
@@ -15808,6 +16183,7 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
15808
16183
|
inputVideoStream: this.options.inputVideoStream,
|
|
15809
16184
|
inputAudioDeviceConstraints: this.inputAudioDeviceConstraints,
|
|
15810
16185
|
inputVideoDeviceConstraints: this.inputVideoDeviceConstraints,
|
|
16186
|
+
screenShareAudio: this.options.screenShareAudio,
|
|
15811
16187
|
getUserMedia: async (constraints) => this.getUserMedia(constraints),
|
|
15812
16188
|
getDisplayMedia: async (options$1) => this.getDisplayMedia(options$1)
|
|
15813
16189
|
});
|
|
@@ -15834,6 +16210,13 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
15834
16210
|
get memberId() {
|
|
15835
16211
|
return this._memberId;
|
|
15836
16212
|
}
|
|
16213
|
+
/** The node this leg's invite landed on — auxiliary legs are placed independently. */
|
|
16214
|
+
setNodeId(nodeId) {
|
|
16215
|
+
this._nodeId = nodeId;
|
|
16216
|
+
}
|
|
16217
|
+
get nodeId() {
|
|
16218
|
+
return this._nodeId;
|
|
16219
|
+
}
|
|
15837
16220
|
stopTrackSender(kind, options = { updateTransceiverDirection: false }) {
|
|
15838
16221
|
const audioCovered = kind === "audio" || kind === "both";
|
|
15839
16222
|
if (audioCovered && this._localAudioPipeline) this.stopRawAudioInputForPipeline();
|
|
@@ -15843,10 +16226,7 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
15843
16226
|
}
|
|
15844
16227
|
stopRawAudioInputForPipeline() {
|
|
15845
16228
|
const rawTracks = this.localStreamController.localAudioTracks;
|
|
15846
|
-
for (const track of rawTracks) if (track.readyState === "live")
|
|
15847
|
-
track.stop();
|
|
15848
|
-
this.localStreamController.removeTrack(track.id);
|
|
15849
|
-
}
|
|
16229
|
+
for (const track of rawTracks) if (track.readyState === "live") this.localStreamController.removeTrack(track.id);
|
|
15850
16230
|
this._localAudioPipeline?.setInputTrack(null);
|
|
15851
16231
|
}
|
|
15852
16232
|
get isNegotiating$() {
|
|
@@ -15879,6 +16259,10 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
15879
16259
|
get remoteDescription$() {
|
|
15880
16260
|
return this.cachedObservable("remoteDescription$", () => this._remoteDescription$.asObservable().pipe((0, import_cjs$16.takeUntil)(this.destroyed$)));
|
|
15881
16261
|
}
|
|
16262
|
+
/** Emits once local media is settled — acquired, or knowingly receive-only. */
|
|
16263
|
+
get localMediaSettled$() {
|
|
16264
|
+
return this.cachedObservable("localMediaSettled$", () => this._localMediaSettled$.asObservable().pipe((0, import_cjs$16.takeUntil)(this.destroyed$)));
|
|
16265
|
+
}
|
|
15882
16266
|
get localStream$() {
|
|
15883
16267
|
return this.cachedObservable("localStream$", () => this.localStreamController.localStream$.pipe((0, import_cjs$16.takeUntil)(this.destroyed$)));
|
|
15884
16268
|
}
|
|
@@ -15906,6 +16290,9 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
15906
16290
|
get propose() {
|
|
15907
16291
|
return this.options.propose ?? "main";
|
|
15908
16292
|
}
|
|
16293
|
+
get connectionState() {
|
|
16294
|
+
return this.peerConnection?.connectionState;
|
|
16295
|
+
}
|
|
15909
16296
|
get isAdditionalDevice() {
|
|
15910
16297
|
return this.propose === "additional-device";
|
|
15911
16298
|
}
|
|
@@ -16034,7 +16421,7 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
16034
16421
|
this._isNegotiating$.next(true);
|
|
16035
16422
|
await this._setRemoteDescription(this.sdpInit);
|
|
16036
16423
|
} else {
|
|
16037
|
-
await this.setupTrackHandling();
|
|
16424
|
+
if (!await this.setupTrackHandling()) return;
|
|
16038
16425
|
this._initialized$.next(true);
|
|
16039
16426
|
}
|
|
16040
16427
|
} catch (error) {
|
|
@@ -16158,7 +16545,10 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
16158
16545
|
inputVideoDeviceConstraints: this.inputVideoDeviceConstraints
|
|
16159
16546
|
});
|
|
16160
16547
|
}
|
|
16161
|
-
await this.setupLocalTracks()
|
|
16548
|
+
if (!await this.setupLocalTracks()) {
|
|
16549
|
+
logger$17.debug("[RTCPeerConnectionController] Inbound answer abandoned; the connection went away.");
|
|
16550
|
+
return;
|
|
16551
|
+
}
|
|
16162
16552
|
const { answerOptions } = this;
|
|
16163
16553
|
logger$17.debug("[RTCPeerConnectionController] Creating inbound answer with options:", answerOptions);
|
|
16164
16554
|
await this.createAnswer(answerOptions);
|
|
@@ -16296,35 +16686,52 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
16296
16686
|
}
|
|
16297
16687
|
/**
|
|
16298
16688
|
* Setup track handling for remote tracks.
|
|
16689
|
+
*
|
|
16690
|
+
* @returns `false` when the connection went away while local media was being
|
|
16691
|
+
* acquired — see {@link setupLocalTracks}.
|
|
16299
16692
|
*/
|
|
16300
16693
|
async setupTrackHandling() {
|
|
16301
16694
|
if (!this.peerConnection) throw new DependencyError("RTCPeerConnection is not initialized");
|
|
16302
|
-
await this.setupLocalTracks();
|
|
16695
|
+
if (!await this.setupLocalTracks()) return false;
|
|
16303
16696
|
await this.setupRemoteTracks();
|
|
16697
|
+
return true;
|
|
16304
16698
|
}
|
|
16699
|
+
/**
|
|
16700
|
+
* @returns `false` when the connection was torn down while getUserMedia was
|
|
16701
|
+
* in flight. The acquisition is not cancellable, so the caller must stop
|
|
16702
|
+
* rather than go on to touch a peer connection that is closed or gone.
|
|
16703
|
+
*/
|
|
16305
16704
|
async setupLocalTracks() {
|
|
16306
16705
|
logger$17.debug("[RTCPeerConnectionController] Setting up local tracks/transceivers.");
|
|
16307
16706
|
if (this.hasNoLocalMediaToSend()) {
|
|
16308
16707
|
if (!this.receiveAudio && !this.receiveVideo) throw new InvalidParams("Call requests no media: enable audio/video or receiveAudio/receiveVideo");
|
|
16309
16708
|
logger$17.debug("[RTCPeerConnectionController] No local media requested; negotiating receive-only.");
|
|
16310
16709
|
this.setupReceiveOnlyTransceivers();
|
|
16311
|
-
|
|
16710
|
+
this._localMediaSettled$.next();
|
|
16711
|
+
return true;
|
|
16312
16712
|
}
|
|
16313
16713
|
let localStream;
|
|
16314
16714
|
try {
|
|
16315
16715
|
localStream = this.localStream ?? await this.localStreamController.buildLocalStream();
|
|
16316
16716
|
} catch (error) {
|
|
16317
16717
|
this.handleLocalMediaFailure(error);
|
|
16318
|
-
|
|
16718
|
+
this._localMediaSettled$.next();
|
|
16719
|
+
return true;
|
|
16720
|
+
}
|
|
16721
|
+
if (!this.peerConnection || this.peerConnection.signalingState === "closed") {
|
|
16722
|
+
logger$17.debug("[RTCPeerConnectionController] Local media arrived after teardown; releasing it.");
|
|
16723
|
+
localStream.getTracks().forEach((track) => track.stop());
|
|
16724
|
+
return false;
|
|
16319
16725
|
}
|
|
16726
|
+
this._localMediaSettled$.next();
|
|
16320
16727
|
if (this.transceiverController?.useAddStream ?? false) {
|
|
16321
16728
|
logger$17.warn("[RTCPeerConnectionController] Using deprecated addStream API to add local stream.");
|
|
16322
|
-
this.peerConnection
|
|
16729
|
+
this.peerConnection.addStream(localStream);
|
|
16323
16730
|
if (!this.isNegotiating) {
|
|
16324
16731
|
logger$17.debug("[RTCPeerConnectionController] Forcing negotiationneeded after local tracks setup.");
|
|
16325
16732
|
this.negotiationNeeded$.next();
|
|
16326
16733
|
}
|
|
16327
|
-
return;
|
|
16734
|
+
return true;
|
|
16328
16735
|
}
|
|
16329
16736
|
for (const kind of ["audio", "video"]) {
|
|
16330
16737
|
const tracks = (kind === "audio" ? localStream.getAudioTracks() : localStream.getVideoTracks()).map((track, index) => ({
|
|
@@ -16338,10 +16745,11 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
16338
16745
|
await this.transceiverController?.setupTransceiverSender(track, localStream, transceivers[index]);
|
|
16339
16746
|
} else {
|
|
16340
16747
|
logger$17.debug(`[RTCPeerConnectionController] Using addTrack for local ${kind} track:`, track.id);
|
|
16341
|
-
this.peerConnection
|
|
16748
|
+
this.peerConnection.addTrack(track, localStream);
|
|
16342
16749
|
}
|
|
16343
16750
|
}
|
|
16344
16751
|
}
|
|
16752
|
+
return true;
|
|
16345
16753
|
}
|
|
16346
16754
|
/** True for a main connection with no local media to send. */
|
|
16347
16755
|
hasNoLocalMediaToSend() {
|
|
@@ -16430,6 +16838,61 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
16430
16838
|
this._localAudioPipeline.setInputTrack(newTrack);
|
|
16431
16839
|
}
|
|
16432
16840
|
/**
|
|
16841
|
+
* Capture the newly selected device, leaving the current capture running.
|
|
16842
|
+
*
|
|
16843
|
+
* A rejection must leave the current track sending, so nothing is released
|
|
16844
|
+
* until the replacement is in hand. The one exception is hardware that admits
|
|
16845
|
+
* a single opener — a phone's front and back cameras, typically — which
|
|
16846
|
+
* rejects the second capture until the first is closed.
|
|
16847
|
+
*/
|
|
16848
|
+
async acquireInputTrack(kind, constraints, deviceInfo, currentTrack) {
|
|
16849
|
+
try {
|
|
16850
|
+
return await this.captureTrack(kind, constraints, deviceInfo.deviceId);
|
|
16851
|
+
} catch (error) {
|
|
16852
|
+
if (!isMediaDeviceInUse(error)) throw error;
|
|
16853
|
+
logger$17.warn(`[RTCPeerConnectionController] ${kind} device is held exclusively; releasing the current capture to retry:`, error);
|
|
16854
|
+
const previousDeviceId = currentTrack.getSettings().deviceId;
|
|
16855
|
+
this.stopTrackSender(kind);
|
|
16856
|
+
try {
|
|
16857
|
+
return await this.captureTrack(kind, constraints, deviceInfo.deviceId);
|
|
16858
|
+
} catch (retryError) {
|
|
16859
|
+
await this.restorePreviousInputTrack(kind, constraints, previousDeviceId, currentTrack);
|
|
16860
|
+
throw retryError;
|
|
16861
|
+
}
|
|
16862
|
+
}
|
|
16863
|
+
}
|
|
16864
|
+
async captureTrack(kind, constraints, deviceId) {
|
|
16865
|
+
const { stream, fallbackLevel } = await getUserMediaWithFallback({ getUserMedia: async (c) => this.getUserMedia(c) }, { [kind]: constraints }, kind, deviceId);
|
|
16866
|
+
const track = stream.getTracks().find((t) => t.kind === kind);
|
|
16867
|
+
if (!track) {
|
|
16868
|
+
stream.getTracks().forEach((t) => t.stop());
|
|
16869
|
+
throw new MediaTrackError("updateSelectedInputDevice", kind, /* @__PURE__ */ new Error("getUserMedia returned no track of the requested kind"));
|
|
16870
|
+
}
|
|
16871
|
+
if (fallbackLevel !== "exact") logger$17.warn(`[RTCPeerConnectionController] ${kind} device acquired at fallback level '${fallbackLevel}'; the capture may not be the requested device.`);
|
|
16872
|
+
return track;
|
|
16873
|
+
}
|
|
16874
|
+
/** Best-effort return to the device that was released for an exclusive retry. */
|
|
16875
|
+
async restorePreviousInputTrack(kind, constraints, previousDeviceId, releasedTrack) {
|
|
16876
|
+
try {
|
|
16877
|
+
const restored = await this.captureTrack(kind, constraints, previousDeviceId);
|
|
16878
|
+
await this.attachInputTrack(kind, restored, releasedTrack);
|
|
16879
|
+
} catch (error) {
|
|
16880
|
+
logger$17.error(`[RTCPeerConnectionController] Failed to restore the previous ${kind} device:`, error);
|
|
16881
|
+
}
|
|
16882
|
+
}
|
|
16883
|
+
async attachInputTrack(kind, newTrack, oldTrack) {
|
|
16884
|
+
const pipelineOwnsAudio = kind === "audio" && this._localAudioPipeline;
|
|
16885
|
+
if (!pipelineOwnsAudio) try {
|
|
16886
|
+
await this.transceiverController?.replaceSenderTrack(kind, newTrack);
|
|
16887
|
+
} catch (error) {
|
|
16888
|
+
newTrack.stop();
|
|
16889
|
+
throw error;
|
|
16890
|
+
}
|
|
16891
|
+
this.localStreamController.removeTrack(oldTrack.id);
|
|
16892
|
+
this.localStreamController.addTrack(newTrack);
|
|
16893
|
+
if (pipelineOwnsAudio) this._localAudioPipeline?.setInputTrack(newTrack);
|
|
16894
|
+
}
|
|
16895
|
+
/**
|
|
16433
16896
|
* Return the lazily-created {@link LocalAudioPipeline}, constructing it on
|
|
16434
16897
|
* first access. On creation the current audio sender's track is routed
|
|
16435
16898
|
* through the pipeline (input → gain → analyser → destination) and the
|
|
@@ -16462,6 +16925,7 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
16462
16925
|
const sender = (this.transceiverController?.audioTransceivers.at(0))?.sender ?? this.peerConnection.getSenders().find((s) => s.track?.kind === "audio");
|
|
16463
16926
|
if (!sender || !raw) return;
|
|
16464
16927
|
try {
|
|
16928
|
+
this.localStreamController.setTrackOrigin(this._localAudioPipeline.outputTrack, "processed");
|
|
16465
16929
|
await sender.replaceTrack(this._localAudioPipeline.outputTrack);
|
|
16466
16930
|
} catch (error) {
|
|
16467
16931
|
logger$17.warn("[RTCPeerConnectionController] Failed to route audio sender through pipeline:", error);
|
|
@@ -16522,37 +16986,68 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
16522
16986
|
for (const existingTrack of existingTracks) this.removeLocalTrack(existingTrack.id);
|
|
16523
16987
|
this.addLocalTrack(track);
|
|
16524
16988
|
}
|
|
16989
|
+
/**
|
|
16990
|
+
* @returns whether the constraints reached the media the leg is sending.
|
|
16991
|
+
*
|
|
16992
|
+
* With the pipeline engaged the audio sender carries the processed
|
|
16993
|
+
* destination track, so the sender scan would find nothing it may touch and
|
|
16994
|
+
* every audio constraint API would silently no-op. The constraints belong to
|
|
16995
|
+
* the pipeline's device source, which is the capture that sender ultimately
|
|
16996
|
+
* carries.
|
|
16997
|
+
*/
|
|
16525
16998
|
async updateSendersConstraints(kind, constraints) {
|
|
16526
|
-
|
|
16999
|
+
if (kind === "audio" && this._localAudioPipeline) {
|
|
17000
|
+
if (!constraints) {
|
|
17001
|
+
this.stopTrackSender("audio");
|
|
17002
|
+
return false;
|
|
17003
|
+
}
|
|
17004
|
+
return this.applyPipelineSourceConstraints(constraints);
|
|
17005
|
+
}
|
|
17006
|
+
return await this.transceiverController?.updateSendersConstraints(kind, constraints) ?? false;
|
|
16527
17007
|
}
|
|
16528
17008
|
/**
|
|
16529
|
-
*
|
|
16530
|
-
*
|
|
16531
|
-
*
|
|
16532
|
-
*
|
|
17009
|
+
* Mirror of the sender path for a piped audio leg: same merge, same fallback
|
|
17010
|
+
* ladder, same device-capture invariant — but the swap target is the pipeline
|
|
17011
|
+
* input, so the sender keeps emitting the pipeline's output track and its
|
|
17012
|
+
* identity survives the change.
|
|
16533
17013
|
*/
|
|
16534
|
-
async
|
|
16535
|
-
const
|
|
16536
|
-
|
|
16537
|
-
|
|
16538
|
-
|
|
17014
|
+
async applyPipelineSourceConstraints(constraints) {
|
|
17015
|
+
const pipeline = this._localAudioPipeline;
|
|
17016
|
+
const source = this.localStreamController.localAudioTracks.at(0);
|
|
17017
|
+
if (!pipeline || !source) {
|
|
17018
|
+
logger$17.debug("[RTCPeerConnectionController] No pipeline input to constrain.");
|
|
17019
|
+
return false;
|
|
16539
17020
|
}
|
|
16540
|
-
|
|
16541
|
-
|
|
16542
|
-
|
|
16543
|
-
|
|
16544
|
-
|
|
16545
|
-
|
|
16546
|
-
|
|
16547
|
-
|
|
16548
|
-
|
|
16549
|
-
|
|
16550
|
-
|
|
16551
|
-
|
|
16552
|
-
|
|
16553
|
-
|
|
17021
|
+
if (!this.localStreamController.isDeviceCapture(source)) {
|
|
17022
|
+
logger$17.debug(`[RTCPeerConnectionController] Skipping audio constraints for a non-device pipeline input (origin: ${this.localStreamController.getTrackOrigin(source) ?? "unrecorded"}).`);
|
|
17023
|
+
return false;
|
|
17024
|
+
}
|
|
17025
|
+
const merged = {
|
|
17026
|
+
...source.getConstraints(),
|
|
17027
|
+
...constraints
|
|
17028
|
+
};
|
|
17029
|
+
try {
|
|
17030
|
+
await source.applyConstraints(merged);
|
|
17031
|
+
logger$17.debug("[RTCPeerConnectionController] Pipeline input constraints updated:", merged);
|
|
17032
|
+
return true;
|
|
17033
|
+
} catch (error) {
|
|
17034
|
+
logger$17.warn("[RTCPeerConnectionController] applyConstraints failed on the pipeline input, re-acquiring:", error);
|
|
17035
|
+
}
|
|
17036
|
+
try {
|
|
17037
|
+
const { stream } = await getUserMediaWithFallback({ getUserMedia: async (c) => this.getUserMedia(c) }, { audio: merged }, "audio", source.getSettings().deviceId);
|
|
17038
|
+
const newTrack = stream.getAudioTracks().at(0);
|
|
17039
|
+
if (!newTrack) {
|
|
17040
|
+
stream.getTracks().forEach((track) => track.stop());
|
|
17041
|
+
throw new Error("getUserMedia returned no audio track");
|
|
17042
|
+
}
|
|
17043
|
+
this.localStreamController.removeTrack(source.id);
|
|
16554
17044
|
this.localStreamController.addTrack(newTrack);
|
|
16555
|
-
|
|
17045
|
+
pipeline.setInputTrack(newTrack);
|
|
17046
|
+
return true;
|
|
17047
|
+
} catch (error) {
|
|
17048
|
+
logger$17.warn("[RTCPeerConnectionController] Failed to re-acquire the pipeline input for constraints:", error);
|
|
17049
|
+
this._errors$.next(new MediaTrackError("updateSendersConstraints", "audio", error));
|
|
17050
|
+
return false;
|
|
16556
17051
|
}
|
|
16557
17052
|
}
|
|
16558
17053
|
/**
|
|
@@ -16672,6 +17167,48 @@ const logger$16 = getLogger();
|
|
|
16672
17167
|
function resolveInviteNodeId(args) {
|
|
16673
17168
|
return args.isInvite && !args.reattach && !args.explicitNodeId ? "" : args.currentNodeId ?? "";
|
|
16674
17169
|
}
|
|
17170
|
+
/**
|
|
17171
|
+
* Surface the real outcome of a `webrtc.verto` reply.
|
|
17172
|
+
*
|
|
17173
|
+
* A webrtc.verto response nests several envelopes, each keyed by a verto-style
|
|
17174
|
+
* string `code` ("200" ok, "400"/etc. fail) rather than a JSON-RPC `error`. An outer
|
|
17175
|
+
* layer reports only whether the frame was delivered; an inner layer carries the op's
|
|
17176
|
+
* own outcome:
|
|
17177
|
+
*
|
|
17178
|
+
* response.result = { code:"200", result:{…} } ← delivery acknowledgement
|
|
17179
|
+
* .result = { jsonrpc, id, result:{…} } ← the reply payload
|
|
17180
|
+
* .result = { code:"400", message:"Bad request" } ← the actual op outcome
|
|
17181
|
+
*
|
|
17182
|
+
* A failure can appear at any layer (delivery refused, or the op itself rejected
|
|
17183
|
+
* deeper down), so walk every nested `.result` object and return the FIRST non-2xx
|
|
17184
|
+
* `code` with its message. Returns null when every `code` seen is 2xx or absent —
|
|
17185
|
+
* i.e. the op succeeded. This is the only way to detect that e.g. a mute/kick was
|
|
17186
|
+
* rejected, since the outer delivery `code` is "200" (delivered) even then.
|
|
17187
|
+
*
|
|
17188
|
+
* Pure function — exported for unit testing.
|
|
17189
|
+
*/
|
|
17190
|
+
function findNestedVertoFailure(response) {
|
|
17191
|
+
let node = response;
|
|
17192
|
+
while (node !== null && typeof node === "object") {
|
|
17193
|
+
const obj = node;
|
|
17194
|
+
const err = obj.error;
|
|
17195
|
+
if (err !== null && typeof err === "object") {
|
|
17196
|
+
const e = err;
|
|
17197
|
+
const errCode = typeof e.code === "string" || typeof e.code === "number" ? String(e.code) : void 0;
|
|
17198
|
+
if (errCode !== void 0 && !/^2\d\d$/.test(errCode)) return {
|
|
17199
|
+
code: errCode,
|
|
17200
|
+
message: typeof e.message === "string" ? e.message : void 0
|
|
17201
|
+
};
|
|
17202
|
+
}
|
|
17203
|
+
const code = typeof obj.code === "string" || typeof obj.code === "number" ? String(obj.code) : void 0;
|
|
17204
|
+
if (code !== void 0 && !/^2\d\d$/.test(code)) return {
|
|
17205
|
+
code,
|
|
17206
|
+
message: typeof obj.message === "string" ? obj.message : void 0
|
|
17207
|
+
};
|
|
17208
|
+
node = obj.result !== null && typeof obj.result === "object" ? obj.result : null;
|
|
17209
|
+
}
|
|
17210
|
+
return null;
|
|
17211
|
+
}
|
|
16675
17212
|
var VertoManager = class extends Destroyable {
|
|
16676
17213
|
constructor(callSession) {
|
|
16677
17214
|
super();
|
|
@@ -16694,7 +17231,8 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
16694
17231
|
this._signalingStatus$ = this.createReplaySubject(1);
|
|
16695
17232
|
this._screenShareStatus$ = this.createBehaviorSubject("none");
|
|
16696
17233
|
this._rtcPeerConnectionsMap = /* @__PURE__ */ new Map();
|
|
16697
|
-
this.
|
|
17234
|
+
this._lastVertoPings = /* @__PURE__ */ new Map();
|
|
17235
|
+
this._legErrors$ = this.createSubject();
|
|
16698
17236
|
this._nodeId$ = this.createBehaviorSubject(options.nodeId ?? null);
|
|
16699
17237
|
this.onError = options.onError;
|
|
16700
17238
|
this.onModifyFailed = options.onModifyFailed;
|
|
@@ -16742,6 +17280,10 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
16742
17280
|
get selfId$() {
|
|
16743
17281
|
return this._selfId$.asObservable();
|
|
16744
17282
|
}
|
|
17283
|
+
/** Separates the media phase of call creation from the signalling phase. */
|
|
17284
|
+
get localMediaSettled$() {
|
|
17285
|
+
return this.mainPeerConnection.localMediaSettled$;
|
|
17286
|
+
}
|
|
16745
17287
|
get localStream() {
|
|
16746
17288
|
return this._rtcPeerConnectionsMap.get(this.webRtcCallSession.id)?.localStream ?? null;
|
|
16747
17289
|
}
|
|
@@ -16798,25 +17340,89 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
16798
17340
|
const { mediaParams, callID } = event;
|
|
16799
17341
|
const rtcPeerConnController = this._rtcPeerConnectionsMap.get(callID);
|
|
16800
17342
|
const { audio, video } = mediaParams;
|
|
16801
|
-
|
|
16802
|
-
|
|
16803
|
-
|
|
16804
|
-
|
|
16805
|
-
|
|
16806
|
-
audio,
|
|
16807
|
-
video,
|
|
16808
|
-
timestamp: Date.now()
|
|
16809
|
-
});
|
|
16810
|
-
} catch (error) {
|
|
16811
|
-
logger$16.warn("[WebRTCManager] Error applying server-pushed media params:", error);
|
|
16812
|
-
this.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
16813
|
-
}
|
|
16814
|
-
})();
|
|
17343
|
+
if (!rtcPeerConnController) {
|
|
17344
|
+
logger$16.warn(`[WebRTCManager] Ignoring server-pushed media params for unknown leg ${callID}`);
|
|
17345
|
+
return;
|
|
17346
|
+
}
|
|
17347
|
+
this.applyServerMediaParams(rtcPeerConnController, audio, video);
|
|
16815
17348
|
});
|
|
16816
17349
|
this.subscribeTo(this.vertoPing$, (vertoPing) => {
|
|
16817
|
-
this.
|
|
17350
|
+
this._lastVertoPings.set(vertoPing.callID, vertoPing);
|
|
17351
|
+
this.attachManager.refresh(this.buildAttachableCall());
|
|
16818
17352
|
this.sendVertoPong(vertoPing);
|
|
16819
17353
|
});
|
|
17354
|
+
this.subscribeTo(this.webRtcCallSession.clientSession.authenticated$.pipe((0, import_cjs$15.skip)(1), (0, import_cjs$15.filter)(Boolean)), () => {
|
|
17355
|
+
this._lastVertoPings.forEach((vertoPing) => void this.sendVertoPong(vertoPing));
|
|
17356
|
+
});
|
|
17357
|
+
}
|
|
17358
|
+
/**
|
|
17359
|
+
* An auxiliary-leg failure must never destroy the call; main-leg and
|
|
17360
|
+
* call-level errors keep `CallFactory.isFatalError`'s classification.
|
|
17361
|
+
*
|
|
17362
|
+
* Every site holding a peer connection reports through here, so the invariant
|
|
17363
|
+
* is structural rather than per-call-site — which is how cloud-product#20523
|
|
17364
|
+
* happened, with only one of fourteen sites passing `{ fatal: false }`.
|
|
17365
|
+
*
|
|
17366
|
+
* `override` composes rather than replaces: a caller may force non-fatal for a
|
|
17367
|
+
* reason of its own (a `verto.info` frame is best-effort whichever leg carries
|
|
17368
|
+
* it), and an auxiliary leg stays non-fatal regardless.
|
|
17369
|
+
*/
|
|
17370
|
+
reportLegError(error, rtcPeerConnController, override) {
|
|
17371
|
+
const leg = rtcPeerConnController?.propose;
|
|
17372
|
+
const legId = rtcPeerConnController?.id;
|
|
17373
|
+
const auxiliary = Boolean(rtcPeerConnController) && !rtcPeerConnController?.isMainDevice;
|
|
17374
|
+
this.onError?.(error, {
|
|
17375
|
+
...override?.fatal === false || auxiliary ? { fatal: false } : {},
|
|
17376
|
+
...leg ? { leg } : {},
|
|
17377
|
+
...legId ? { legId } : {}
|
|
17378
|
+
});
|
|
17379
|
+
if (legId) this._legErrors$.next({
|
|
17380
|
+
legId,
|
|
17381
|
+
error
|
|
17382
|
+
});
|
|
17383
|
+
}
|
|
17384
|
+
/**
|
|
17385
|
+
* Errors reported for one leg, as a stream that fails with them.
|
|
17386
|
+
*
|
|
17387
|
+
* Signaling failures are reported, never thrown — so nothing that waits on a
|
|
17388
|
+
* leg's progress would otherwise learn of a rejected invite. Merging this in
|
|
17389
|
+
* lets the wait end with the reason the server gave.
|
|
17390
|
+
*/
|
|
17391
|
+
legError$(legId) {
|
|
17392
|
+
return this._legErrors$.pipe((0, import_cjs$15.filter)((report) => report.legId === legId), (0, import_cjs$15.map)((report) => {
|
|
17393
|
+
throw report.error;
|
|
17394
|
+
}));
|
|
17395
|
+
}
|
|
17396
|
+
/**
|
|
17397
|
+
* Audio and video are applied independently so a failure in one cannot
|
|
17398
|
+
* suppress the other, and `mediaParamsUpdated` is emitted whatever happens —
|
|
17399
|
+
* an application should not be starved of the params by a constraint failure.
|
|
17400
|
+
*/
|
|
17401
|
+
async applyServerMediaParams(rtcPeerConnController, audio, video) {
|
|
17402
|
+
const failures = [];
|
|
17403
|
+
let applied = true;
|
|
17404
|
+
if (audio) try {
|
|
17405
|
+
applied = await rtcPeerConnController.updateSendersConstraints("audio", audio) && applied;
|
|
17406
|
+
} catch (error) {
|
|
17407
|
+
applied = false;
|
|
17408
|
+
failures.push(toError(error));
|
|
17409
|
+
}
|
|
17410
|
+
if (video) try {
|
|
17411
|
+
applied = await rtcPeerConnController.updateSendersConstraints("video", video) && applied;
|
|
17412
|
+
} catch (error) {
|
|
17413
|
+
applied = false;
|
|
17414
|
+
failures.push(toError(error));
|
|
17415
|
+
}
|
|
17416
|
+
this.webRtcCallSession.emitMediaParamsUpdated({
|
|
17417
|
+
audio,
|
|
17418
|
+
video,
|
|
17419
|
+
timestamp: Date.now(),
|
|
17420
|
+
applied
|
|
17421
|
+
});
|
|
17422
|
+
for (const failure of failures) {
|
|
17423
|
+
logger$16.warn("[WebRTCManager] Error applying server-pushed media params:", failure);
|
|
17424
|
+
this.reportLegError(failure, rtcPeerConnController, { fatal: false });
|
|
17425
|
+
}
|
|
16820
17426
|
}
|
|
16821
17427
|
/**
|
|
16822
17428
|
* Set node_id/selfId only when the current value is null.
|
|
@@ -16825,8 +17431,8 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
16825
17431
|
* these identifiers before the `verto.invite` RPC response (`CALL CREATED`)
|
|
16826
17432
|
* arrives. These methods let early events populate them eagerly so that
|
|
16827
17433
|
* downstream RPC calls (e.g. `call.layout.list`) don't fail with empty
|
|
16828
|
-
* identifiers. `processInviteResponse()` remains the authoritative source
|
|
16829
|
-
*
|
|
17434
|
+
* identifiers. `processInviteResponse()` remains the authoritative source and
|
|
17435
|
+
* overwrites unconditionally — for selfId, on the main leg only.
|
|
16830
17436
|
*/
|
|
16831
17437
|
setNodeIdIfNull(nodeId) {
|
|
16832
17438
|
if (!this._nodeId$.value && nodeId) {
|
|
@@ -16849,24 +17455,33 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
16849
17455
|
this.onError?.(new VertoPongError(error));
|
|
16850
17456
|
}
|
|
16851
17457
|
}
|
|
17458
|
+
/**
|
|
17459
|
+
* @returns whether the constraints reached the media the call is sending.
|
|
17460
|
+
* `false` is an outcome, not a failure: the leg may have no live sender of
|
|
17461
|
+
* the kind, or carry media the SDK did not capture and may not replace.
|
|
17462
|
+
* A failure behind it still reaches the call's `errors$`, so a caller that
|
|
17463
|
+
* ignores this value learns of it there.
|
|
17464
|
+
*/
|
|
16852
17465
|
async updateMediaConstraints(options = {}) {
|
|
16853
17466
|
const { audio, video } = options;
|
|
17467
|
+
let applied = true;
|
|
16854
17468
|
try {
|
|
16855
|
-
if (audio) await this.mainPeerConnection.updateSendersConstraints("audio", audio);
|
|
16856
|
-
if (video) await this.mainPeerConnection.updateSendersConstraints("video", video);
|
|
17469
|
+
if (audio) applied = await this.mainPeerConnection.updateSendersConstraints("audio", audio) && applied;
|
|
17470
|
+
if (video) applied = await this.mainPeerConnection.updateSendersConstraints("video", video) && applied;
|
|
16857
17471
|
} catch (error) {
|
|
16858
17472
|
logger$16.warn("[WebRTCManager] Error updating media constraints:", error);
|
|
16859
|
-
this.
|
|
17473
|
+
this.reportLegError(toError(error), this.mainPeerConnection);
|
|
16860
17474
|
throw error;
|
|
16861
17475
|
}
|
|
17476
|
+
return applied;
|
|
16862
17477
|
}
|
|
16863
17478
|
get selfId() {
|
|
16864
17479
|
return this._selfId$.value;
|
|
16865
17480
|
}
|
|
16866
17481
|
/** Build an AttachableCall from the current call state. */
|
|
16867
|
-
buildAttachableCall(idOverride) {
|
|
17482
|
+
buildAttachableCall(idOverride, nodeIdOverride) {
|
|
16868
17483
|
return {
|
|
16869
|
-
nodeId: this.nodeId ?? void 0,
|
|
17484
|
+
nodeId: nodeIdOverride ?? this.nodeId ?? void 0,
|
|
16870
17485
|
id: idOverride ?? this.webRtcCallSession.id,
|
|
16871
17486
|
to: this.webRtcCallSession.to,
|
|
16872
17487
|
mediaDirections: this.webRtcCallSession.mediaDirections
|
|
@@ -16996,24 +17611,49 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
16996
17611
|
get vertoPing$() {
|
|
16997
17612
|
return this.cachedObservable("vertoPing$", () => this.webRtcCallSession.webrtcMessages$.pipe(filterAs(isVertoPingInnerParams, "params"), (0, import_cjs$15.takeUntil)(this.destroyed$)));
|
|
16998
17613
|
}
|
|
17614
|
+
/**
|
|
17615
|
+
* Send a member-control op in-dialog via verto.info.
|
|
17616
|
+
*
|
|
17617
|
+
* The control payload rides in `params.command` — a sibling of `dialogParams`,
|
|
17618
|
+
* at the same level as `dtmf` in {@link sendDigits} — and the inner verto.info
|
|
17619
|
+
* is matched to this call's channel by `dialogParams.callID`, the in-dialog
|
|
17620
|
+
* convention for member-scoped frames. Because it is delivered on the dialog
|
|
17621
|
+
* itself, control lands on the call's own channel with no {node_id,call_id,member_id}
|
|
17622
|
+
* "self" tuple to get wrong. The outer webrtc.verto envelope (added by executeVerto)
|
|
17623
|
+
* still carries the own-leg callID + node_id for session routing.
|
|
17624
|
+
*
|
|
17625
|
+
* Keep `command` OUT of `dialogParams`: it is read at the params level, and
|
|
17626
|
+
* filterVertoParams rewrites/filters dialogParams keys but passes params-level
|
|
17627
|
+
* keys through verbatim.
|
|
17628
|
+
*/
|
|
17629
|
+
async sendCallControl(method, params) {
|
|
17630
|
+
const response = await this.executeVerto(VertoInfo({
|
|
17631
|
+
dialogParams: { callID: this.webRtcCallSession.id },
|
|
17632
|
+
command: {
|
|
17633
|
+
method,
|
|
17634
|
+
params
|
|
17635
|
+
}
|
|
17636
|
+
}));
|
|
17637
|
+
const failure = findNestedVertoFailure(response);
|
|
17638
|
+
if (failure) throw new JSONRPCError(Number.parseInt(failure.code, 10) || 0, `Call control "${method}" failed (code ${failure.code})${failure.message ? `: ${failure.message}` : ""}`, void 0);
|
|
17639
|
+
return response;
|
|
17640
|
+
}
|
|
16999
17641
|
async executeVerto(message, optionals = {}) {
|
|
17000
|
-
const
|
|
17642
|
+
const params = {
|
|
17001
17643
|
callID: optionals.callID ?? this.webRtcCallSession.id,
|
|
17002
17644
|
node_id: optionals.node_id ?? this._nodeId$.value ?? "",
|
|
17003
17645
|
message,
|
|
17004
17646
|
subscribe: optionals.subscribe
|
|
17005
|
-
}
|
|
17647
|
+
};
|
|
17648
|
+
const webrtcVertoMessage = WebrtcVerto(params);
|
|
17006
17649
|
const response = await this.webRtcCallSession.execute(webrtcVertoMessage);
|
|
17007
|
-
|
|
17008
|
-
const error = new JSONRPCError(response.error.code, response.error.message, response.error.data);
|
|
17009
|
-
this.onError?.(error);
|
|
17010
|
-
return response;
|
|
17011
|
-
}
|
|
17650
|
+
const nonFatal = message.method === "verto.info" ? { fatal: false } : void 0;
|
|
17012
17651
|
const innerResult = getValueFrom(response, "result.result");
|
|
17013
|
-
|
|
17014
|
-
|
|
17015
|
-
|
|
17016
|
-
|
|
17652
|
+
const failure = response.error ?? innerResult?.error;
|
|
17653
|
+
if (failure) {
|
|
17654
|
+
const error = new JSONRPCError(failure.code, failure.message, failure.data);
|
|
17655
|
+
if (message.method === "verto.invite" || message.method === "verto.answer") throw error;
|
|
17656
|
+
this.reportLegError(error, this._rtcPeerConnectionsMap.get(params.callID), nonFatal);
|
|
17017
17657
|
}
|
|
17018
17658
|
return response;
|
|
17019
17659
|
}
|
|
@@ -17032,8 +17672,9 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
17032
17672
|
default:
|
|
17033
17673
|
}
|
|
17034
17674
|
} catch (error) {
|
|
17675
|
+
if (vertoMethod === "verto.answer") throw error;
|
|
17035
17676
|
logger$16.error(`[WebRTCManager] Error sending Verto ${vertoMethod}:`, error);
|
|
17036
|
-
this.
|
|
17677
|
+
this.reportLegError(toError(error), rtcPeerConnController);
|
|
17037
17678
|
if (vertoMethod === "verto.modify") this.onModifyFailed?.();
|
|
17038
17679
|
}
|
|
17039
17680
|
}
|
|
@@ -17049,7 +17690,7 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
17049
17690
|
} catch (error) {
|
|
17050
17691
|
logger$16.warn("[WebRTCManager] Error processing modify response:", error);
|
|
17051
17692
|
const modifyError = error instanceof Error ? error : new Error(String(error), { cause: error });
|
|
17052
|
-
this.
|
|
17693
|
+
this.reportLegError(modifyError, rtcPeerConnController);
|
|
17053
17694
|
}
|
|
17054
17695
|
}
|
|
17055
17696
|
}
|
|
@@ -17062,37 +17703,35 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
17062
17703
|
status,
|
|
17063
17704
|
signalingError
|
|
17064
17705
|
});
|
|
17065
|
-
this.
|
|
17706
|
+
this.reportLegError(signalingError, null, { fatal: false });
|
|
17066
17707
|
return;
|
|
17067
17708
|
}
|
|
17068
17709
|
if (rtcPeerConnController.isMainDevice) this._signalingStatus$.next(status);
|
|
17069
17710
|
}
|
|
17070
17711
|
processInviteResponse(response, rtcPeerConnController) {
|
|
17071
|
-
if (
|
|
17712
|
+
if (getValueFrom(response, "result.result.result.message") === "CALL CREATED") {
|
|
17072
17713
|
this.emitMainSignalingStatus(rtcPeerConnController.id, "trying");
|
|
17073
|
-
|
|
17714
|
+
const nodeId = getValueFrom(response, "result.node_id") ?? null;
|
|
17074
17715
|
const memberId = getValueFrom(response, "result.result.result.memberID") ?? null;
|
|
17075
|
-
const callId = getValueFrom(response, "result.result.result.callID")
|
|
17716
|
+
const callId = getValueFrom(response, "result.result.result.callID");
|
|
17076
17717
|
logger$16.debug("[WebRTCManager] Verto invite response:", {
|
|
17077
17718
|
callId,
|
|
17078
17719
|
memberId,
|
|
17079
17720
|
response
|
|
17080
17721
|
});
|
|
17081
|
-
this._selfId$.next(memberId);
|
|
17082
17722
|
rtcPeerConnController.setMemberId(memberId);
|
|
17083
|
-
|
|
17084
|
-
|
|
17085
|
-
this.
|
|
17086
|
-
|
|
17087
|
-
|
|
17088
|
-
|
|
17089
|
-
|
|
17723
|
+
rtcPeerConnController.setNodeId(nodeId);
|
|
17724
|
+
if (rtcPeerConnController.isMainDevice) {
|
|
17725
|
+
this._selfId$.next(memberId);
|
|
17726
|
+
this._nodeId$.next(nodeId);
|
|
17727
|
+
this.attachManager.attach(this.buildAttachableCall(callId, nodeId ?? void 0));
|
|
17728
|
+
}
|
|
17729
|
+
if (callId) this.webRtcCallSession.addCallId(callId);
|
|
17090
17730
|
logger$16.info("[WebRTCManager] Verto invite successful");
|
|
17091
17731
|
logger$16.debug(`[WebRTCManager] nodeid: ${this._nodeId$.value}, selfId: ${this._selfId$.value}`);
|
|
17092
17732
|
} else {
|
|
17093
17733
|
logger$16.error("[WebRTCManager] Verto invite failed:", response);
|
|
17094
|
-
|
|
17095
|
-
this.onError?.(inviteError);
|
|
17734
|
+
this.reportLegError(/* @__PURE__ */ new Error("Verto invite failed: unexpected response"), rtcPeerConnController);
|
|
17096
17735
|
}
|
|
17097
17736
|
}
|
|
17098
17737
|
get RTCPeerConnectionConfig() {
|
|
@@ -17131,7 +17770,7 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
17131
17770
|
this._rtcPeerConnectionsMap.set(rtcPeerConnController.id, rtcPeerConnController);
|
|
17132
17771
|
this._rtcPeerConnections$.next(Array.from(this._rtcPeerConnectionsMap.values()));
|
|
17133
17772
|
this.subscribeTo(rtcPeerConnController.errors$, (error) => {
|
|
17134
|
-
this.
|
|
17773
|
+
this.reportLegError(error, rtcPeerConnController);
|
|
17135
17774
|
});
|
|
17136
17775
|
if (options.initOffer) this.handleInboundAnswer(rtcPeerConnController);
|
|
17137
17776
|
}
|
|
@@ -17160,7 +17799,7 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
17160
17799
|
await rtcPeerConnController.acceptInbound(answerOptions);
|
|
17161
17800
|
} catch (error) {
|
|
17162
17801
|
logger$16.error("[WebRTCManager] Error creating inbound answer:", error);
|
|
17163
|
-
this.
|
|
17802
|
+
this.reportLegError(toError(error), rtcPeerConnController);
|
|
17164
17803
|
}
|
|
17165
17804
|
}
|
|
17166
17805
|
}
|
|
@@ -17232,7 +17871,7 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
17232
17871
|
isInvite: isVertoInviteMessage(vertoMessage),
|
|
17233
17872
|
reattach: this.webRtcCallSession.options.reattach === true,
|
|
17234
17873
|
explicitNodeId: this.webRtcCallSession.options.nodeId,
|
|
17235
|
-
currentNodeId: this._nodeId$.value
|
|
17874
|
+
currentNodeId: rtcPeerConnController.nodeId ?? this._nodeId$.value
|
|
17236
17875
|
}),
|
|
17237
17876
|
subscribe
|
|
17238
17877
|
};
|
|
@@ -17264,7 +17903,7 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
17264
17903
|
await this.attachManager.attach(this.buildAttachableCall());
|
|
17265
17904
|
} catch (error) {
|
|
17266
17905
|
logger$16.error("[WebRTCManager] Error sending Verto answer:", error);
|
|
17267
|
-
this.
|
|
17906
|
+
this.reportLegError(toError(error), rtcPeerConnectionController);
|
|
17268
17907
|
await rtcPeerConnectionController.updateAnswerStatus({ status: "failed" });
|
|
17269
17908
|
}
|
|
17270
17909
|
}
|
|
@@ -17336,15 +17975,19 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
17336
17975
|
await this.mainPeerConnection.restoreTrackSender(deviceKind);
|
|
17337
17976
|
} else {
|
|
17338
17977
|
const error = new InvalidParams("No valid device to be added");
|
|
17339
|
-
this.
|
|
17978
|
+
this.reportLegError(error, this.mainPeerConnection);
|
|
17340
17979
|
throw error;
|
|
17341
17980
|
}
|
|
17342
17981
|
}
|
|
17343
|
-
async addScreenMedia(options = {
|
|
17344
|
-
await this.initAdditionalPeerConnection("screenshare",
|
|
17982
|
+
async addScreenMedia(options = {}) {
|
|
17983
|
+
await this.initAdditionalPeerConnection("screenshare", {
|
|
17984
|
+
audio: false,
|
|
17985
|
+
screenShareAudio: options.audio ?? false
|
|
17986
|
+
});
|
|
17345
17987
|
}
|
|
17346
17988
|
async initAdditionalPeerConnection(propose, options) {
|
|
17347
17989
|
const isScreenShare = propose === "screenshare";
|
|
17990
|
+
if (isScreenShare && this._screenShareId && this._rtcPeerConnectionsMap.has(this._screenShareId)) throw new ScreenShareAlreadyActiveError(this._screenShareId);
|
|
17348
17991
|
let firstPeerConnectionError;
|
|
17349
17992
|
let rtcPeerConnController = null;
|
|
17350
17993
|
try {
|
|
@@ -17361,21 +18004,36 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
17361
18004
|
this._rtcPeerConnections$.next(Array.from(this._rtcPeerConnectionsMap.values()));
|
|
17362
18005
|
this.subscribeTo(rtcPeerConnController.errors$, (error) => {
|
|
17363
18006
|
firstPeerConnectionError ??= error;
|
|
17364
|
-
this.
|
|
18007
|
+
this.reportLegError(error, rtcPeerConnController);
|
|
17365
18008
|
});
|
|
17366
|
-
|
|
18009
|
+
const pc = rtcPeerConnController;
|
|
18010
|
+
await (0, import_cjs$15.firstValueFrom)((0, import_cjs$15.merge)(pc.localMediaSettled$.pipe((0, import_cjs$15.take)(1), (0, import_cjs$15.switchMap)(() => pc.connectionState$.pipe((0, import_cjs$15.filter)((state) => state === "connected"), (0, import_cjs$15.take)(1), (0, import_cjs$15.timeout)(DEFAULT_AUX_LEG_CONNECT_TIMEOUT_MS)))), this.legError$(pc.id)).pipe((0, import_cjs$15.takeUntil)((0, import_cjs$15.merge)(this.destroyed$, pc.destroyed$))));
|
|
17367
18011
|
if (isScreenShare) this._screenShareStatus$.next("started");
|
|
17368
18012
|
logger$16.info(`[WebRTCManager] Additional peer connection connected (${propose}).`);
|
|
17369
18013
|
return rtcPeerConnController.id;
|
|
17370
18014
|
} catch (error) {
|
|
17371
|
-
|
|
17372
|
-
|
|
17373
|
-
if (
|
|
18015
|
+
const cancelled = error instanceof AuxiliaryLegCancelledError;
|
|
18016
|
+
const aborted = error instanceof import_cjs$15.EmptyError && !firstPeerConnectionError;
|
|
18017
|
+
if (!cancelled && !aborted) logger$16.warn("[WebRTCManager] Error initializing additional peer connection:", error);
|
|
18018
|
+
if (rtcPeerConnController && this._rtcPeerConnectionsMap.has(rtcPeerConnController.id)) {
|
|
18019
|
+
rtcPeerConnController.destroy();
|
|
18020
|
+
this._rtcPeerConnectionsMap.delete(rtcPeerConnController.id);
|
|
18021
|
+
this._rtcPeerConnections$.next(Array.from(this._rtcPeerConnectionsMap.values()));
|
|
18022
|
+
}
|
|
18023
|
+
if (isScreenShare) {
|
|
18024
|
+
this._screenShareId = void 0;
|
|
18025
|
+
this._screenShareStatus$.next("none");
|
|
18026
|
+
}
|
|
18027
|
+
if (cancelled) {
|
|
18028
|
+
logger$16.debug("[WebRTCManager] Additional peer connection removed before connecting.");
|
|
18029
|
+
throw error;
|
|
18030
|
+
}
|
|
17374
18031
|
if (firstPeerConnectionError) throw firstPeerConnectionError instanceof MediaAccessError && firstPeerConnectionError.originalError instanceof Error ? firstPeerConnectionError.originalError : firstPeerConnectionError;
|
|
17375
18032
|
if (error instanceof import_cjs$15.EmptyError) {
|
|
17376
18033
|
logger$16.debug("[WebRTCManager] Additional peer connection aborted before connecting.");
|
|
17377
18034
|
return;
|
|
17378
18035
|
}
|
|
18036
|
+
if (error instanceof import_cjs$15.TimeoutError) throw new AuxiliaryLegTimeoutError(propose, error);
|
|
17379
18037
|
throw error instanceof Error ? error : new Error(String(error), { cause: error });
|
|
17380
18038
|
}
|
|
17381
18039
|
}
|
|
@@ -17392,7 +18050,10 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
17392
18050
|
if (removeTrack) return this.mainPeerConnection.stopTrackSender(removeTrack, { updateTransceiverDirection: true });
|
|
17393
18051
|
}
|
|
17394
18052
|
async removeScreenMedia() {
|
|
17395
|
-
if (!["starting", "started"].includes(this._screenShareStatus$.value))
|
|
18053
|
+
if (!["starting", "started"].includes(this._screenShareStatus$.value)) {
|
|
18054
|
+
logger$16.warn("[WebRTCManager] No active screen share to stop.");
|
|
18055
|
+
return;
|
|
18056
|
+
}
|
|
17396
18057
|
if (!this._screenShareId) {
|
|
17397
18058
|
logger$16.debug("[WebRTCManager] No screen share peer connection found.");
|
|
17398
18059
|
return;
|
|
@@ -17407,6 +18068,10 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
17407
18068
|
try {
|
|
17408
18069
|
if (rtcPeerConnController) await this.executeVertoBye(rtcPeerConnController);
|
|
17409
18070
|
} finally {
|
|
18071
|
+
if (rtcPeerConnController && rtcPeerConnController.connectionState !== "connected") this._legErrors$.next({
|
|
18072
|
+
legId: id,
|
|
18073
|
+
error: new AuxiliaryLegCancelledError(rtcPeerConnController.propose)
|
|
18074
|
+
});
|
|
17410
18075
|
rtcPeerConnController?.destroy();
|
|
17411
18076
|
this._rtcPeerConnectionsMap.delete(id);
|
|
17412
18077
|
this._rtcPeerConnections$.next(Array.from(this._rtcPeerConnectionsMap.values()));
|
|
@@ -17421,7 +18086,10 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
17421
18086
|
await this.executeVerto(VertoBye({
|
|
17422
18087
|
...causeParams,
|
|
17423
18088
|
dialogParams: this.dialogParams(rtcPeerConnController)
|
|
17424
|
-
})
|
|
18089
|
+
}), {
|
|
18090
|
+
callID: rtcPeerConnController.id,
|
|
18091
|
+
node_id: rtcPeerConnController.nodeId ?? void 0
|
|
18092
|
+
});
|
|
17425
18093
|
} catch (error) {
|
|
17426
18094
|
logger$16.warn("[WebRTCManager] Call might already be disconnected, error sending Verto bye:", error);
|
|
17427
18095
|
throw error;
|
|
@@ -18300,17 +18968,38 @@ function mosToQualityLevel(mos) {
|
|
|
18300
18968
|
return "critical";
|
|
18301
18969
|
}
|
|
18302
18970
|
|
|
18971
|
+
//#endregion
|
|
18972
|
+
//#region src/utils/unwrapVertoReply.ts
|
|
18973
|
+
/**
|
|
18974
|
+
* Unwrap a method's own reply from a `webrtc.verto` envelope.
|
|
18975
|
+
*
|
|
18976
|
+
* A control verb sent in-dialog comes back nested two levels deep:
|
|
18977
|
+
*
|
|
18978
|
+
* ```
|
|
18979
|
+
* { result: { node_id, code, result: { jsonrpc, id, result: <method payload> } } }
|
|
18980
|
+
* ```
|
|
18981
|
+
*
|
|
18982
|
+
* whereas the routed transport resolves the method payload directly under
|
|
18983
|
+
* `.result`. Readers want the latter shape, and the difference is silent when it
|
|
18984
|
+
* is wrong — `response.result.layouts` simply evaluates to `undefined` against the
|
|
18985
|
+
* envelope, so the data arrives, nothing throws, and the caller sees an empty
|
|
18986
|
+
* value. That exact failure produced an empty layout dropdown with a successful
|
|
18987
|
+
* request behind it.
|
|
18988
|
+
*
|
|
18989
|
+
* Accepting both shapes here keeps every reader indifferent to which transport
|
|
18990
|
+
* produced the response. Anything that is not a nested envelope (a plain ack, or
|
|
18991
|
+
* an already-unwrapped reply) passes through untouched.
|
|
18992
|
+
*/
|
|
18993
|
+
function unwrapVertoReply(response) {
|
|
18994
|
+
const inner = getValueFrom(response, "result.result");
|
|
18995
|
+
return inner && typeof inner === "object" && "result" in inner ? inner : response;
|
|
18996
|
+
}
|
|
18997
|
+
|
|
18303
18998
|
//#endregion
|
|
18304
18999
|
//#region src/core/entities/Call.ts
|
|
18305
19000
|
var import_cjs$11 = require_cjs();
|
|
18306
19001
|
const logger$12 = getLogger();
|
|
18307
19002
|
/**
|
|
18308
|
-
* Verto method for setting member layout positions. Its gateway DTO requires a
|
|
18309
|
-
* `targets` array whose entries are `{ target, position }` (NOT bare targets),
|
|
18310
|
-
* so {@link WebRTCCall.buildMethodParams} special-cases it. See issue #19400.
|
|
18311
|
-
*/
|
|
18312
|
-
const POSITION_SET_METHOD = "call.member.position.set";
|
|
18313
|
-
/**
|
|
18314
19003
|
* Ratio between the critical and warning RTT spike multipliers.
|
|
18315
19004
|
* Warning threshold = baseline * warningMultiplier (default 3x)
|
|
18316
19005
|
* Critical threshold = baseline * warningMultiplier * RTT_CRITICAL_TO_WARNING_RATIO
|
|
@@ -18405,8 +19094,11 @@ var WebRTCCall = class extends Destroyable {
|
|
|
18405
19094
|
emitError(callError) {
|
|
18406
19095
|
if (this._status$.value === "destroyed" || this._status$.value === "failed") return;
|
|
18407
19096
|
this._errors$.next(callError);
|
|
18408
|
-
if (callError.fatal) {
|
|
19097
|
+
if (callError.fatal && this._status$.value !== "disconnecting") {
|
|
18409
19098
|
this._status$.next("failed");
|
|
19099
|
+
this.vertoManager.bye().catch((error) => {
|
|
19100
|
+
logger$12.debug("[Call] fatal-teardown bye failed (signaling likely already dead):", error);
|
|
19101
|
+
});
|
|
18410
19102
|
this.destroy();
|
|
18411
19103
|
}
|
|
18412
19104
|
}
|
|
@@ -18461,7 +19153,7 @@ var WebRTCCall = class extends Destroyable {
|
|
|
18461
19153
|
/** Toggles the call lock state, preventing or allowing new participants from joining. */
|
|
18462
19154
|
async toggleLock() {
|
|
18463
19155
|
const method = this.locked ? "call.unlock" : "call.lock";
|
|
18464
|
-
await this.executeMethod(this.
|
|
19156
|
+
await this.executeMethod(this.callSelf, method, {});
|
|
18465
19157
|
}
|
|
18466
19158
|
/**
|
|
18467
19159
|
* Toggles the hold state of the call (pauses/resumes local media transmission).
|
|
@@ -18510,14 +19202,25 @@ var WebRTCCall = class extends Destroyable {
|
|
|
18510
19202
|
*
|
|
18511
19203
|
* Constructs call context (node_id, call_id, member_id) and sends the RPC request.
|
|
18512
19204
|
*
|
|
18513
|
-
* @param target - Target
|
|
19205
|
+
* @param target - Target {@link MemberTarget} triple, or the local member's
|
|
19206
|
+
* ID string for self-operations (any other string is rejected — a bare
|
|
19207
|
+
* member id cannot carry the remote member's own call context).
|
|
18514
19208
|
* @param method - Verto method name (e.g. `'call.mute'`, `'call.member.remove'`).
|
|
18515
19209
|
* @param args - Parameters for the RPC method.
|
|
18516
19210
|
* @returns The RPC response.
|
|
19211
|
+
* @throws {CallNotReadyError} If the call has no self member context yet.
|
|
19212
|
+
* @throws {InvalidParams} If a string target is not the local member's ID.
|
|
18517
19213
|
* @throws {JSONRPCError} If the RPC call returns an error.
|
|
18518
19214
|
*/
|
|
18519
19215
|
async executeMethod(target, method, args) {
|
|
18520
|
-
const
|
|
19216
|
+
const self = this.callSelf;
|
|
19217
|
+
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}`);
|
|
19218
|
+
if (this.clientSession.callControl === "in-dialog") return this.executeMethodInDialog(target, method, args);
|
|
19219
|
+
const params = {
|
|
19220
|
+
...args,
|
|
19221
|
+
self,
|
|
19222
|
+
target: typeof target === "string" ? self : target
|
|
19223
|
+
};
|
|
18521
19224
|
const request = buildRPCRequest({
|
|
18522
19225
|
method,
|
|
18523
19226
|
params
|
|
@@ -18531,29 +19234,98 @@ var WebRTCCall = class extends Destroyable {
|
|
|
18531
19234
|
throw error;
|
|
18532
19235
|
}
|
|
18533
19236
|
}
|
|
18534
|
-
|
|
18535
|
-
|
|
18536
|
-
|
|
18537
|
-
|
|
18538
|
-
|
|
18539
|
-
|
|
18540
|
-
|
|
18541
|
-
|
|
18542
|
-
|
|
18543
|
-
|
|
18544
|
-
|
|
18545
|
-
|
|
18546
|
-
|
|
18547
|
-
|
|
18548
|
-
};
|
|
18549
|
-
|
|
18550
|
-
|
|
18551
|
-
|
|
18552
|
-
|
|
18553
|
-
|
|
19237
|
+
/**
|
|
19238
|
+
* `executeMethod` for a call opened with `callControl: 'in-dialog'`.
|
|
19239
|
+
*
|
|
19240
|
+
* Translates the routed transport's calling convention into the in-dialog one. No
|
|
19241
|
+
* `self` tuple is sent, but a `target` is — the same {call_id, member_id} the routed
|
|
19242
|
+
* transport puts in `target` (minus node_id), for self-ops and cross-member ops alike.
|
|
19243
|
+
*
|
|
19244
|
+
* Target shapes are per-verb and irregular, so they are centralised here rather
|
|
19245
|
+
* than left to callers: most verbs take a singular `target`, `call.member.remove`
|
|
19246
|
+
* takes a plural `targets` array, and `call.member.position.set` takes a flat
|
|
19247
|
+
* `targets` of `{call_id, position}` — the one verb keyed on call_id rather than
|
|
19248
|
+
* member_id, so the member triple `Participant.setPosition` built is unwrapped.
|
|
19249
|
+
*/
|
|
19250
|
+
async executeMethodInDialog(target, method, args) {
|
|
19251
|
+
const control = { ...args };
|
|
19252
|
+
if (method === "call.member.position.set") control.targets = (args.targets ?? []).map((entry) => ({
|
|
19253
|
+
call_id: entry.target?.call_id ?? entry.call_id,
|
|
19254
|
+
position: entry.position
|
|
19255
|
+
}));
|
|
19256
|
+
else {
|
|
19257
|
+
const member = typeof target === "object" ? {
|
|
19258
|
+
call_id: target.call_id,
|
|
19259
|
+
member_id: target.member_id
|
|
19260
|
+
} : {
|
|
18554
19261
|
call_id: this.id,
|
|
18555
19262
|
member_id: target
|
|
18556
|
-
}
|
|
19263
|
+
};
|
|
19264
|
+
if (method === "call.member.remove") control.targets = [member];
|
|
19265
|
+
else control.target = member;
|
|
19266
|
+
}
|
|
19267
|
+
return this.sendCommand(method, control);
|
|
19268
|
+
}
|
|
19269
|
+
/**
|
|
19270
|
+
* Sends a `call.*` control verb **in-dialog** via `verto.info`, as an alternative
|
|
19271
|
+
* to the routed {@link executeMethod} transport.
|
|
19272
|
+
*
|
|
19273
|
+
* Why both exist: `executeMethod` addresses the member with an explicit
|
|
19274
|
+
* `{node_id, call_id, member_id}` tuple, which does not resolve for every conference,
|
|
19275
|
+
* so the op can fail. An in-dialog frame carries the verb on the member's own
|
|
19276
|
+
* signaling channel instead, so control works without the client needing to know how
|
|
19277
|
+
* the conference is hosted.
|
|
19278
|
+
*
|
|
19279
|
+
* The trade-off is reach: the in-dialog transport is only accepted for calls that
|
|
19280
|
+
* join a conference over SWML (e.g. an SWML `join_conference`); use the routed
|
|
19281
|
+
* default otherwise.
|
|
19282
|
+
*
|
|
19283
|
+
* `params` are sent verbatim — nothing is built for you, which includes the target.
|
|
19284
|
+
* **A self-directed op still needs one**, or it is refused; name yourself explicitly:
|
|
19285
|
+
*
|
|
19286
|
+
* ```ts
|
|
19287
|
+
* const { call_id, member_id } = call.self.target;
|
|
19288
|
+
* await call.sendCommand('call.mute', { channels: ['audio'], target: { call_id, member_id } });
|
|
19289
|
+
* ```
|
|
19290
|
+
*
|
|
19291
|
+
* Never include `node_id` — only the two ids. The shapes are per-verb: most take a
|
|
19292
|
+
* singular `target`, `call.member.remove` takes a plural `targets` array, and
|
|
19293
|
+
* `call.member.position.set` takes a flat `targets: [{call_id, position}]` (the one
|
|
19294
|
+
* verb keyed on `call_id` rather than `member_id`). Verbs that act on the call as a
|
|
19295
|
+
* whole, or that the SDK does not wrap at all, take no target.
|
|
19296
|
+
*
|
|
19297
|
+
* For the typed alternative that handles all of this, create the client with
|
|
19298
|
+
* `callControl: 'in-dialog'` and use the ordinary `Call`/`Participant` methods.
|
|
19299
|
+
*
|
|
19300
|
+
* @internal Not part of the supported surface while the in-dialog transport is still
|
|
19301
|
+
* rolling out. `WebRTCCall` is exported from the package entry, so without this tag
|
|
19302
|
+
* TypeDoc publishes the method — and the example above — as public API.
|
|
19303
|
+
*
|
|
19304
|
+
* @param method - A `call.*` method name (e.g. `'call.mute'`).
|
|
19305
|
+
* @param params - Method parameters, sent verbatim.
|
|
19306
|
+
* @returns The method's own reply, unwrapped from the `verto.info` envelope.
|
|
19307
|
+
* @throws {JSONRPCError} If the control op fails.
|
|
19308
|
+
*/
|
|
19309
|
+
async sendCommand(method, params = {}) {
|
|
19310
|
+
return unwrapVertoReply(await this.vertoManager.sendCallControl(method, params));
|
|
19311
|
+
}
|
|
19312
|
+
/**
|
|
19313
|
+
* The local leg's member triple — sent as `self` in every member RPC
|
|
19314
|
+
* envelope, and as the `target` of call-scoped self-operations (e.g. lock,
|
|
19315
|
+
* layout).
|
|
19316
|
+
*
|
|
19317
|
+
* @throws {CallNotReadyError} Before `call.joined` delivers the self member
|
|
19318
|
+
* context (`selfId`/`nodeId`) — an RPC without it cannot be routed, so fail
|
|
19319
|
+
* fast instead of sending a doomed request.
|
|
19320
|
+
*/
|
|
19321
|
+
get callSelf() {
|
|
19322
|
+
const node_id = this.nodeId;
|
|
19323
|
+
const member_id = this.vertoManager.selfId;
|
|
19324
|
+
if (!node_id || !member_id) throw new CallNotReadyError(this.id);
|
|
19325
|
+
return {
|
|
19326
|
+
node_id,
|
|
19327
|
+
call_id: this.id,
|
|
19328
|
+
member_id
|
|
18557
19329
|
};
|
|
18558
19330
|
}
|
|
18559
19331
|
/** Observable of the current call status (e.g. `'ringing'`, `'connected'`). */
|
|
@@ -18709,13 +19481,13 @@ var WebRTCCall = class extends Destroyable {
|
|
|
18709
19481
|
get bandwidthConstrained$() {
|
|
18710
19482
|
return this.deferEmission(this._bandwidthConstrained$.asObservable());
|
|
18711
19483
|
}
|
|
18712
|
-
/** Observable that emits when server
|
|
19484
|
+
/** Observable that emits when the server pushes media params. */
|
|
18713
19485
|
get mediaParamsUpdated$() {
|
|
18714
19486
|
return this.deferEmission(this._mediaParamsUpdated$.asObservable());
|
|
18715
19487
|
}
|
|
18716
19488
|
/**
|
|
18717
19489
|
* @internal Emit a media params update event.
|
|
18718
|
-
* Called by the VertoManager when server
|
|
19490
|
+
* Called by the VertoManager when the server pushes media params.
|
|
18719
19491
|
*/
|
|
18720
19492
|
emitMediaParamsUpdated(event) {
|
|
18721
19493
|
this._mediaParamsUpdated$.next(event);
|
|
@@ -18923,6 +19695,10 @@ var WebRTCCall = class extends Destroyable {
|
|
|
18923
19695
|
get selfId$() {
|
|
18924
19696
|
return this.vertoManager.selfId$;
|
|
18925
19697
|
}
|
|
19698
|
+
/** @internal Lets call creation bound the media and signalling phases apart. */
|
|
19699
|
+
get localMediaSettled$() {
|
|
19700
|
+
return this.vertoManager.localMediaSettled$;
|
|
19701
|
+
}
|
|
18926
19702
|
/** Local participant's member ID, or `null` if not joined. */
|
|
18927
19703
|
get selfId() {
|
|
18928
19704
|
return this.vertoManager.selfId;
|
|
@@ -19116,12 +19892,17 @@ var WebRTCCall = class extends Destroyable {
|
|
|
19116
19892
|
*
|
|
19117
19893
|
* **These operations are NOT atomic.** The layout is applied first, then each
|
|
19118
19894
|
* member position sequentially, so members may briefly flash into their
|
|
19119
|
-
* default slots before being moved to the requested positions.
|
|
19895
|
+
* default slots before being moved to the requested positions. Targeted
|
|
19896
|
+
* members are validated upfront, though: when any of them has no
|
|
19897
|
+
* {@link Participant.target | member call context} yet, the whole call
|
|
19898
|
+
* rejects before any request is sent and the layout is left unchanged.
|
|
19120
19899
|
*
|
|
19121
19900
|
* @param layout - Layout name (must be one of {@link layouts}).
|
|
19122
19901
|
* @param positions - Optional map of member IDs to {@link VideoPosition} values.
|
|
19123
19902
|
* When omitted or empty, only the layout is changed.
|
|
19124
19903
|
* @throws {InvalidParams} If the layout is not in the available {@link layouts}.
|
|
19904
|
+
* @throws {ParticipantNotReadyError} If a targeted member's call context has
|
|
19905
|
+
* not been received yet — thrown before any request is sent.
|
|
19125
19906
|
*
|
|
19126
19907
|
* @example
|
|
19127
19908
|
* ```ts
|
|
@@ -19132,18 +19913,19 @@ var WebRTCCall = class extends Destroyable {
|
|
|
19132
19913
|
*/
|
|
19133
19914
|
async setLayout(layout, positions) {
|
|
19134
19915
|
if (!this.layouts.includes(layout)) throw new InvalidParams(`Layout ${layout} is not available in the current call layouts: ${this.layouts.join(", ")}`);
|
|
19135
|
-
const
|
|
19136
|
-
|
|
19137
|
-
const positionEntries = Object.entries(positions ?? {});
|
|
19138
|
-
if (positionEntries.length === 0) return;
|
|
19139
|
-
for (const [memberId, position] of positionEntries) {
|
|
19916
|
+
const targets = [];
|
|
19917
|
+
for (const [memberId, position] of Object.entries(positions ?? {})) {
|
|
19140
19918
|
const participant = this.participants.find((p) => p.id === memberId);
|
|
19141
19919
|
if (!participant) {
|
|
19142
19920
|
logger$12.warn(`[Call] setLayout: member ${memberId} not found in participants; skipping position ${position}`);
|
|
19143
19921
|
continue;
|
|
19144
19922
|
}
|
|
19145
|
-
|
|
19923
|
+
participant.target;
|
|
19924
|
+
targets.push([participant, position]);
|
|
19146
19925
|
}
|
|
19926
|
+
const selfId = await (0, import_cjs$11.firstValueFrom)(this.selfId$.pipe((0, import_cjs$11.filter)((id) => id !== null)));
|
|
19927
|
+
await this.executeMethod(selfId, "call.layout.set", { layout });
|
|
19928
|
+
for (const [participant, position] of targets) await participant.setPosition(position);
|
|
19147
19929
|
}
|
|
19148
19930
|
/**
|
|
19149
19931
|
* Transfers the call to another destination.
|
|
@@ -19245,17 +20027,28 @@ var WebRTCCall = class extends Destroyable {
|
|
|
19245
20027
|
* (notably iOS Safari) fall back to re-acquiring the track with the new
|
|
19246
20028
|
* constraint set and plumbing the replacement through the local audio
|
|
19247
20029
|
* pipeline if one is active.
|
|
20030
|
+
*
|
|
20031
|
+
* @returns whether the constraint reached the microphone. `false` is an
|
|
20032
|
+
* outcome rather than an error — a leg sending media the SDK did not capture
|
|
20033
|
+
* is left alone — so a UI that reflects the toggle must read it. Any failure
|
|
20034
|
+
* behind a `false` is also reported on {@link errors$}.
|
|
19248
20035
|
*/
|
|
19249
20036
|
async setEchoCancellation(enabled) {
|
|
19250
|
-
|
|
20037
|
+
return this.vertoManager.updateMediaConstraints({ audio: { echoCancellation: enabled } });
|
|
19251
20038
|
}
|
|
19252
|
-
/**
|
|
20039
|
+
/**
|
|
20040
|
+
* Toggle browser noise suppression on the local mic at runtime.
|
|
20041
|
+
* @returns whether the constraint reached the microphone.
|
|
20042
|
+
*/
|
|
19253
20043
|
async setNoiseSuppression(enabled) {
|
|
19254
|
-
|
|
20044
|
+
return this.vertoManager.updateMediaConstraints({ audio: { noiseSuppression: enabled } });
|
|
19255
20045
|
}
|
|
19256
|
-
/**
|
|
20046
|
+
/**
|
|
20047
|
+
* Toggle browser automatic gain control on the local mic at runtime.
|
|
20048
|
+
* @returns whether the constraint reached the microphone.
|
|
20049
|
+
*/
|
|
19257
20050
|
async setAutoGainControl(enabled) {
|
|
19258
|
-
|
|
20051
|
+
return this.vertoManager.updateMediaConstraints({ audio: { autoGainControl: enabled } });
|
|
19259
20052
|
}
|
|
19260
20053
|
/**
|
|
19261
20054
|
* Observable of the aggregate remote audio level, 0..1 RMS. The server
|
|
@@ -19312,6 +20105,9 @@ var WebRTCCall = class extends Destroyable {
|
|
|
19312
20105
|
/**
|
|
19313
20106
|
* Infers the semantic error category from a raw Error thrown by VertoManager
|
|
19314
20107
|
* or an RTCPeerConnection layer.
|
|
20108
|
+
*
|
|
20109
|
+
* Pure function — exported for unit testing.
|
|
20110
|
+
* @internal
|
|
19315
20111
|
*/
|
|
19316
20112
|
function inferCallErrorKind(error) {
|
|
19317
20113
|
if (error instanceof RPCTimeoutError) return "timeout";
|
|
@@ -19330,7 +20126,14 @@ const RECOVERABLE_RPC_CODES = new Set([
|
|
|
19330
20126
|
RPC_ERROR_AUTHENTICATION_FAILED,
|
|
19331
20127
|
RPC_ERROR_INVALID_PARAMS
|
|
19332
20128
|
]);
|
|
19333
|
-
/**
|
|
20129
|
+
/**
|
|
20130
|
+
* A *fallback*: callers knowing which leg failed pass an explicit `fatal` and
|
|
20131
|
+
* never reach here, so the default-fatal branch only sees call- and main-leg
|
|
20132
|
+
* errors. Auxiliary legs go through `WebRTCVertoManager.reportLegError`.
|
|
20133
|
+
*
|
|
20134
|
+
* Pure function — exported for unit testing.
|
|
20135
|
+
* @internal
|
|
20136
|
+
*/
|
|
19334
20137
|
function isFatalError(error) {
|
|
19335
20138
|
if (error instanceof VertoPongError) return false;
|
|
19336
20139
|
if (error instanceof MediaTrackError) return false;
|
|
@@ -19365,7 +20168,9 @@ var CallFactory = class {
|
|
|
19365
20168
|
kind: inferCallErrorKind(error),
|
|
19366
20169
|
fatal: options$1?.fatal ?? isFatalError(error),
|
|
19367
20170
|
error,
|
|
19368
|
-
callId: callInstance.id
|
|
20171
|
+
callId: callInstance.id,
|
|
20172
|
+
...options$1?.leg ? { leg: options$1.leg } : {},
|
|
20173
|
+
...options$1?.legId ? { legId: options$1.legId } : {}
|
|
19369
20174
|
};
|
|
19370
20175
|
callInstance.emitError(callError);
|
|
19371
20176
|
},
|
|
@@ -19837,6 +20642,30 @@ const logger$9 = getLogger();
|
|
|
19837
20642
|
function shouldAbortDial(callError) {
|
|
19838
20643
|
return callError.fatal || !(callError.error instanceof MediaAccessError);
|
|
19839
20644
|
}
|
|
20645
|
+
/**
|
|
20646
|
+
* Wait for a dialed call to be ready, or for the failure that stops it.
|
|
20647
|
+
*
|
|
20648
|
+
* Local media acquisition is deliberately unbounded: a permission prompt or a
|
|
20649
|
+
* device picker is human time, and `getUserMedia` cannot be cancelled anyway.
|
|
20650
|
+
* The clock starts only once acquisition settles, so a slow human never spends
|
|
20651
|
+
* the server's budget.
|
|
20652
|
+
*
|
|
20653
|
+
* `merge` rather than `race`, because the two legs settle asymmetrically. A
|
|
20654
|
+
* fatal acquisition failure reports the error and then destroys the call in the
|
|
20655
|
+
* same synchronous step; `errors$` defers delivery by a microtask while
|
|
20656
|
+
* `localMediaSettled$` completes immediately. Under `race` that bare completion
|
|
20657
|
+
* ended the wait first and `dial()` rejected with an RxJS `EmptyError`, burying
|
|
20658
|
+
* the `NotAllowedError` applications are told to inspect. Under `merge` the
|
|
20659
|
+
* completed leg is simply spent, and the queued error — enqueued before the
|
|
20660
|
+
* completion, so delivered before it — arrives to reject the wait. A dial
|
|
20661
|
+
* abandoned with no error at all still ends both legs, and the resulting
|
|
20662
|
+
* `EmptyError` remains the benign-cancel signal.
|
|
20663
|
+
*
|
|
20664
|
+
* Exported for unit testing.
|
|
20665
|
+
*/
|
|
20666
|
+
async function awaitDialReady(session, signalingTimeoutMs) {
|
|
20667
|
+
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)))));
|
|
20668
|
+
}
|
|
19840
20669
|
const getAddressSearchURI = (options) => {
|
|
19841
20670
|
const to = options.to?.split("?")[0];
|
|
19842
20671
|
const from$9 = options.from?.startsWith("subscriber://") ? options.from.replace("subscriber://", "") : options.from;
|
|
@@ -19853,7 +20682,6 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
19853
20682
|
this.authorizationStateKey = authorizationStateKey;
|
|
19854
20683
|
this.attachManager = attachManager;
|
|
19855
20684
|
this.dpopManager = dpopManager;
|
|
19856
|
-
this.callCreateTimeout = 6e3;
|
|
19857
20685
|
this.agent = `signalwire-js/4.0.0`;
|
|
19858
20686
|
this.eventAcks = true;
|
|
19859
20687
|
this.authorizationState$ = this.createReplaySubject(1);
|
|
@@ -19862,6 +20690,7 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
19862
20690
|
minor: 0,
|
|
19863
20691
|
revision: 0
|
|
19864
20692
|
};
|
|
20693
|
+
this.callControl = "routed";
|
|
19865
20694
|
this._authorization$ = this.createBehaviorSubject(void 0);
|
|
19866
20695
|
this._errors$ = this.createReplaySubject(1);
|
|
19867
20696
|
this._authState$ = this.createBehaviorSubject({ kind: "unauthenticated" });
|
|
@@ -20061,22 +20890,23 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
20061
20890
|
await (0, import_cjs$7.firstValueFrom)(this.authenticated$.pipe((0, import_cjs$7.takeUntil)(this.destroyed$), (0, import_cjs$7.filter)(Boolean), (0, import_cjs$7.take)(1), (0, import_cjs$7.timeout)({ first: 15e3 })));
|
|
20062
20891
|
}
|
|
20063
20892
|
async handleAuthenticationError(error) {
|
|
20893
|
+
if (error instanceof TransportConnectionError) {
|
|
20894
|
+
logger$9.debug("[Session] Socket closed during authentication, waiting for reconnect");
|
|
20895
|
+
return;
|
|
20896
|
+
}
|
|
20064
20897
|
logger$9.error("Authentication error:", error);
|
|
20065
|
-
const isRecoverableAuthError = error instanceof JSONRPCError && (error.code === RPC_ERROR_REQUESTER_VALIDATION_FAILED || error.code === RPC_ERROR_INVALID_PARAMS || error.code === RPC_ERROR_AUTHENTICATION_FAILED);
|
|
20898
|
+
const isRecoverableAuthError$1 = error instanceof JSONRPCError && (error.code === RPC_ERROR_REQUESTER_VALIDATION_FAILED || error.code === RPC_ERROR_INVALID_PARAMS || error.code === RPC_ERROR_AUTHENTICATION_FAILED);
|
|
20066
20899
|
const hasStoredState = await (0, import_cjs$7.firstValueFrom)(this.authorizationState$.pipe((0, import_cjs$7.take)(1))) !== void 0;
|
|
20067
|
-
if (isRecoverableAuthError && hasStoredState) {
|
|
20900
|
+
if (isRecoverableAuthError$1 && hasStoredState) {
|
|
20068
20901
|
logger$9.debug("[Session] Recoverable auth error — cleaning up stored state and reconnecting fresh");
|
|
20069
|
-
|
|
20070
|
-
await this.cleanupStoredConnectionParams();
|
|
20071
|
-
} catch (cleanupError) {
|
|
20072
|
-
logger$9.error("Failed to cleanup stored connection params:", cleanupError);
|
|
20073
|
-
} finally {
|
|
20074
|
-
this.transport.reconnect();
|
|
20075
|
-
}
|
|
20902
|
+
await this.discardResumeStateAndReconnect();
|
|
20076
20903
|
} else this._errors$.next(error);
|
|
20077
20904
|
}
|
|
20078
20905
|
/**
|
|
20079
|
-
* Clear the resume state (authorization_state + protocol)
|
|
20906
|
+
* Clear the resume state (authorization_state + protocol) and ask the
|
|
20907
|
+
* transport to reconnect. The `connected` event re-triggers
|
|
20908
|
+
* `authenticate()`, which now has no stored state and so performs a fresh
|
|
20909
|
+
* connect.
|
|
20080
20910
|
*
|
|
20081
20911
|
* This is the stale-auth-state recovery helper used by handleAuthError:
|
|
20082
20912
|
* the server rejected a reconnect, so the resume state is discarded and a
|
|
@@ -20084,9 +20914,24 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
20084
20914
|
* session lives on through the reconnect and reattachCalls() needs the
|
|
20085
20915
|
* stored call references afterwards. Do NOT add detachAll() here.
|
|
20086
20916
|
*
|
|
20917
|
+
* Connect-time recovery only. A *request* refused on an already
|
|
20918
|
+
* authenticated session is never healed here: dropping the resume state
|
|
20919
|
+
* destroys the association between the socket and the previous session,
|
|
20920
|
+
* which is what reattach depends on. That path mints a fresh credential and
|
|
20921
|
+
* reauthenticates instead (see `SignalWire.recoverAndRetry`).
|
|
20922
|
+
*
|
|
20087
20923
|
* For public teardown (disconnect/destroy), use {@link teardownSessionState}
|
|
20088
20924
|
* instead, which clears the attach records as well.
|
|
20089
20925
|
*/
|
|
20926
|
+
async discardResumeStateAndReconnect() {
|
|
20927
|
+
try {
|
|
20928
|
+
await this.cleanupStoredConnectionParams();
|
|
20929
|
+
} catch (cleanupError) {
|
|
20930
|
+
logger$9.error("Failed to cleanup stored connection params:", cleanupError);
|
|
20931
|
+
} finally {
|
|
20932
|
+
this.transport.reconnect();
|
|
20933
|
+
}
|
|
20934
|
+
}
|
|
20090
20935
|
async cleanupStoredConnectionParams() {
|
|
20091
20936
|
await this.transport.setProtocol(void 0);
|
|
20092
20937
|
await this.updateAuthorizationStateInStorage(void 0);
|
|
@@ -20160,11 +21005,15 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
20160
21005
|
const isReconnect = hasReconnectState && storedToken;
|
|
20161
21006
|
let dpopToken;
|
|
20162
21007
|
if (isReconnect) logger$9.debug("[Session] Reconnecting with stored jwt_token + authorization_state");
|
|
20163
|
-
else
|
|
20164
|
-
|
|
20165
|
-
|
|
21008
|
+
else {
|
|
21009
|
+
const credential = this.getCredential();
|
|
21010
|
+
const credentialExpired = credential.expiry_at !== void 0 && credential.expiry_at <= Date.now() + CREDENTIAL_EXPIRY_SKEW_MS;
|
|
21011
|
+
if (this.onBeforeReconnect && (this.clientBound || credentialExpired)) {
|
|
21012
|
+
logger$9.debug("[Session] Refreshing credentials before fresh connect");
|
|
21013
|
+
await this.onBeforeReconnect();
|
|
21014
|
+
}
|
|
20166
21015
|
}
|
|
20167
|
-
if (
|
|
21016
|
+
if (this.dpopManager?.initialized) try {
|
|
20168
21017
|
dpopToken = await this.dpopManager.createRpcProof({ method: "signalwire.connect" });
|
|
20169
21018
|
} catch (error) {
|
|
20170
21019
|
if (this.clientBound) throw error;
|
|
@@ -20184,7 +21033,9 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
20184
21033
|
protocol: persistedParams.protocol
|
|
20185
21034
|
} : {}
|
|
20186
21035
|
});
|
|
20187
|
-
|
|
21036
|
+
if (this.transport.connectionStatus !== "connected") throw new TransportConnectionError("WebSocket closed during authentication");
|
|
21037
|
+
const socketClosed$ = this.transport.connectionStatus$.pipe((0, import_cjs$7.skip)(1), (0, import_cjs$7.filter)((status) => status === "reconnecting" || status === "disconnected"), (0, import_cjs$7.take)(1), (0, import_cjs$7.switchMap)(() => (0, import_cjs$7.throwError)(() => new TransportConnectionError("WebSocket closed during authentication"))));
|
|
21038
|
+
const response = await (0, import_cjs$7.lastValueFrom)((0, import_cjs$7.race)((0, import_cjs$7.from)(this.transport.execute(rpcConnectRequest)), socketClosed$).pipe(throwOnRPCError(), (0, import_cjs$7.map)((res) => res.result), (0, import_cjs$7.filter)(isRPCConnectResult), (0, import_cjs$7.tap)(() => {
|
|
20188
21039
|
logger$9.debug("[Session] Response passed filter, processing authentication result");
|
|
20189
21040
|
}), (0, import_cjs$7.take)(1), (0, import_cjs$7.catchError)((err) => {
|
|
20190
21041
|
logger$9.error("[Session] Authentication RPC failed:", err);
|
|
@@ -20197,7 +21048,9 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
20197
21048
|
});
|
|
20198
21049
|
if (response.protocol) await this.transport.setProtocol(response.protocol);
|
|
20199
21050
|
this._authorization$.next(response.authorization);
|
|
21051
|
+
if (response.authorization.cnf?.jkt) this._wasClientBound = true;
|
|
20200
21052
|
this._iceServers$.next(response.ice_servers ?? []);
|
|
21053
|
+
this.transport.setAuthenticated();
|
|
20201
21054
|
this._authState$.next({ kind: "authenticated" });
|
|
20202
21055
|
logger$9.debug("[Session] Authentication completed successfully");
|
|
20203
21056
|
}
|
|
@@ -20263,7 +21116,7 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
20263
21116
|
to: destinationURI,
|
|
20264
21117
|
...options
|
|
20265
21118
|
});
|
|
20266
|
-
await (
|
|
21119
|
+
await awaitDialReady(callSession, DEFAULT_CALL_SIGNALING_TIMEOUT_MS);
|
|
20267
21120
|
this._calls$.next({
|
|
20268
21121
|
[`${callSession.id}`]: callSession,
|
|
20269
21122
|
...this._calls$.value
|
|
@@ -20316,12 +21169,23 @@ var ClientSessionWrapper = class {
|
|
|
20316
21169
|
get authenticated() {
|
|
20317
21170
|
return this.clientSessionManager.authenticated;
|
|
20318
21171
|
}
|
|
21172
|
+
/**
|
|
21173
|
+
* Whether the session is using a Client Bound SAT (DPoP). Sticky — set
|
|
21174
|
+
* when the binding is established or restored from a resumed session's
|
|
21175
|
+
* server authorization.
|
|
21176
|
+
*/
|
|
21177
|
+
get clientBound() {
|
|
21178
|
+
return this.clientSessionManager.clientBound;
|
|
21179
|
+
}
|
|
20319
21180
|
get signalingEvent$() {
|
|
20320
21181
|
return this.clientSessionManager.signalingEvent$;
|
|
20321
21182
|
}
|
|
20322
21183
|
get iceServers() {
|
|
20323
21184
|
return this.clientSessionManager.iceServers;
|
|
20324
21185
|
}
|
|
21186
|
+
get callControl() {
|
|
21187
|
+
return this.clientSessionManager.callControl;
|
|
21188
|
+
}
|
|
20325
21189
|
async execute(request, options) {
|
|
20326
21190
|
return this.clientSessionManager.execute(request, options);
|
|
20327
21191
|
}
|
|
@@ -20518,7 +21382,7 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
20518
21382
|
await session.reauthenticate(tokenData.token, rpcProof, { clientBound: true });
|
|
20519
21383
|
updateCredential({ token: tokenData.token });
|
|
20520
21384
|
logger$7.info("[DeviceToken] Client Bound SAT activated successfully");
|
|
20521
|
-
this.
|
|
21385
|
+
this.emitCurrentToken(tokenData);
|
|
20522
21386
|
return { activated: true };
|
|
20523
21387
|
} catch (error) {
|
|
20524
21388
|
logger$7.error("[DeviceToken] Failed to activate Client Bound SAT:", error);
|
|
@@ -20530,6 +21394,21 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
20530
21394
|
}
|
|
20531
21395
|
}
|
|
20532
21396
|
/**
|
|
21397
|
+
* Emit a freshly received token to the reactive pipeline, stamping an
|
|
21398
|
+
* absolute `expires_at` when the response carried only `expires_in`.
|
|
21399
|
+
* Resolving the expiry at RECEIVE time (not at read time) is what lets
|
|
21400
|
+
* {@link refreshNowIfDue} detect due-ness on resume: a bare `expires_in`
|
|
21401
|
+
* re-resolved later would always compute a full TTL from "now" and never
|
|
21402
|
+
* cross the refresh buffer.
|
|
21403
|
+
*/
|
|
21404
|
+
emitCurrentToken(token) {
|
|
21405
|
+
const stamped = token.expires_at ? token : {
|
|
21406
|
+
...token,
|
|
21407
|
+
expires_at: resolveExpiresAt(token)
|
|
21408
|
+
};
|
|
21409
|
+
this._currentToken$.next(stamped);
|
|
21410
|
+
}
|
|
21411
|
+
/**
|
|
20533
21412
|
* Returns true when the cached token has enough headroom before expiry to
|
|
20534
21413
|
* be safely reused on reactivation. The headroom matches the refresh
|
|
20535
21414
|
* buffer, so a token within the refresh window is treated as stale (the
|
|
@@ -20547,7 +21426,7 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
20547
21426
|
method: "POST",
|
|
20548
21427
|
uri: DEVICE_TOKEN_ENDPOINT
|
|
20549
21428
|
});
|
|
20550
|
-
const response = await this.http.request({
|
|
21429
|
+
const response = await this.http().request({
|
|
20551
21430
|
url: DEVICE_TOKEN_ENDPOINT,
|
|
20552
21431
|
...POST_PARAMS,
|
|
20553
21432
|
body: JSON.stringify({
|
|
@@ -20574,7 +21453,7 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
20574
21453
|
uri: DEVICE_REFRESH_ENDPOINT,
|
|
20575
21454
|
accessToken: currentToken
|
|
20576
21455
|
});
|
|
20577
|
-
const response = await this.http.request({
|
|
21456
|
+
const response = await this.http().request({
|
|
20578
21457
|
url: DEVICE_REFRESH_ENDPOINT,
|
|
20579
21458
|
...POST_PARAMS,
|
|
20580
21459
|
body: JSON.stringify({
|
|
@@ -20622,7 +21501,7 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
20622
21501
|
const currentToken = this.getCredential().token;
|
|
20623
21502
|
if (!currentToken) throw new TokenRefreshError("No current token available for refresh");
|
|
20624
21503
|
const newTokenData = await this.retryRefresh(session, currentToken, updateCredential);
|
|
20625
|
-
this.
|
|
21504
|
+
this.emitCurrentToken(newTokenData);
|
|
20626
21505
|
} catch (error) {
|
|
20627
21506
|
logger$7.error("[DeviceToken] Automatic Client Bound SAT refresh failed:", error);
|
|
20628
21507
|
this.errorHandler(error instanceof TokenRefreshError ? error : new TokenRefreshError("Automatic token refresh failed", error));
|
|
@@ -20649,6 +21528,22 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
20649
21528
|
throw lastError instanceof Error ? lastError : new TokenRefreshError("All refresh retries exhausted", lastError);
|
|
20650
21529
|
}
|
|
20651
21530
|
/**
|
|
21531
|
+
* Force an immediate refresh when the cached Client Bound SAT is already
|
|
21532
|
+
* past its refresh window. Called on resume from suspension where
|
|
21533
|
+
* background-tab throttling can delay the reactive timer past the buffer.
|
|
21534
|
+
* A no-op when no token is cached or it still has headroom; the normal
|
|
21535
|
+
* {@link executeRefresh} guards (paused / in-progress / unauthenticated)
|
|
21536
|
+
* still apply.
|
|
21537
|
+
*/
|
|
21538
|
+
refreshNowIfDue() {
|
|
21539
|
+
const token = this._currentToken$.value;
|
|
21540
|
+
if (!token) return;
|
|
21541
|
+
if (resolveExpiresAt(token) * 1e3 - Date.now() <= DEVICE_TOKEN_REFRESH_BUFFER_MS) {
|
|
21542
|
+
logger$7.debug("[DeviceToken] Resume: cached SAT past refresh window; refreshing now");
|
|
21543
|
+
this.executeRefresh();
|
|
21544
|
+
}
|
|
21545
|
+
}
|
|
21546
|
+
/**
|
|
20652
21547
|
* Stops the reactive refresh pipeline from firing. Use when the underlying
|
|
20653
21548
|
* session is being torn down (e.g., during {@link SignalWire.disconnect})
|
|
20654
21549
|
* so a scheduled refresh cannot fire against a destroyed session.
|
|
@@ -20699,6 +21594,7 @@ var CredentialRefreshCoordinator = class extends Destroyable {
|
|
|
20699
21594
|
this.deps = deps;
|
|
20700
21595
|
this._activating = false;
|
|
20701
21596
|
this._activationGeneration = 0;
|
|
21597
|
+
this._developerRefreshInProgress = false;
|
|
20702
21598
|
if (dpopManager?.initialized) this._deviceTokenManager = (deps.deviceTokenManagerFactory ?? defaultDeviceTokenManagerFactory)(dpopManager, deps.http, (error) => deps.notifier.onError(error), () => deps.store.read());
|
|
20703
21599
|
}
|
|
20704
21600
|
/** True when the Client Bound SAT path is available (DPoP initialized). */
|
|
@@ -20717,28 +21613,120 @@ var CredentialRefreshCoordinator = class extends Destroyable {
|
|
|
20717
21613
|
* invokes `deps.onRefreshExhausted` so the orchestrator can disconnect.
|
|
20718
21614
|
*/
|
|
20719
21615
|
scheduleDeveloperRefresh(provider, expiresAt, attempt = 0) {
|
|
21616
|
+
this._activeProvider = provider;
|
|
20720
21617
|
if (this._developerTimerId !== void 0) clearTimeout(this._developerTimerId);
|
|
20721
21618
|
const refreshInterval = attempt === 0 ? Math.max(expiresAt - Date.now() - CREDENTIAL_REFRESH_BUFFER_MS, 1e3) : Math.min(CREDENTIAL_REFRESH_RETRY_BASE_MS * Math.pow(2, attempt) * (.5 + Math.random() * .5), CREDENTIAL_REFRESH_MAX_DELAY_MS);
|
|
20722
|
-
this._developerTimerId = setTimeout(
|
|
21619
|
+
this._developerTimerId = setTimeout(() => {
|
|
21620
|
+
this._developerTimerId = void 0;
|
|
21621
|
+
this.executeDeveloperRefresh(provider, expiresAt, attempt);
|
|
21622
|
+
}, refreshInterval);
|
|
21623
|
+
}
|
|
21624
|
+
/**
|
|
21625
|
+
* Runs the developer-provided refresh once: mints a new credential, stores
|
|
21626
|
+
* and persists it, reauthenticates the live session (via the notifier), and
|
|
21627
|
+
* reschedules against the new expiry. On failure retries with backoff up to
|
|
21628
|
+
* {@link CREDENTIAL_REFRESH_MAX_RETRIES}, then signals exhaustion.
|
|
21629
|
+
*
|
|
21630
|
+
* Shared by the scheduled timer tick and {@link forceRefreshIfDue}. The
|
|
21631
|
+
* `_developerRefreshInProgress` guard prevents the two from overlapping.
|
|
21632
|
+
*/
|
|
21633
|
+
async executeDeveloperRefresh(provider, expiresAt, attempt) {
|
|
21634
|
+
if (this._developerRefreshInProgress) {
|
|
21635
|
+
logger$6.debug("[Coordinator] Developer refresh already in progress; skipping");
|
|
21636
|
+
return;
|
|
21637
|
+
}
|
|
21638
|
+
this._developerRefreshInProgress = true;
|
|
21639
|
+
try {
|
|
21640
|
+
const newCredentials = await this.refreshCredential(provider);
|
|
21641
|
+
this.deps.store.write(newCredentials);
|
|
21642
|
+
this.deps.store.persist(newCredentials);
|
|
20723
21643
|
try {
|
|
20724
|
-
|
|
20725
|
-
|
|
20726
|
-
|
|
20727
|
-
this.deps.store.persist(newCredentials);
|
|
20728
|
-
logger$6.info("[Coordinator] Credentials refreshed successfully.");
|
|
20729
|
-
if (newCredentials.expiry_at) this.scheduleDeveloperRefresh(provider, newCredentials.expiry_at, 0);
|
|
20730
|
-
} catch (error) {
|
|
20731
|
-
const nextAttempt = attempt + 1;
|
|
20732
|
-
logger$6.error(`[Coordinator] Credential refresh failed (attempt ${nextAttempt}/${CREDENTIAL_REFRESH_MAX_RETRIES}):`, error);
|
|
20733
|
-
this.deps.notifier.onError(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
20734
|
-
if (nextAttempt < CREDENTIAL_REFRESH_MAX_RETRIES) this.scheduleDeveloperRefresh(provider, expiresAt, nextAttempt);
|
|
20735
|
-
else {
|
|
20736
|
-
logger$6.error("[Coordinator] Credential refresh exhausted all retries. Disconnecting.");
|
|
20737
|
-
this.deps.notifier.onError(new TokenRefreshError("Credential refresh failed after max retries"));
|
|
20738
|
-
this.deps.notifier.onRefreshExhausted();
|
|
20739
|
-
}
|
|
21644
|
+
await this.deps.notifier.onCredentialRefreshed(newCredentials);
|
|
21645
|
+
} catch (reauthError) {
|
|
21646
|
+
logger$6.warn("[Coordinator] onCredentialRefreshed rejected (non-fatal):", reauthError);
|
|
20740
21647
|
}
|
|
20741
|
-
|
|
21648
|
+
logger$6.info("[Coordinator] Credentials refreshed successfully.");
|
|
21649
|
+
if (newCredentials.expiry_at) this.scheduleDeveloperRefresh(provider, newCredentials.expiry_at, 0);
|
|
21650
|
+
} catch (error) {
|
|
21651
|
+
const nextAttempt = attempt + 1;
|
|
21652
|
+
logger$6.error(`[Coordinator] Credential refresh failed (attempt ${nextAttempt}/${CREDENTIAL_REFRESH_MAX_RETRIES}):`, error);
|
|
21653
|
+
this.deps.notifier.onError(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
21654
|
+
if (nextAttempt < CREDENTIAL_REFRESH_MAX_RETRIES) this.scheduleDeveloperRefresh(provider, expiresAt, nextAttempt);
|
|
21655
|
+
else {
|
|
21656
|
+
logger$6.error("[Coordinator] Credential refresh exhausted all retries. Disconnecting.");
|
|
21657
|
+
this.deps.notifier.onError(new TokenRefreshError("Credential refresh failed after max retries"));
|
|
21658
|
+
this.deps.notifier.onRefreshExhausted();
|
|
21659
|
+
}
|
|
21660
|
+
} finally {
|
|
21661
|
+
this._developerRefreshInProgress = false;
|
|
21662
|
+
}
|
|
21663
|
+
}
|
|
21664
|
+
/**
|
|
21665
|
+
* Force an immediate refresh when the current credential is already past its
|
|
21666
|
+
* scheduled refresh window. Called on resume from suspension, where
|
|
21667
|
+
* background-tab timer throttling can delay the armed refresh well past
|
|
21668
|
+
* expiry, leaving the live session stale.
|
|
21669
|
+
*
|
|
21670
|
+
* Routes to whichever mechanism is armed: the developer timer if armed,
|
|
21671
|
+
* otherwise the Client Bound SAT pipeline. A no-op when nothing is due.
|
|
21672
|
+
*/
|
|
21673
|
+
forceRefreshIfDue() {
|
|
21674
|
+
if (this._developerTimerId !== void 0 && this._activeProvider) {
|
|
21675
|
+
const expiry = this.deps.store.read().expiry_at;
|
|
21676
|
+
if (expiry !== void 0 && Date.now() >= expiry - CREDENTIAL_REFRESH_BUFFER_MS) {
|
|
21677
|
+
logger$6.debug("[Coordinator] Resume: credential past refresh window; forcing refresh");
|
|
21678
|
+
clearTimeout(this._developerTimerId);
|
|
21679
|
+
this._developerTimerId = void 0;
|
|
21680
|
+
this.executeDeveloperRefresh(this._activeProvider, expiry, 0);
|
|
21681
|
+
}
|
|
21682
|
+
return;
|
|
21683
|
+
}
|
|
21684
|
+
this._deviceTokenManager?.refreshNowIfDue();
|
|
21685
|
+
}
|
|
21686
|
+
/**
|
|
21687
|
+
* Sync the credential's expiry from the server-provided authorization (the
|
|
21688
|
+
* `signalwire.connect` result). SATs are opaque JWE, so
|
|
21689
|
+
* `fabric_subscriber.expires_at` is the authoritative expiry of the token
|
|
21690
|
+
* the session actually connected with — the provider-reported `expiry_at`
|
|
21691
|
+
* is only a hint (and may be wrong or absent). Corrects the stored
|
|
21692
|
+
* credential and re-arms the developer refresh timer against the real
|
|
21693
|
+
* deadline when the provider supports `refresh()`.
|
|
21694
|
+
*/
|
|
21695
|
+
syncExpiryFromAuthorization(authorization, provider) {
|
|
21696
|
+
const expiresAtSec = authorization?.fabric_subscriber?.expires_at;
|
|
21697
|
+
if (!expiresAtSec) return;
|
|
21698
|
+
const expiryAt = expiresAtSec * 1e3;
|
|
21699
|
+
const credential = this.deps.store.read();
|
|
21700
|
+
if (credential.expiry_at === expiryAt) return;
|
|
21701
|
+
logger$6.debug(`[Coordinator] Correcting credential expiry from server authorization: ${new Date(expiryAt).toISOString()}`);
|
|
21702
|
+
const updated = {
|
|
21703
|
+
...credential,
|
|
21704
|
+
expiry_at: expiryAt
|
|
21705
|
+
};
|
|
21706
|
+
this.deps.store.write(updated);
|
|
21707
|
+
this.deps.store.persist(updated);
|
|
21708
|
+
if (provider?.refresh) this.scheduleDeveloperRefresh(provider, expiryAt);
|
|
21709
|
+
}
|
|
21710
|
+
/**
|
|
21711
|
+
* Invoke `provider.refresh()` deduped against any concurrent developer
|
|
21712
|
+
* refresh. Concurrent callers — the scheduled tick, a resume-forced refresh,
|
|
21713
|
+
* and the orchestrator's -32003 recovery / reconnect re-mint — share one
|
|
21714
|
+
* in-flight promise, so a provider backed by one-time-use rotating refresh
|
|
21715
|
+
* tokens is never invoked twice in parallel.
|
|
21716
|
+
*
|
|
21717
|
+
* The caller owns applying the returned credential (store write, session
|
|
21718
|
+
* reauth, rescheduling); this method only serializes the network call.
|
|
21719
|
+
*/
|
|
21720
|
+
async refreshCredential(provider) {
|
|
21721
|
+
if (this._refreshInFlight) return this._refreshInFlight;
|
|
21722
|
+
if (!provider.refresh) throw new InvalidCredentialsError("Credential provider does not support refresh");
|
|
21723
|
+
const run = provider.refresh();
|
|
21724
|
+
this._refreshInFlight = run;
|
|
21725
|
+
const clear = () => {
|
|
21726
|
+
if (this._refreshInFlight === run) this._refreshInFlight = void 0;
|
|
21727
|
+
};
|
|
21728
|
+
run.then(clear, clear);
|
|
21729
|
+
return run;
|
|
20742
21730
|
}
|
|
20743
21731
|
/**
|
|
20744
21732
|
* Cancels any scheduled developer-provided refresh. Idempotent.
|
|
@@ -21259,6 +22247,7 @@ var WebSocketController = class WebSocketController extends Destroyable {
|
|
|
21259
22247
|
}
|
|
21260
22248
|
handleClose(_event) {
|
|
21261
22249
|
this.clearConnectionTimeout();
|
|
22250
|
+
this.messageQueue = [];
|
|
21262
22251
|
if (this.shouldReconnect) {
|
|
21263
22252
|
this._status$.next("reconnecting");
|
|
21264
22253
|
this.scheduleReconnection();
|
|
@@ -21328,6 +22317,9 @@ var WebSocketController = class WebSocketController extends Destroyable {
|
|
|
21328
22317
|
function isSignalwirePingRequest(value) {
|
|
21329
22318
|
return isJSONRPCRequest(value) && value.method === "signalwire.ping";
|
|
21330
22319
|
}
|
|
22320
|
+
function isSignalwireConnectRequest(value) {
|
|
22321
|
+
return isJSONRPCRequest(value) && value.method === "signalwire.connect";
|
|
22322
|
+
}
|
|
21331
22323
|
|
|
21332
22324
|
//#endregion
|
|
21333
22325
|
//#region src/managers/TransportManager.ts
|
|
@@ -21342,6 +22334,10 @@ var TransportManager = class extends Destroyable {
|
|
|
21342
22334
|
this.protocol$ = this.createReplaySubject(1);
|
|
21343
22335
|
this.isConnecting = false;
|
|
21344
22336
|
this.isConnected = false;
|
|
22337
|
+
this.socketStatus = "disconnected";
|
|
22338
|
+
this.sessionAuthenticated = false;
|
|
22339
|
+
this.heldRequests = [];
|
|
22340
|
+
this.serverPingWatch = 0;
|
|
21345
22341
|
this.ackEvent = () => {
|
|
21346
22342
|
return (0, import_cjs$2.tap)((message) => {
|
|
21347
22343
|
if (isSignalwireRequest(message)) try {
|
|
@@ -21357,6 +22353,7 @@ var TransportManager = class extends Destroyable {
|
|
|
21357
22353
|
if (isSignalwirePingRequest(message)) {
|
|
21358
22354
|
try {
|
|
21359
22355
|
logger$2.debug("[Transport] Received ping, sending pong", { pingId: message.id });
|
|
22356
|
+
if (this.sessionAuthenticated) this.armServerPingWatchdog();
|
|
21360
22357
|
this.send(RPCPingResponse(message.id));
|
|
21361
22358
|
} catch (error) {
|
|
21362
22359
|
logger$2.error("[Transport] Failed to send ping response:", error);
|
|
@@ -21394,6 +22391,14 @@ var TransportManager = class extends Destroyable {
|
|
|
21394
22391
|
this.subscribeTo(this._webSocketConnections.errors$, (error) => {
|
|
21395
22392
|
this.onError?.(error);
|
|
21396
22393
|
});
|
|
22394
|
+
this.subscribeTo(this._webSocketConnections.status$, (status) => {
|
|
22395
|
+
this.socketStatus = status;
|
|
22396
|
+
if (status !== "connected") {
|
|
22397
|
+
this.sessionAuthenticated = false;
|
|
22398
|
+
this.clearServerPingWatchdog();
|
|
22399
|
+
}
|
|
22400
|
+
if (status === "disconnected") this.heldRequests = [];
|
|
22401
|
+
});
|
|
21397
22402
|
this.initialized$ = (0, import_cjs$2.defer)(() => (0, import_cjs$2.from)(this._init())).pipe((0, import_cjs$2.shareReplay)(1), (0, import_cjs$2.takeUntil)(this.destroyed$));
|
|
21398
22403
|
this._jsonRPCMessage$ = this._webSocketConnections.incomingMessages$.pipe((0, import_cjs$2.map)((event) => {
|
|
21399
22404
|
try {
|
|
@@ -21422,6 +22427,9 @@ var TransportManager = class extends Destroyable {
|
|
|
21422
22427
|
get connectionStatus$() {
|
|
21423
22428
|
return this._webSocketConnections.status$;
|
|
21424
22429
|
}
|
|
22430
|
+
get connectionStatus() {
|
|
22431
|
+
return this.socketStatus;
|
|
22432
|
+
}
|
|
21425
22433
|
async connect() {
|
|
21426
22434
|
if (this.isConnecting || this.isConnected) {
|
|
21427
22435
|
logger$2.warn("[Transport] Already connecting or connected");
|
|
@@ -21466,16 +22474,45 @@ var TransportManager = class extends Destroyable {
|
|
|
21466
22474
|
}
|
|
21467
22475
|
async execute(request, options) {
|
|
21468
22476
|
this.send(request);
|
|
21469
|
-
|
|
22477
|
+
const pending = new PendingRPC(request, this._jsonRPCResponse$, options).promise;
|
|
22478
|
+
pending.catch(() => {
|
|
22479
|
+
this.heldRequests = this.heldRequests.filter((held) => held.id !== request.id);
|
|
22480
|
+
});
|
|
22481
|
+
return pending;
|
|
21470
22482
|
}
|
|
21471
22483
|
send(message) {
|
|
21472
22484
|
const payload = JSON.stringify(message);
|
|
22485
|
+
if (isJSONRPCResponse(message)) {
|
|
22486
|
+
if (this.socketStatus !== "connected") {
|
|
22487
|
+
logger$2.debug("[Transport] Dropping response while disconnected", { id: message.id });
|
|
22488
|
+
return;
|
|
22489
|
+
}
|
|
22490
|
+
} else if (!this.sessionAuthenticated && !isSignalwireConnectRequest(message)) {
|
|
22491
|
+
this.heldRequests.push({
|
|
22492
|
+
id: message.id,
|
|
22493
|
+
payload
|
|
22494
|
+
});
|
|
22495
|
+
return;
|
|
22496
|
+
}
|
|
21473
22497
|
this._outgoingMessages$.next(payload);
|
|
21474
22498
|
}
|
|
22499
|
+
/**
|
|
22500
|
+
* Called by the session when signalwire.connect succeeds on the current
|
|
22501
|
+
* socket. Sends the requests held since the socket opened, in order.
|
|
22502
|
+
*/
|
|
22503
|
+
setAuthenticated() {
|
|
22504
|
+
if (this.socketStatus !== "connected") return;
|
|
22505
|
+
this.sessionAuthenticated = true;
|
|
22506
|
+
this.armServerPingWatchdog();
|
|
22507
|
+
const held = this.heldRequests;
|
|
22508
|
+
this.heldRequests = [];
|
|
22509
|
+
held.forEach(({ payload }) => this._outgoingMessages$.next(payload));
|
|
22510
|
+
}
|
|
21475
22511
|
disconnect() {
|
|
21476
22512
|
logger$2.debug("[Transport] Disconnecting");
|
|
21477
22513
|
this.isConnected = false;
|
|
21478
22514
|
this.isConnecting = false;
|
|
22515
|
+
this.clearServerPingWatchdog();
|
|
21479
22516
|
this._webSocketConnections.disconnect();
|
|
21480
22517
|
}
|
|
21481
22518
|
destroy() {
|
|
@@ -21484,6 +22521,28 @@ var TransportManager = class extends Destroyable {
|
|
|
21484
22521
|
super.destroy();
|
|
21485
22522
|
this._webSocketConnections.destroy();
|
|
21486
22523
|
}
|
|
22524
|
+
armServerPingWatchdog() {
|
|
22525
|
+
this.clearServerPingWatchdog();
|
|
22526
|
+
const watch = this.serverPingWatch;
|
|
22527
|
+
this.serverPingTimer = setTimeout(() => void this.probeServer(watch), SERVER_PING_TIMEOUT_MS);
|
|
22528
|
+
}
|
|
22529
|
+
/** Bumps the watch so a probe still in flight no longer acts on its result. */
|
|
22530
|
+
clearServerPingWatchdog() {
|
|
22531
|
+
clearTimeout(this.serverPingTimer);
|
|
22532
|
+
this.serverPingTimer = void 0;
|
|
22533
|
+
this.serverPingWatch += 1;
|
|
22534
|
+
}
|
|
22535
|
+
async probeServer(watch) {
|
|
22536
|
+
logger$2.debug(`[Transport] No signalwire.ping for ${SERVER_PING_TIMEOUT_MS}ms, probing the server`);
|
|
22537
|
+
try {
|
|
22538
|
+
await this.execute(RPCPing(), { timeoutMs: SERVER_PING_PROBE_TIMEOUT_MS });
|
|
22539
|
+
if (watch === this.serverPingWatch) this.armServerPingWatchdog();
|
|
22540
|
+
} catch (error) {
|
|
22541
|
+
if (watch !== this.serverPingWatch) return;
|
|
22542
|
+
logger$2.warn("[Transport] Server did not answer the ping probe, replacing the socket", error);
|
|
22543
|
+
this.reconnect();
|
|
22544
|
+
}
|
|
22545
|
+
}
|
|
21487
22546
|
async _loadProtocolFromStorage() {
|
|
21488
22547
|
try {
|
|
21489
22548
|
const storedProtocol = await this.storage.getItem(this.protocolKey);
|
|
@@ -21522,6 +22581,13 @@ var TransportManager = class extends Destroyable {
|
|
|
21522
22581
|
//#region src/clients/SignalWire.ts
|
|
21523
22582
|
var import_cjs$1 = require_cjs();
|
|
21524
22583
|
const logger$1 = getLogger();
|
|
22584
|
+
/**
|
|
22585
|
+
* Storage key for the client-bound marker. The SAT and authorization_state are
|
|
22586
|
+
* both opaque to the SDK, so on a page reload the preflight recovery — which
|
|
22587
|
+
* runs before any session exists — has no other way to know the session was
|
|
22588
|
+
* client-bound. See {@link SignalWire.persistClientBoundMarker}.
|
|
22589
|
+
*/
|
|
22590
|
+
const CLIENT_BOUND_STORAGE_KEY = "sw:client_bound";
|
|
21525
22591
|
const buildOptionsFromDestination = (destination) => {
|
|
21526
22592
|
if (typeof destination === "string") {
|
|
21527
22593
|
const queryStartIndex = destination.indexOf("?");
|
|
@@ -21565,6 +22631,7 @@ var SignalWire = class extends Destroyable {
|
|
|
21565
22631
|
this.preferences = new ClientPreferences();
|
|
21566
22632
|
this._user$ = this.createBehaviorSubject(void 0);
|
|
21567
22633
|
this._directory$ = this.createBehaviorSubject(void 0);
|
|
22634
|
+
this._credentialRecovered = false;
|
|
21568
22635
|
this._isConnected$ = this.createBehaviorSubject(false);
|
|
21569
22636
|
this._isRegistered$ = this.createBehaviorSubject(false);
|
|
21570
22637
|
this._errors$ = this.createReplaySubject(1);
|
|
@@ -21603,6 +22670,16 @@ var SignalWire = class extends Destroyable {
|
|
|
21603
22670
|
});
|
|
21604
22671
|
}
|
|
21605
22672
|
/**
|
|
22673
|
+
* Build the refresh path's own HTTP controller, against whatever host is current.
|
|
22674
|
+
*
|
|
22675
|
+
* Called on first use rather than up front, so `apiHost` already reflects the
|
|
22676
|
+
* token's `ch` claim. Same credential source as the container's controller — only
|
|
22677
|
+
* the instance, and therefore its observable streams, is separate.
|
|
22678
|
+
*/
|
|
22679
|
+
createRefreshHttpController() {
|
|
22680
|
+
return new HTTPRequestController(this._deps.apiHost, () => this._deps.credential);
|
|
22681
|
+
}
|
|
22682
|
+
/**
|
|
21606
22683
|
* Initializes DPoP if not already set up. Returns the fingerprint on success.
|
|
21607
22684
|
*/
|
|
21608
22685
|
async initDPoP() {
|
|
@@ -21629,11 +22706,19 @@ var SignalWire = class extends Destroyable {
|
|
|
21629
22706
|
async resolveCredentials() {
|
|
21630
22707
|
const fingerprint = await this.initDPoP();
|
|
21631
22708
|
this._refreshCoordinator = new CredentialRefreshCoordinator(this._dpopManager, {
|
|
21632
|
-
http:
|
|
22709
|
+
http: () => {
|
|
22710
|
+
if (!this._refreshHttp || this._refreshHttpHost !== this._deps.apiHost) {
|
|
22711
|
+
this._refreshHttp?.destroy();
|
|
22712
|
+
this._refreshHttp = this.createRefreshHttpController();
|
|
22713
|
+
this._refreshHttpHost = this._deps.apiHost;
|
|
22714
|
+
}
|
|
22715
|
+
return this._refreshHttp;
|
|
22716
|
+
},
|
|
21633
22717
|
notifier: {
|
|
21634
22718
|
onError: (error) => this._errors$.next(error),
|
|
21635
22719
|
onWarning: (warning) => this._warnings$.next(warning),
|
|
21636
|
-
onRefreshExhausted: () => void this.disconnect()
|
|
22720
|
+
onRefreshExhausted: () => void this.disconnect(),
|
|
22721
|
+
onCredentialRefreshed: async (credential) => this.reauthenticateLiveSession(credential)
|
|
21637
22722
|
},
|
|
21638
22723
|
store: {
|
|
21639
22724
|
read: () => this._deps.credential,
|
|
@@ -21645,6 +22730,7 @@ var SignalWire = class extends Destroyable {
|
|
|
21645
22730
|
...this._deps.credential,
|
|
21646
22731
|
...partial
|
|
21647
22732
|
};
|
|
22733
|
+
this.persistCredential(this._deps.credential);
|
|
21648
22734
|
},
|
|
21649
22735
|
persist: (credential) => this.persistCredential(credential)
|
|
21650
22736
|
}
|
|
@@ -21690,20 +22776,181 @@ var SignalWire = class extends Destroyable {
|
|
|
21690
22776
|
}
|
|
21691
22777
|
this._deps.credential = _credentials;
|
|
21692
22778
|
this.persistCredential(_credentials);
|
|
21693
|
-
|
|
21694
|
-
|
|
22779
|
+
await this.reauthenticateLiveSession(_credentials);
|
|
22780
|
+
}
|
|
22781
|
+
/**
|
|
22782
|
+
* Reauthenticate the currently-open session with a freshly obtained
|
|
22783
|
+
* credential so the new token takes effect on the live socket immediately —
|
|
22784
|
+
* not just on the next reconnect. No-op when the session is not
|
|
22785
|
+
* connected/authenticated or the credential carries no token (e.g. an
|
|
22786
|
+
* authorization-state-only refresh). Non-fatal: reauth failures surface on
|
|
22787
|
+
* `errors$` without aborting the refresh that triggered this.
|
|
22788
|
+
*/
|
|
22789
|
+
async reauthenticateLiveSession(credential) {
|
|
22790
|
+
if (!this.isConnected || !this._clientSession.authenticated || !credential.token) return;
|
|
22791
|
+
try {
|
|
22792
|
+
await this._clientSession.reauthenticate(credential.token);
|
|
21695
22793
|
logger$1.info("[SignalWire] Session refreshed with new credentials.");
|
|
21696
22794
|
} catch (error) {
|
|
21697
22795
|
logger$1.error("[SignalWire] Failed to refresh session with new credentials:", error);
|
|
21698
22796
|
this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
21699
22797
|
}
|
|
21700
22798
|
}
|
|
22799
|
+
/**
|
|
22800
|
+
* Recover a session the server is refusing: mint a fresh credential,
|
|
22801
|
+
* reauthenticate the live session with it, and retry the operation.
|
|
22802
|
+
*
|
|
22803
|
+
* The connection is deliberately kept. A reload authenticates the new socket
|
|
22804
|
+
* against the persisted `authorization_state`, and that handshake is what
|
|
22805
|
+
* associates the socket with the previous session — the association reattach
|
|
22806
|
+
* depends on. `signalwire.reauthenticate` swaps the credential *on that same
|
|
22807
|
+
* session*, so recovery never touches the resume state. Discarding it would
|
|
22808
|
+
* heal the credential by destroying the very thing the caller is trying to
|
|
22809
|
+
* get back to.
|
|
22810
|
+
*
|
|
22811
|
+
* The operation is still the verdict, never the RPC. Reauthenticating with
|
|
22812
|
+
* the in-memory token is accepted by a resume even while requests stay
|
|
22813
|
+
* refused, because the persisted `authorization_state` short-circuits token
|
|
22814
|
+
* validation — and `signalwire.reauthenticate` with a *freshly minted* token
|
|
22815
|
+
* has also been observed accepted while `subscriber.online` keeps being
|
|
22816
|
+
* refused (staging run 33826974634). Both look like success and are not.
|
|
22817
|
+
*
|
|
22818
|
+
* @returns the operation's value, or the reason recovery could not deliver
|
|
22819
|
+
* one. `error` is undefined when there was no way to mint at all.
|
|
22820
|
+
*/
|
|
22821
|
+
async recoverAndRetry(operation) {
|
|
22822
|
+
if (!await this.remintAndReauthenticate()) return { ok: false };
|
|
22823
|
+
try {
|
|
22824
|
+
const value = await operation();
|
|
22825
|
+
this._credentialRecovered = true;
|
|
22826
|
+
return {
|
|
22827
|
+
ok: true,
|
|
22828
|
+
value
|
|
22829
|
+
};
|
|
22830
|
+
} catch (error) {
|
|
22831
|
+
logger$1.warn("[SignalWire] Reauthentication was accepted but the operation is still refused:", error);
|
|
22832
|
+
return {
|
|
22833
|
+
ok: false,
|
|
22834
|
+
error
|
|
22835
|
+
};
|
|
22836
|
+
}
|
|
22837
|
+
}
|
|
22838
|
+
/**
|
|
22839
|
+
* Re-mint a credential and adopt it only if the live session accepts it.
|
|
22840
|
+
*
|
|
22841
|
+
* The mechanism follows the binding: a client-bound session re-mints a bound
|
|
22842
|
+
* base SAT through `authenticate()` with the DPoP fingerprint, because the
|
|
22843
|
+
* developer refresh handler would hand back an unbound token and silently
|
|
22844
|
+
* degrade the session. An unbound session uses the refresh handler. Rotation
|
|
22845
|
+
* cost is not a reason to skip this — the only reason is having no mechanism.
|
|
22846
|
+
*
|
|
22847
|
+
* @returns whether the session is now running on a freshly accepted credential.
|
|
22848
|
+
*/
|
|
22849
|
+
async remintAndReauthenticate() {
|
|
22850
|
+
const provider = this._credentialProvider;
|
|
22851
|
+
if (!provider) return false;
|
|
22852
|
+
const { clientBound } = this._clientSession;
|
|
22853
|
+
if (!clientBound && !provider.refresh) {
|
|
22854
|
+
logger$1.debug("[SignalWire] [SW-NO-REFRESH-HANDLER] Unbound session with no refresh handler; cannot re-mint.");
|
|
22855
|
+
return false;
|
|
22856
|
+
}
|
|
22857
|
+
try {
|
|
22858
|
+
const newCredentials = clientBound ? await provider.authenticate(this._dpopManager?.initialized ? { fingerprint: this._dpopManager.fingerprint } : void 0) : await this.remintCredential(provider);
|
|
22859
|
+
if (!newCredentials.token) {
|
|
22860
|
+
logger$1.warn("[SignalWire] Re-minted credential has no token; keeping the current one.");
|
|
22861
|
+
return false;
|
|
22862
|
+
}
|
|
22863
|
+
await this._clientSession.reauthenticate(newCredentials.token);
|
|
22864
|
+
this._deps.credential = newCredentials;
|
|
22865
|
+
this.persistCredential(newCredentials);
|
|
22866
|
+
if (newCredentials.expiry_at && provider.refresh) this._refreshCoordinator?.scheduleDeveloperRefresh(provider, newCredentials.expiry_at);
|
|
22867
|
+
return true;
|
|
22868
|
+
} catch (error) {
|
|
22869
|
+
logger$1.warn("[SignalWire] Re-mint recovery failed:", error);
|
|
22870
|
+
return false;
|
|
22871
|
+
}
|
|
22872
|
+
}
|
|
22873
|
+
/**
|
|
22874
|
+
* Re-mint a credential via `provider.refresh()`, routed through the
|
|
22875
|
+
* coordinator's shared in-flight guard so concurrent re-mint paths (a
|
|
22876
|
+
* scheduled/resume refresh, -32003 recovery, and reconnect) never fire a
|
|
22877
|
+
* second `provider.refresh()` in parallel — which rotating one-time-use
|
|
22878
|
+
* refresh tokens reject. Falls back to a direct call only if the coordinator
|
|
22879
|
+
* has not been constructed yet.
|
|
22880
|
+
*/
|
|
22881
|
+
async remintCredential(provider) {
|
|
22882
|
+
if (this._refreshCoordinator) return this._refreshCoordinator.refreshCredential(provider);
|
|
22883
|
+
if (!provider.refresh) throw new InvalidCredentialsError("Credential provider does not support refresh");
|
|
22884
|
+
return provider.refresh();
|
|
22885
|
+
}
|
|
22886
|
+
/**
|
|
22887
|
+
* Re-mint credentials before a fresh (re)connect (`onBeforeReconnect` hook).
|
|
22888
|
+
* The session invokes this only when it is client-bound OR the in-memory
|
|
22889
|
+
* token is expired. The re-mint mechanism depends on the binding:
|
|
22890
|
+
* - Client-bound: `authenticate()` with the DPoP fingerprint to obtain a
|
|
22891
|
+
* fresh base SAT the upcoming reconnect can re-bind (the
|
|
22892
|
+
* DeviceTokenManager re-activates afterwards).
|
|
22893
|
+
* - Unbound: the developer's non-interactive `refresh()` handler.
|
|
22894
|
+
* `authenticate()` is deliberately NOT used here — it may be interactive
|
|
22895
|
+
* (a login prompt) and must not fire on a background reconnect.
|
|
22896
|
+
*
|
|
22897
|
+
* Rejects on failure so the session aborts the reconnect rather than
|
|
22898
|
+
* replaying a stale token.
|
|
22899
|
+
*/
|
|
22900
|
+
async refreshCredentialForReconnect() {
|
|
22901
|
+
if (!this._credentialProvider) return;
|
|
22902
|
+
try {
|
|
22903
|
+
let newCredentials;
|
|
22904
|
+
if (this._clientSession?.clientBound ?? await this.wasClientBound()) {
|
|
22905
|
+
logger$1.debug("[SignalWire] Re-minting client-bound base SAT before reconnect");
|
|
22906
|
+
newCredentials = await this._credentialProvider.authenticate(this._dpopManager?.initialized ? { fingerprint: this._dpopManager.fingerprint } : void 0);
|
|
22907
|
+
} else if (this._credentialProvider.refresh) {
|
|
22908
|
+
logger$1.debug("[SignalWire] Refreshing unbound credential before reconnect");
|
|
22909
|
+
newCredentials = await this.remintCredential(this._credentialProvider);
|
|
22910
|
+
} else {
|
|
22911
|
+
logger$1.warn("[SignalWire] [SW-NO-REFRESH-HANDLER] Token expired on reconnect but no refresh handler; reconnecting with the existing token.");
|
|
22912
|
+
return;
|
|
22913
|
+
}
|
|
22914
|
+
if (!newCredentials.token) {
|
|
22915
|
+
logger$1.warn("[SignalWire] Re-minted credential has no token; keeping the existing credential for reconnect.");
|
|
22916
|
+
return;
|
|
22917
|
+
}
|
|
22918
|
+
this._deps.credential = newCredentials;
|
|
22919
|
+
this.persistCredential(newCredentials);
|
|
22920
|
+
if (newCredentials.expiry_at && this._credentialProvider.refresh) this._refreshCoordinator?.scheduleDeveloperRefresh(this._credentialProvider, newCredentials.expiry_at);
|
|
22921
|
+
logger$1.debug("[SignalWire] Credential refreshed successfully for reconnect");
|
|
22922
|
+
} catch (error) {
|
|
22923
|
+
logger$1.error("[SignalWire] Failed to refresh credentials for reconnect:", error);
|
|
22924
|
+
this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
22925
|
+
throw error;
|
|
22926
|
+
}
|
|
22927
|
+
}
|
|
21701
22928
|
/** Persist credential to localStorage when persistSession is enabled. */
|
|
21702
22929
|
persistCredential(credential) {
|
|
21703
22930
|
if (!credential.token) return;
|
|
21704
22931
|
this._deps.storage.setItem("sw:cached_credential", credential);
|
|
21705
22932
|
if (this._deps.persistSession) this._deps.storage.setItem("sw:cached_credential", credential, "local");
|
|
21706
22933
|
}
|
|
22934
|
+
/**
|
|
22935
|
+
* Persist whether the session is client-bound, mirroring the credential's
|
|
22936
|
+
* storage scopes so it survives a reload. The preflight recovery reads it
|
|
22937
|
+
* before any session exists to decide whether to re-bind via `authenticate()`
|
|
22938
|
+
* or refresh an unbound token; the marker tracks the latest binding, so an
|
|
22939
|
+
* unbound reconnect clears a stale marker from an earlier client-bound login.
|
|
22940
|
+
*/
|
|
22941
|
+
persistClientBoundMarker(bound) {
|
|
22942
|
+
const scopes = this._deps.persistSession ? ["session", "local"] : ["session"];
|
|
22943
|
+
for (const scope of scopes) if (bound) this._deps.storage.setItem(CLIENT_BOUND_STORAGE_KEY, true, scope);
|
|
22944
|
+
else this._deps.storage.removeItem(CLIENT_BOUND_STORAGE_KEY, scope);
|
|
22945
|
+
}
|
|
22946
|
+
/** Read the persisted client-bound marker (see {@link persistClientBoundMarker}). */
|
|
22947
|
+
async wasClientBound() {
|
|
22948
|
+
const scopes = this._deps.persistSession ? ["local", "session"] : ["session"];
|
|
22949
|
+
for (const scope of scopes) try {
|
|
22950
|
+
if (await this._deps.storage.getItem(CLIENT_BOUND_STORAGE_KEY, scope)) return true;
|
|
22951
|
+
} catch {}
|
|
22952
|
+
return false;
|
|
22953
|
+
}
|
|
21707
22954
|
async init() {
|
|
21708
22955
|
this._user$.next(new User(this._deps.http));
|
|
21709
22956
|
if (!this._options.skipConnection) await this.connect();
|
|
@@ -21727,6 +22974,42 @@ var SignalWire = class extends Destroyable {
|
|
|
21727
22974
|
} catch (error) {
|
|
21728
22975
|
logger$1.error("[SignalWire] Failed to reattach calls:", error);
|
|
21729
22976
|
this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
22977
|
+
} finally {
|
|
22978
|
+
this._credentialRecovered = false;
|
|
22979
|
+
}
|
|
22980
|
+
}
|
|
22981
|
+
/**
|
|
22982
|
+
* Fetch the authenticated user profile, recovering a stale credential.
|
|
22983
|
+
*
|
|
22984
|
+
* On a reload the persisted credential can be expired. Unlike the WS resume —
|
|
22985
|
+
* which the server accepts against the persisted `authorization_state` even
|
|
22986
|
+
* with an expired token — this REST preflight has no such short-circuit and is
|
|
22987
|
+
* refused (401). There is no session yet to reauthenticate, so recovery
|
|
22988
|
+
* re-mints the credential through the provider ({@link refreshCredentialForReconnect})
|
|
22989
|
+
* and retries with a FRESH {@link User}: Fetchable memoizes its result
|
|
22990
|
+
* (shareReplay), so reusing the instance would replay the 401 instead of
|
|
22991
|
+
* re-fetching with the new token. Without the user id the transport/session —
|
|
22992
|
+
* and the reattach a reload is trying to preserve — cannot even be addressed.
|
|
22993
|
+
*/
|
|
22994
|
+
async fetchUserOrRecover() {
|
|
22995
|
+
const fetchUser = async (user$1) => {
|
|
22996
|
+
if (!await (0, import_cjs$1.firstValueFrom)(user$1.fetched$)) throw new UnexpectedError("Failed to fetch user information - fetched$ emitted false");
|
|
22997
|
+
this._deps.user = user$1;
|
|
22998
|
+
};
|
|
22999
|
+
const user = this._user$.value;
|
|
23000
|
+
if (!user) throw new UnexpectedError("User not initialized before connect");
|
|
23001
|
+
try {
|
|
23002
|
+
await fetchUser(user);
|
|
23003
|
+
} catch (firstError) {
|
|
23004
|
+
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.`);
|
|
23005
|
+
try {
|
|
23006
|
+
await this.refreshCredentialForReconnect();
|
|
23007
|
+
const refetched = new User(this._deps.http);
|
|
23008
|
+
await fetchUser(refetched);
|
|
23009
|
+
this._user$.next(refetched);
|
|
23010
|
+
} catch (retryError) {
|
|
23011
|
+
throw new UnexpectedError("Error fetching user information", { cause: retryError });
|
|
23012
|
+
}
|
|
21730
23013
|
}
|
|
21731
23014
|
}
|
|
21732
23015
|
/**
|
|
@@ -21768,40 +23051,23 @@ var SignalWire = class extends Destroyable {
|
|
|
21768
23051
|
*/
|
|
21769
23052
|
async connect() {
|
|
21770
23053
|
await this.teardownTransportAndSession();
|
|
21771
|
-
|
|
21772
|
-
const user = this._user$.value;
|
|
21773
|
-
if (!user) throw new UnexpectedError("User not initialized before connect");
|
|
21774
|
-
if (!await (0, import_cjs$1.firstValueFrom)(user.fetched$)) throw new UnexpectedError("Failed to fetch user information - fetched$ emitted false");
|
|
21775
|
-
this._deps.user = user;
|
|
21776
|
-
} catch (error) {
|
|
21777
|
-
logger$1.error(`[SignalWire] Failed to fetch user information: ${error instanceof Error ? error.message : "Unknown error"}. This usually means the user token is invalid or expired.`);
|
|
21778
|
-
throw new UnexpectedError("Error fetching user information", { cause: error });
|
|
21779
|
-
}
|
|
23054
|
+
await this.fetchUserOrRecover();
|
|
21780
23055
|
const errorHandler = (error) => {
|
|
21781
23056
|
this._errors$.next(error);
|
|
21782
23057
|
};
|
|
21783
23058
|
this._transport = new TransportManager(this._deps.storage, this._deps.protocolKey, this._deps.WebSocket, PreferencesContainer.instance.relayHost ?? this._deps.relayHost, errorHandler);
|
|
21784
|
-
this._attachManager = new AttachManager(this._deps.storage, this._deps.deviceController, PreferencesContainer.instance.reconnectCallsTimeout, this._deps.attachedCallsKey);
|
|
23059
|
+
this._attachManager = new AttachManager(this._deps.storage, this._deps.deviceController, PreferencesContainer.instance.reconnectCallsTimeout, this._deps.attachedCallsKey, () => this._credentialRecovered);
|
|
21785
23060
|
this._clientSession = new ClientSessionManager(() => this._deps.credential, this._transport, this._deps.storage, this._deps.authorizationStateKey, this._deps.deviceController, this._attachManager, this._deps.webRTCApiProvider, this._dpopManager, this._networkMonitor?.networkChange$);
|
|
21786
23061
|
this._publicSession = new ClientSessionWrapper(this._clientSession);
|
|
21787
|
-
this._clientSession.
|
|
21788
|
-
|
|
21789
|
-
try {
|
|
21790
|
-
const fingerprint = this._dpopManager?.initialized ? this._dpopManager.fingerprint : void 0;
|
|
21791
|
-
logger$1.debug("[SignalWire] Credential expired, refreshing before reconnect");
|
|
21792
|
-
const newCredentials = await this._credentialProvider.authenticate(fingerprint ? { fingerprint } : void 0);
|
|
21793
|
-
this._deps.credential = newCredentials;
|
|
21794
|
-
if (newCredentials.expiry_at && this._credentialProvider.refresh) this._refreshCoordinator?.scheduleDeveloperRefresh(this._credentialProvider, newCredentials.expiry_at);
|
|
21795
|
-
logger$1.debug("[SignalWire] Credential refreshed successfully for reconnect");
|
|
21796
|
-
} catch (error) {
|
|
21797
|
-
logger$1.error("[SignalWire] Failed to refresh credentials for reconnect:", error);
|
|
21798
|
-
this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
21799
|
-
throw error;
|
|
21800
|
-
}
|
|
21801
|
-
};
|
|
23062
|
+
this._clientSession.callControl = this._options.callControl ?? "routed";
|
|
23063
|
+
this._clientSession.onBeforeReconnect = async () => this.refreshCredentialForReconnect();
|
|
21802
23064
|
this.subscribeTo(this._clientSession.errors$, (error) => {
|
|
21803
23065
|
this._errors$.next(error);
|
|
21804
23066
|
});
|
|
23067
|
+
this.subscribeTo(this._clientSession.authorization$, (authorization) => {
|
|
23068
|
+
this._refreshCoordinator?.syncExpiryFromAuthorization(authorization, this._credentialProvider);
|
|
23069
|
+
this.persistClientBoundMarker(Boolean(authorization?.cnf?.jkt));
|
|
23070
|
+
});
|
|
21805
23071
|
await this._clientSession.connect();
|
|
21806
23072
|
await this._refreshCoordinator?.activate(this._deps.user, this._clientSession);
|
|
21807
23073
|
this.subscribeTo(this._clientSession.authenticated$.pipe((0, import_cjs$1.skip)(1), (0, import_cjs$1.filter)(Boolean)), async () => {
|
|
@@ -21964,6 +23230,13 @@ var SignalWire = class extends Destroyable {
|
|
|
21964
23230
|
}
|
|
21965
23231
|
try {
|
|
21966
23232
|
this._visibilityController = new VisibilityController();
|
|
23233
|
+
this.subscribeTo(this._visibilityController.visibilityChange$.pipe((0, import_cjs$1.filter)((event) => event.to === "visible")), () => {
|
|
23234
|
+
try {
|
|
23235
|
+
this._refreshCoordinator?.forceRefreshIfDue();
|
|
23236
|
+
} catch (error) {
|
|
23237
|
+
logger$1.warn("[SignalWire] Resume credential revalidation failed (non-fatal):", error);
|
|
23238
|
+
}
|
|
23239
|
+
});
|
|
21967
23240
|
this.subscribeTo(this._visibilityController.visibilityChange$.pipe((0, import_cjs$1.filter)((event) => event.to === "visible" && PreferencesContainer.instance.refreshDevicesOnVisible)), () => {
|
|
21968
23241
|
logger$1.debug("[SignalWire] Page visible, re-enumerating devices");
|
|
21969
23242
|
try {
|
|
@@ -21975,7 +23248,7 @@ var SignalWire = class extends Destroyable {
|
|
|
21975
23248
|
logger$1.warn("[SignalWire] Failed to initialize VisibilityController:", error);
|
|
21976
23249
|
}
|
|
21977
23250
|
try {
|
|
21978
|
-
this._diagnosticsCollector = new DiagnosticsCollector({ sdkVersion: "4.0.0-rc.
|
|
23251
|
+
this._diagnosticsCollector = new DiagnosticsCollector({ sdkVersion: "4.0.0-rc.4" });
|
|
21979
23252
|
} catch (error) {
|
|
21980
23253
|
logger$1.warn("[SignalWire] Failed to initialize DiagnosticsCollector:", error);
|
|
21981
23254
|
}
|
|
@@ -22046,21 +23319,22 @@ var SignalWire = class extends Destroyable {
|
|
|
22046
23319
|
this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
22047
23320
|
throw error;
|
|
22048
23321
|
}
|
|
22049
|
-
logger$1.debug("[SignalWire] Failed to register user,
|
|
22050
|
-
|
|
22051
|
-
await this._clientSession.reauthenticate(this._deps.credential.token);
|
|
22052
|
-
logger$1.debug("[SignalWire] Reauthentication successful, retrying register()");
|
|
23322
|
+
logger$1.debug("[SignalWire] Failed to register user, attempting credential recovery...");
|
|
23323
|
+
const outcome = await this.recoverAndRetry(async () => {
|
|
22053
23324
|
await this._transport.execute(RPCExecute({
|
|
22054
23325
|
method: "subscriber.online",
|
|
22055
23326
|
params: {}
|
|
22056
23327
|
}));
|
|
23328
|
+
});
|
|
23329
|
+
if (outcome.ok) {
|
|
23330
|
+
logger$1.debug("[SignalWire] Recovery restored registration");
|
|
22057
23331
|
this._isRegistered$.next(true);
|
|
22058
|
-
|
|
22059
|
-
logger$1.error("[SignalWire] Reauthentication failed during register():", reauthError);
|
|
22060
|
-
const registerError = new InvalidCredentialsError("Failed to register user, and reauthentication attempt also failed. Please check your credentials.", { cause: reauthError instanceof Error ? reauthError : new Error(String(reauthError), { cause: reauthError }) });
|
|
22061
|
-
this._errors$.next(registerError);
|
|
22062
|
-
throw registerError;
|
|
23332
|
+
return;
|
|
22063
23333
|
}
|
|
23334
|
+
const failureCause = outcome.error ?? error;
|
|
23335
|
+
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 }) });
|
|
23336
|
+
this._errors$.next(registerError);
|
|
23337
|
+
throw registerError;
|
|
22064
23338
|
}
|
|
22065
23339
|
}
|
|
22066
23340
|
/**
|
|
@@ -22091,6 +23365,11 @@ var SignalWire = class extends Destroyable {
|
|
|
22091
23365
|
* Returns a {@link Call} in `'ringing'` state. Subscribe to {@link Call.status$}
|
|
22092
23366
|
* to track progression through `'connected'` → `'disconnected'`.
|
|
22093
23367
|
*
|
|
23368
|
+
* Local media acquisition is deliberately unbounded: an unanswered permission
|
|
23369
|
+
* prompt leaves this promise pending indefinitely, so apply your own bound if
|
|
23370
|
+
* your UI needs one. The 12 s signaling budget starts only once acquisition
|
|
23371
|
+
* settles.
|
|
23372
|
+
*
|
|
22094
23373
|
* @param destination - Address URI string (e.g. `'/public/my-room'`) or {@link Address} instance.
|
|
22095
23374
|
* @param options - Media and dial options (audio/video, device constraints). Overrides defaults.
|
|
22096
23375
|
* @returns The created {@link Call} instance.
|
|
@@ -22113,7 +23392,18 @@ var SignalWire = class extends Destroyable {
|
|
|
22113
23392
|
};
|
|
22114
23393
|
await this.waitAuthentication();
|
|
22115
23394
|
logger$1.debug("[SignalWire] Dialing with options:", computed_options);
|
|
22116
|
-
|
|
23395
|
+
try {
|
|
23396
|
+
return await this._clientSession.createOutboundCall(destination, computed_options);
|
|
23397
|
+
} catch (error) {
|
|
23398
|
+
if (!isRecoverableAuthError(error)) throw error;
|
|
23399
|
+
logger$1.debug("[SignalWire] Dial hit a recoverable auth error; recovering the session and retrying");
|
|
23400
|
+
const outcome = await this.recoverAndRetry(async () => {
|
|
23401
|
+
await this.waitAuthentication();
|
|
23402
|
+
return this._clientSession.createOutboundCall(destination, computed_options);
|
|
23403
|
+
});
|
|
23404
|
+
if (outcome.ok) return outcome.value;
|
|
23405
|
+
throw outcome.error ?? error;
|
|
23406
|
+
}
|
|
22117
23407
|
}
|
|
22118
23408
|
/**
|
|
22119
23409
|
* Runs a multi-phase connectivity test against the given destination.
|
|
@@ -22394,10 +23684,13 @@ var SignalWire = class extends Destroyable {
|
|
|
22394
23684
|
destroy() {
|
|
22395
23685
|
this._refreshCoordinator?.destroy();
|
|
22396
23686
|
this._refreshCoordinator = void 0;
|
|
23687
|
+
this._refreshHttp?.destroy();
|
|
23688
|
+
this._refreshHttp = void 0;
|
|
22397
23689
|
this._dpopManager?.destroy();
|
|
22398
|
-
this._clientSession
|
|
22399
|
-
|
|
22400
|
-
this.
|
|
23690
|
+
const session = this._clientSession;
|
|
23691
|
+
session?.teardownSessionState();
|
|
23692
|
+
this._transport?.destroy();
|
|
23693
|
+
session?.destroy();
|
|
22401
23694
|
try {
|
|
22402
23695
|
this._networkMonitor?.destroy();
|
|
22403
23696
|
} catch {}
|
|
@@ -22510,7 +23803,7 @@ var StaticCredentialProvider = class {
|
|
|
22510
23803
|
/**
|
|
22511
23804
|
* Library version from package.json, injected at build time.
|
|
22512
23805
|
*/
|
|
22513
|
-
const version = "4.0.0-rc.
|
|
23806
|
+
const version = "4.0.0-rc.4";
|
|
22514
23807
|
/**
|
|
22515
23808
|
* Flag indicating the library has been loaded and is ready to use.
|
|
22516
23809
|
* For UMD builds: `window.SignalWire.ready`
|
|
@@ -22532,7 +23825,7 @@ const ready = true;
|
|
|
22532
23825
|
*/
|
|
22533
23826
|
const emitReadyEvent = () => {
|
|
22534
23827
|
if (typeof window !== "undefined") {
|
|
22535
|
-
const event = new CustomEvent("signalwire:js:ready", { detail: { version: "4.0.0-rc.
|
|
23828
|
+
const event = new CustomEvent("signalwire:js:ready", { detail: { version: "4.0.0-rc.4" } });
|
|
22536
23829
|
window.dispatchEvent(event);
|
|
22537
23830
|
}
|
|
22538
23831
|
};
|
|
@@ -22557,7 +23850,10 @@ if (typeof process === "undefined") globalThis.process = { env: { NODE_ENV: "pro
|
|
|
22557
23850
|
|
|
22558
23851
|
//#endregion
|
|
22559
23852
|
exports.Address = Address;
|
|
23853
|
+
exports.AuxiliaryLegCancelledError = AuxiliaryLegCancelledError;
|
|
23854
|
+
exports.AuxiliaryLegTimeoutError = AuxiliaryLegTimeoutError;
|
|
22560
23855
|
exports.CallCreateError = CallCreateError;
|
|
23856
|
+
exports.CallNotReadyError = CallNotReadyError;
|
|
22561
23857
|
exports.ClientPreferences = ClientPreferences;
|
|
22562
23858
|
exports.CollectionFetchError = CollectionFetchError;
|
|
22563
23859
|
exports.DPoPInitError = DPoPInitError;
|
|
@@ -22569,8 +23865,10 @@ exports.MediaTrackError = MediaTrackError;
|
|
|
22569
23865
|
exports.MessageParseError = MessageParseError;
|
|
22570
23866
|
exports.OverconstrainedFallbackError = OverconstrainedFallbackError;
|
|
22571
23867
|
exports.Participant = Participant;
|
|
23868
|
+
exports.ParticipantNotReadyError = ParticipantNotReadyError;
|
|
22572
23869
|
exports.PreflightError = PreflightError;
|
|
22573
23870
|
exports.RecoveryError = RecoveryError;
|
|
23871
|
+
exports.ScreenShareAlreadyActiveError = ScreenShareAlreadyActiveError;
|
|
22574
23872
|
exports.SelfCapabilities = SelfCapabilities;
|
|
22575
23873
|
exports.SelfParticipant = SelfParticipant;
|
|
22576
23874
|
exports.SignalWire = SignalWire;
|