@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.mjs
CHANGED
|
@@ -4186,7 +4186,7 @@ var require_merge$1 = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
4186
4186
|
var empty_1$7 = require_empty();
|
|
4187
4187
|
var args_1$8 = require_args();
|
|
4188
4188
|
var from_1$4 = require_from();
|
|
4189
|
-
function merge$
|
|
4189
|
+
function merge$7() {
|
|
4190
4190
|
var args = [];
|
|
4191
4191
|
for (var _i = 0; _i < arguments.length; _i++) args[_i] = arguments[_i];
|
|
4192
4192
|
var scheduler = args_1$8.popScheduler(args);
|
|
@@ -4194,7 +4194,7 @@ var require_merge$1 = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
4194
4194
|
var sources = args;
|
|
4195
4195
|
return !sources.length ? empty_1$7.EMPTY : sources.length === 1 ? innerFrom_1$31.innerFrom(sources[0]) : mergeAll_1$3.mergeAll(concurrent)(from_1$4.from(sources, scheduler));
|
|
4196
4196
|
}
|
|
4197
|
-
exports.merge = merge$
|
|
4197
|
+
exports.merge = merge$7;
|
|
4198
4198
|
}));
|
|
4199
4199
|
|
|
4200
4200
|
//#endregion
|
|
@@ -6076,7 +6076,7 @@ var require_merge = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
6076
6076
|
var mergeAll_1$2 = require_mergeAll();
|
|
6077
6077
|
var args_1$3 = require_args();
|
|
6078
6078
|
var from_1$1 = require_from();
|
|
6079
|
-
function merge$
|
|
6079
|
+
function merge$6() {
|
|
6080
6080
|
var args = [];
|
|
6081
6081
|
for (var _i = 0; _i < arguments.length; _i++) args[_i] = arguments[_i];
|
|
6082
6082
|
var scheduler = args_1$3.popScheduler(args);
|
|
@@ -6085,7 +6085,7 @@ var require_merge = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
6085
6085
|
mergeAll_1$2.mergeAll(concurrent)(from_1$1.from(__spreadArray$8([source], __read$8(args)), scheduler)).subscribe(subscriber);
|
|
6086
6086
|
});
|
|
6087
6087
|
}
|
|
6088
|
-
exports.merge = merge$
|
|
6088
|
+
exports.merge = merge$6;
|
|
6089
6089
|
}));
|
|
6090
6090
|
|
|
6091
6091
|
//#endregion
|
|
@@ -6823,12 +6823,12 @@ var require_skip = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
6823
6823
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6824
6824
|
exports.skip = void 0;
|
|
6825
6825
|
var filter_1$3 = require_filter();
|
|
6826
|
-
function skip$
|
|
6826
|
+
function skip$5(count$1) {
|
|
6827
6827
|
return filter_1$3.filter(function(_, index) {
|
|
6828
6828
|
return count$1 <= index;
|
|
6829
6829
|
});
|
|
6830
6830
|
}
|
|
6831
|
-
exports.skip = skip$
|
|
6831
|
+
exports.skip = skip$5;
|
|
6832
6832
|
}));
|
|
6833
6833
|
|
|
6834
6834
|
//#endregion
|
|
@@ -6932,7 +6932,7 @@ var require_switchMap = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
6932
6932
|
var innerFrom_1$6 = require_innerFrom();
|
|
6933
6933
|
var lift_1$13 = require_lift();
|
|
6934
6934
|
var OperatorSubscriber_1$11 = require_OperatorSubscriber();
|
|
6935
|
-
function switchMap$
|
|
6935
|
+
function switchMap$8(project, resultSelector) {
|
|
6936
6936
|
return lift_1$13.operate(function(source, subscriber) {
|
|
6937
6937
|
var innerSubscriber = null;
|
|
6938
6938
|
var index = 0;
|
|
@@ -6956,7 +6956,7 @@ var require_switchMap = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
6956
6956
|
}));
|
|
6957
6957
|
});
|
|
6958
6958
|
}
|
|
6959
|
-
exports.switchMap = switchMap$
|
|
6959
|
+
exports.switchMap = switchMap$8;
|
|
6960
6960
|
}));
|
|
6961
6961
|
|
|
6962
6962
|
//#endregion
|
|
@@ -9022,6 +9022,37 @@ const DEFAULT_ICE_CANDIDATE_TIMEOUT_MS = 600;
|
|
|
9022
9022
|
const DEFAULT_ICE_GATHERING_TIMEOUT_MS = 6e3;
|
|
9023
9023
|
const DEFAULT_RECONNECT_CALLS_TIMEOUT_MS = 300 * 1e3;
|
|
9024
9024
|
const DEFAULT_CONNECTION_TIMEOUT_MS = 1e4;
|
|
9025
|
+
/**
|
|
9026
|
+
* The server sends `signalwire.ping` every 10s. A socket that stays silent
|
|
9027
|
+
* longer than this is half-open: the server has dropped the session but the
|
|
9028
|
+
* browser never fired `close`.
|
|
9029
|
+
*/
|
|
9030
|
+
const SERVER_PING_TIMEOUT_MS = 15e3;
|
|
9031
|
+
/**
|
|
9032
|
+
* How long a `signalwire.ping` the client sends after the server's pings
|
|
9033
|
+
* stopped may take to be answered. The server defers its own pings while its
|
|
9034
|
+
* threadpool is congested, so silence alone does not mean the socket is dead.
|
|
9035
|
+
*/
|
|
9036
|
+
const SERVER_PING_PROBE_TIMEOUT_MS = 1e4;
|
|
9037
|
+
/**
|
|
9038
|
+
* How long call setup may wait for the network, measured from local media
|
|
9039
|
+
* settling until the member id arrives.
|
|
9040
|
+
*
|
|
9041
|
+
* Local media acquisition is deliberately NOT inside this: a permission prompt
|
|
9042
|
+
* or a device picker is human time, and a human may take as long as they like
|
|
9043
|
+
* without spending the server's budget. The clock starts when acquisition ends.
|
|
9044
|
+
*
|
|
9045
|
+
* Must exceed `iceGatheringTimeout + the RPC timeout`, which both run inside it.
|
|
9046
|
+
*/
|
|
9047
|
+
const DEFAULT_CALL_SIGNALING_TIMEOUT_MS = 12e3;
|
|
9048
|
+
/**
|
|
9049
|
+
* How long an auxiliary leg (screen share, additional device) may take to
|
|
9050
|
+
* connect once its media is in hand.
|
|
9051
|
+
*
|
|
9052
|
+
* One bound for both: the picker is human time and sits outside it. What remains
|
|
9053
|
+
* — offer, ICE, invite, answer, DTLS — is the same work for either leg kind.
|
|
9054
|
+
*/
|
|
9055
|
+
const DEFAULT_AUX_LEG_CONNECT_TIMEOUT_MS = 15e3;
|
|
9025
9056
|
const DEFAULT_RECONNECT_DELAY_MIN_MS = 100;
|
|
9026
9057
|
const DEFAULT_RECONNECT_DELAY_MAX_MS = 3e3;
|
|
9027
9058
|
const DEFAULT_DEVICE_DEBOUNCE_TIME_MS = 1500;
|
|
@@ -9049,6 +9080,14 @@ const CREDENTIAL_REFRESH_MAX_DELAY_MS = 3e4;
|
|
|
9049
9080
|
/** Buffer in milliseconds before token expiry to trigger refresh. */
|
|
9050
9081
|
const CREDENTIAL_REFRESH_BUFFER_MS = 5e3;
|
|
9051
9082
|
/**
|
|
9083
|
+
* Clock-skew allowance (ms) for treating an in-memory credential as expired
|
|
9084
|
+
* when deciding whether a fresh (re)connect must re-mint the token before
|
|
9085
|
+
* authenticating. A token within this window of its `expiry_at` is treated as
|
|
9086
|
+
* stale so the reconnect re-mints via the credential provider instead of
|
|
9087
|
+
* replaying a dead token (which the server rejects with -32003).
|
|
9088
|
+
*/
|
|
9089
|
+
const CREDENTIAL_EXPIRY_SKEW_MS = 3e4;
|
|
9090
|
+
/**
|
|
9052
9091
|
* Maximum time the coordinator will wait for `DeviceTokenManager.activate()`
|
|
9053
9092
|
* to resolve before treating the activation as failed and falling back to
|
|
9054
9093
|
* the developer-provided refresh path. Prevents a wedged HTTP layer from
|
|
@@ -9067,6 +9106,8 @@ const MEDIA_ACCESS_DENIAL_NAMES = [
|
|
|
9067
9106
|
"SecurityError",
|
|
9068
9107
|
"PermissionDeniedError"
|
|
9069
9108
|
];
|
|
9109
|
+
/** Error names browsers use when the capture hardware is already held exclusively. */
|
|
9110
|
+
const MEDIA_DEVICE_IN_USE_NAMES = ["NotReadableError", "TrackStartError"];
|
|
9070
9111
|
/** Default polling interval for RTCPeerConnection.getStats() in milliseconds. */
|
|
9071
9112
|
const DEFAULT_STATS_POLLING_INTERVAL_MS = 1e3;
|
|
9072
9113
|
/** Number of initial samples used to build a baseline for spike detection. */
|
|
@@ -9281,6 +9322,20 @@ var CallCreateError = class extends Error {
|
|
|
9281
9322
|
this.name = "CallCreateError";
|
|
9282
9323
|
}
|
|
9283
9324
|
};
|
|
9325
|
+
var CallNotReadyError = class extends Error {
|
|
9326
|
+
constructor(callId, options) {
|
|
9327
|
+
super(`Call "${callId}" has no self member context yet: selfId/nodeId have not been received from the server`, options);
|
|
9328
|
+
this.callId = callId;
|
|
9329
|
+
this.name = "CallNotReadyError";
|
|
9330
|
+
}
|
|
9331
|
+
};
|
|
9332
|
+
var ParticipantNotReadyError = class extends Error {
|
|
9333
|
+
constructor(memberId, options) {
|
|
9334
|
+
super(`Participant "${memberId}" has no call context yet: its member state (call_id/node_id) has not been received from the server`, options);
|
|
9335
|
+
this.memberId = memberId;
|
|
9336
|
+
this.name = "ParticipantNotReadyError";
|
|
9337
|
+
}
|
|
9338
|
+
};
|
|
9284
9339
|
var JSONRPCError = class extends Error {
|
|
9285
9340
|
constructor(code, message, data, options, requestId) {
|
|
9286
9341
|
super(message, options);
|
|
@@ -9352,6 +9407,32 @@ var CollectionFetchError = class extends Error {
|
|
|
9352
9407
|
this.name = "CollectionFetchError";
|
|
9353
9408
|
}
|
|
9354
9409
|
};
|
|
9410
|
+
/**
|
|
9411
|
+
* An auxiliary leg did not connect within its budget. Typed rather than a bare
|
|
9412
|
+
* RxJS `TimeoutError` so the leg and cause survive.
|
|
9413
|
+
*/
|
|
9414
|
+
var AuxiliaryLegTimeoutError = class extends Error {
|
|
9415
|
+
constructor(leg, originalError) {
|
|
9416
|
+
super(`Timed out waiting for the ${leg} connection to be established`, { cause: originalError });
|
|
9417
|
+
this.leg = leg;
|
|
9418
|
+
this.originalError = originalError;
|
|
9419
|
+
this.name = "AuxiliaryLegTimeoutError";
|
|
9420
|
+
}
|
|
9421
|
+
};
|
|
9422
|
+
/**
|
|
9423
|
+
* An auxiliary leg was removed before it finished connecting.
|
|
9424
|
+
*
|
|
9425
|
+
* Typed rather than a bare resolve so a caller awaiting the start can tell a
|
|
9426
|
+
* cancel apart from a share that actually came up — the public methods return
|
|
9427
|
+
* `void`, so the promise is the only signal they have.
|
|
9428
|
+
*/
|
|
9429
|
+
var AuxiliaryLegCancelledError = class extends Error {
|
|
9430
|
+
constructor(leg) {
|
|
9431
|
+
super(`The ${leg} leg was removed before it finished connecting`);
|
|
9432
|
+
this.leg = leg;
|
|
9433
|
+
this.name = "AuxiliaryLegCancelledError";
|
|
9434
|
+
}
|
|
9435
|
+
};
|
|
9355
9436
|
var MediaTrackError = class extends Error {
|
|
9356
9437
|
constructor(operation, kind, originalError) {
|
|
9357
9438
|
super(`Media track ${operation} failed for ${kind}`, { cause: originalError instanceof Error ? originalError : void 0 });
|
|
@@ -9365,6 +9446,10 @@ var MediaTrackError = class extends Error {
|
|
|
9365
9446
|
function isMediaAccessDenial(originalError) {
|
|
9366
9447
|
return originalError instanceof Error && MEDIA_ACCESS_DENIAL_NAMES.includes(originalError.name);
|
|
9367
9448
|
}
|
|
9449
|
+
/** True when a `getUserMedia` rejection means the hardware is already held exclusively. */
|
|
9450
|
+
function isMediaDeviceInUse(originalError) {
|
|
9451
|
+
return originalError instanceof Error && MEDIA_DEVICE_IN_USE_NAMES.includes(originalError.name);
|
|
9452
|
+
}
|
|
9368
9453
|
/**
|
|
9369
9454
|
* Failure to acquire local media (camera, microphone, or screen capture)
|
|
9370
9455
|
* via `getUserMedia`/`getDisplayMedia`.
|
|
@@ -9388,6 +9473,22 @@ var MediaAccessError = class extends Error {
|
|
|
9388
9473
|
return isMediaAccessDenial(this.originalError);
|
|
9389
9474
|
}
|
|
9390
9475
|
};
|
|
9476
|
+
/**
|
|
9477
|
+
* Thrown by `startScreenShare()` when the call is already sharing a screen.
|
|
9478
|
+
*
|
|
9479
|
+
* A call carries at most one screen share. Accepting a second one would
|
|
9480
|
+
* overwrite the only reference the SDK holds to the first, leaving it
|
|
9481
|
+
* capturing and sending with no way to stop it — so the second request is
|
|
9482
|
+
* rejected and the live share is left untouched. Call `stopScreenShare()`
|
|
9483
|
+
* first to replace it.
|
|
9484
|
+
*/
|
|
9485
|
+
var ScreenShareAlreadyActiveError = class extends Error {
|
|
9486
|
+
constructor(screenShareId, options) {
|
|
9487
|
+
super(`A screen share is already active on this call (${screenShareId}). Call stopScreenShare() before starting another one.`, options);
|
|
9488
|
+
this.screenShareId = screenShareId;
|
|
9489
|
+
this.name = "ScreenShareAlreadyActiveError";
|
|
9490
|
+
}
|
|
9491
|
+
};
|
|
9391
9492
|
var DPoPInitError = class extends Error {
|
|
9392
9493
|
constructor(originalError, message = "Failed to initialize DPoP key pair") {
|
|
9393
9494
|
super(message, { cause: originalError instanceof Error ? originalError : void 0 });
|
|
@@ -9628,9 +9729,9 @@ var require_loglevel = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
9628
9729
|
defaultLogger$1 = new Logger();
|
|
9629
9730
|
defaultLogger$1.getLogger = function getLogger$1(name) {
|
|
9630
9731
|
if (typeof name !== "symbol" && typeof name !== "string" || name === "") throw new TypeError("You must supply a name when creating a logger.");
|
|
9631
|
-
var logger$
|
|
9632
|
-
if (!logger$
|
|
9633
|
-
return logger$
|
|
9732
|
+
var logger$34 = _loggersByName[name];
|
|
9733
|
+
if (!logger$34) logger$34 = _loggersByName[name] = new Logger(name, defaultLogger$1.methodFactory);
|
|
9734
|
+
return logger$34;
|
|
9634
9735
|
};
|
|
9635
9736
|
var _log = typeof window !== undefinedType ? window.log : void 0;
|
|
9636
9737
|
defaultLogger$1.noConflict = function() {
|
|
@@ -9666,8 +9767,8 @@ const defaultLoggerLevel = defaultLogger.levels.WARN;
|
|
|
9666
9767
|
defaultLogger.setLevel(defaultLoggerLevel);
|
|
9667
9768
|
let userLogger = null;
|
|
9668
9769
|
/** Replace the built-in logger with a custom implementation. Pass `null` to restore defaults. */
|
|
9669
|
-
const setLogger = (logger$
|
|
9670
|
-
userLogger = logger$
|
|
9770
|
+
const setLogger = (logger$34) => {
|
|
9771
|
+
userLogger = logger$34;
|
|
9671
9772
|
};
|
|
9672
9773
|
let debugOptions = {};
|
|
9673
9774
|
/** Configure debug options (e.g., `{ logWsTraffic: true }`). */
|
|
@@ -9711,8 +9812,8 @@ const wsTraffic = (options) => {
|
|
|
9711
9812
|
loggerInstance.debug(`${options.type.toUpperCase()}: \n`, msg, "\n");
|
|
9712
9813
|
};
|
|
9713
9814
|
const getLogger = () => {
|
|
9714
|
-
const logger$
|
|
9715
|
-
return new Proxy(logger$
|
|
9815
|
+
const logger$34 = getLoggerInstance();
|
|
9816
|
+
return new Proxy(logger$34, { get(_target, prop, _receiver) {
|
|
9716
9817
|
if (prop === "wsTraffic") return wsTraffic;
|
|
9717
9818
|
const instance = getLoggerInstance();
|
|
9718
9819
|
const value = Reflect.get(instance, prop);
|
|
@@ -9764,7 +9865,7 @@ const asyncRetry = async ({ asyncCallable, maxRetries: retries = DEFAULT_MAX_RET
|
|
|
9764
9865
|
|
|
9765
9866
|
//#endregion
|
|
9766
9867
|
//#region src/controllers/HTTPRequestController.ts
|
|
9767
|
-
const logger$
|
|
9868
|
+
const logger$33 = getLogger();
|
|
9768
9869
|
const GET_PARAMS = {
|
|
9769
9870
|
method: "GET",
|
|
9770
9871
|
headers: { Accept: "application/json" }
|
|
@@ -9828,7 +9929,7 @@ var HTTPRequestController = class HTTPRequestController extends Destroyable {
|
|
|
9828
9929
|
this._responses$.next(response);
|
|
9829
9930
|
return response;
|
|
9830
9931
|
} catch (error) {
|
|
9831
|
-
logger$
|
|
9932
|
+
logger$33.error("[HTTPRequestController] Request error:", error);
|
|
9832
9933
|
this._status$.next("error");
|
|
9833
9934
|
const err = error instanceof Error ? error : new Error("HTTP request failed", { cause: error });
|
|
9834
9935
|
this._errors$.next(err);
|
|
@@ -9855,7 +9956,7 @@ var HTTPRequestController = class HTTPRequestController extends Destroyable {
|
|
|
9855
9956
|
const url = this.buildURL(request.url);
|
|
9856
9957
|
const headers = this.buildHeaders(request.headers);
|
|
9857
9958
|
const timeout$5 = request.timeout ?? this.requestTimeout;
|
|
9858
|
-
logger$
|
|
9959
|
+
logger$33.debug("[HTTPRequestController] Executing request:", {
|
|
9859
9960
|
method: request.method,
|
|
9860
9961
|
url,
|
|
9861
9962
|
headers: Object.keys(headers).reduce((acc, key) => {
|
|
@@ -9875,7 +9976,7 @@ var HTTPRequestController = class HTTPRequestController extends Destroyable {
|
|
|
9875
9976
|
});
|
|
9876
9977
|
clearTimeout(timeoutId);
|
|
9877
9978
|
const httpResponse = await this.convertResponse(response);
|
|
9878
|
-
logger$
|
|
9979
|
+
logger$33.debug("[HTTPRequestController] Response received:", {
|
|
9879
9980
|
status: response.status,
|
|
9880
9981
|
statusText: response.statusText,
|
|
9881
9982
|
headers: [...response.headers.entries()],
|
|
@@ -9885,7 +9986,7 @@ var HTTPRequestController = class HTTPRequestController extends Destroyable {
|
|
|
9885
9986
|
} catch (error) {
|
|
9886
9987
|
clearTimeout(timeoutId);
|
|
9887
9988
|
if (error instanceof Error && error.name === "AbortError") throw new RequestTimeoutError(`Request timeout after ${timeout$5}ms`, { cause: error });
|
|
9888
|
-
logger$
|
|
9989
|
+
logger$33.error("[HTTPRequestController] Request failed:", error);
|
|
9889
9990
|
throw error;
|
|
9890
9991
|
}
|
|
9891
9992
|
}
|
|
@@ -9899,8 +10000,8 @@ var HTTPRequestController = class HTTPRequestController extends Destroyable {
|
|
|
9899
10000
|
const credential = this.getCredential();
|
|
9900
10001
|
if (credential.token) {
|
|
9901
10002
|
headers.Authorization = `Bearer ${credential.token}`;
|
|
9902
|
-
logger$
|
|
9903
|
-
} else logger$
|
|
10003
|
+
logger$33.debug("[HTTPRequestController] Using Bearer token auth, token length:", credential.token.length);
|
|
10004
|
+
} else logger$33.warn("[HTTPRequestController] No credentials available for authentication");
|
|
9904
10005
|
return headers;
|
|
9905
10006
|
}
|
|
9906
10007
|
/**
|
|
@@ -10048,7 +10149,7 @@ function fromMsToSec(milliseconds) {
|
|
|
10048
10149
|
|
|
10049
10150
|
//#endregion
|
|
10050
10151
|
//#region src/containers/PreferencesContainer.ts
|
|
10051
|
-
const logger$
|
|
10152
|
+
const logger$32 = getLogger();
|
|
10052
10153
|
var PreferencesContainer = class PreferencesContainer {
|
|
10053
10154
|
static get instance() {
|
|
10054
10155
|
this._instance ??= new PreferencesContainer();
|
|
@@ -10710,7 +10811,7 @@ var ClientPreferences = class {
|
|
|
10710
10811
|
if (!this._storage) return;
|
|
10711
10812
|
const data = collectStoredPreferences();
|
|
10712
10813
|
this._storage.setItem(PREFERENCES_STORAGE_KEY, data, "local").catch((error) => {
|
|
10713
|
-
logger$
|
|
10814
|
+
logger$32.error(`[ClientPreferences] Failed to save preferences: ${String(error)}`);
|
|
10714
10815
|
});
|
|
10715
10816
|
}
|
|
10716
10817
|
/** Loads preferences from storage and applies them to the container. */
|
|
@@ -10719,7 +10820,7 @@ var ClientPreferences = class {
|
|
|
10719
10820
|
this._storage.getItem(PREFERENCES_STORAGE_KEY, "local").then((stored) => {
|
|
10720
10821
|
if (stored) applyStoredPreferences(stored);
|
|
10721
10822
|
}).catch((error) => {
|
|
10722
|
-
logger$
|
|
10823
|
+
logger$32.error(`[ClientPreferences] Failed to load preferences: ${String(error)}`);
|
|
10723
10824
|
});
|
|
10724
10825
|
}
|
|
10725
10826
|
};
|
|
@@ -10741,7 +10842,7 @@ function toError(value) {
|
|
|
10741
10842
|
//#endregion
|
|
10742
10843
|
//#region src/controllers/NavigatorDeviceController.ts
|
|
10743
10844
|
var import_cjs$29 = require_cjs();
|
|
10744
|
-
const logger$
|
|
10845
|
+
const logger$31 = getLogger();
|
|
10745
10846
|
/** Maps a device kind to its storage key. */
|
|
10746
10847
|
const DEVICE_STORAGE_KEYS = {
|
|
10747
10848
|
audioinput: DEVICE_STORAGE_KEY_AUDIO_INPUT,
|
|
@@ -10763,7 +10864,7 @@ var NavigatorDeviceController = class extends Destroyable {
|
|
|
10763
10864
|
super();
|
|
10764
10865
|
this.webRTCApiProvider = webRTCApiProvider;
|
|
10765
10866
|
this.deviceChangeHandler = () => {
|
|
10766
|
-
logger$
|
|
10867
|
+
logger$31.debug("[DeviceController] Device change detected");
|
|
10767
10868
|
this.enumerateDevices();
|
|
10768
10869
|
};
|
|
10769
10870
|
this._devicesState$ = this.createBehaviorSubject(initialDevicesState);
|
|
@@ -10828,13 +10929,13 @@ var NavigatorDeviceController = class extends Destroyable {
|
|
|
10828
10929
|
return this.cachedObservable("videoInputDevices$", () => this._devicesState$.pipe((0, import_cjs$29.map)((state) => state.videoinput), (0, import_cjs$29.distinctUntilChanged)(), (0, import_cjs$29.takeUntil)(this.destroyed$)));
|
|
10829
10930
|
}
|
|
10830
10931
|
get selectedAudioInputDevice$() {
|
|
10831
|
-
return this.cachedObservable("selectedAudioInputDevice$", () => this._selectedDevicesState$.asObservable().pipe((0, import_cjs$29.map)((state) => state.audioinput), (0, import_cjs$29.distinctUntilChanged)(), (0, import_cjs$29.takeUntil)(this.destroyed$), (0, import_cjs$29.tap)((info) => logger$
|
|
10932
|
+
return this.cachedObservable("selectedAudioInputDevice$", () => this._selectedDevicesState$.asObservable().pipe((0, import_cjs$29.map)((state) => state.audioinput), (0, import_cjs$29.distinctUntilChanged)(), (0, import_cjs$29.takeUntil)(this.destroyed$), (0, import_cjs$29.tap)((info) => logger$31.debug("[DeviceController] Selected audio input device changed:", info))));
|
|
10832
10933
|
}
|
|
10833
10934
|
get selectedAudioOutputDevice$() {
|
|
10834
|
-
return this.cachedObservable("selectedAudioOutputDevice$", () => this._selectedDevicesState$.asObservable().pipe((0, import_cjs$29.map)((state) => state.audiooutput), (0, import_cjs$29.distinctUntilChanged)(), (0, import_cjs$29.takeUntil)(this.destroyed$), (0, import_cjs$29.tap)((info) => logger$
|
|
10935
|
+
return this.cachedObservable("selectedAudioOutputDevice$", () => this._selectedDevicesState$.asObservable().pipe((0, import_cjs$29.map)((state) => state.audiooutput), (0, import_cjs$29.distinctUntilChanged)(), (0, import_cjs$29.takeUntil)(this.destroyed$), (0, import_cjs$29.tap)((info) => logger$31.debug("[DeviceController] Selected audio output device changed:", info))));
|
|
10835
10936
|
}
|
|
10836
10937
|
get selectedVideoInputDevice$() {
|
|
10837
|
-
return this.cachedObservable("selectedVideoInputDevice$", () => this._selectedDevicesState$.asObservable().pipe((0, import_cjs$29.map)((state) => state.videoinput), (0, import_cjs$29.distinctUntilChanged)(), (0, import_cjs$29.takeUntil)(this.destroyed$), (0, import_cjs$29.tap)((info) => logger$
|
|
10938
|
+
return this.cachedObservable("selectedVideoInputDevice$", () => this._selectedDevicesState$.asObservable().pipe((0, import_cjs$29.map)((state) => state.videoinput), (0, import_cjs$29.distinctUntilChanged)(), (0, import_cjs$29.takeUntil)(this.destroyed$), (0, import_cjs$29.tap)((info) => logger$31.debug("[DeviceController] Selected video input device changed:", info))));
|
|
10838
10939
|
}
|
|
10839
10940
|
get selectedAudioInputDevice() {
|
|
10840
10941
|
if (this._audioInputDisabled$.value) return null;
|
|
@@ -10909,7 +11010,7 @@ var NavigatorDeviceController = class extends Destroyable {
|
|
|
10909
11010
|
if (device) this.persistDeviceSelection("audioinput", device);
|
|
10910
11011
|
}
|
|
10911
11012
|
selectVideoInputDevice(device) {
|
|
10912
|
-
logger$
|
|
11013
|
+
logger$31.debug("[DeviceController] Setting selected video input device:", device);
|
|
10913
11014
|
if (this._videoInputDisabled$.value && device) this._videoInputDisabled$.next(false);
|
|
10914
11015
|
const previous = this._selectedDevicesState$.value.videoinput;
|
|
10915
11016
|
if (previous && previous.deviceId !== device?.deviceId) this._deviceHistory.push("videoinput", previous);
|
|
@@ -10966,7 +11067,7 @@ var NavigatorDeviceController = class extends Destroyable {
|
|
|
10966
11067
|
}
|
|
10967
11068
|
const fromHistory = this._deviceHistory.findInHistory(kind, devices);
|
|
10968
11069
|
if (fromHistory) {
|
|
10969
|
-
logger$
|
|
11070
|
+
logger$31.debug(`[DeviceController] Device disappeared, falling back to history: ${fromHistory.label}`);
|
|
10970
11071
|
this.emitDeviceRecovered(kind, selected, fromHistory, "device_disconnected");
|
|
10971
11072
|
return fromHistory;
|
|
10972
11073
|
}
|
|
@@ -11019,7 +11120,7 @@ var NavigatorDeviceController = class extends Destroyable {
|
|
|
11019
11120
|
try {
|
|
11020
11121
|
await this._storageManager.setItem(DEVICE_STORAGE_KEYS[kind], stored, "local");
|
|
11021
11122
|
} catch (error) {
|
|
11022
|
-
logger$
|
|
11123
|
+
logger$31.error(`[DeviceController] Failed to persist device selection for ${kind}:`, error);
|
|
11023
11124
|
}
|
|
11024
11125
|
}
|
|
11025
11126
|
async loadPersistedDevices() {
|
|
@@ -11035,7 +11136,7 @@ var NavigatorDeviceController = class extends Destroyable {
|
|
|
11035
11136
|
[kind]: stored
|
|
11036
11137
|
};
|
|
11037
11138
|
} catch (error) {
|
|
11038
|
-
logger$
|
|
11139
|
+
logger$31.error(`[DeviceController] Failed to load persisted device for ${kind}:`, error);
|
|
11039
11140
|
}
|
|
11040
11141
|
}
|
|
11041
11142
|
/** Clears device history, persisted selections, and re-enumerates devices. */
|
|
@@ -11053,7 +11154,7 @@ var NavigatorDeviceController = class extends Destroyable {
|
|
|
11053
11154
|
this.disableDeviceMonitoring();
|
|
11054
11155
|
this.webRTCApiProvider.mediaDevices.addEventListener("devicechange", this.deviceChangeHandler);
|
|
11055
11156
|
if (PreferencesContainer.instance.devicePollingInterval > 0) this._devicesPoolingSubscription = (0, import_cjs$29.interval)(PreferencesContainer.instance.devicePollingInterval).subscribe(() => {
|
|
11056
|
-
logger$
|
|
11157
|
+
logger$31.debug("[DeviceController] Polling devices due to interval");
|
|
11057
11158
|
this.enumerateDevices();
|
|
11058
11159
|
});
|
|
11059
11160
|
this.enumerateDevices();
|
|
@@ -11079,13 +11180,13 @@ var NavigatorDeviceController = class extends Destroyable {
|
|
|
11079
11180
|
videoinput: []
|
|
11080
11181
|
});
|
|
11081
11182
|
this._devicesState$.next(devicesByKind);
|
|
11082
|
-
logger$
|
|
11183
|
+
logger$31.debug("[DeviceController] Devices enumerated:", {
|
|
11083
11184
|
audioInputs: devicesByKind.audioinput.length,
|
|
11084
11185
|
audioOutputs: devicesByKind.audiooutput.length,
|
|
11085
11186
|
videoInputs: devicesByKind.videoinput.length
|
|
11086
11187
|
});
|
|
11087
11188
|
} catch (error) {
|
|
11088
|
-
logger$
|
|
11189
|
+
logger$31.error("[DeviceController] Failed to enumerate devices:", error);
|
|
11089
11190
|
this._errors$.next(toError(error));
|
|
11090
11191
|
}
|
|
11091
11192
|
}
|
|
@@ -11101,7 +11202,7 @@ var NavigatorDeviceController = class extends Destroyable {
|
|
|
11101
11202
|
stream.getTracks().forEach((t) => t.stop());
|
|
11102
11203
|
return capabilities;
|
|
11103
11204
|
} catch (error) {
|
|
11104
|
-
logger$
|
|
11205
|
+
logger$31.error("[DeviceController] Failed to get device capabilities:", error);
|
|
11105
11206
|
this._errors$.next(toError(error));
|
|
11106
11207
|
throw error;
|
|
11107
11208
|
}
|
|
@@ -11352,7 +11453,7 @@ var DependencyContainer = class {
|
|
|
11352
11453
|
|
|
11353
11454
|
//#endregion
|
|
11354
11455
|
//#region src/controllers/CryptoController.ts
|
|
11355
|
-
const logger$
|
|
11456
|
+
const logger$30 = getLogger();
|
|
11356
11457
|
const DPOP_DB_NAME = "sw-dpop";
|
|
11357
11458
|
const DPOP_DB_VERSION = 1;
|
|
11358
11459
|
const DPOP_STORE_NAME = "keys";
|
|
@@ -11411,7 +11512,7 @@ async function loadKeyPairFromDB() {
|
|
|
11411
11512
|
tx.oncomplete = () => db.close();
|
|
11412
11513
|
});
|
|
11413
11514
|
} catch (error) {
|
|
11414
|
-
logger$
|
|
11515
|
+
logger$30.warn("[DPoP] Failed to load key pair from IndexedDB:", error);
|
|
11415
11516
|
return null;
|
|
11416
11517
|
}
|
|
11417
11518
|
}
|
|
@@ -11431,7 +11532,7 @@ async function saveKeyPairToDB(keyPair) {
|
|
|
11431
11532
|
};
|
|
11432
11533
|
});
|
|
11433
11534
|
} catch (error) {
|
|
11434
|
-
logger$
|
|
11535
|
+
logger$30.warn("[DPoP] Failed to save key pair to IndexedDB:", error);
|
|
11435
11536
|
}
|
|
11436
11537
|
}
|
|
11437
11538
|
async function deleteKeyPairFromDB() {
|
|
@@ -11450,7 +11551,7 @@ async function deleteKeyPairFromDB() {
|
|
|
11450
11551
|
};
|
|
11451
11552
|
});
|
|
11452
11553
|
} catch (error) {
|
|
11453
|
-
logger$
|
|
11554
|
+
logger$30.warn("[DPoP] Failed to delete key pair from IndexedDB:", error);
|
|
11454
11555
|
}
|
|
11455
11556
|
}
|
|
11456
11557
|
/**
|
|
@@ -11510,13 +11611,13 @@ var CryptoController = class {
|
|
|
11510
11611
|
this._publicJwk = await crypto.subtle.exportKey("jwk", stored.publicKey);
|
|
11511
11612
|
this._fingerprint = await computeJwkThumbprint(this._publicJwk);
|
|
11512
11613
|
this._initialized = true;
|
|
11513
|
-
logger$
|
|
11614
|
+
logger$30.debug("[DPoP] Key pair restored from IndexedDB, fingerprint:", this._fingerprint);
|
|
11514
11615
|
return this._fingerprint;
|
|
11515
11616
|
} catch (error) {
|
|
11516
|
-
logger$
|
|
11617
|
+
logger$30.warn("[DPoP] Stored key pair unusable, generating new one:", error);
|
|
11517
11618
|
await deleteKeyPairFromDB();
|
|
11518
11619
|
}
|
|
11519
|
-
logger$
|
|
11620
|
+
logger$30.debug("[DPoP] Generating RSA key pair");
|
|
11520
11621
|
this._keyPair = await crypto.subtle.generateKey({
|
|
11521
11622
|
name: "RSASSA-PKCS1-v1_5",
|
|
11522
11623
|
modulusLength: 2048,
|
|
@@ -11531,7 +11632,7 @@ var CryptoController = class {
|
|
|
11531
11632
|
this._fingerprint = await computeJwkThumbprint(this._publicJwk);
|
|
11532
11633
|
this._initialized = true;
|
|
11533
11634
|
await saveKeyPairToDB(this._keyPair);
|
|
11534
|
-
logger$
|
|
11635
|
+
logger$30.debug("[DPoP] Key pair generated and persisted, fingerprint:", this._fingerprint);
|
|
11535
11636
|
return this._fingerprint;
|
|
11536
11637
|
}
|
|
11537
11638
|
/**
|
|
@@ -11597,7 +11698,7 @@ var CryptoController = class {
|
|
|
11597
11698
|
this._fingerprint = null;
|
|
11598
11699
|
this._initialized = false;
|
|
11599
11700
|
deleteKeyPairFromDB();
|
|
11600
|
-
logger$
|
|
11701
|
+
logger$30.debug("[DPoP] Controller destroyed");
|
|
11601
11702
|
}
|
|
11602
11703
|
get publicJwk() {
|
|
11603
11704
|
if (!this._publicJwk) throw new DPoPInitError("CryptoController not initialized. Call init() first.");
|
|
@@ -11621,7 +11722,7 @@ var CryptoController = class {
|
|
|
11621
11722
|
//#endregion
|
|
11622
11723
|
//#region src/controllers/NetworkMonitor.ts
|
|
11623
11724
|
var import_cjs$28 = require_cjs();
|
|
11624
|
-
const logger$
|
|
11725
|
+
const logger$29 = getLogger();
|
|
11625
11726
|
/**
|
|
11626
11727
|
* Safely check whether we are running in a browser environment
|
|
11627
11728
|
* with `window` and the relevant event targets.
|
|
@@ -11678,7 +11779,7 @@ var NetworkMonitor = class extends Destroyable {
|
|
|
11678
11779
|
}
|
|
11679
11780
|
attachListeners() {
|
|
11680
11781
|
if (!hasBrowserNetworkEvents()) {
|
|
11681
|
-
logger$
|
|
11782
|
+
logger$29.debug("NetworkMonitor: no browser environment detected, skipping event listeners");
|
|
11682
11783
|
return;
|
|
11683
11784
|
}
|
|
11684
11785
|
window.addEventListener("online", this._onOnline);
|
|
@@ -11686,7 +11787,7 @@ var NetworkMonitor = class extends Destroyable {
|
|
|
11686
11787
|
const connection = getNetworkConnection();
|
|
11687
11788
|
if (connection) connection.addEventListener("change", this._onConnectionChange);
|
|
11688
11789
|
this._listenersAttached = true;
|
|
11689
|
-
logger$
|
|
11790
|
+
logger$29.debug("NetworkMonitor: event listeners attached");
|
|
11690
11791
|
}
|
|
11691
11792
|
removeListeners() {
|
|
11692
11793
|
if (!this._listenersAttached) return;
|
|
@@ -11697,10 +11798,10 @@ var NetworkMonitor = class extends Destroyable {
|
|
|
11697
11798
|
if (connection) connection.removeEventListener("change", this._onConnectionChange);
|
|
11698
11799
|
}
|
|
11699
11800
|
this._listenersAttached = false;
|
|
11700
|
-
logger$
|
|
11801
|
+
logger$29.debug("NetworkMonitor: event listeners removed");
|
|
11701
11802
|
}
|
|
11702
11803
|
handleOnline() {
|
|
11703
|
-
logger$
|
|
11804
|
+
logger$29.info("NetworkMonitor: browser went online");
|
|
11704
11805
|
this._isOnline$.next(true);
|
|
11705
11806
|
this._networkChange$.next({
|
|
11706
11807
|
type: "online",
|
|
@@ -11709,7 +11810,7 @@ var NetworkMonitor = class extends Destroyable {
|
|
|
11709
11810
|
});
|
|
11710
11811
|
}
|
|
11711
11812
|
handleOffline() {
|
|
11712
|
-
logger$
|
|
11813
|
+
logger$29.info("NetworkMonitor: browser went offline");
|
|
11713
11814
|
this._isOnline$.next(false);
|
|
11714
11815
|
this._networkChange$.next({
|
|
11715
11816
|
type: "offline",
|
|
@@ -11718,7 +11819,7 @@ var NetworkMonitor = class extends Destroyable {
|
|
|
11718
11819
|
}
|
|
11719
11820
|
handleConnectionChange() {
|
|
11720
11821
|
const networkType = getNetworkType();
|
|
11721
|
-
logger$
|
|
11822
|
+
logger$29.info(`NetworkMonitor: connection changed — effectiveType=${networkType ?? "unknown"}`);
|
|
11722
11823
|
this._networkChange$.next({
|
|
11723
11824
|
type: "connection_change",
|
|
11724
11825
|
timestamp: Date.now(),
|
|
@@ -11834,7 +11935,7 @@ function getNavigatorMediaDevices() {
|
|
|
11834
11935
|
//#endregion
|
|
11835
11936
|
//#region src/controllers/PreflightRunner.ts
|
|
11836
11937
|
var import_cjs$27 = require_cjs();
|
|
11837
|
-
const logger$
|
|
11938
|
+
const logger$28 = getLogger();
|
|
11838
11939
|
const DEFAULT_MEDIA_TEST_DURATION_S = 10;
|
|
11839
11940
|
const ICE_GATHERING_TIMEOUT_MS = 1e4;
|
|
11840
11941
|
const SIGNALING_RTT_TIMEOUT_MS = 5e3;
|
|
@@ -11883,7 +11984,7 @@ var PreflightRunner = class extends Destroyable {
|
|
|
11883
11984
|
if (!this._options.skipMediaTest) try {
|
|
11884
11985
|
bandwidth = await this.testMediaBandwidth(destination);
|
|
11885
11986
|
} catch (error) {
|
|
11886
|
-
logger$
|
|
11987
|
+
logger$28.warn("[PreflightRunner] Media bandwidth test failed:", error);
|
|
11887
11988
|
warnings.push("Media bandwidth test failed");
|
|
11888
11989
|
}
|
|
11889
11990
|
return {
|
|
@@ -11895,7 +11996,7 @@ var PreflightRunner = class extends Destroyable {
|
|
|
11895
11996
|
warnings
|
|
11896
11997
|
};
|
|
11897
11998
|
} catch (error) {
|
|
11898
|
-
logger$
|
|
11999
|
+
logger$28.error("[PreflightRunner] Preflight test failed:", error);
|
|
11899
12000
|
throw new PreflightError("preflight", error instanceof Error ? error : new Error(String(error)));
|
|
11900
12001
|
} finally {
|
|
11901
12002
|
this.destroy();
|
|
@@ -11926,7 +12027,7 @@ var PreflightRunner = class extends Destroyable {
|
|
|
11926
12027
|
if (track.kind === "video" && track.readyState === "live") videoWorking = true;
|
|
11927
12028
|
}
|
|
11928
12029
|
} catch (error) {
|
|
11929
|
-
logger$
|
|
12030
|
+
logger$28.warn("[PreflightRunner] Device test failed:", error);
|
|
11930
12031
|
} finally {
|
|
11931
12032
|
if (audioStream) audioStream.getTracks().forEach((t) => t.stop());
|
|
11932
12033
|
}
|
|
@@ -11984,7 +12085,7 @@ var PreflightRunner = class extends Destroyable {
|
|
|
11984
12085
|
rttMs
|
|
11985
12086
|
};
|
|
11986
12087
|
} catch (error) {
|
|
11987
|
-
logger$
|
|
12088
|
+
logger$28.warn("[PreflightRunner] ICE connectivity test failed:", error);
|
|
11988
12089
|
return {
|
|
11989
12090
|
type: "failed",
|
|
11990
12091
|
turnReachable: false,
|
|
@@ -12032,7 +12133,7 @@ var PreflightRunner = class extends Destroyable {
|
|
|
12032
12133
|
//#endregion
|
|
12033
12134
|
//#region src/controllers/VisibilityController.ts
|
|
12034
12135
|
var import_cjs$26 = require_cjs();
|
|
12035
|
-
const logger$
|
|
12136
|
+
const logger$27 = getLogger();
|
|
12036
12137
|
/**
|
|
12037
12138
|
* Checks whether the document visibility API is available.
|
|
12038
12139
|
*/
|
|
@@ -12069,8 +12170,8 @@ var VisibilityController = class extends Destroyable {
|
|
|
12069
12170
|
this._boundHandler = this._handleVisibilityChange.bind(this);
|
|
12070
12171
|
if (this._hasVisibilityApi) {
|
|
12071
12172
|
document.addEventListener("visibilitychange", this._boundHandler);
|
|
12072
|
-
logger$
|
|
12073
|
-
} else logger$
|
|
12173
|
+
logger$27.debug("VisibilityController: listening for visibilitychange events");
|
|
12174
|
+
} else logger$27.debug("VisibilityController: document visibility API not available, defaulting to visible");
|
|
12074
12175
|
}
|
|
12075
12176
|
/**
|
|
12076
12177
|
* Observable of the current visibility state.
|
|
@@ -12095,7 +12196,7 @@ var VisibilityController = class extends Destroyable {
|
|
|
12095
12196
|
destroy() {
|
|
12096
12197
|
if (this._hasVisibilityApi) {
|
|
12097
12198
|
document.removeEventListener("visibilitychange", this._boundHandler);
|
|
12098
|
-
logger$
|
|
12199
|
+
logger$27.debug("VisibilityController: removed visibilitychange listener");
|
|
12099
12200
|
}
|
|
12100
12201
|
super.destroy();
|
|
12101
12202
|
}
|
|
@@ -12113,7 +12214,7 @@ var VisibilityController = class extends Destroyable {
|
|
|
12113
12214
|
timestamp: Date.now()
|
|
12114
12215
|
};
|
|
12115
12216
|
this._visibilityChange$.next(changeEvent);
|
|
12116
|
-
logger$
|
|
12217
|
+
logger$27.debug("VisibilityController: visibility changed", {
|
|
12117
12218
|
from: previousState,
|
|
12118
12219
|
to: newState
|
|
12119
12220
|
});
|
|
@@ -12264,6 +12365,12 @@ const RPCReauthenticate = (params) => {
|
|
|
12264
12365
|
|
|
12265
12366
|
//#endregion
|
|
12266
12367
|
//#region src/core/RPCMessages/RPCPing.ts
|
|
12368
|
+
const RPCPing = () => {
|
|
12369
|
+
return buildRPCRequest({
|
|
12370
|
+
method: "signalwire.ping",
|
|
12371
|
+
params: { timestamp: Date.now() / 1e3 }
|
|
12372
|
+
});
|
|
12373
|
+
};
|
|
12267
12374
|
const RPCPingResponse = (id, timestamp$1) => {
|
|
12268
12375
|
return makeRPCResponse({
|
|
12269
12376
|
id,
|
|
@@ -12352,15 +12459,57 @@ const RPCEventAckResponse = (id) => makeRPCResponse({
|
|
|
12352
12459
|
result: {}
|
|
12353
12460
|
});
|
|
12354
12461
|
|
|
12462
|
+
//#endregion
|
|
12463
|
+
//#region src/utils/authRecovery.ts
|
|
12464
|
+
/**
|
|
12465
|
+
* Walk an error's `error`/`cause` chain looking for a {@link JSONRPCError}.
|
|
12466
|
+
* Errors thrown by call creation are wrapped (e.g. `CallCreateError`), so the
|
|
12467
|
+
* underlying signaling error is nested. Bounded by a visited set to guard
|
|
12468
|
+
* against cyclic causes.
|
|
12469
|
+
*/
|
|
12470
|
+
function findJSONRPCError(error) {
|
|
12471
|
+
const seen = /* @__PURE__ */ new Set();
|
|
12472
|
+
let current = error;
|
|
12473
|
+
while (current instanceof Error && !seen.has(current)) {
|
|
12474
|
+
seen.add(current);
|
|
12475
|
+
if (current instanceof JSONRPCError) return current;
|
|
12476
|
+
current = current.error ?? current.cause;
|
|
12477
|
+
}
|
|
12478
|
+
}
|
|
12479
|
+
/**
|
|
12480
|
+
* Whether an error is a session-recoverable authentication failure
|
|
12481
|
+
* (`-32002` authentication failed or `-32003` requester validation failed)
|
|
12482
|
+
* that a credential re-mint + retry can heal.
|
|
12483
|
+
*/
|
|
12484
|
+
function isRecoverableAuthError(error) {
|
|
12485
|
+
const rpcError = findJSONRPCError(error);
|
|
12486
|
+
return rpcError !== void 0 && (rpcError.code === RPC_ERROR_REQUESTER_VALIDATION_FAILED || rpcError.code === RPC_ERROR_AUTHENTICATION_FAILED);
|
|
12487
|
+
}
|
|
12488
|
+
/**
|
|
12489
|
+
* Whether an error is specifically a requester-validation rejection
|
|
12490
|
+
* (`-32003`) — the server refusing the session's credential.
|
|
12491
|
+
*
|
|
12492
|
+
* Narrower than {@link isRecoverableAuthError} on purpose. `-32002` is
|
|
12493
|
+
* overloaded server-side: a rejected reattach arrives as `-32002` with
|
|
12494
|
+
* `cause: INVALID_MSG_UNSPECIFIED` and message `CALL ERROR`, which is a
|
|
12495
|
+
* call-level rejection and says nothing about the credential. Use this where
|
|
12496
|
+
* the decision must not be fooled by that, such as deciding whether retrying
|
|
12497
|
+
* an operation could possibly succeed.
|
|
12498
|
+
*/
|
|
12499
|
+
function isRequesterValidationError(error) {
|
|
12500
|
+
return findJSONRPCError(error)?.code === RPC_ERROR_REQUESTER_VALIDATION_FAILED;
|
|
12501
|
+
}
|
|
12502
|
+
|
|
12355
12503
|
//#endregion
|
|
12356
12504
|
//#region src/managers/AttachManager.ts
|
|
12357
|
-
const logger$
|
|
12505
|
+
const logger$26 = getLogger();
|
|
12358
12506
|
var AttachManager = class {
|
|
12359
|
-
constructor(storage, deviceController, reconnectCallsTimeout, attachKey) {
|
|
12507
|
+
constructor(storage, deviceController, reconnectCallsTimeout, attachKey, credentialRecovered) {
|
|
12360
12508
|
this.storage = storage;
|
|
12361
12509
|
this.deviceController = deviceController;
|
|
12362
12510
|
this.reconnectCallsTimeout = reconnectCallsTimeout;
|
|
12363
12511
|
this.attachKey = attachKey;
|
|
12512
|
+
this.credentialRecovered = credentialRecovered;
|
|
12364
12513
|
this.writeQueue = Promise.resolve();
|
|
12365
12514
|
}
|
|
12366
12515
|
async detachAll() {
|
|
@@ -12375,7 +12524,7 @@ var AttachManager = class {
|
|
|
12375
12524
|
try {
|
|
12376
12525
|
return await this.storage.getItem(this.attachKey) ?? {};
|
|
12377
12526
|
} catch (error) {
|
|
12378
|
-
logger$
|
|
12527
|
+
logger$26.warn("[AttachManager] Failed to retrieve attached calls from storage", error);
|
|
12379
12528
|
return {};
|
|
12380
12529
|
}
|
|
12381
12530
|
}
|
|
@@ -12383,7 +12532,7 @@ var AttachManager = class {
|
|
|
12383
12532
|
try {
|
|
12384
12533
|
await this.storage.setItem(this.attachKey, attached);
|
|
12385
12534
|
} catch (error) {
|
|
12386
|
-
logger$
|
|
12535
|
+
logger$26.warn("[AttachManager] Failed to write attached calls to storage", error);
|
|
12387
12536
|
}
|
|
12388
12537
|
}
|
|
12389
12538
|
/**
|
|
@@ -12402,11 +12551,39 @@ var AttachManager = class {
|
|
|
12402
12551
|
}
|
|
12403
12552
|
async attach(call) {
|
|
12404
12553
|
if (!call.to) {
|
|
12405
|
-
logger$
|
|
12554
|
+
logger$26.warn("[AttachManager] Skip attach for calls with no destination");
|
|
12406
12555
|
return;
|
|
12407
12556
|
}
|
|
12557
|
+
const attachment = this.buildAttachment(call, call.to);
|
|
12558
|
+
await this.mutate((attached) => ({
|
|
12559
|
+
...attached,
|
|
12560
|
+
[call.id]: attachment
|
|
12561
|
+
}));
|
|
12562
|
+
}
|
|
12563
|
+
/**
|
|
12564
|
+
* Keep an already-stored call's reference alive and current — the periodic
|
|
12565
|
+
* refresh the `verto.ping` keepalive drives.
|
|
12566
|
+
*
|
|
12567
|
+
* Only ever updates: a call with no record is one nothing wants reattached,
|
|
12568
|
+
* and re-creating it here would undo a `detach`. That matters because a ping
|
|
12569
|
+
* can land in the window between `bye()` detaching and the call being torn
|
|
12570
|
+
* down, and a record revived there survives the hangup — so the next page
|
|
12571
|
+
* load dials a call nobody is on. The existence check and the write share
|
|
12572
|
+
* one {@link mutate} turn, so a concurrent detach cannot slip between them.
|
|
12573
|
+
*/
|
|
12574
|
+
async refresh(call) {
|
|
12575
|
+
if (!call.to) return;
|
|
12408
12576
|
const destination = call.to;
|
|
12409
|
-
|
|
12577
|
+
await this.mutate((attached) => {
|
|
12578
|
+
if (!Object.hasOwn(attached, call.id)) return attached;
|
|
12579
|
+
return {
|
|
12580
|
+
...attached,
|
|
12581
|
+
[call.id]: this.buildAttachment(call, destination)
|
|
12582
|
+
};
|
|
12583
|
+
});
|
|
12584
|
+
}
|
|
12585
|
+
buildAttachment(call, destination) {
|
|
12586
|
+
return {
|
|
12410
12587
|
nodeId: call.nodeId,
|
|
12411
12588
|
destination,
|
|
12412
12589
|
mediaDirections: call.mediaDirections,
|
|
@@ -12414,10 +12591,6 @@ var AttachManager = class {
|
|
|
12414
12591
|
videoInputDevice: call.mediaDirections.video !== "inactive" ? this.deviceController.selectedVideoInputDevice : null,
|
|
12415
12592
|
attachedAt: Date.now()
|
|
12416
12593
|
};
|
|
12417
|
-
await this.mutate((attached) => ({
|
|
12418
|
-
...attached,
|
|
12419
|
-
[call.id]: attachment
|
|
12420
|
-
}));
|
|
12421
12594
|
}
|
|
12422
12595
|
async detach(call) {
|
|
12423
12596
|
await this.mutate((attached) => {
|
|
@@ -12440,8 +12613,14 @@ var AttachManager = class {
|
|
|
12440
12613
|
* rejecting. Once that fix is deployed, this will work for both
|
|
12441
12614
|
* page reloads and WebSocket reconnects.
|
|
12442
12615
|
*
|
|
12443
|
-
*
|
|
12444
|
-
*
|
|
12616
|
+
* A failed reattach does NOT generally cost the stored reference. It is
|
|
12617
|
+
* discarded only when the server denied the reattach on a session whose
|
|
12618
|
+
* credential it had already accepted — a verified reauthentication followed
|
|
12619
|
+
* by a refusal is the server saying the call is gone, and that is the one
|
|
12620
|
+
* refusal worth acting on. Until then the credential may be what is being
|
|
12621
|
+
* refused, and the record is the only way a later reload can try again;
|
|
12622
|
+
* keeping it costs nothing, since `detachExpired` reaps it once it is older
|
|
12623
|
+
* than `reconnectCallsTimeout`.
|
|
12445
12624
|
*/
|
|
12446
12625
|
async reattachCalls() {
|
|
12447
12626
|
const attached = await this.readAttached();
|
|
@@ -12450,25 +12629,31 @@ var AttachManager = class {
|
|
|
12450
12629
|
const { destination } = attachment;
|
|
12451
12630
|
const options = this.buildCallOptions(attachment);
|
|
12452
12631
|
let succeeded = false;
|
|
12632
|
+
let refusedOnCredentials = false;
|
|
12453
12633
|
for (let attempt = 1; attempt <= 3; attempt++) try {
|
|
12454
12634
|
await this.session.createOutboundCall(destination, {
|
|
12455
12635
|
callId,
|
|
12456
12636
|
...options
|
|
12457
12637
|
});
|
|
12458
|
-
logger$
|
|
12638
|
+
logger$26.info(`[AttachManager] Reattached call ${callId} (attempt ${attempt})`);
|
|
12459
12639
|
succeeded = true;
|
|
12460
12640
|
break;
|
|
12461
12641
|
} catch (error) {
|
|
12462
|
-
logger$
|
|
12642
|
+
logger$26.warn(`[AttachManager] Reattach attempt ${attempt}/3 failed for call ${callId}:`, error);
|
|
12643
|
+
if (isRequesterValidationError(error)) {
|
|
12644
|
+
refusedOnCredentials = true;
|
|
12645
|
+
logger$26.warn(`[AttachManager] Reattach of ${callId} was refused on credentials; not retrying.`);
|
|
12646
|
+
break;
|
|
12647
|
+
}
|
|
12463
12648
|
if (attempt < 3) await new Promise((r) => setTimeout(r, (attempt + 1) * 1e3));
|
|
12464
12649
|
}
|
|
12465
|
-
if (!succeeded) {
|
|
12466
|
-
logger$
|
|
12650
|
+
if (!succeeded) if (this.credentialRecovered() && !refusedOnCredentials) {
|
|
12651
|
+
logger$26.warn(`[AttachManager] Reattach of ${callId} was denied after a verified reauthentication, removing reference`);
|
|
12467
12652
|
await this.detach({
|
|
12468
12653
|
id: callId,
|
|
12469
12654
|
mediaDirections: attachment.mediaDirections
|
|
12470
12655
|
});
|
|
12471
|
-
}
|
|
12656
|
+
} else logger$26.warn(`[AttachManager] Reattach failed for call ${callId}; keeping the reference (credential refused or never proven good)`);
|
|
12472
12657
|
}
|
|
12473
12658
|
}
|
|
12474
12659
|
/**
|
|
@@ -13634,7 +13819,7 @@ function toggleHandraiseMethod(is) {
|
|
|
13634
13819
|
|
|
13635
13820
|
//#endregion
|
|
13636
13821
|
//#region src/core/entities/Participant.ts
|
|
13637
|
-
const logger$
|
|
13822
|
+
const logger$25 = getLogger();
|
|
13638
13823
|
const initialState = {};
|
|
13639
13824
|
/**
|
|
13640
13825
|
* Represents a participant in a call.
|
|
@@ -13644,9 +13829,9 @@ const initialState = {};
|
|
|
13644
13829
|
* the local participant with additional device control.
|
|
13645
13830
|
*/
|
|
13646
13831
|
var Participant = class extends Destroyable {
|
|
13647
|
-
constructor(id,
|
|
13832
|
+
constructor(id, callExecuteMethod, deviceController) {
|
|
13648
13833
|
super();
|
|
13649
|
-
this.
|
|
13834
|
+
this.callExecuteMethod = callExecuteMethod;
|
|
13650
13835
|
this.deviceController = deviceController;
|
|
13651
13836
|
this._state$ = this.createBehaviorSubject(initialState);
|
|
13652
13837
|
this.id = id;
|
|
@@ -13868,22 +14053,55 @@ var Participant = class extends Destroyable {
|
|
|
13868
14053
|
get value() {
|
|
13869
14054
|
return this._state$.value;
|
|
13870
14055
|
}
|
|
14056
|
+
/**
|
|
14057
|
+
* Target triple for member RPCs, built from the participant's own state.
|
|
14058
|
+
* The backend locates the member's session by the target `call_id`/`node_id`,
|
|
14059
|
+
* so this must always be the participant's own call context — never the
|
|
14060
|
+
* local call's id (issue #19400).
|
|
14061
|
+
*
|
|
14062
|
+
* Reading it doubles as a readiness probe: it throws until the first full
|
|
14063
|
+
* member event (`member.joined`/`member.updated` or the `call.joined`
|
|
14064
|
+
* roster) arrives, and never regresses afterwards.
|
|
14065
|
+
*
|
|
14066
|
+
* @throws {ParticipantNotReadyError} If the member state has not been
|
|
14067
|
+
* received yet (e.g. a participant first seen via `member.talking`) — an
|
|
14068
|
+
* empty call context can never address the member, so fail fast instead of
|
|
14069
|
+
* sending a doomed RPC.
|
|
14070
|
+
*/
|
|
14071
|
+
get target() {
|
|
14072
|
+
const { call_id, node_id } = this._state$.value;
|
|
14073
|
+
if (!call_id || !node_id) throw new ParticipantNotReadyError(this.id);
|
|
14074
|
+
return {
|
|
14075
|
+
member_id: this.id,
|
|
14076
|
+
call_id,
|
|
14077
|
+
node_id
|
|
14078
|
+
};
|
|
14079
|
+
}
|
|
14080
|
+
/**
|
|
14081
|
+
* Executes a member RPC against this participant, injecting its own
|
|
14082
|
+
* {@link target} as the target.
|
|
14083
|
+
*
|
|
14084
|
+
* @throws {ParticipantNotReadyError} Via {@link target}, when the
|
|
14085
|
+
* member state has not been received yet.
|
|
14086
|
+
*/
|
|
14087
|
+
async executeMethod(method, args) {
|
|
14088
|
+
return this.callExecuteMethod(this.target, method, args);
|
|
14089
|
+
}
|
|
13871
14090
|
/** Toggles the deafened state (mutes/unmutes incoming audio). */
|
|
13872
14091
|
async toggleDeaf() {
|
|
13873
|
-
|
|
13874
|
-
await this.executeMethod(this.id, method, {});
|
|
14092
|
+
await this.executeMethod(toggleDeafMethod(this.deaf), {});
|
|
13875
14093
|
}
|
|
13876
14094
|
/** Toggles the hand-raised state. */
|
|
13877
14095
|
async toggleHandraise() {
|
|
13878
|
-
await this.executeMethod(
|
|
14096
|
+
await this.executeMethod(toggleHandraiseMethod(this.handraised), {});
|
|
13879
14097
|
}
|
|
13880
14098
|
/** Mutes the participant's audio. */
|
|
13881
14099
|
async mute() {
|
|
13882
|
-
await this.executeMethod(
|
|
14100
|
+
await this.executeMethod("call.mute", { channels: ["audio"] });
|
|
13883
14101
|
}
|
|
13884
14102
|
/** Unmutes the participant's audio. */
|
|
13885
14103
|
async unmute() {
|
|
13886
|
-
await this.executeMethod(
|
|
14104
|
+
await this.executeMethod("call.unmute", { channels: ["audio"] });
|
|
13887
14105
|
}
|
|
13888
14106
|
/** Toggles the participant's audio mute state. */
|
|
13889
14107
|
async toggleMute() {
|
|
@@ -13891,11 +14109,11 @@ var Participant = class extends Destroyable {
|
|
|
13891
14109
|
}
|
|
13892
14110
|
/** Mutes the participant's video. */
|
|
13893
14111
|
async muteVideo() {
|
|
13894
|
-
await this.executeMethod(
|
|
14112
|
+
await this.executeMethod("call.mute", { channels: ["video"] });
|
|
13895
14113
|
}
|
|
13896
14114
|
/** Unmutes the participant's video. */
|
|
13897
14115
|
async unmuteVideo() {
|
|
13898
|
-
await this.executeMethod(
|
|
14116
|
+
await this.executeMethod("call.unmute", { channels: ["video"] });
|
|
13899
14117
|
}
|
|
13900
14118
|
/** Toggles the participant's video mute state. */
|
|
13901
14119
|
async toggleMuteVideo() {
|
|
@@ -13903,7 +14121,7 @@ var Participant = class extends Destroyable {
|
|
|
13903
14121
|
}
|
|
13904
14122
|
/** Toggles echo cancellation on the audio input. */
|
|
13905
14123
|
async toggleEchoCancellation() {
|
|
13906
|
-
await this.executeMethod(
|
|
14124
|
+
await this.executeMethod("call.audioflags.set", {
|
|
13907
14125
|
echo_cancellation: !this.echoCancellation,
|
|
13908
14126
|
auto_gain: this.autoGain,
|
|
13909
14127
|
noise_suppression: this.noiseSuppression
|
|
@@ -13911,7 +14129,7 @@ var Participant = class extends Destroyable {
|
|
|
13911
14129
|
}
|
|
13912
14130
|
/** Toggles automatic gain control on the audio input. */
|
|
13913
14131
|
async toggleAudioInputAutoGain() {
|
|
13914
|
-
await this.executeMethod(
|
|
14132
|
+
await this.executeMethod("call.audioflags.set", {
|
|
13915
14133
|
echo_cancellation: this.echoCancellation,
|
|
13916
14134
|
auto_gain: !this.autoGain,
|
|
13917
14135
|
noise_suppression: this.noiseSuppression
|
|
@@ -13919,7 +14137,7 @@ var Participant = class extends Destroyable {
|
|
|
13919
14137
|
}
|
|
13920
14138
|
/** Toggles noise suppression on the audio input. */
|
|
13921
14139
|
async toggleNoiseSuppression() {
|
|
13922
|
-
await this.executeMethod(
|
|
14140
|
+
await this.executeMethod("call.audioflags.set", {
|
|
13923
14141
|
echo_cancellation: this.echoCancellation,
|
|
13924
14142
|
auto_gain: this.autoGain,
|
|
13925
14143
|
noise_suppression: !this.noiseSuppression
|
|
@@ -13927,7 +14145,7 @@ var Participant = class extends Destroyable {
|
|
|
13927
14145
|
}
|
|
13928
14146
|
/** Toggles low-bitrate mode for this participant's media. */
|
|
13929
14147
|
async toggleLowbitrate() {
|
|
13930
|
-
await this.executeMethod(
|
|
14148
|
+
await this.executeMethod("call.lowbitrate.set", { lowbitrate: !this.lowbitrate });
|
|
13931
14149
|
}
|
|
13932
14150
|
/**
|
|
13933
14151
|
* Adjusts the **conference-only** microphone energy gate / sensitivity level
|
|
@@ -13943,7 +14161,7 @@ var Participant = class extends Destroyable {
|
|
|
13943
14161
|
* (integer, larger values are more sensitive).
|
|
13944
14162
|
*/
|
|
13945
14163
|
async setAudioInputSensitivity(value) {
|
|
13946
|
-
await this.executeMethod(
|
|
14164
|
+
await this.executeMethod("call.microphone.sensitivity.set", { sensitivity: value });
|
|
13947
14165
|
}
|
|
13948
14166
|
/**
|
|
13949
14167
|
* Sets the **server-side** microphone volume on this participant's bridged
|
|
@@ -13956,7 +14174,7 @@ var Participant = class extends Destroyable {
|
|
|
13956
14174
|
* @param value - Volume level (0-100).
|
|
13957
14175
|
*/
|
|
13958
14176
|
async setAudioInputVolume(value) {
|
|
13959
|
-
await this.executeMethod(
|
|
14177
|
+
await this.executeMethod("call.microphone.volume.set", { volume: value });
|
|
13960
14178
|
}
|
|
13961
14179
|
/**
|
|
13962
14180
|
* Sets the **server-side** speaker volume on this participant's bridged call
|
|
@@ -13970,45 +14188,31 @@ var Participant = class extends Destroyable {
|
|
|
13970
14188
|
* @param value - Volume level (0-100).
|
|
13971
14189
|
*/
|
|
13972
14190
|
async setAudioOutputVolume(value) {
|
|
13973
|
-
await this.executeMethod(
|
|
14191
|
+
await this.executeMethod("call.speaker.volume.set", { volume: value });
|
|
13974
14192
|
}
|
|
13975
14193
|
/**
|
|
13976
14194
|
* Sets the participant's position in the video layout.
|
|
13977
14195
|
*
|
|
13978
|
-
* Requires the `member.position` capability. The gateway
|
|
13979
|
-
*
|
|
13980
|
-
*
|
|
13981
|
-
*
|
|
13982
|
-
* guarantee a visible change: the backend silently returns `200` (no-op) for
|
|
13983
|
-
* non-conference targets.
|
|
14196
|
+
* Requires the `member.position` capability. The gateway requires a
|
|
14197
|
+
* `targets` array of `{ target, position }` entries (issue #19400). A
|
|
14198
|
+
* resolved promise does not guarantee a visible change: the backend silently
|
|
14199
|
+
* returns `200` (no-op) for non-conference targets.
|
|
13984
14200
|
*
|
|
13985
14201
|
* @param value - The {@link VideoPosition} to assign (e.g. `'auto'`, `'reserved-0'`).
|
|
13986
14202
|
*/
|
|
13987
14203
|
async setPosition(value) {
|
|
13988
|
-
|
|
13989
|
-
|
|
13990
|
-
member_id: this.id,
|
|
13991
|
-
call_id: state.call_id ?? "",
|
|
13992
|
-
node_id: state.node_id ?? ""
|
|
13993
|
-
};
|
|
13994
|
-
await this.executeMethod(target, "call.member.position.set", { targets: [{
|
|
13995
|
-
target,
|
|
14204
|
+
await this.executeMethod("call.member.position.set", { targets: [{
|
|
14205
|
+
target: this.target,
|
|
13996
14206
|
position: value
|
|
13997
14207
|
}] });
|
|
13998
14208
|
}
|
|
13999
14209
|
/** Removes this participant from the call. */
|
|
14000
14210
|
async remove() {
|
|
14001
|
-
|
|
14002
|
-
const target = {
|
|
14003
|
-
member_id: this.id,
|
|
14004
|
-
call_id: state.call_id ?? "",
|
|
14005
|
-
node_id: state.node_id ?? ""
|
|
14006
|
-
};
|
|
14007
|
-
await this.executeMethod(target, "call.member.remove", {});
|
|
14211
|
+
await this.executeMethod("call.member.remove", { targets: [this.target] });
|
|
14008
14212
|
}
|
|
14009
14213
|
/** Ends the call for this participant. */
|
|
14010
14214
|
async end() {
|
|
14011
|
-
await this.executeMethod(
|
|
14215
|
+
await this.executeMethod("call.end", {});
|
|
14012
14216
|
}
|
|
14013
14217
|
/**
|
|
14014
14218
|
* Replaces custom metadata for this participant.
|
|
@@ -14028,7 +14232,7 @@ var Participant = class extends Destroyable {
|
|
|
14028
14232
|
}
|
|
14029
14233
|
/** Destroys the participant, releasing all subscriptions and references. */
|
|
14030
14234
|
destroy() {
|
|
14031
|
-
this.
|
|
14235
|
+
this.callExecuteMethod = void 0;
|
|
14032
14236
|
super.destroy();
|
|
14033
14237
|
}
|
|
14034
14238
|
};
|
|
@@ -14040,8 +14244,8 @@ var Participant = class extends Destroyable {
|
|
|
14040
14244
|
*/
|
|
14041
14245
|
var SelfParticipant = class extends Participant {
|
|
14042
14246
|
/** @internal */
|
|
14043
|
-
constructor(id,
|
|
14044
|
-
super(id,
|
|
14247
|
+
constructor(id, callExecuteMethod, vertoManager, deviceController) {
|
|
14248
|
+
super(id, callExecuteMethod, deviceController);
|
|
14045
14249
|
this.vertoManager = vertoManager;
|
|
14046
14250
|
this._studioAudio$ = this.createBehaviorSubject(false);
|
|
14047
14251
|
this.capabilities = new SelfCapabilities();
|
|
@@ -14065,7 +14269,7 @@ var SelfParticipant = class extends Participant {
|
|
|
14065
14269
|
async enableStudioAudio() {
|
|
14066
14270
|
if (this._studioAudio$.value) return;
|
|
14067
14271
|
this._studioAudio$.next(true);
|
|
14068
|
-
await this.executeMethod(
|
|
14272
|
+
await this.executeMethod("call.audioflags.set", {
|
|
14069
14273
|
echo_cancellation: false,
|
|
14070
14274
|
auto_gain: false,
|
|
14071
14275
|
noise_suppression: false
|
|
@@ -14078,7 +14282,7 @@ var SelfParticipant = class extends Participant {
|
|
|
14078
14282
|
async disableStudioAudio() {
|
|
14079
14283
|
if (!this._studioAudio$.value) return;
|
|
14080
14284
|
this._studioAudio$.next(false);
|
|
14081
|
-
await this.executeMethod(
|
|
14285
|
+
await this.executeMethod("call.audioflags.set", {
|
|
14082
14286
|
echo_cancellation: true,
|
|
14083
14287
|
auto_gain: true,
|
|
14084
14288
|
noise_suppression: true
|
|
@@ -14087,17 +14291,27 @@ var SelfParticipant = class extends Participant {
|
|
|
14087
14291
|
/**
|
|
14088
14292
|
* Starts sharing the local screen.
|
|
14089
14293
|
*
|
|
14294
|
+
* A call carries at most one screen share. Read `screenShareStatus` before
|
|
14295
|
+
* calling and treat `'starting'`/`'stopping'` as busy.
|
|
14296
|
+
*
|
|
14090
14297
|
* The call is unaffected when acquisition fails.
|
|
14091
14298
|
*
|
|
14299
|
+
* @param options - Pass `{ audio: true }` to also request the shared
|
|
14300
|
+
* surface's audio. Defaults to video only.
|
|
14301
|
+
* @throws {ScreenShareAlreadyActiveError} When this call is already
|
|
14302
|
+
* sharing a screen. Call {@link stopScreenShare} before starting another.
|
|
14303
|
+
* @throws {AuxiliaryLegCancelledError} When {@link stopScreenShare} removes
|
|
14304
|
+
* the share before its leg finishes connecting.
|
|
14092
14305
|
* @throws The raw `getDisplayMedia` error. A dismissed picker or a
|
|
14093
14306
|
* permission denial rejects with a `NotAllowedError` `DOMException` —
|
|
14094
14307
|
* inspect `error.name` to tell benign cancels apart from real failures.
|
|
14095
14308
|
*/
|
|
14096
|
-
async startScreenShare() {
|
|
14309
|
+
async startScreenShare(options) {
|
|
14097
14310
|
try {
|
|
14098
|
-
await this.vertoManager.addScreenMedia();
|
|
14311
|
+
await this.vertoManager.addScreenMedia(options);
|
|
14099
14312
|
} catch (error) {
|
|
14100
|
-
logger$
|
|
14313
|
+
if (error instanceof AuxiliaryLegCancelledError) logger$25.debug("[Participant.startScreenShare] Screen share cancelled before connecting.");
|
|
14314
|
+
else logger$25.error("[Participant.startScreenShare] Screen share error:", error);
|
|
14101
14315
|
throw error;
|
|
14102
14316
|
}
|
|
14103
14317
|
}
|
|
@@ -14118,14 +14332,18 @@ var SelfParticipant = class extends Participant {
|
|
|
14118
14332
|
*
|
|
14119
14333
|
* The call is unaffected when acquisition fails.
|
|
14120
14334
|
*
|
|
14335
|
+
* @throws {AuxiliaryLegCancelledError} When {@link removeAdditionalDevice}
|
|
14336
|
+
* removes the device before its leg finishes connecting.
|
|
14121
14337
|
* @throws The raw `getUserMedia` error (e.g. `NotAllowedError` on
|
|
14122
|
-
* permission denial) — inspect `error.name` to decide how to react
|
|
14338
|
+
* permission denial) — inspect `error.name` to decide how to react — or
|
|
14339
|
+
* `AuxiliaryLegTimeoutError` if the leg does not connect in time.
|
|
14123
14340
|
*/
|
|
14124
14341
|
async addAdditionalDevice(options) {
|
|
14125
14342
|
try {
|
|
14126
14343
|
await this.vertoManager.addInputDevice(options);
|
|
14127
14344
|
} catch (error) {
|
|
14128
|
-
logger$
|
|
14345
|
+
if (error instanceof AuxiliaryLegCancelledError) logger$25.debug("[Participant.addAdditionalDevice] Device removed before connecting.");
|
|
14346
|
+
else logger$25.error("[Participant.addAdditionalDevice] Additional device error:", error);
|
|
14129
14347
|
throw error;
|
|
14130
14348
|
}
|
|
14131
14349
|
}
|
|
@@ -14160,22 +14378,31 @@ var SelfParticipant = class extends Participant {
|
|
|
14160
14378
|
this.deviceController.selectAudioInputDevice(device);
|
|
14161
14379
|
if (options.savePreference) PreferencesContainer.instance.preferredAudioInput = device;
|
|
14162
14380
|
}
|
|
14163
|
-
/**
|
|
14381
|
+
/**
|
|
14382
|
+
* Updates the audio input track constraints for the active call.
|
|
14383
|
+
* @returns whether the constraints reached the media the call is sending.
|
|
14384
|
+
*/
|
|
14164
14385
|
async setAudioInputDeviceConstraints(constraints) {
|
|
14165
|
-
|
|
14386
|
+
return this.vertoManager.updateMediaConstraints({ audio: constraints });
|
|
14166
14387
|
}
|
|
14167
|
-
/**
|
|
14388
|
+
/**
|
|
14389
|
+
* Updates both audio and video input track constraints for the active call.
|
|
14390
|
+
* @returns whether both kinds took the constraints.
|
|
14391
|
+
*/
|
|
14168
14392
|
async setInputDevicesConstraints(constraints) {
|
|
14169
|
-
|
|
14393
|
+
return this.vertoManager.updateMediaConstraints(constraints);
|
|
14170
14394
|
}
|
|
14171
14395
|
/** Selects the video input device for future calls. Optionally saves as a preference. */
|
|
14172
14396
|
selectVideoInputDevice(device, options = {}) {
|
|
14173
14397
|
this.deviceController.selectVideoInputDevice(device);
|
|
14174
14398
|
if (options.savePreference) PreferencesContainer.instance.preferredVideoInput = device;
|
|
14175
14399
|
}
|
|
14176
|
-
/**
|
|
14400
|
+
/**
|
|
14401
|
+
* Updates the video input track constraints for the active call.
|
|
14402
|
+
* @returns whether the constraints reached the media the call is sending.
|
|
14403
|
+
*/
|
|
14177
14404
|
async setVideoInputDeviceConstraints(constraints) {
|
|
14178
|
-
|
|
14405
|
+
return this.vertoManager.updateMediaConstraints({ video: constraints });
|
|
14179
14406
|
}
|
|
14180
14407
|
/** Selects the audio output device. Optionally saves as a preference. */
|
|
14181
14408
|
selectAudioOutputDevice(device, options = {}) {
|
|
@@ -14188,7 +14415,7 @@ var SelfParticipant = class extends Participant {
|
|
|
14188
14415
|
*/
|
|
14189
14416
|
exitStudioModeIfActive() {
|
|
14190
14417
|
if (this._studioAudio$.value) {
|
|
14191
|
-
logger$
|
|
14418
|
+
logger$25.debug("[SelfParticipant] Exiting studio audio mode due to individual flag toggle");
|
|
14192
14419
|
this._studioAudio$.next(false);
|
|
14193
14420
|
}
|
|
14194
14421
|
}
|
|
@@ -14212,7 +14439,7 @@ var SelfParticipant = class extends Participant {
|
|
|
14212
14439
|
try {
|
|
14213
14440
|
await super.mute();
|
|
14214
14441
|
} catch (error) {
|
|
14215
|
-
logger$
|
|
14442
|
+
logger$25.warn("[Participant.toggleAudioInput] Server Error while muting audio input, proceeding with local toggle anyway", error);
|
|
14216
14443
|
} finally {
|
|
14217
14444
|
this.vertoManager.muteMainAudioInputDevice();
|
|
14218
14445
|
}
|
|
@@ -14222,7 +14449,7 @@ var SelfParticipant = class extends Participant {
|
|
|
14222
14449
|
try {
|
|
14223
14450
|
await super.unmute();
|
|
14224
14451
|
} catch (error) {
|
|
14225
|
-
logger$
|
|
14452
|
+
logger$25.warn("[Participant.toggleAudioInput] Server Error while unmuting audio input, proceeding with local toggle anyway", error);
|
|
14226
14453
|
} finally {
|
|
14227
14454
|
await this.vertoManager.unmuteMainAudioInputDevice();
|
|
14228
14455
|
}
|
|
@@ -14232,7 +14459,7 @@ var SelfParticipant = class extends Participant {
|
|
|
14232
14459
|
try {
|
|
14233
14460
|
await super.muteVideo();
|
|
14234
14461
|
} catch (error) {
|
|
14235
|
-
logger$
|
|
14462
|
+
logger$25.warn("[Participant.toggleVideoInput] Server Error while muting video input, proceeding with local toggle anyway", error);
|
|
14236
14463
|
} finally {
|
|
14237
14464
|
this.vertoManager.muteMainVideoInputDevice();
|
|
14238
14465
|
}
|
|
@@ -14242,7 +14469,7 @@ var SelfParticipant = class extends Participant {
|
|
|
14242
14469
|
try {
|
|
14243
14470
|
await super.unmuteVideo();
|
|
14244
14471
|
} catch (error) {
|
|
14245
|
-
logger$
|
|
14472
|
+
logger$25.warn("[Participant.toggleVideoInput] Server Error while unmuting video input, proceeding with local toggle anyway", error);
|
|
14246
14473
|
} finally {
|
|
14247
14474
|
await this.vertoManager.unmuteMainVideoInputDevice();
|
|
14248
14475
|
}
|
|
@@ -14447,7 +14674,7 @@ function filterAs(predicate, resultPath) {
|
|
|
14447
14674
|
//#endregion
|
|
14448
14675
|
//#region src/operators/throwOnRPCError.ts
|
|
14449
14676
|
var import_cjs$21 = require_cjs();
|
|
14450
|
-
const logger$
|
|
14677
|
+
const logger$24 = getLogger();
|
|
14451
14678
|
/**
|
|
14452
14679
|
* RxJS operator that throws a {@link JSONRPCError} when the RPC response contains an error.
|
|
14453
14680
|
* Passes successful responses through unchanged.
|
|
@@ -14455,14 +14682,14 @@ const logger$23 = getLogger();
|
|
|
14455
14682
|
function throwOnRPCError() {
|
|
14456
14683
|
return (0, import_cjs$21.map)((response) => {
|
|
14457
14684
|
if (response.error) {
|
|
14458
|
-
logger$
|
|
14685
|
+
logger$24.error("[throwOnRPCError] RPC error response:", {
|
|
14459
14686
|
code: response.error.code,
|
|
14460
14687
|
message: response.error.message,
|
|
14461
14688
|
data: response.error.data
|
|
14462
14689
|
});
|
|
14463
14690
|
throw new JSONRPCError(response.error.code, response.error.message, response.error.data);
|
|
14464
14691
|
}
|
|
14465
|
-
logger$
|
|
14692
|
+
logger$24.debug("[throwOnRPCError] RPC successful response:", response);
|
|
14466
14693
|
return response;
|
|
14467
14694
|
});
|
|
14468
14695
|
}
|
|
@@ -14470,7 +14697,7 @@ function throwOnRPCError() {
|
|
|
14470
14697
|
//#endregion
|
|
14471
14698
|
//#region src/managers/CallEventsManager.ts
|
|
14472
14699
|
var import_cjs$20 = require_cjs();
|
|
14473
|
-
const logger$
|
|
14700
|
+
const logger$23 = getLogger();
|
|
14474
14701
|
const initialSessionState = {};
|
|
14475
14702
|
/** @internal */
|
|
14476
14703
|
var CallEventsManager = class extends Destroyable {
|
|
@@ -14574,7 +14801,7 @@ var CallEventsManager = class extends Destroyable {
|
|
|
14574
14801
|
}
|
|
14575
14802
|
initSubscriptions() {
|
|
14576
14803
|
this.subscribeTo(this.callJoinedEvent$, (callJoinedEvent) => {
|
|
14577
|
-
logger$
|
|
14804
|
+
logger$23.debug("[CallEventsManager] Handling call.joined event for call/session IDs:", {
|
|
14578
14805
|
callId: callJoinedEvent.call_id,
|
|
14579
14806
|
roomSessionId: callJoinedEvent.room_session_id
|
|
14580
14807
|
});
|
|
@@ -14601,19 +14828,19 @@ var CallEventsManager = class extends Destroyable {
|
|
|
14601
14828
|
if (this._self$.value?.capabilities.setLayout) this.updateLayouts();
|
|
14602
14829
|
});
|
|
14603
14830
|
this.subscribeTo(this.memberUpdates$, (member) => {
|
|
14604
|
-
logger$
|
|
14831
|
+
logger$23.debug("[CallEventsManager] Handling member update event for member ID:", member);
|
|
14605
14832
|
this.upsertParticipant(member);
|
|
14606
14833
|
});
|
|
14607
14834
|
this.subscribeTo(this.webRtcCallSession.memberLeft$, (memberLeftEvent) => {
|
|
14608
|
-
logger$
|
|
14835
|
+
logger$23.debug("[CallEventsManager] Handling member.left event for member ID:", memberLeftEvent.member.member_id);
|
|
14609
14836
|
const participants = { ...this._participants$.value };
|
|
14610
14837
|
if (memberLeftEvent.member.member_id in participants) {
|
|
14611
14838
|
delete participants[memberLeftEvent.member.member_id];
|
|
14612
14839
|
this._participants$.next(participants);
|
|
14613
|
-
} else logger$
|
|
14840
|
+
} else logger$23.warn(`[CallEventsManager] Received member.left event for unknown member ID: ${memberLeftEvent.member.member_id}`);
|
|
14614
14841
|
});
|
|
14615
14842
|
this.subscribeTo(this.webRtcCallSession.callUpdated$, (callUpdatedEvent) => {
|
|
14616
|
-
logger$
|
|
14843
|
+
logger$23.debug("[CallEventsManager] Handling call.updated event:", callUpdatedEvent);
|
|
14617
14844
|
const roomSession = callUpdatedEvent.room_session;
|
|
14618
14845
|
this._sessionState$.next({
|
|
14619
14846
|
...this._sessionState$.value,
|
|
@@ -14628,7 +14855,7 @@ var CallEventsManager = class extends Destroyable {
|
|
|
14628
14855
|
});
|
|
14629
14856
|
});
|
|
14630
14857
|
this.subscribeTo(this.layoutChangedEvent$, (layoutChangedEvent) => {
|
|
14631
|
-
logger$
|
|
14858
|
+
logger$23.debug("[CallEventsManager] Handling layout.changed event:", layoutChangedEvent);
|
|
14632
14859
|
this._sessionState$.next({
|
|
14633
14860
|
...this._sessionState$.value,
|
|
14634
14861
|
layout_name: layoutChangedEvent.id,
|
|
@@ -14638,10 +14865,10 @@ var CallEventsManager = class extends Destroyable {
|
|
|
14638
14865
|
});
|
|
14639
14866
|
}
|
|
14640
14867
|
updateParticipantPositions(layoutChangedEvent) {
|
|
14641
|
-
if (Object.keys(this._participants$.value).length > 0 && !layoutChangedEvent.layers.some((layer) => !!layer.member_id)) logger$
|
|
14868
|
+
if (Object.keys(this._participants$.value).length > 0 && !layoutChangedEvent.layers.some((layer) => !!layer.member_id)) logger$23.warn("[CallEventsManager] No layers with member_id found in layout.changed event. Nothing to update.");
|
|
14642
14869
|
layoutChangedEvent.layers.filter((layer) => !!layer.member_id).filter((layer) => {
|
|
14643
14870
|
if (!(layer.member_id in this._participants$.value)) {
|
|
14644
|
-
logger$
|
|
14871
|
+
logger$23.warn(`[CallEventsManager] Skipping layout layer for unknown member_id: ${layer.member_id}`);
|
|
14645
14872
|
return false;
|
|
14646
14873
|
}
|
|
14647
14874
|
return true;
|
|
@@ -14659,12 +14886,17 @@ var CallEventsManager = class extends Destroyable {
|
|
|
14659
14886
|
updateLayouts() {
|
|
14660
14887
|
if (!this.selfId) return;
|
|
14661
14888
|
this.webRtcCallSession.executeMethod(this.selfId, "call.layout.list", {}).then((response) => {
|
|
14889
|
+
const layouts = response.result?.layouts;
|
|
14890
|
+
if (!layouts) {
|
|
14891
|
+
logger$23.warn("[CallEventsManager] Layout list response carried no layouts; keeping current layouts");
|
|
14892
|
+
return;
|
|
14893
|
+
}
|
|
14662
14894
|
this._sessionState$.next({
|
|
14663
14895
|
...this._sessionState$.value,
|
|
14664
|
-
layouts
|
|
14896
|
+
layouts
|
|
14665
14897
|
});
|
|
14666
14898
|
}).catch((error) => {
|
|
14667
|
-
logger$
|
|
14899
|
+
logger$23.error("[CallEventsManager] Error fetching layouts:", error);
|
|
14668
14900
|
});
|
|
14669
14901
|
}
|
|
14670
14902
|
updateParticipants(members) {
|
|
@@ -14680,7 +14912,7 @@ var CallEventsManager = class extends Destroyable {
|
|
|
14680
14912
|
}
|
|
14681
14913
|
const participant = this._participants$.value[member.member_id];
|
|
14682
14914
|
const oldValue = participant.value;
|
|
14683
|
-
logger$
|
|
14915
|
+
logger$23.debug("[CallEventsManager] Updating participant:", member.member_id, {
|
|
14684
14916
|
oldValue,
|
|
14685
14917
|
newValue: member
|
|
14686
14918
|
});
|
|
@@ -14693,17 +14925,17 @@ var CallEventsManager = class extends Destroyable {
|
|
|
14693
14925
|
}
|
|
14694
14926
|
get callJoinedEvent$() {
|
|
14695
14927
|
return this.cachedObservable("callJoinedEvent$", () => this.webRtcCallSession.callEvent$.pipe((0, import_cjs$20.filter)(isCallJoinedPayload), (0, import_cjs$20.tap)((event) => {
|
|
14696
|
-
logger$
|
|
14928
|
+
logger$23.debug("[CallEventsManager] Call joined event:", event);
|
|
14697
14929
|
})));
|
|
14698
14930
|
}
|
|
14699
14931
|
get layoutChangedEvent$() {
|
|
14700
14932
|
return this.cachedObservable("layoutChangedEvent$", () => this.webRtcCallSession.callEvent$.pipe(filterAs(isLayoutChangedPayload, "layout"), (0, import_cjs$20.tap)((event) => {
|
|
14701
|
-
logger$
|
|
14933
|
+
logger$23.debug("[CallEventsManager] Layout changed event:", event);
|
|
14702
14934
|
})));
|
|
14703
14935
|
}
|
|
14704
14936
|
get memberUpdates$() {
|
|
14705
14937
|
return this.cachedObservable("memberUpdates$", () => (0, import_cjs$20.merge)(this.webRtcCallSession.memberJoined$, this.webRtcCallSession.memberUpdated$, this.webRtcCallSession.memberTalking$).pipe((0, import_cjs$20.map)((event) => event.member), (0, import_cjs$20.tap)((event) => {
|
|
14706
|
-
logger$
|
|
14938
|
+
logger$23.debug("[CallEventsManager] Member update event:", event);
|
|
14707
14939
|
})));
|
|
14708
14940
|
}
|
|
14709
14941
|
destroy() {
|
|
@@ -14722,6 +14954,111 @@ var CallEventsManager = class extends Destroyable {
|
|
|
14722
14954
|
}
|
|
14723
14955
|
};
|
|
14724
14956
|
|
|
14957
|
+
//#endregion
|
|
14958
|
+
//#region src/controllers/ConstraintFallbackHelper.ts
|
|
14959
|
+
/**
|
|
14960
|
+
* ConstraintFallbackHelper - Provides getUserMedia with automatic constraint
|
|
14961
|
+
* fallback on OverconstrainedError.
|
|
14962
|
+
*
|
|
14963
|
+
* When a specific device ID is requested, the helper tries progressively
|
|
14964
|
+
* looser constraints:
|
|
14965
|
+
* 1. `{ deviceId: { exact: deviceId } }` -- exact match
|
|
14966
|
+
* 2. `{ deviceId: deviceId }` -- preferred (browser may pick another)
|
|
14967
|
+
* 3. `{}` -- no constraint, browser default
|
|
14968
|
+
*
|
|
14969
|
+
* This prevents stale device IDs from blocking call setup.
|
|
14970
|
+
*
|
|
14971
|
+
* @see Section 5.8 and Section 11 of the Implementation Guide
|
|
14972
|
+
*/
|
|
14973
|
+
const logger$22 = getLogger();
|
|
14974
|
+
/**
|
|
14975
|
+
* Attempts getUserMedia with progressively looser constraints.
|
|
14976
|
+
*
|
|
14977
|
+
* The function tries three levels of constraint specificity for the given
|
|
14978
|
+
* device kind. Each level is only attempted if the previous one fails with
|
|
14979
|
+
* an OverconstrainedError. Non-OverconstrainedError failures (e.g.,
|
|
14980
|
+
* NotAllowedError) are thrown immediately without fallback.
|
|
14981
|
+
*
|
|
14982
|
+
* @param mediaDevices - Anything exposing `getUserMedia` (a full
|
|
14983
|
+
* `WebRTCMediaDevices`, or a shim wrapping one)
|
|
14984
|
+
* @param constraints - The full MediaStreamConstraints to use as a base
|
|
14985
|
+
* @param kind - Which track kind to apply fallback to ('audio' | 'video')
|
|
14986
|
+
* @param deviceId - The device ID to try (if undefined, calls getUserMedia as-is)
|
|
14987
|
+
* @returns The stream and the fallback level that succeeded
|
|
14988
|
+
* @throws When all fallback levels fail, or when a non-OverconstrainedError occurs
|
|
14989
|
+
*/
|
|
14990
|
+
async function getUserMediaWithFallback(mediaDevices, constraints, kind, deviceId) {
|
|
14991
|
+
if (!deviceId) return {
|
|
14992
|
+
stream: await mediaDevices.getUserMedia(constraints),
|
|
14993
|
+
fallbackLevel: "default"
|
|
14994
|
+
};
|
|
14995
|
+
const baseConstraints = typeof constraints[kind] === "object" ? constraints[kind] : {};
|
|
14996
|
+
try {
|
|
14997
|
+
const exactConstraints = {
|
|
14998
|
+
...constraints,
|
|
14999
|
+
[kind]: {
|
|
15000
|
+
...baseConstraints,
|
|
15001
|
+
deviceId: { exact: deviceId }
|
|
15002
|
+
}
|
|
15003
|
+
};
|
|
15004
|
+
return {
|
|
15005
|
+
stream: await mediaDevices.getUserMedia(exactConstraints),
|
|
15006
|
+
fallbackLevel: "exact"
|
|
15007
|
+
};
|
|
15008
|
+
} catch (error) {
|
|
15009
|
+
if (!isOverconstrainedError(error)) throw error;
|
|
15010
|
+
logger$22.debug(`[ConstraintFallbackHelper] Exact constraint failed for ${kind}, trying preferred`, { deviceId });
|
|
15011
|
+
}
|
|
15012
|
+
try {
|
|
15013
|
+
const preferredConstraints = {
|
|
15014
|
+
...constraints,
|
|
15015
|
+
[kind]: {
|
|
15016
|
+
...baseConstraints,
|
|
15017
|
+
deviceId
|
|
15018
|
+
}
|
|
15019
|
+
};
|
|
15020
|
+
return {
|
|
15021
|
+
stream: await mediaDevices.getUserMedia(preferredConstraints),
|
|
15022
|
+
fallbackLevel: "preferred"
|
|
15023
|
+
};
|
|
15024
|
+
} catch (error) {
|
|
15025
|
+
if (!isOverconstrainedError(error)) throw error;
|
|
15026
|
+
logger$22.debug(`[ConstraintFallbackHelper] Preferred constraint failed for ${kind}, trying default`, { deviceId });
|
|
15027
|
+
}
|
|
15028
|
+
try {
|
|
15029
|
+
const defaultConstraints = {
|
|
15030
|
+
...constraints,
|
|
15031
|
+
[kind]: { ...baseConstraints }
|
|
15032
|
+
};
|
|
15033
|
+
if (typeof defaultConstraints[kind] === "object") {
|
|
15034
|
+
const { deviceId: _removed, ...rest } = defaultConstraints[kind];
|
|
15035
|
+
defaultConstraints[kind] = rest;
|
|
15036
|
+
}
|
|
15037
|
+
const stream = await mediaDevices.getUserMedia(defaultConstraints);
|
|
15038
|
+
logger$22.warn(`[ConstraintFallbackHelper] Fell back to browser default for ${kind}`, { requestedDeviceId: deviceId });
|
|
15039
|
+
return {
|
|
15040
|
+
stream,
|
|
15041
|
+
fallbackLevel: "default"
|
|
15042
|
+
};
|
|
15043
|
+
} catch (error) {
|
|
15044
|
+
logger$22.error(`[ConstraintFallbackHelper] All fallback levels exhausted for ${kind}`, {
|
|
15045
|
+
deviceId,
|
|
15046
|
+
error
|
|
15047
|
+
});
|
|
15048
|
+
throw error;
|
|
15049
|
+
}
|
|
15050
|
+
}
|
|
15051
|
+
/**
|
|
15052
|
+
* Checks whether an error is an OverconstrainedError.
|
|
15053
|
+
*
|
|
15054
|
+
* Browsers may throw either a native OverconstrainedError or a DOMException
|
|
15055
|
+
* with a specific name.
|
|
15056
|
+
*/
|
|
15057
|
+
function isOverconstrainedError(error) {
|
|
15058
|
+
if (error instanceof Error) return error.name === "OverconstrainedError" || error.name === "ConstraintNotSatisfiedError";
|
|
15059
|
+
return false;
|
|
15060
|
+
}
|
|
15061
|
+
|
|
14725
15062
|
//#endregion
|
|
14726
15063
|
//#region src/helpers/SDPHelper.ts
|
|
14727
15064
|
/**
|
|
@@ -15280,6 +15617,7 @@ var LocalStreamController = class extends Destroyable {
|
|
|
15280
15617
|
this._localAudioTracks$ = this.createBehaviorSubject([]);
|
|
15281
15618
|
this._localVideoTracks$ = this.createBehaviorSubject([]);
|
|
15282
15619
|
this._mediaTrackEnded$ = this.createSubject();
|
|
15620
|
+
this._trackOrigins = /* @__PURE__ */ new WeakMap();
|
|
15283
15621
|
}
|
|
15284
15622
|
get localStream$() {
|
|
15285
15623
|
return this._localStream$.asObservable().pipe((0, import_cjs$17.takeUntil)(this.destroyed$));
|
|
@@ -15302,6 +15640,22 @@ var LocalStreamController = class extends Destroyable {
|
|
|
15302
15640
|
get localVideoTracks() {
|
|
15303
15641
|
return this._localVideoTracks$.value;
|
|
15304
15642
|
}
|
|
15643
|
+
tagTracks(tracks, origin) {
|
|
15644
|
+
for (const track of tracks) this._trackOrigins.set(track, origin);
|
|
15645
|
+
}
|
|
15646
|
+
setTrackOrigin(track, origin) {
|
|
15647
|
+
this._trackOrigins.set(track, origin);
|
|
15648
|
+
}
|
|
15649
|
+
getTrackOrigin(track) {
|
|
15650
|
+
return this._trackOrigins.get(track);
|
|
15651
|
+
}
|
|
15652
|
+
/**
|
|
15653
|
+
* Fail-safe: an unrecorded track reads as not-a-device-capture, so a missed
|
|
15654
|
+
* tagging site leaves media alone rather than destroying it.
|
|
15655
|
+
*/
|
|
15656
|
+
isDeviceCapture(track) {
|
|
15657
|
+
return this._trackOrigins.get(track) === "device";
|
|
15658
|
+
}
|
|
15305
15659
|
/**
|
|
15306
15660
|
* Build the local media stream based on the provided options.
|
|
15307
15661
|
*/
|
|
@@ -15311,13 +15665,16 @@ var LocalStreamController = class extends Destroyable {
|
|
|
15311
15665
|
if (this.options.inputAudioStream ?? this.options.inputVideoStream) {
|
|
15312
15666
|
const tracks = [...this.options.inputAudioStream?.getTracks() ?? [], ...this.options.inputVideoStream?.getTracks() ?? []];
|
|
15313
15667
|
stream = new MediaStream(tracks);
|
|
15668
|
+
this.tagTracks(tracks, "application");
|
|
15314
15669
|
} else if (this.options.propose === "screenshare") {
|
|
15315
|
-
|
|
15670
|
+
const audio = this.options.screenShareAudio ?? false;
|
|
15671
|
+
logger$19.debug("[LocalStreamController] Requesting display media for screen sharing with audio:", audio);
|
|
15316
15672
|
stream = await this.options.getDisplayMedia({
|
|
15317
15673
|
video: true,
|
|
15318
|
-
audio
|
|
15674
|
+
audio
|
|
15319
15675
|
});
|
|
15320
15676
|
logger$19.debug("[LocalStreamController] Screen share media obtained:", stream);
|
|
15677
|
+
this.tagTracks(stream.getTracks(), "display");
|
|
15321
15678
|
} else {
|
|
15322
15679
|
const constraints = {
|
|
15323
15680
|
audio: this.options.inputAudioDeviceConstraints,
|
|
@@ -15326,6 +15683,7 @@ var LocalStreamController = class extends Destroyable {
|
|
|
15326
15683
|
logger$19.debug("[LocalStreamController] Requesting user media with constraints:", constraints);
|
|
15327
15684
|
stream = await this.options.getUserMedia(constraints);
|
|
15328
15685
|
logger$19.debug("[LocalStreamController] User media obtained:", stream);
|
|
15686
|
+
this.tagTracks(stream.getTracks(), "device");
|
|
15329
15687
|
}
|
|
15330
15688
|
this._localStream$.next(stream);
|
|
15331
15689
|
this._localAudioTracks$.next(stream.getAudioTracks());
|
|
@@ -15335,10 +15693,13 @@ var LocalStreamController = class extends Destroyable {
|
|
|
15335
15693
|
/**
|
|
15336
15694
|
* Add a local media track to the local stream.
|
|
15337
15695
|
* @param track - The MediaStreamTrack to add
|
|
15696
|
+
* @param origin - Defaults to `'device'`; every internal caller passes a
|
|
15697
|
+
* fresh `getUserMedia` capture.
|
|
15338
15698
|
* @returns The MediaStream (either existing or newly created)
|
|
15339
15699
|
*/
|
|
15340
|
-
addTrack(track) {
|
|
15700
|
+
addTrack(track, origin = "device") {
|
|
15341
15701
|
const localStream = this._localStream$.value ?? new MediaStream();
|
|
15702
|
+
this._trackOrigins.set(track, origin);
|
|
15342
15703
|
track.addEventListener("ended", this.mediaTrackEndedHandler);
|
|
15343
15704
|
localStream.addTrack(track);
|
|
15344
15705
|
this._localStream$.next(localStream);
|
|
@@ -15608,15 +15969,27 @@ var TransceiverController = class extends Destroyable {
|
|
|
15608
15969
|
for (let i = 0; i < Number(msStreamsNumber); i++) this.peerConnection.addTransceiver("video", { direction: "recvonly" });
|
|
15609
15970
|
}
|
|
15610
15971
|
}
|
|
15972
|
+
/**
|
|
15973
|
+
* @returns whether every live sender of the kind took the constraints. A
|
|
15974
|
+
* skipped non-device sender, an exhausted fallback, and having no live sender
|
|
15975
|
+
* at all all report `false` — `mediaParamsUpdated.applied` is built from this,
|
|
15976
|
+
* and an application told `true` cannot tell a working push from a no-op.
|
|
15977
|
+
*/
|
|
15611
15978
|
async updateSendersConstraints(kind, constraints) {
|
|
15612
15979
|
if (!constraints) {
|
|
15613
15980
|
this.stopTrackSender(kind);
|
|
15614
|
-
return
|
|
15981
|
+
return false;
|
|
15615
15982
|
}
|
|
15616
15983
|
const senders = this.peerConnection.getSenders().filter((sender) => sender.track?.kind === kind && sender.track.readyState === "live");
|
|
15984
|
+
let applied = senders.length > 0;
|
|
15617
15985
|
for (const sender of senders) {
|
|
15618
15986
|
const { track } = sender;
|
|
15619
15987
|
if (track) {
|
|
15988
|
+
if (!this.options.localStreamController.isDeviceCapture(track)) {
|
|
15989
|
+
logger$18.debug(`[TransceiverController] Skipping ${kind} constraints for a non-device track (origin: ${this.options.localStreamController.getTrackOrigin(track) ?? "unrecorded"}), track ${track.id}`);
|
|
15990
|
+
applied = false;
|
|
15991
|
+
continue;
|
|
15992
|
+
}
|
|
15620
15993
|
const constraintsToApply = {
|
|
15621
15994
|
...track.getConstraints(),
|
|
15622
15995
|
...constraints
|
|
@@ -15632,33 +16005,39 @@ var TransceiverController = class extends Destroyable {
|
|
|
15632
16005
|
} catch (fallbackError) {
|
|
15633
16006
|
logger$18.warn(`[TransceiverController] Track replacement fallback also failed for ${kind} track:`, fallbackError);
|
|
15634
16007
|
this.options.onError?.(new MediaTrackError("updateSendersConstraints", kind, fallbackError));
|
|
16008
|
+
applied = false;
|
|
15635
16009
|
}
|
|
15636
16010
|
}
|
|
15637
16011
|
}
|
|
15638
16012
|
}
|
|
16013
|
+
return applied;
|
|
15639
16014
|
}
|
|
15640
16015
|
/**
|
|
15641
|
-
* Fallback when applyConstraints fails
|
|
15642
|
-
* one via getUserMedia with the merged constraints (preserving the current
|
|
15643
|
-
* deviceId), replace the sender track, and update the localStream.
|
|
16016
|
+
* Fallback when applyConstraints fails, which on iOS Safari it silently does.
|
|
15644
16017
|
*
|
|
15645
|
-
*
|
|
15646
|
-
*
|
|
16018
|
+
* Order matters: acquiring before stopping means a failed acquisition leaves
|
|
16019
|
+
* the existing media playing. The deviceId goes through the fallback ladder
|
|
16020
|
+
* rather than pinned `{ exact }`, so a stale id degrades instead of failing.
|
|
15647
16021
|
*/
|
|
15648
16022
|
async replaceTrackFallback(sender, oldTrack, kind, mergedConstraints) {
|
|
15649
16023
|
const { deviceId } = oldTrack.getSettings();
|
|
15650
|
-
const
|
|
15651
|
-
|
|
15652
|
-
|
|
15653
|
-
|
|
15654
|
-
|
|
16024
|
+
const { stream, fallbackLevel } = await getUserMediaWithFallback({ getUserMedia: this.options.getUserMedia }, { [kind]: mergedConstraints }, kind, deviceId);
|
|
16025
|
+
const newTrack = stream.getTracks().find((t) => t.kind === kind);
|
|
16026
|
+
if (!newTrack) {
|
|
16027
|
+
stream.getTracks().forEach((t) => t.stop());
|
|
16028
|
+
throw new MediaTrackError("replaceTrackFallback", kind, /* @__PURE__ */ new Error("getUserMedia returned no track of the requested kind"));
|
|
16029
|
+
}
|
|
16030
|
+
try {
|
|
16031
|
+
await sender.replaceTrack(newTrack);
|
|
16032
|
+
} catch (error) {
|
|
16033
|
+
stream.getTracks().forEach((t) => t.stop());
|
|
16034
|
+
throw error;
|
|
16035
|
+
}
|
|
16036
|
+
const oldTrackId = oldTrack.id;
|
|
16037
|
+
this.options.localStreamController.removeTrack(oldTrackId);
|
|
15655
16038
|
oldTrack.stop();
|
|
15656
|
-
this.options.localStreamController.removeTrack(trackId);
|
|
15657
|
-
const newTrack = (await this.options.getUserMedia({ [kind]: constraintsWithDevice })).getTracks().find((t) => t.kind === kind);
|
|
15658
|
-
if (!newTrack) throw new MediaTrackError("replaceTrackFallback", kind, /* @__PURE__ */ new Error("getUserMedia returned no track of the requested kind"));
|
|
15659
|
-
await sender.replaceTrack(newTrack);
|
|
15660
16039
|
this.options.localStreamController.addTrack(newTrack);
|
|
15661
|
-
logger$18.debug(`[TransceiverController] Track replacement fallback succeeded for ${kind}. New track: ${newTrack.id}`);
|
|
16040
|
+
logger$18.debug(`[TransceiverController] Track replacement fallback succeeded for ${kind} (deviceId fallback level: ${fallbackLevel}). New track: ${newTrack.id}`);
|
|
15662
16041
|
}
|
|
15663
16042
|
getMediaDirections() {
|
|
15664
16043
|
if (this.peerConnection.connectionState === "connected") return this.peerConnection.getTransceivers().reduce((acc, transceiver) => {
|
|
@@ -15728,48 +16107,44 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
15728
16107
|
this.negotiationNeeded$.next();
|
|
15729
16108
|
};
|
|
15730
16109
|
this.updateSelectedInputDevice = async (kind, deviceInfo) => {
|
|
16110
|
+
const { localStream } = this;
|
|
16111
|
+
if (!localStream) {
|
|
16112
|
+
logger$17.warn("[RTCPeerConnectionController] No local stream available to update input device.");
|
|
16113
|
+
return;
|
|
16114
|
+
}
|
|
16115
|
+
const currentTrack = localStream.getTracks().find((track) => track.kind === kind);
|
|
16116
|
+
if (!currentTrack) {
|
|
16117
|
+
logger$17.debug(`[RTCPeerConnectionController] No ${kind} track to switch.`);
|
|
16118
|
+
return;
|
|
16119
|
+
}
|
|
16120
|
+
if (!deviceInfo) {
|
|
16121
|
+
logger$17.debug(`[RTCPeerConnectionController] ${kind} input device selected: none`);
|
|
16122
|
+
this.stopTrackSender(kind);
|
|
16123
|
+
return;
|
|
16124
|
+
}
|
|
16125
|
+
const constraints = {
|
|
16126
|
+
...currentTrack.getConstraints(),
|
|
16127
|
+
...this.deviceController.deviceInfoToConstraints(deviceInfo)
|
|
16128
|
+
};
|
|
15731
16129
|
try {
|
|
15732
|
-
const
|
|
15733
|
-
|
|
15734
|
-
|
|
15735
|
-
return;
|
|
15736
|
-
}
|
|
15737
|
-
logger$17.debug(`[RTCPeerConnectionController] Updating selected ${kind} input device:`, localStream.getTracks());
|
|
15738
|
-
const track = localStream.getTracks().find((track$1) => track$1.kind === kind);
|
|
15739
|
-
if (track) {
|
|
15740
|
-
this.transceiverController?.stopTrackSender(kind);
|
|
15741
|
-
this.localStreamController.removeTrack(track.id);
|
|
15742
|
-
logger$17.debug(`[RTCPeerConnectionController] Stopped existing ${kind} track: ${track.id}`, localStream.getTracks());
|
|
15743
|
-
if (!deviceInfo) {
|
|
15744
|
-
logger$17.debug(`[RTCPeerConnectionController] ${kind} input device selected: none`);
|
|
15745
|
-
return;
|
|
15746
|
-
}
|
|
15747
|
-
const streamTrack = (await this.getUserMedia({ [kind]: {
|
|
15748
|
-
...track.getConstraints(),
|
|
15749
|
-
...this.deviceController.deviceInfoToConstraints(deviceInfo)
|
|
15750
|
-
} })).getTracks().find((t) => t.kind === kind);
|
|
15751
|
-
if (streamTrack) {
|
|
15752
|
-
logger$17.debug(`[RTCPeerConnectionController] Adding new ${kind} track: ${streamTrack.id}`);
|
|
15753
|
-
this.localStreamController.addTrack(streamTrack);
|
|
15754
|
-
await this.transceiverController?.replaceSenderTrack(kind, streamTrack);
|
|
15755
|
-
logger$17.debug(`[RTCPeerConnectionController] Added new ${kind} track: ${streamTrack.id}`, this.localStream?.getTracks());
|
|
15756
|
-
}
|
|
15757
|
-
}
|
|
15758
|
-
logger$17.debug(`[RTCPeerConnectionController] ${kind} input device selected:`, deviceInfo?.label);
|
|
16130
|
+
const newTrack = await this.acquireInputTrack(kind, constraints, deviceInfo, currentTrack);
|
|
16131
|
+
await this.attachInputTrack(kind, newTrack, currentTrack);
|
|
16132
|
+
logger$17.debug(`[RTCPeerConnectionController] ${kind} input device selected:`, deviceInfo.label, newTrack.id);
|
|
15759
16133
|
} catch (error) {
|
|
15760
16134
|
logger$17.error(`[RTCPeerConnectionController] Failed to select ${kind} input device:`, error);
|
|
15761
16135
|
this._errors$.next(new MediaTrackError("updateSelectedInputDevice", kind, error));
|
|
15762
|
-
throw error;
|
|
15763
16136
|
}
|
|
15764
16137
|
};
|
|
15765
16138
|
this._isNegotiating$ = this.createBehaviorSubject(false);
|
|
15766
16139
|
this._memberId = null;
|
|
16140
|
+
this._nodeId = null;
|
|
15767
16141
|
this._iceConnectionState$ = this.createReplaySubject(1);
|
|
15768
16142
|
this._connectionState$ = this.createReplaySubject(1);
|
|
15769
16143
|
this._signalingState$ = this.createReplaySubject(1);
|
|
15770
16144
|
this._iceGatheringState$ = this.createReplaySubject(1);
|
|
15771
16145
|
this._errors$ = this.createReplaySubject(1);
|
|
15772
16146
|
this._iceCandidates$ = this.createReplaySubject(1);
|
|
16147
|
+
this._localMediaSettled$ = this.createReplaySubject(1);
|
|
15773
16148
|
this._initialized$ = this.createReplaySubject(1);
|
|
15774
16149
|
this._remoteDescription$ = this.createReplaySubject(1);
|
|
15775
16150
|
this._remoteStream$ = this.createBehaviorSubject(null);
|
|
@@ -15802,6 +16177,7 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
15802
16177
|
inputVideoStream: this.options.inputVideoStream,
|
|
15803
16178
|
inputAudioDeviceConstraints: this.inputAudioDeviceConstraints,
|
|
15804
16179
|
inputVideoDeviceConstraints: this.inputVideoDeviceConstraints,
|
|
16180
|
+
screenShareAudio: this.options.screenShareAudio,
|
|
15805
16181
|
getUserMedia: async (constraints) => this.getUserMedia(constraints),
|
|
15806
16182
|
getDisplayMedia: async (options$1) => this.getDisplayMedia(options$1)
|
|
15807
16183
|
});
|
|
@@ -15828,6 +16204,13 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
15828
16204
|
get memberId() {
|
|
15829
16205
|
return this._memberId;
|
|
15830
16206
|
}
|
|
16207
|
+
/** The node this leg's invite landed on — auxiliary legs are placed independently. */
|
|
16208
|
+
setNodeId(nodeId) {
|
|
16209
|
+
this._nodeId = nodeId;
|
|
16210
|
+
}
|
|
16211
|
+
get nodeId() {
|
|
16212
|
+
return this._nodeId;
|
|
16213
|
+
}
|
|
15831
16214
|
stopTrackSender(kind, options = { updateTransceiverDirection: false }) {
|
|
15832
16215
|
const audioCovered = kind === "audio" || kind === "both";
|
|
15833
16216
|
if (audioCovered && this._localAudioPipeline) this.stopRawAudioInputForPipeline();
|
|
@@ -15837,10 +16220,7 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
15837
16220
|
}
|
|
15838
16221
|
stopRawAudioInputForPipeline() {
|
|
15839
16222
|
const rawTracks = this.localStreamController.localAudioTracks;
|
|
15840
|
-
for (const track of rawTracks) if (track.readyState === "live")
|
|
15841
|
-
track.stop();
|
|
15842
|
-
this.localStreamController.removeTrack(track.id);
|
|
15843
|
-
}
|
|
16223
|
+
for (const track of rawTracks) if (track.readyState === "live") this.localStreamController.removeTrack(track.id);
|
|
15844
16224
|
this._localAudioPipeline?.setInputTrack(null);
|
|
15845
16225
|
}
|
|
15846
16226
|
get isNegotiating$() {
|
|
@@ -15873,6 +16253,10 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
15873
16253
|
get remoteDescription$() {
|
|
15874
16254
|
return this.cachedObservable("remoteDescription$", () => this._remoteDescription$.asObservable().pipe((0, import_cjs$16.takeUntil)(this.destroyed$)));
|
|
15875
16255
|
}
|
|
16256
|
+
/** Emits once local media is settled — acquired, or knowingly receive-only. */
|
|
16257
|
+
get localMediaSettled$() {
|
|
16258
|
+
return this.cachedObservable("localMediaSettled$", () => this._localMediaSettled$.asObservable().pipe((0, import_cjs$16.takeUntil)(this.destroyed$)));
|
|
16259
|
+
}
|
|
15876
16260
|
get localStream$() {
|
|
15877
16261
|
return this.cachedObservable("localStream$", () => this.localStreamController.localStream$.pipe((0, import_cjs$16.takeUntil)(this.destroyed$)));
|
|
15878
16262
|
}
|
|
@@ -15900,6 +16284,9 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
15900
16284
|
get propose() {
|
|
15901
16285
|
return this.options.propose ?? "main";
|
|
15902
16286
|
}
|
|
16287
|
+
get connectionState() {
|
|
16288
|
+
return this.peerConnection?.connectionState;
|
|
16289
|
+
}
|
|
15903
16290
|
get isAdditionalDevice() {
|
|
15904
16291
|
return this.propose === "additional-device";
|
|
15905
16292
|
}
|
|
@@ -16028,7 +16415,7 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
16028
16415
|
this._isNegotiating$.next(true);
|
|
16029
16416
|
await this._setRemoteDescription(this.sdpInit);
|
|
16030
16417
|
} else {
|
|
16031
|
-
await this.setupTrackHandling();
|
|
16418
|
+
if (!await this.setupTrackHandling()) return;
|
|
16032
16419
|
this._initialized$.next(true);
|
|
16033
16420
|
}
|
|
16034
16421
|
} catch (error) {
|
|
@@ -16152,7 +16539,10 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
16152
16539
|
inputVideoDeviceConstraints: this.inputVideoDeviceConstraints
|
|
16153
16540
|
});
|
|
16154
16541
|
}
|
|
16155
|
-
await this.setupLocalTracks()
|
|
16542
|
+
if (!await this.setupLocalTracks()) {
|
|
16543
|
+
logger$17.debug("[RTCPeerConnectionController] Inbound answer abandoned; the connection went away.");
|
|
16544
|
+
return;
|
|
16545
|
+
}
|
|
16156
16546
|
const { answerOptions } = this;
|
|
16157
16547
|
logger$17.debug("[RTCPeerConnectionController] Creating inbound answer with options:", answerOptions);
|
|
16158
16548
|
await this.createAnswer(answerOptions);
|
|
@@ -16290,35 +16680,52 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
16290
16680
|
}
|
|
16291
16681
|
/**
|
|
16292
16682
|
* Setup track handling for remote tracks.
|
|
16683
|
+
*
|
|
16684
|
+
* @returns `false` when the connection went away while local media was being
|
|
16685
|
+
* acquired — see {@link setupLocalTracks}.
|
|
16293
16686
|
*/
|
|
16294
16687
|
async setupTrackHandling() {
|
|
16295
16688
|
if (!this.peerConnection) throw new DependencyError("RTCPeerConnection is not initialized");
|
|
16296
|
-
await this.setupLocalTracks();
|
|
16689
|
+
if (!await this.setupLocalTracks()) return false;
|
|
16297
16690
|
await this.setupRemoteTracks();
|
|
16691
|
+
return true;
|
|
16298
16692
|
}
|
|
16693
|
+
/**
|
|
16694
|
+
* @returns `false` when the connection was torn down while getUserMedia was
|
|
16695
|
+
* in flight. The acquisition is not cancellable, so the caller must stop
|
|
16696
|
+
* rather than go on to touch a peer connection that is closed or gone.
|
|
16697
|
+
*/
|
|
16299
16698
|
async setupLocalTracks() {
|
|
16300
16699
|
logger$17.debug("[RTCPeerConnectionController] Setting up local tracks/transceivers.");
|
|
16301
16700
|
if (this.hasNoLocalMediaToSend()) {
|
|
16302
16701
|
if (!this.receiveAudio && !this.receiveVideo) throw new InvalidParams("Call requests no media: enable audio/video or receiveAudio/receiveVideo");
|
|
16303
16702
|
logger$17.debug("[RTCPeerConnectionController] No local media requested; negotiating receive-only.");
|
|
16304
16703
|
this.setupReceiveOnlyTransceivers();
|
|
16305
|
-
|
|
16704
|
+
this._localMediaSettled$.next();
|
|
16705
|
+
return true;
|
|
16306
16706
|
}
|
|
16307
16707
|
let localStream;
|
|
16308
16708
|
try {
|
|
16309
16709
|
localStream = this.localStream ?? await this.localStreamController.buildLocalStream();
|
|
16310
16710
|
} catch (error) {
|
|
16311
16711
|
this.handleLocalMediaFailure(error);
|
|
16312
|
-
|
|
16712
|
+
this._localMediaSettled$.next();
|
|
16713
|
+
return true;
|
|
16714
|
+
}
|
|
16715
|
+
if (!this.peerConnection || this.peerConnection.signalingState === "closed") {
|
|
16716
|
+
logger$17.debug("[RTCPeerConnectionController] Local media arrived after teardown; releasing it.");
|
|
16717
|
+
localStream.getTracks().forEach((track) => track.stop());
|
|
16718
|
+
return false;
|
|
16313
16719
|
}
|
|
16720
|
+
this._localMediaSettled$.next();
|
|
16314
16721
|
if (this.transceiverController?.useAddStream ?? false) {
|
|
16315
16722
|
logger$17.warn("[RTCPeerConnectionController] Using deprecated addStream API to add local stream.");
|
|
16316
|
-
this.peerConnection
|
|
16723
|
+
this.peerConnection.addStream(localStream);
|
|
16317
16724
|
if (!this.isNegotiating) {
|
|
16318
16725
|
logger$17.debug("[RTCPeerConnectionController] Forcing negotiationneeded after local tracks setup.");
|
|
16319
16726
|
this.negotiationNeeded$.next();
|
|
16320
16727
|
}
|
|
16321
|
-
return;
|
|
16728
|
+
return true;
|
|
16322
16729
|
}
|
|
16323
16730
|
for (const kind of ["audio", "video"]) {
|
|
16324
16731
|
const tracks = (kind === "audio" ? localStream.getAudioTracks() : localStream.getVideoTracks()).map((track, index) => ({
|
|
@@ -16332,10 +16739,11 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
16332
16739
|
await this.transceiverController?.setupTransceiverSender(track, localStream, transceivers[index]);
|
|
16333
16740
|
} else {
|
|
16334
16741
|
logger$17.debug(`[RTCPeerConnectionController] Using addTrack for local ${kind} track:`, track.id);
|
|
16335
|
-
this.peerConnection
|
|
16742
|
+
this.peerConnection.addTrack(track, localStream);
|
|
16336
16743
|
}
|
|
16337
16744
|
}
|
|
16338
16745
|
}
|
|
16746
|
+
return true;
|
|
16339
16747
|
}
|
|
16340
16748
|
/** True for a main connection with no local media to send. */
|
|
16341
16749
|
hasNoLocalMediaToSend() {
|
|
@@ -16424,6 +16832,61 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
16424
16832
|
this._localAudioPipeline.setInputTrack(newTrack);
|
|
16425
16833
|
}
|
|
16426
16834
|
/**
|
|
16835
|
+
* Capture the newly selected device, leaving the current capture running.
|
|
16836
|
+
*
|
|
16837
|
+
* A rejection must leave the current track sending, so nothing is released
|
|
16838
|
+
* until the replacement is in hand. The one exception is hardware that admits
|
|
16839
|
+
* a single opener — a phone's front and back cameras, typically — which
|
|
16840
|
+
* rejects the second capture until the first is closed.
|
|
16841
|
+
*/
|
|
16842
|
+
async acquireInputTrack(kind, constraints, deviceInfo, currentTrack) {
|
|
16843
|
+
try {
|
|
16844
|
+
return await this.captureTrack(kind, constraints, deviceInfo.deviceId);
|
|
16845
|
+
} catch (error) {
|
|
16846
|
+
if (!isMediaDeviceInUse(error)) throw error;
|
|
16847
|
+
logger$17.warn(`[RTCPeerConnectionController] ${kind} device is held exclusively; releasing the current capture to retry:`, error);
|
|
16848
|
+
const previousDeviceId = currentTrack.getSettings().deviceId;
|
|
16849
|
+
this.stopTrackSender(kind);
|
|
16850
|
+
try {
|
|
16851
|
+
return await this.captureTrack(kind, constraints, deviceInfo.deviceId);
|
|
16852
|
+
} catch (retryError) {
|
|
16853
|
+
await this.restorePreviousInputTrack(kind, constraints, previousDeviceId, currentTrack);
|
|
16854
|
+
throw retryError;
|
|
16855
|
+
}
|
|
16856
|
+
}
|
|
16857
|
+
}
|
|
16858
|
+
async captureTrack(kind, constraints, deviceId) {
|
|
16859
|
+
const { stream, fallbackLevel } = await getUserMediaWithFallback({ getUserMedia: async (c) => this.getUserMedia(c) }, { [kind]: constraints }, kind, deviceId);
|
|
16860
|
+
const track = stream.getTracks().find((t) => t.kind === kind);
|
|
16861
|
+
if (!track) {
|
|
16862
|
+
stream.getTracks().forEach((t) => t.stop());
|
|
16863
|
+
throw new MediaTrackError("updateSelectedInputDevice", kind, /* @__PURE__ */ new Error("getUserMedia returned no track of the requested kind"));
|
|
16864
|
+
}
|
|
16865
|
+
if (fallbackLevel !== "exact") logger$17.warn(`[RTCPeerConnectionController] ${kind} device acquired at fallback level '${fallbackLevel}'; the capture may not be the requested device.`);
|
|
16866
|
+
return track;
|
|
16867
|
+
}
|
|
16868
|
+
/** Best-effort return to the device that was released for an exclusive retry. */
|
|
16869
|
+
async restorePreviousInputTrack(kind, constraints, previousDeviceId, releasedTrack) {
|
|
16870
|
+
try {
|
|
16871
|
+
const restored = await this.captureTrack(kind, constraints, previousDeviceId);
|
|
16872
|
+
await this.attachInputTrack(kind, restored, releasedTrack);
|
|
16873
|
+
} catch (error) {
|
|
16874
|
+
logger$17.error(`[RTCPeerConnectionController] Failed to restore the previous ${kind} device:`, error);
|
|
16875
|
+
}
|
|
16876
|
+
}
|
|
16877
|
+
async attachInputTrack(kind, newTrack, oldTrack) {
|
|
16878
|
+
const pipelineOwnsAudio = kind === "audio" && this._localAudioPipeline;
|
|
16879
|
+
if (!pipelineOwnsAudio) try {
|
|
16880
|
+
await this.transceiverController?.replaceSenderTrack(kind, newTrack);
|
|
16881
|
+
} catch (error) {
|
|
16882
|
+
newTrack.stop();
|
|
16883
|
+
throw error;
|
|
16884
|
+
}
|
|
16885
|
+
this.localStreamController.removeTrack(oldTrack.id);
|
|
16886
|
+
this.localStreamController.addTrack(newTrack);
|
|
16887
|
+
if (pipelineOwnsAudio) this._localAudioPipeline?.setInputTrack(newTrack);
|
|
16888
|
+
}
|
|
16889
|
+
/**
|
|
16427
16890
|
* Return the lazily-created {@link LocalAudioPipeline}, constructing it on
|
|
16428
16891
|
* first access. On creation the current audio sender's track is routed
|
|
16429
16892
|
* through the pipeline (input → gain → analyser → destination) and the
|
|
@@ -16456,6 +16919,7 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
16456
16919
|
const sender = (this.transceiverController?.audioTransceivers.at(0))?.sender ?? this.peerConnection.getSenders().find((s) => s.track?.kind === "audio");
|
|
16457
16920
|
if (!sender || !raw) return;
|
|
16458
16921
|
try {
|
|
16922
|
+
this.localStreamController.setTrackOrigin(this._localAudioPipeline.outputTrack, "processed");
|
|
16459
16923
|
await sender.replaceTrack(this._localAudioPipeline.outputTrack);
|
|
16460
16924
|
} catch (error) {
|
|
16461
16925
|
logger$17.warn("[RTCPeerConnectionController] Failed to route audio sender through pipeline:", error);
|
|
@@ -16516,37 +16980,68 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
16516
16980
|
for (const existingTrack of existingTracks) this.removeLocalTrack(existingTrack.id);
|
|
16517
16981
|
this.addLocalTrack(track);
|
|
16518
16982
|
}
|
|
16983
|
+
/**
|
|
16984
|
+
* @returns whether the constraints reached the media the leg is sending.
|
|
16985
|
+
*
|
|
16986
|
+
* With the pipeline engaged the audio sender carries the processed
|
|
16987
|
+
* destination track, so the sender scan would find nothing it may touch and
|
|
16988
|
+
* every audio constraint API would silently no-op. The constraints belong to
|
|
16989
|
+
* the pipeline's device source, which is the capture that sender ultimately
|
|
16990
|
+
* carries.
|
|
16991
|
+
*/
|
|
16519
16992
|
async updateSendersConstraints(kind, constraints) {
|
|
16520
|
-
|
|
16993
|
+
if (kind === "audio" && this._localAudioPipeline) {
|
|
16994
|
+
if (!constraints) {
|
|
16995
|
+
this.stopTrackSender("audio");
|
|
16996
|
+
return false;
|
|
16997
|
+
}
|
|
16998
|
+
return this.applyPipelineSourceConstraints(constraints);
|
|
16999
|
+
}
|
|
17000
|
+
return await this.transceiverController?.updateSendersConstraints(kind, constraints) ?? false;
|
|
16521
17001
|
}
|
|
16522
17002
|
/**
|
|
16523
|
-
*
|
|
16524
|
-
*
|
|
16525
|
-
*
|
|
16526
|
-
*
|
|
17003
|
+
* Mirror of the sender path for a piped audio leg: same merge, same fallback
|
|
17004
|
+
* ladder, same device-capture invariant — but the swap target is the pipeline
|
|
17005
|
+
* input, so the sender keeps emitting the pipeline's output track and its
|
|
17006
|
+
* identity survives the change.
|
|
16527
17007
|
*/
|
|
16528
|
-
async
|
|
16529
|
-
const
|
|
16530
|
-
|
|
16531
|
-
|
|
16532
|
-
|
|
17008
|
+
async applyPipelineSourceConstraints(constraints) {
|
|
17009
|
+
const pipeline = this._localAudioPipeline;
|
|
17010
|
+
const source = this.localStreamController.localAudioTracks.at(0);
|
|
17011
|
+
if (!pipeline || !source) {
|
|
17012
|
+
logger$17.debug("[RTCPeerConnectionController] No pipeline input to constrain.");
|
|
17013
|
+
return false;
|
|
16533
17014
|
}
|
|
16534
|
-
|
|
16535
|
-
|
|
16536
|
-
|
|
16537
|
-
|
|
16538
|
-
|
|
16539
|
-
|
|
16540
|
-
|
|
16541
|
-
|
|
16542
|
-
|
|
16543
|
-
|
|
16544
|
-
|
|
16545
|
-
|
|
16546
|
-
|
|
16547
|
-
|
|
17015
|
+
if (!this.localStreamController.isDeviceCapture(source)) {
|
|
17016
|
+
logger$17.debug(`[RTCPeerConnectionController] Skipping audio constraints for a non-device pipeline input (origin: ${this.localStreamController.getTrackOrigin(source) ?? "unrecorded"}).`);
|
|
17017
|
+
return false;
|
|
17018
|
+
}
|
|
17019
|
+
const merged = {
|
|
17020
|
+
...source.getConstraints(),
|
|
17021
|
+
...constraints
|
|
17022
|
+
};
|
|
17023
|
+
try {
|
|
17024
|
+
await source.applyConstraints(merged);
|
|
17025
|
+
logger$17.debug("[RTCPeerConnectionController] Pipeline input constraints updated:", merged);
|
|
17026
|
+
return true;
|
|
17027
|
+
} catch (error) {
|
|
17028
|
+
logger$17.warn("[RTCPeerConnectionController] applyConstraints failed on the pipeline input, re-acquiring:", error);
|
|
17029
|
+
}
|
|
17030
|
+
try {
|
|
17031
|
+
const { stream } = await getUserMediaWithFallback({ getUserMedia: async (c) => this.getUserMedia(c) }, { audio: merged }, "audio", source.getSettings().deviceId);
|
|
17032
|
+
const newTrack = stream.getAudioTracks().at(0);
|
|
17033
|
+
if (!newTrack) {
|
|
17034
|
+
stream.getTracks().forEach((track) => track.stop());
|
|
17035
|
+
throw new Error("getUserMedia returned no audio track");
|
|
17036
|
+
}
|
|
17037
|
+
this.localStreamController.removeTrack(source.id);
|
|
16548
17038
|
this.localStreamController.addTrack(newTrack);
|
|
16549
|
-
|
|
17039
|
+
pipeline.setInputTrack(newTrack);
|
|
17040
|
+
return true;
|
|
17041
|
+
} catch (error) {
|
|
17042
|
+
logger$17.warn("[RTCPeerConnectionController] Failed to re-acquire the pipeline input for constraints:", error);
|
|
17043
|
+
this._errors$.next(new MediaTrackError("updateSendersConstraints", "audio", error));
|
|
17044
|
+
return false;
|
|
16550
17045
|
}
|
|
16551
17046
|
}
|
|
16552
17047
|
/**
|
|
@@ -16666,6 +17161,48 @@ const logger$16 = getLogger();
|
|
|
16666
17161
|
function resolveInviteNodeId(args) {
|
|
16667
17162
|
return args.isInvite && !args.reattach && !args.explicitNodeId ? "" : args.currentNodeId ?? "";
|
|
16668
17163
|
}
|
|
17164
|
+
/**
|
|
17165
|
+
* Surface the real outcome of a `webrtc.verto` reply.
|
|
17166
|
+
*
|
|
17167
|
+
* A webrtc.verto response nests several envelopes, each keyed by a verto-style
|
|
17168
|
+
* string `code` ("200" ok, "400"/etc. fail) rather than a JSON-RPC `error`. An outer
|
|
17169
|
+
* layer reports only whether the frame was delivered; an inner layer carries the op's
|
|
17170
|
+
* own outcome:
|
|
17171
|
+
*
|
|
17172
|
+
* response.result = { code:"200", result:{…} } ← delivery acknowledgement
|
|
17173
|
+
* .result = { jsonrpc, id, result:{…} } ← the reply payload
|
|
17174
|
+
* .result = { code:"400", message:"Bad request" } ← the actual op outcome
|
|
17175
|
+
*
|
|
17176
|
+
* A failure can appear at any layer (delivery refused, or the op itself rejected
|
|
17177
|
+
* deeper down), so walk every nested `.result` object and return the FIRST non-2xx
|
|
17178
|
+
* `code` with its message. Returns null when every `code` seen is 2xx or absent —
|
|
17179
|
+
* i.e. the op succeeded. This is the only way to detect that e.g. a mute/kick was
|
|
17180
|
+
* rejected, since the outer delivery `code` is "200" (delivered) even then.
|
|
17181
|
+
*
|
|
17182
|
+
* Pure function — exported for unit testing.
|
|
17183
|
+
*/
|
|
17184
|
+
function findNestedVertoFailure(response) {
|
|
17185
|
+
let node = response;
|
|
17186
|
+
while (node !== null && typeof node === "object") {
|
|
17187
|
+
const obj = node;
|
|
17188
|
+
const err = obj.error;
|
|
17189
|
+
if (err !== null && typeof err === "object") {
|
|
17190
|
+
const e = err;
|
|
17191
|
+
const errCode = typeof e.code === "string" || typeof e.code === "number" ? String(e.code) : void 0;
|
|
17192
|
+
if (errCode !== void 0 && !/^2\d\d$/.test(errCode)) return {
|
|
17193
|
+
code: errCode,
|
|
17194
|
+
message: typeof e.message === "string" ? e.message : void 0
|
|
17195
|
+
};
|
|
17196
|
+
}
|
|
17197
|
+
const code = typeof obj.code === "string" || typeof obj.code === "number" ? String(obj.code) : void 0;
|
|
17198
|
+
if (code !== void 0 && !/^2\d\d$/.test(code)) return {
|
|
17199
|
+
code,
|
|
17200
|
+
message: typeof obj.message === "string" ? obj.message : void 0
|
|
17201
|
+
};
|
|
17202
|
+
node = obj.result !== null && typeof obj.result === "object" ? obj.result : null;
|
|
17203
|
+
}
|
|
17204
|
+
return null;
|
|
17205
|
+
}
|
|
16669
17206
|
var VertoManager = class extends Destroyable {
|
|
16670
17207
|
constructor(callSession) {
|
|
16671
17208
|
super();
|
|
@@ -16688,7 +17225,8 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
16688
17225
|
this._signalingStatus$ = this.createReplaySubject(1);
|
|
16689
17226
|
this._screenShareStatus$ = this.createBehaviorSubject("none");
|
|
16690
17227
|
this._rtcPeerConnectionsMap = /* @__PURE__ */ new Map();
|
|
16691
|
-
this.
|
|
17228
|
+
this._lastVertoPings = /* @__PURE__ */ new Map();
|
|
17229
|
+
this._legErrors$ = this.createSubject();
|
|
16692
17230
|
this._nodeId$ = this.createBehaviorSubject(options.nodeId ?? null);
|
|
16693
17231
|
this.onError = options.onError;
|
|
16694
17232
|
this.onModifyFailed = options.onModifyFailed;
|
|
@@ -16736,6 +17274,10 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
16736
17274
|
get selfId$() {
|
|
16737
17275
|
return this._selfId$.asObservable();
|
|
16738
17276
|
}
|
|
17277
|
+
/** Separates the media phase of call creation from the signalling phase. */
|
|
17278
|
+
get localMediaSettled$() {
|
|
17279
|
+
return this.mainPeerConnection.localMediaSettled$;
|
|
17280
|
+
}
|
|
16739
17281
|
get localStream() {
|
|
16740
17282
|
return this._rtcPeerConnectionsMap.get(this.webRtcCallSession.id)?.localStream ?? null;
|
|
16741
17283
|
}
|
|
@@ -16792,25 +17334,89 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
16792
17334
|
const { mediaParams, callID } = event;
|
|
16793
17335
|
const rtcPeerConnController = this._rtcPeerConnectionsMap.get(callID);
|
|
16794
17336
|
const { audio, video } = mediaParams;
|
|
16795
|
-
|
|
16796
|
-
|
|
16797
|
-
|
|
16798
|
-
|
|
16799
|
-
|
|
16800
|
-
audio,
|
|
16801
|
-
video,
|
|
16802
|
-
timestamp: Date.now()
|
|
16803
|
-
});
|
|
16804
|
-
} catch (error) {
|
|
16805
|
-
logger$16.warn("[WebRTCManager] Error applying server-pushed media params:", error);
|
|
16806
|
-
this.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
16807
|
-
}
|
|
16808
|
-
})();
|
|
17337
|
+
if (!rtcPeerConnController) {
|
|
17338
|
+
logger$16.warn(`[WebRTCManager] Ignoring server-pushed media params for unknown leg ${callID}`);
|
|
17339
|
+
return;
|
|
17340
|
+
}
|
|
17341
|
+
this.applyServerMediaParams(rtcPeerConnController, audio, video);
|
|
16809
17342
|
});
|
|
16810
17343
|
this.subscribeTo(this.vertoPing$, (vertoPing) => {
|
|
16811
|
-
this.
|
|
17344
|
+
this._lastVertoPings.set(vertoPing.callID, vertoPing);
|
|
17345
|
+
this.attachManager.refresh(this.buildAttachableCall());
|
|
16812
17346
|
this.sendVertoPong(vertoPing);
|
|
16813
17347
|
});
|
|
17348
|
+
this.subscribeTo(this.webRtcCallSession.clientSession.authenticated$.pipe((0, import_cjs$15.skip)(1), (0, import_cjs$15.filter)(Boolean)), () => {
|
|
17349
|
+
this._lastVertoPings.forEach((vertoPing) => void this.sendVertoPong(vertoPing));
|
|
17350
|
+
});
|
|
17351
|
+
}
|
|
17352
|
+
/**
|
|
17353
|
+
* An auxiliary-leg failure must never destroy the call; main-leg and
|
|
17354
|
+
* call-level errors keep `CallFactory.isFatalError`'s classification.
|
|
17355
|
+
*
|
|
17356
|
+
* Every site holding a peer connection reports through here, so the invariant
|
|
17357
|
+
* is structural rather than per-call-site — which is how cloud-product#20523
|
|
17358
|
+
* happened, with only one of fourteen sites passing `{ fatal: false }`.
|
|
17359
|
+
*
|
|
17360
|
+
* `override` composes rather than replaces: a caller may force non-fatal for a
|
|
17361
|
+
* reason of its own (a `verto.info` frame is best-effort whichever leg carries
|
|
17362
|
+
* it), and an auxiliary leg stays non-fatal regardless.
|
|
17363
|
+
*/
|
|
17364
|
+
reportLegError(error, rtcPeerConnController, override) {
|
|
17365
|
+
const leg = rtcPeerConnController?.propose;
|
|
17366
|
+
const legId = rtcPeerConnController?.id;
|
|
17367
|
+
const auxiliary = Boolean(rtcPeerConnController) && !rtcPeerConnController?.isMainDevice;
|
|
17368
|
+
this.onError?.(error, {
|
|
17369
|
+
...override?.fatal === false || auxiliary ? { fatal: false } : {},
|
|
17370
|
+
...leg ? { leg } : {},
|
|
17371
|
+
...legId ? { legId } : {}
|
|
17372
|
+
});
|
|
17373
|
+
if (legId) this._legErrors$.next({
|
|
17374
|
+
legId,
|
|
17375
|
+
error
|
|
17376
|
+
});
|
|
17377
|
+
}
|
|
17378
|
+
/**
|
|
17379
|
+
* Errors reported for one leg, as a stream that fails with them.
|
|
17380
|
+
*
|
|
17381
|
+
* Signaling failures are reported, never thrown — so nothing that waits on a
|
|
17382
|
+
* leg's progress would otherwise learn of a rejected invite. Merging this in
|
|
17383
|
+
* lets the wait end with the reason the server gave.
|
|
17384
|
+
*/
|
|
17385
|
+
legError$(legId) {
|
|
17386
|
+
return this._legErrors$.pipe((0, import_cjs$15.filter)((report) => report.legId === legId), (0, import_cjs$15.map)((report) => {
|
|
17387
|
+
throw report.error;
|
|
17388
|
+
}));
|
|
17389
|
+
}
|
|
17390
|
+
/**
|
|
17391
|
+
* Audio and video are applied independently so a failure in one cannot
|
|
17392
|
+
* suppress the other, and `mediaParamsUpdated` is emitted whatever happens —
|
|
17393
|
+
* an application should not be starved of the params by a constraint failure.
|
|
17394
|
+
*/
|
|
17395
|
+
async applyServerMediaParams(rtcPeerConnController, audio, video) {
|
|
17396
|
+
const failures = [];
|
|
17397
|
+
let applied = true;
|
|
17398
|
+
if (audio) try {
|
|
17399
|
+
applied = await rtcPeerConnController.updateSendersConstraints("audio", audio) && applied;
|
|
17400
|
+
} catch (error) {
|
|
17401
|
+
applied = false;
|
|
17402
|
+
failures.push(toError(error));
|
|
17403
|
+
}
|
|
17404
|
+
if (video) try {
|
|
17405
|
+
applied = await rtcPeerConnController.updateSendersConstraints("video", video) && applied;
|
|
17406
|
+
} catch (error) {
|
|
17407
|
+
applied = false;
|
|
17408
|
+
failures.push(toError(error));
|
|
17409
|
+
}
|
|
17410
|
+
this.webRtcCallSession.emitMediaParamsUpdated({
|
|
17411
|
+
audio,
|
|
17412
|
+
video,
|
|
17413
|
+
timestamp: Date.now(),
|
|
17414
|
+
applied
|
|
17415
|
+
});
|
|
17416
|
+
for (const failure of failures) {
|
|
17417
|
+
logger$16.warn("[WebRTCManager] Error applying server-pushed media params:", failure);
|
|
17418
|
+
this.reportLegError(failure, rtcPeerConnController, { fatal: false });
|
|
17419
|
+
}
|
|
16814
17420
|
}
|
|
16815
17421
|
/**
|
|
16816
17422
|
* Set node_id/selfId only when the current value is null.
|
|
@@ -16819,8 +17425,8 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
16819
17425
|
* these identifiers before the `verto.invite` RPC response (`CALL CREATED`)
|
|
16820
17426
|
* arrives. These methods let early events populate them eagerly so that
|
|
16821
17427
|
* downstream RPC calls (e.g. `call.layout.list`) don't fail with empty
|
|
16822
|
-
* identifiers. `processInviteResponse()` remains the authoritative source
|
|
16823
|
-
*
|
|
17428
|
+
* identifiers. `processInviteResponse()` remains the authoritative source and
|
|
17429
|
+
* overwrites unconditionally — for selfId, on the main leg only.
|
|
16824
17430
|
*/
|
|
16825
17431
|
setNodeIdIfNull(nodeId) {
|
|
16826
17432
|
if (!this._nodeId$.value && nodeId) {
|
|
@@ -16843,24 +17449,33 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
16843
17449
|
this.onError?.(new VertoPongError(error));
|
|
16844
17450
|
}
|
|
16845
17451
|
}
|
|
17452
|
+
/**
|
|
17453
|
+
* @returns whether the constraints reached the media the call is sending.
|
|
17454
|
+
* `false` is an outcome, not a failure: the leg may have no live sender of
|
|
17455
|
+
* the kind, or carry media the SDK did not capture and may not replace.
|
|
17456
|
+
* A failure behind it still reaches the call's `errors$`, so a caller that
|
|
17457
|
+
* ignores this value learns of it there.
|
|
17458
|
+
*/
|
|
16846
17459
|
async updateMediaConstraints(options = {}) {
|
|
16847
17460
|
const { audio, video } = options;
|
|
17461
|
+
let applied = true;
|
|
16848
17462
|
try {
|
|
16849
|
-
if (audio) await this.mainPeerConnection.updateSendersConstraints("audio", audio);
|
|
16850
|
-
if (video) await this.mainPeerConnection.updateSendersConstraints("video", video);
|
|
17463
|
+
if (audio) applied = await this.mainPeerConnection.updateSendersConstraints("audio", audio) && applied;
|
|
17464
|
+
if (video) applied = await this.mainPeerConnection.updateSendersConstraints("video", video) && applied;
|
|
16851
17465
|
} catch (error) {
|
|
16852
17466
|
logger$16.warn("[WebRTCManager] Error updating media constraints:", error);
|
|
16853
|
-
this.
|
|
17467
|
+
this.reportLegError(toError(error), this.mainPeerConnection);
|
|
16854
17468
|
throw error;
|
|
16855
17469
|
}
|
|
17470
|
+
return applied;
|
|
16856
17471
|
}
|
|
16857
17472
|
get selfId() {
|
|
16858
17473
|
return this._selfId$.value;
|
|
16859
17474
|
}
|
|
16860
17475
|
/** Build an AttachableCall from the current call state. */
|
|
16861
|
-
buildAttachableCall(idOverride) {
|
|
17476
|
+
buildAttachableCall(idOverride, nodeIdOverride) {
|
|
16862
17477
|
return {
|
|
16863
|
-
nodeId: this.nodeId ?? void 0,
|
|
17478
|
+
nodeId: nodeIdOverride ?? this.nodeId ?? void 0,
|
|
16864
17479
|
id: idOverride ?? this.webRtcCallSession.id,
|
|
16865
17480
|
to: this.webRtcCallSession.to,
|
|
16866
17481
|
mediaDirections: this.webRtcCallSession.mediaDirections
|
|
@@ -16990,24 +17605,49 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
16990
17605
|
get vertoPing$() {
|
|
16991
17606
|
return this.cachedObservable("vertoPing$", () => this.webRtcCallSession.webrtcMessages$.pipe(filterAs(isVertoPingInnerParams, "params"), (0, import_cjs$15.takeUntil)(this.destroyed$)));
|
|
16992
17607
|
}
|
|
17608
|
+
/**
|
|
17609
|
+
* Send a member-control op in-dialog via verto.info.
|
|
17610
|
+
*
|
|
17611
|
+
* The control payload rides in `params.command` — a sibling of `dialogParams`,
|
|
17612
|
+
* at the same level as `dtmf` in {@link sendDigits} — and the inner verto.info
|
|
17613
|
+
* is matched to this call's channel by `dialogParams.callID`, the in-dialog
|
|
17614
|
+
* convention for member-scoped frames. Because it is delivered on the dialog
|
|
17615
|
+
* itself, control lands on the call's own channel with no {node_id,call_id,member_id}
|
|
17616
|
+
* "self" tuple to get wrong. The outer webrtc.verto envelope (added by executeVerto)
|
|
17617
|
+
* still carries the own-leg callID + node_id for session routing.
|
|
17618
|
+
*
|
|
17619
|
+
* Keep `command` OUT of `dialogParams`: it is read at the params level, and
|
|
17620
|
+
* filterVertoParams rewrites/filters dialogParams keys but passes params-level
|
|
17621
|
+
* keys through verbatim.
|
|
17622
|
+
*/
|
|
17623
|
+
async sendCallControl(method, params) {
|
|
17624
|
+
const response = await this.executeVerto(VertoInfo({
|
|
17625
|
+
dialogParams: { callID: this.webRtcCallSession.id },
|
|
17626
|
+
command: {
|
|
17627
|
+
method,
|
|
17628
|
+
params
|
|
17629
|
+
}
|
|
17630
|
+
}));
|
|
17631
|
+
const failure = findNestedVertoFailure(response);
|
|
17632
|
+
if (failure) throw new JSONRPCError(Number.parseInt(failure.code, 10) || 0, `Call control "${method}" failed (code ${failure.code})${failure.message ? `: ${failure.message}` : ""}`, void 0);
|
|
17633
|
+
return response;
|
|
17634
|
+
}
|
|
16993
17635
|
async executeVerto(message, optionals = {}) {
|
|
16994
|
-
const
|
|
17636
|
+
const params = {
|
|
16995
17637
|
callID: optionals.callID ?? this.webRtcCallSession.id,
|
|
16996
17638
|
node_id: optionals.node_id ?? this._nodeId$.value ?? "",
|
|
16997
17639
|
message,
|
|
16998
17640
|
subscribe: optionals.subscribe
|
|
16999
|
-
}
|
|
17641
|
+
};
|
|
17642
|
+
const webrtcVertoMessage = WebrtcVerto(params);
|
|
17000
17643
|
const response = await this.webRtcCallSession.execute(webrtcVertoMessage);
|
|
17001
|
-
|
|
17002
|
-
const error = new JSONRPCError(response.error.code, response.error.message, response.error.data);
|
|
17003
|
-
this.onError?.(error);
|
|
17004
|
-
return response;
|
|
17005
|
-
}
|
|
17644
|
+
const nonFatal = message.method === "verto.info" ? { fatal: false } : void 0;
|
|
17006
17645
|
const innerResult = getValueFrom(response, "result.result");
|
|
17007
|
-
|
|
17008
|
-
|
|
17009
|
-
|
|
17010
|
-
|
|
17646
|
+
const failure = response.error ?? innerResult?.error;
|
|
17647
|
+
if (failure) {
|
|
17648
|
+
const error = new JSONRPCError(failure.code, failure.message, failure.data);
|
|
17649
|
+
if (message.method === "verto.invite" || message.method === "verto.answer") throw error;
|
|
17650
|
+
this.reportLegError(error, this._rtcPeerConnectionsMap.get(params.callID), nonFatal);
|
|
17011
17651
|
}
|
|
17012
17652
|
return response;
|
|
17013
17653
|
}
|
|
@@ -17026,8 +17666,9 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
17026
17666
|
default:
|
|
17027
17667
|
}
|
|
17028
17668
|
} catch (error) {
|
|
17669
|
+
if (vertoMethod === "verto.answer") throw error;
|
|
17029
17670
|
logger$16.error(`[WebRTCManager] Error sending Verto ${vertoMethod}:`, error);
|
|
17030
|
-
this.
|
|
17671
|
+
this.reportLegError(toError(error), rtcPeerConnController);
|
|
17031
17672
|
if (vertoMethod === "verto.modify") this.onModifyFailed?.();
|
|
17032
17673
|
}
|
|
17033
17674
|
}
|
|
@@ -17043,7 +17684,7 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
17043
17684
|
} catch (error) {
|
|
17044
17685
|
logger$16.warn("[WebRTCManager] Error processing modify response:", error);
|
|
17045
17686
|
const modifyError = error instanceof Error ? error : new Error(String(error), { cause: error });
|
|
17046
|
-
this.
|
|
17687
|
+
this.reportLegError(modifyError, rtcPeerConnController);
|
|
17047
17688
|
}
|
|
17048
17689
|
}
|
|
17049
17690
|
}
|
|
@@ -17056,37 +17697,35 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
17056
17697
|
status,
|
|
17057
17698
|
signalingError
|
|
17058
17699
|
});
|
|
17059
|
-
this.
|
|
17700
|
+
this.reportLegError(signalingError, null, { fatal: false });
|
|
17060
17701
|
return;
|
|
17061
17702
|
}
|
|
17062
17703
|
if (rtcPeerConnController.isMainDevice) this._signalingStatus$.next(status);
|
|
17063
17704
|
}
|
|
17064
17705
|
processInviteResponse(response, rtcPeerConnController) {
|
|
17065
|
-
if (
|
|
17706
|
+
if (getValueFrom(response, "result.result.result.message") === "CALL CREATED") {
|
|
17066
17707
|
this.emitMainSignalingStatus(rtcPeerConnController.id, "trying");
|
|
17067
|
-
|
|
17708
|
+
const nodeId = getValueFrom(response, "result.node_id") ?? null;
|
|
17068
17709
|
const memberId = getValueFrom(response, "result.result.result.memberID") ?? null;
|
|
17069
|
-
const callId = getValueFrom(response, "result.result.result.callID")
|
|
17710
|
+
const callId = getValueFrom(response, "result.result.result.callID");
|
|
17070
17711
|
logger$16.debug("[WebRTCManager] Verto invite response:", {
|
|
17071
17712
|
callId,
|
|
17072
17713
|
memberId,
|
|
17073
17714
|
response
|
|
17074
17715
|
});
|
|
17075
|
-
this._selfId$.next(memberId);
|
|
17076
17716
|
rtcPeerConnController.setMemberId(memberId);
|
|
17077
|
-
|
|
17078
|
-
|
|
17079
|
-
this.
|
|
17080
|
-
|
|
17081
|
-
|
|
17082
|
-
|
|
17083
|
-
|
|
17717
|
+
rtcPeerConnController.setNodeId(nodeId);
|
|
17718
|
+
if (rtcPeerConnController.isMainDevice) {
|
|
17719
|
+
this._selfId$.next(memberId);
|
|
17720
|
+
this._nodeId$.next(nodeId);
|
|
17721
|
+
this.attachManager.attach(this.buildAttachableCall(callId, nodeId ?? void 0));
|
|
17722
|
+
}
|
|
17723
|
+
if (callId) this.webRtcCallSession.addCallId(callId);
|
|
17084
17724
|
logger$16.info("[WebRTCManager] Verto invite successful");
|
|
17085
17725
|
logger$16.debug(`[WebRTCManager] nodeid: ${this._nodeId$.value}, selfId: ${this._selfId$.value}`);
|
|
17086
17726
|
} else {
|
|
17087
17727
|
logger$16.error("[WebRTCManager] Verto invite failed:", response);
|
|
17088
|
-
|
|
17089
|
-
this.onError?.(inviteError);
|
|
17728
|
+
this.reportLegError(/* @__PURE__ */ new Error("Verto invite failed: unexpected response"), rtcPeerConnController);
|
|
17090
17729
|
}
|
|
17091
17730
|
}
|
|
17092
17731
|
get RTCPeerConnectionConfig() {
|
|
@@ -17125,7 +17764,7 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
17125
17764
|
this._rtcPeerConnectionsMap.set(rtcPeerConnController.id, rtcPeerConnController);
|
|
17126
17765
|
this._rtcPeerConnections$.next(Array.from(this._rtcPeerConnectionsMap.values()));
|
|
17127
17766
|
this.subscribeTo(rtcPeerConnController.errors$, (error) => {
|
|
17128
|
-
this.
|
|
17767
|
+
this.reportLegError(error, rtcPeerConnController);
|
|
17129
17768
|
});
|
|
17130
17769
|
if (options.initOffer) this.handleInboundAnswer(rtcPeerConnController);
|
|
17131
17770
|
}
|
|
@@ -17154,7 +17793,7 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
17154
17793
|
await rtcPeerConnController.acceptInbound(answerOptions);
|
|
17155
17794
|
} catch (error) {
|
|
17156
17795
|
logger$16.error("[WebRTCManager] Error creating inbound answer:", error);
|
|
17157
|
-
this.
|
|
17796
|
+
this.reportLegError(toError(error), rtcPeerConnController);
|
|
17158
17797
|
}
|
|
17159
17798
|
}
|
|
17160
17799
|
}
|
|
@@ -17226,7 +17865,7 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
17226
17865
|
isInvite: isVertoInviteMessage(vertoMessage),
|
|
17227
17866
|
reattach: this.webRtcCallSession.options.reattach === true,
|
|
17228
17867
|
explicitNodeId: this.webRtcCallSession.options.nodeId,
|
|
17229
|
-
currentNodeId: this._nodeId$.value
|
|
17868
|
+
currentNodeId: rtcPeerConnController.nodeId ?? this._nodeId$.value
|
|
17230
17869
|
}),
|
|
17231
17870
|
subscribe
|
|
17232
17871
|
};
|
|
@@ -17258,7 +17897,7 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
17258
17897
|
await this.attachManager.attach(this.buildAttachableCall());
|
|
17259
17898
|
} catch (error) {
|
|
17260
17899
|
logger$16.error("[WebRTCManager] Error sending Verto answer:", error);
|
|
17261
|
-
this.
|
|
17900
|
+
this.reportLegError(toError(error), rtcPeerConnectionController);
|
|
17262
17901
|
await rtcPeerConnectionController.updateAnswerStatus({ status: "failed" });
|
|
17263
17902
|
}
|
|
17264
17903
|
}
|
|
@@ -17330,15 +17969,19 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
17330
17969
|
await this.mainPeerConnection.restoreTrackSender(deviceKind);
|
|
17331
17970
|
} else {
|
|
17332
17971
|
const error = new InvalidParams("No valid device to be added");
|
|
17333
|
-
this.
|
|
17972
|
+
this.reportLegError(error, this.mainPeerConnection);
|
|
17334
17973
|
throw error;
|
|
17335
17974
|
}
|
|
17336
17975
|
}
|
|
17337
|
-
async addScreenMedia(options = {
|
|
17338
|
-
await this.initAdditionalPeerConnection("screenshare",
|
|
17976
|
+
async addScreenMedia(options = {}) {
|
|
17977
|
+
await this.initAdditionalPeerConnection("screenshare", {
|
|
17978
|
+
audio: false,
|
|
17979
|
+
screenShareAudio: options.audio ?? false
|
|
17980
|
+
});
|
|
17339
17981
|
}
|
|
17340
17982
|
async initAdditionalPeerConnection(propose, options) {
|
|
17341
17983
|
const isScreenShare = propose === "screenshare";
|
|
17984
|
+
if (isScreenShare && this._screenShareId && this._rtcPeerConnectionsMap.has(this._screenShareId)) throw new ScreenShareAlreadyActiveError(this._screenShareId);
|
|
17342
17985
|
let firstPeerConnectionError;
|
|
17343
17986
|
let rtcPeerConnController = null;
|
|
17344
17987
|
try {
|
|
@@ -17355,21 +17998,36 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
17355
17998
|
this._rtcPeerConnections$.next(Array.from(this._rtcPeerConnectionsMap.values()));
|
|
17356
17999
|
this.subscribeTo(rtcPeerConnController.errors$, (error) => {
|
|
17357
18000
|
firstPeerConnectionError ??= error;
|
|
17358
|
-
this.
|
|
18001
|
+
this.reportLegError(error, rtcPeerConnController);
|
|
17359
18002
|
});
|
|
17360
|
-
|
|
18003
|
+
const pc = rtcPeerConnController;
|
|
18004
|
+
await (0, import_cjs$15.firstValueFrom)((0, import_cjs$15.merge)(pc.localMediaSettled$.pipe((0, import_cjs$15.take)(1), (0, import_cjs$15.switchMap)(() => pc.connectionState$.pipe((0, import_cjs$15.filter)((state) => state === "connected"), (0, import_cjs$15.take)(1), (0, import_cjs$15.timeout)(DEFAULT_AUX_LEG_CONNECT_TIMEOUT_MS)))), this.legError$(pc.id)).pipe((0, import_cjs$15.takeUntil)((0, import_cjs$15.merge)(this.destroyed$, pc.destroyed$))));
|
|
17361
18005
|
if (isScreenShare) this._screenShareStatus$.next("started");
|
|
17362
18006
|
logger$16.info(`[WebRTCManager] Additional peer connection connected (${propose}).`);
|
|
17363
18007
|
return rtcPeerConnController.id;
|
|
17364
18008
|
} catch (error) {
|
|
17365
|
-
|
|
17366
|
-
|
|
17367
|
-
if (
|
|
18009
|
+
const cancelled = error instanceof AuxiliaryLegCancelledError;
|
|
18010
|
+
const aborted = error instanceof import_cjs$15.EmptyError && !firstPeerConnectionError;
|
|
18011
|
+
if (!cancelled && !aborted) logger$16.warn("[WebRTCManager] Error initializing additional peer connection:", error);
|
|
18012
|
+
if (rtcPeerConnController && this._rtcPeerConnectionsMap.has(rtcPeerConnController.id)) {
|
|
18013
|
+
rtcPeerConnController.destroy();
|
|
18014
|
+
this._rtcPeerConnectionsMap.delete(rtcPeerConnController.id);
|
|
18015
|
+
this._rtcPeerConnections$.next(Array.from(this._rtcPeerConnectionsMap.values()));
|
|
18016
|
+
}
|
|
18017
|
+
if (isScreenShare) {
|
|
18018
|
+
this._screenShareId = void 0;
|
|
18019
|
+
this._screenShareStatus$.next("none");
|
|
18020
|
+
}
|
|
18021
|
+
if (cancelled) {
|
|
18022
|
+
logger$16.debug("[WebRTCManager] Additional peer connection removed before connecting.");
|
|
18023
|
+
throw error;
|
|
18024
|
+
}
|
|
17368
18025
|
if (firstPeerConnectionError) throw firstPeerConnectionError instanceof MediaAccessError && firstPeerConnectionError.originalError instanceof Error ? firstPeerConnectionError.originalError : firstPeerConnectionError;
|
|
17369
18026
|
if (error instanceof import_cjs$15.EmptyError) {
|
|
17370
18027
|
logger$16.debug("[WebRTCManager] Additional peer connection aborted before connecting.");
|
|
17371
18028
|
return;
|
|
17372
18029
|
}
|
|
18030
|
+
if (error instanceof import_cjs$15.TimeoutError) throw new AuxiliaryLegTimeoutError(propose, error);
|
|
17373
18031
|
throw error instanceof Error ? error : new Error(String(error), { cause: error });
|
|
17374
18032
|
}
|
|
17375
18033
|
}
|
|
@@ -17386,7 +18044,10 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
17386
18044
|
if (removeTrack) return this.mainPeerConnection.stopTrackSender(removeTrack, { updateTransceiverDirection: true });
|
|
17387
18045
|
}
|
|
17388
18046
|
async removeScreenMedia() {
|
|
17389
|
-
if (!["starting", "started"].includes(this._screenShareStatus$.value))
|
|
18047
|
+
if (!["starting", "started"].includes(this._screenShareStatus$.value)) {
|
|
18048
|
+
logger$16.warn("[WebRTCManager] No active screen share to stop.");
|
|
18049
|
+
return;
|
|
18050
|
+
}
|
|
17390
18051
|
if (!this._screenShareId) {
|
|
17391
18052
|
logger$16.debug("[WebRTCManager] No screen share peer connection found.");
|
|
17392
18053
|
return;
|
|
@@ -17401,6 +18062,10 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
17401
18062
|
try {
|
|
17402
18063
|
if (rtcPeerConnController) await this.executeVertoBye(rtcPeerConnController);
|
|
17403
18064
|
} finally {
|
|
18065
|
+
if (rtcPeerConnController && rtcPeerConnController.connectionState !== "connected") this._legErrors$.next({
|
|
18066
|
+
legId: id,
|
|
18067
|
+
error: new AuxiliaryLegCancelledError(rtcPeerConnController.propose)
|
|
18068
|
+
});
|
|
17404
18069
|
rtcPeerConnController?.destroy();
|
|
17405
18070
|
this._rtcPeerConnectionsMap.delete(id);
|
|
17406
18071
|
this._rtcPeerConnections$.next(Array.from(this._rtcPeerConnectionsMap.values()));
|
|
@@ -17415,7 +18080,10 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
17415
18080
|
await this.executeVerto(VertoBye({
|
|
17416
18081
|
...causeParams,
|
|
17417
18082
|
dialogParams: this.dialogParams(rtcPeerConnController)
|
|
17418
|
-
})
|
|
18083
|
+
}), {
|
|
18084
|
+
callID: rtcPeerConnController.id,
|
|
18085
|
+
node_id: rtcPeerConnController.nodeId ?? void 0
|
|
18086
|
+
});
|
|
17419
18087
|
} catch (error) {
|
|
17420
18088
|
logger$16.warn("[WebRTCManager] Call might already be disconnected, error sending Verto bye:", error);
|
|
17421
18089
|
throw error;
|
|
@@ -18294,17 +18962,38 @@ function mosToQualityLevel(mos) {
|
|
|
18294
18962
|
return "critical";
|
|
18295
18963
|
}
|
|
18296
18964
|
|
|
18965
|
+
//#endregion
|
|
18966
|
+
//#region src/utils/unwrapVertoReply.ts
|
|
18967
|
+
/**
|
|
18968
|
+
* Unwrap a method's own reply from a `webrtc.verto` envelope.
|
|
18969
|
+
*
|
|
18970
|
+
* A control verb sent in-dialog comes back nested two levels deep:
|
|
18971
|
+
*
|
|
18972
|
+
* ```
|
|
18973
|
+
* { result: { node_id, code, result: { jsonrpc, id, result: <method payload> } } }
|
|
18974
|
+
* ```
|
|
18975
|
+
*
|
|
18976
|
+
* whereas the routed transport resolves the method payload directly under
|
|
18977
|
+
* `.result`. Readers want the latter shape, and the difference is silent when it
|
|
18978
|
+
* is wrong — `response.result.layouts` simply evaluates to `undefined` against the
|
|
18979
|
+
* envelope, so the data arrives, nothing throws, and the caller sees an empty
|
|
18980
|
+
* value. That exact failure produced an empty layout dropdown with a successful
|
|
18981
|
+
* request behind it.
|
|
18982
|
+
*
|
|
18983
|
+
* Accepting both shapes here keeps every reader indifferent to which transport
|
|
18984
|
+
* produced the response. Anything that is not a nested envelope (a plain ack, or
|
|
18985
|
+
* an already-unwrapped reply) passes through untouched.
|
|
18986
|
+
*/
|
|
18987
|
+
function unwrapVertoReply(response) {
|
|
18988
|
+
const inner = getValueFrom(response, "result.result");
|
|
18989
|
+
return inner && typeof inner === "object" && "result" in inner ? inner : response;
|
|
18990
|
+
}
|
|
18991
|
+
|
|
18297
18992
|
//#endregion
|
|
18298
18993
|
//#region src/core/entities/Call.ts
|
|
18299
18994
|
var import_cjs$11 = require_cjs();
|
|
18300
18995
|
const logger$12 = getLogger();
|
|
18301
18996
|
/**
|
|
18302
|
-
* Verto method for setting member layout positions. Its gateway DTO requires a
|
|
18303
|
-
* `targets` array whose entries are `{ target, position }` (NOT bare targets),
|
|
18304
|
-
* so {@link WebRTCCall.buildMethodParams} special-cases it. See issue #19400.
|
|
18305
|
-
*/
|
|
18306
|
-
const POSITION_SET_METHOD = "call.member.position.set";
|
|
18307
|
-
/**
|
|
18308
18997
|
* Ratio between the critical and warning RTT spike multipliers.
|
|
18309
18998
|
* Warning threshold = baseline * warningMultiplier (default 3x)
|
|
18310
18999
|
* Critical threshold = baseline * warningMultiplier * RTT_CRITICAL_TO_WARNING_RATIO
|
|
@@ -18399,8 +19088,11 @@ var WebRTCCall = class extends Destroyable {
|
|
|
18399
19088
|
emitError(callError) {
|
|
18400
19089
|
if (this._status$.value === "destroyed" || this._status$.value === "failed") return;
|
|
18401
19090
|
this._errors$.next(callError);
|
|
18402
|
-
if (callError.fatal) {
|
|
19091
|
+
if (callError.fatal && this._status$.value !== "disconnecting") {
|
|
18403
19092
|
this._status$.next("failed");
|
|
19093
|
+
this.vertoManager.bye().catch((error) => {
|
|
19094
|
+
logger$12.debug("[Call] fatal-teardown bye failed (signaling likely already dead):", error);
|
|
19095
|
+
});
|
|
18404
19096
|
this.destroy();
|
|
18405
19097
|
}
|
|
18406
19098
|
}
|
|
@@ -18455,7 +19147,7 @@ var WebRTCCall = class extends Destroyable {
|
|
|
18455
19147
|
/** Toggles the call lock state, preventing or allowing new participants from joining. */
|
|
18456
19148
|
async toggleLock() {
|
|
18457
19149
|
const method = this.locked ? "call.unlock" : "call.lock";
|
|
18458
|
-
await this.executeMethod(this.
|
|
19150
|
+
await this.executeMethod(this.callSelf, method, {});
|
|
18459
19151
|
}
|
|
18460
19152
|
/**
|
|
18461
19153
|
* Toggles the hold state of the call (pauses/resumes local media transmission).
|
|
@@ -18504,14 +19196,25 @@ var WebRTCCall = class extends Destroyable {
|
|
|
18504
19196
|
*
|
|
18505
19197
|
* Constructs call context (node_id, call_id, member_id) and sends the RPC request.
|
|
18506
19198
|
*
|
|
18507
|
-
* @param target - Target
|
|
19199
|
+
* @param target - Target {@link MemberTarget} triple, or the local member's
|
|
19200
|
+
* ID string for self-operations (any other string is rejected — a bare
|
|
19201
|
+
* member id cannot carry the remote member's own call context).
|
|
18508
19202
|
* @param method - Verto method name (e.g. `'call.mute'`, `'call.member.remove'`).
|
|
18509
19203
|
* @param args - Parameters for the RPC method.
|
|
18510
19204
|
* @returns The RPC response.
|
|
19205
|
+
* @throws {CallNotReadyError} If the call has no self member context yet.
|
|
19206
|
+
* @throws {InvalidParams} If a string target is not the local member's ID.
|
|
18511
19207
|
* @throws {JSONRPCError} If the RPC call returns an error.
|
|
18512
19208
|
*/
|
|
18513
19209
|
async executeMethod(target, method, args) {
|
|
18514
|
-
const
|
|
19210
|
+
const self = this.callSelf;
|
|
19211
|
+
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}`);
|
|
19212
|
+
if (this.clientSession.callControl === "in-dialog") return this.executeMethodInDialog(target, method, args);
|
|
19213
|
+
const params = {
|
|
19214
|
+
...args,
|
|
19215
|
+
self,
|
|
19216
|
+
target: typeof target === "string" ? self : target
|
|
19217
|
+
};
|
|
18515
19218
|
const request = buildRPCRequest({
|
|
18516
19219
|
method,
|
|
18517
19220
|
params
|
|
@@ -18525,29 +19228,98 @@ var WebRTCCall = class extends Destroyable {
|
|
|
18525
19228
|
throw error;
|
|
18526
19229
|
}
|
|
18527
19230
|
}
|
|
18528
|
-
|
|
18529
|
-
|
|
18530
|
-
|
|
18531
|
-
|
|
18532
|
-
|
|
18533
|
-
|
|
18534
|
-
|
|
18535
|
-
|
|
18536
|
-
|
|
18537
|
-
|
|
18538
|
-
|
|
18539
|
-
|
|
18540
|
-
|
|
18541
|
-
|
|
18542
|
-
};
|
|
18543
|
-
|
|
18544
|
-
|
|
18545
|
-
|
|
18546
|
-
|
|
18547
|
-
|
|
19231
|
+
/**
|
|
19232
|
+
* `executeMethod` for a call opened with `callControl: 'in-dialog'`.
|
|
19233
|
+
*
|
|
19234
|
+
* Translates the routed transport's calling convention into the in-dialog one. No
|
|
19235
|
+
* `self` tuple is sent, but a `target` is — the same {call_id, member_id} the routed
|
|
19236
|
+
* transport puts in `target` (minus node_id), for self-ops and cross-member ops alike.
|
|
19237
|
+
*
|
|
19238
|
+
* Target shapes are per-verb and irregular, so they are centralised here rather
|
|
19239
|
+
* than left to callers: most verbs take a singular `target`, `call.member.remove`
|
|
19240
|
+
* takes a plural `targets` array, and `call.member.position.set` takes a flat
|
|
19241
|
+
* `targets` of `{call_id, position}` — the one verb keyed on call_id rather than
|
|
19242
|
+
* member_id, so the member triple `Participant.setPosition` built is unwrapped.
|
|
19243
|
+
*/
|
|
19244
|
+
async executeMethodInDialog(target, method, args) {
|
|
19245
|
+
const control = { ...args };
|
|
19246
|
+
if (method === "call.member.position.set") control.targets = (args.targets ?? []).map((entry) => ({
|
|
19247
|
+
call_id: entry.target?.call_id ?? entry.call_id,
|
|
19248
|
+
position: entry.position
|
|
19249
|
+
}));
|
|
19250
|
+
else {
|
|
19251
|
+
const member = typeof target === "object" ? {
|
|
19252
|
+
call_id: target.call_id,
|
|
19253
|
+
member_id: target.member_id
|
|
19254
|
+
} : {
|
|
18548
19255
|
call_id: this.id,
|
|
18549
19256
|
member_id: target
|
|
18550
|
-
}
|
|
19257
|
+
};
|
|
19258
|
+
if (method === "call.member.remove") control.targets = [member];
|
|
19259
|
+
else control.target = member;
|
|
19260
|
+
}
|
|
19261
|
+
return this.sendCommand(method, control);
|
|
19262
|
+
}
|
|
19263
|
+
/**
|
|
19264
|
+
* Sends a `call.*` control verb **in-dialog** via `verto.info`, as an alternative
|
|
19265
|
+
* to the routed {@link executeMethod} transport.
|
|
19266
|
+
*
|
|
19267
|
+
* Why both exist: `executeMethod` addresses the member with an explicit
|
|
19268
|
+
* `{node_id, call_id, member_id}` tuple, which does not resolve for every conference,
|
|
19269
|
+
* so the op can fail. An in-dialog frame carries the verb on the member's own
|
|
19270
|
+
* signaling channel instead, so control works without the client needing to know how
|
|
19271
|
+
* the conference is hosted.
|
|
19272
|
+
*
|
|
19273
|
+
* The trade-off is reach: the in-dialog transport is only accepted for calls that
|
|
19274
|
+
* join a conference over SWML (e.g. an SWML `join_conference`); use the routed
|
|
19275
|
+
* default otherwise.
|
|
19276
|
+
*
|
|
19277
|
+
* `params` are sent verbatim — nothing is built for you, which includes the target.
|
|
19278
|
+
* **A self-directed op still needs one**, or it is refused; name yourself explicitly:
|
|
19279
|
+
*
|
|
19280
|
+
* ```ts
|
|
19281
|
+
* const { call_id, member_id } = call.self.target;
|
|
19282
|
+
* await call.sendCommand('call.mute', { channels: ['audio'], target: { call_id, member_id } });
|
|
19283
|
+
* ```
|
|
19284
|
+
*
|
|
19285
|
+
* Never include `node_id` — only the two ids. The shapes are per-verb: most take a
|
|
19286
|
+
* singular `target`, `call.member.remove` takes a plural `targets` array, and
|
|
19287
|
+
* `call.member.position.set` takes a flat `targets: [{call_id, position}]` (the one
|
|
19288
|
+
* verb keyed on `call_id` rather than `member_id`). Verbs that act on the call as a
|
|
19289
|
+
* whole, or that the SDK does not wrap at all, take no target.
|
|
19290
|
+
*
|
|
19291
|
+
* For the typed alternative that handles all of this, create the client with
|
|
19292
|
+
* `callControl: 'in-dialog'` and use the ordinary `Call`/`Participant` methods.
|
|
19293
|
+
*
|
|
19294
|
+
* @internal Not part of the supported surface while the in-dialog transport is still
|
|
19295
|
+
* rolling out. `WebRTCCall` is exported from the package entry, so without this tag
|
|
19296
|
+
* TypeDoc publishes the method — and the example above — as public API.
|
|
19297
|
+
*
|
|
19298
|
+
* @param method - A `call.*` method name (e.g. `'call.mute'`).
|
|
19299
|
+
* @param params - Method parameters, sent verbatim.
|
|
19300
|
+
* @returns The method's own reply, unwrapped from the `verto.info` envelope.
|
|
19301
|
+
* @throws {JSONRPCError} If the control op fails.
|
|
19302
|
+
*/
|
|
19303
|
+
async sendCommand(method, params = {}) {
|
|
19304
|
+
return unwrapVertoReply(await this.vertoManager.sendCallControl(method, params));
|
|
19305
|
+
}
|
|
19306
|
+
/**
|
|
19307
|
+
* The local leg's member triple — sent as `self` in every member RPC
|
|
19308
|
+
* envelope, and as the `target` of call-scoped self-operations (e.g. lock,
|
|
19309
|
+
* layout).
|
|
19310
|
+
*
|
|
19311
|
+
* @throws {CallNotReadyError} Before `call.joined` delivers the self member
|
|
19312
|
+
* context (`selfId`/`nodeId`) — an RPC without it cannot be routed, so fail
|
|
19313
|
+
* fast instead of sending a doomed request.
|
|
19314
|
+
*/
|
|
19315
|
+
get callSelf() {
|
|
19316
|
+
const node_id = this.nodeId;
|
|
19317
|
+
const member_id = this.vertoManager.selfId;
|
|
19318
|
+
if (!node_id || !member_id) throw new CallNotReadyError(this.id);
|
|
19319
|
+
return {
|
|
19320
|
+
node_id,
|
|
19321
|
+
call_id: this.id,
|
|
19322
|
+
member_id
|
|
18551
19323
|
};
|
|
18552
19324
|
}
|
|
18553
19325
|
/** Observable of the current call status (e.g. `'ringing'`, `'connected'`). */
|
|
@@ -18703,13 +19475,13 @@ var WebRTCCall = class extends Destroyable {
|
|
|
18703
19475
|
get bandwidthConstrained$() {
|
|
18704
19476
|
return this.deferEmission(this._bandwidthConstrained$.asObservable());
|
|
18705
19477
|
}
|
|
18706
|
-
/** Observable that emits when server
|
|
19478
|
+
/** Observable that emits when the server pushes media params. */
|
|
18707
19479
|
get mediaParamsUpdated$() {
|
|
18708
19480
|
return this.deferEmission(this._mediaParamsUpdated$.asObservable());
|
|
18709
19481
|
}
|
|
18710
19482
|
/**
|
|
18711
19483
|
* @internal Emit a media params update event.
|
|
18712
|
-
* Called by the VertoManager when server
|
|
19484
|
+
* Called by the VertoManager when the server pushes media params.
|
|
18713
19485
|
*/
|
|
18714
19486
|
emitMediaParamsUpdated(event) {
|
|
18715
19487
|
this._mediaParamsUpdated$.next(event);
|
|
@@ -18917,6 +19689,10 @@ var WebRTCCall = class extends Destroyable {
|
|
|
18917
19689
|
get selfId$() {
|
|
18918
19690
|
return this.vertoManager.selfId$;
|
|
18919
19691
|
}
|
|
19692
|
+
/** @internal Lets call creation bound the media and signalling phases apart. */
|
|
19693
|
+
get localMediaSettled$() {
|
|
19694
|
+
return this.vertoManager.localMediaSettled$;
|
|
19695
|
+
}
|
|
18920
19696
|
/** Local participant's member ID, or `null` if not joined. */
|
|
18921
19697
|
get selfId() {
|
|
18922
19698
|
return this.vertoManager.selfId;
|
|
@@ -19110,12 +19886,17 @@ var WebRTCCall = class extends Destroyable {
|
|
|
19110
19886
|
*
|
|
19111
19887
|
* **These operations are NOT atomic.** The layout is applied first, then each
|
|
19112
19888
|
* member position sequentially, so members may briefly flash into their
|
|
19113
|
-
* default slots before being moved to the requested positions.
|
|
19889
|
+
* default slots before being moved to the requested positions. Targeted
|
|
19890
|
+
* members are validated upfront, though: when any of them has no
|
|
19891
|
+
* {@link Participant.target | member call context} yet, the whole call
|
|
19892
|
+
* rejects before any request is sent and the layout is left unchanged.
|
|
19114
19893
|
*
|
|
19115
19894
|
* @param layout - Layout name (must be one of {@link layouts}).
|
|
19116
19895
|
* @param positions - Optional map of member IDs to {@link VideoPosition} values.
|
|
19117
19896
|
* When omitted or empty, only the layout is changed.
|
|
19118
19897
|
* @throws {InvalidParams} If the layout is not in the available {@link layouts}.
|
|
19898
|
+
* @throws {ParticipantNotReadyError} If a targeted member's call context has
|
|
19899
|
+
* not been received yet — thrown before any request is sent.
|
|
19119
19900
|
*
|
|
19120
19901
|
* @example
|
|
19121
19902
|
* ```ts
|
|
@@ -19126,18 +19907,19 @@ var WebRTCCall = class extends Destroyable {
|
|
|
19126
19907
|
*/
|
|
19127
19908
|
async setLayout(layout, positions) {
|
|
19128
19909
|
if (!this.layouts.includes(layout)) throw new InvalidParams(`Layout ${layout} is not available in the current call layouts: ${this.layouts.join(", ")}`);
|
|
19129
|
-
const
|
|
19130
|
-
|
|
19131
|
-
const positionEntries = Object.entries(positions ?? {});
|
|
19132
|
-
if (positionEntries.length === 0) return;
|
|
19133
|
-
for (const [memberId, position] of positionEntries) {
|
|
19910
|
+
const targets = [];
|
|
19911
|
+
for (const [memberId, position] of Object.entries(positions ?? {})) {
|
|
19134
19912
|
const participant = this.participants.find((p) => p.id === memberId);
|
|
19135
19913
|
if (!participant) {
|
|
19136
19914
|
logger$12.warn(`[Call] setLayout: member ${memberId} not found in participants; skipping position ${position}`);
|
|
19137
19915
|
continue;
|
|
19138
19916
|
}
|
|
19139
|
-
|
|
19917
|
+
participant.target;
|
|
19918
|
+
targets.push([participant, position]);
|
|
19140
19919
|
}
|
|
19920
|
+
const selfId = await (0, import_cjs$11.firstValueFrom)(this.selfId$.pipe((0, import_cjs$11.filter)((id) => id !== null)));
|
|
19921
|
+
await this.executeMethod(selfId, "call.layout.set", { layout });
|
|
19922
|
+
for (const [participant, position] of targets) await participant.setPosition(position);
|
|
19141
19923
|
}
|
|
19142
19924
|
/**
|
|
19143
19925
|
* Transfers the call to another destination.
|
|
@@ -19239,17 +20021,28 @@ var WebRTCCall = class extends Destroyable {
|
|
|
19239
20021
|
* (notably iOS Safari) fall back to re-acquiring the track with the new
|
|
19240
20022
|
* constraint set and plumbing the replacement through the local audio
|
|
19241
20023
|
* pipeline if one is active.
|
|
20024
|
+
*
|
|
20025
|
+
* @returns whether the constraint reached the microphone. `false` is an
|
|
20026
|
+
* outcome rather than an error — a leg sending media the SDK did not capture
|
|
20027
|
+
* is left alone — so a UI that reflects the toggle must read it. Any failure
|
|
20028
|
+
* behind a `false` is also reported on {@link errors$}.
|
|
19242
20029
|
*/
|
|
19243
20030
|
async setEchoCancellation(enabled) {
|
|
19244
|
-
|
|
20031
|
+
return this.vertoManager.updateMediaConstraints({ audio: { echoCancellation: enabled } });
|
|
19245
20032
|
}
|
|
19246
|
-
/**
|
|
20033
|
+
/**
|
|
20034
|
+
* Toggle browser noise suppression on the local mic at runtime.
|
|
20035
|
+
* @returns whether the constraint reached the microphone.
|
|
20036
|
+
*/
|
|
19247
20037
|
async setNoiseSuppression(enabled) {
|
|
19248
|
-
|
|
20038
|
+
return this.vertoManager.updateMediaConstraints({ audio: { noiseSuppression: enabled } });
|
|
19249
20039
|
}
|
|
19250
|
-
/**
|
|
20040
|
+
/**
|
|
20041
|
+
* Toggle browser automatic gain control on the local mic at runtime.
|
|
20042
|
+
* @returns whether the constraint reached the microphone.
|
|
20043
|
+
*/
|
|
19251
20044
|
async setAutoGainControl(enabled) {
|
|
19252
|
-
|
|
20045
|
+
return this.vertoManager.updateMediaConstraints({ audio: { autoGainControl: enabled } });
|
|
19253
20046
|
}
|
|
19254
20047
|
/**
|
|
19255
20048
|
* Observable of the aggregate remote audio level, 0..1 RMS. The server
|
|
@@ -19306,6 +20099,9 @@ var WebRTCCall = class extends Destroyable {
|
|
|
19306
20099
|
/**
|
|
19307
20100
|
* Infers the semantic error category from a raw Error thrown by VertoManager
|
|
19308
20101
|
* or an RTCPeerConnection layer.
|
|
20102
|
+
*
|
|
20103
|
+
* Pure function — exported for unit testing.
|
|
20104
|
+
* @internal
|
|
19309
20105
|
*/
|
|
19310
20106
|
function inferCallErrorKind(error) {
|
|
19311
20107
|
if (error instanceof RPCTimeoutError) return "timeout";
|
|
@@ -19324,7 +20120,14 @@ const RECOVERABLE_RPC_CODES = new Set([
|
|
|
19324
20120
|
RPC_ERROR_AUTHENTICATION_FAILED,
|
|
19325
20121
|
RPC_ERROR_INVALID_PARAMS
|
|
19326
20122
|
]);
|
|
19327
|
-
/**
|
|
20123
|
+
/**
|
|
20124
|
+
* A *fallback*: callers knowing which leg failed pass an explicit `fatal` and
|
|
20125
|
+
* never reach here, so the default-fatal branch only sees call- and main-leg
|
|
20126
|
+
* errors. Auxiliary legs go through `WebRTCVertoManager.reportLegError`.
|
|
20127
|
+
*
|
|
20128
|
+
* Pure function — exported for unit testing.
|
|
20129
|
+
* @internal
|
|
20130
|
+
*/
|
|
19328
20131
|
function isFatalError(error) {
|
|
19329
20132
|
if (error instanceof VertoPongError) return false;
|
|
19330
20133
|
if (error instanceof MediaTrackError) return false;
|
|
@@ -19359,7 +20162,9 @@ var CallFactory = class {
|
|
|
19359
20162
|
kind: inferCallErrorKind(error),
|
|
19360
20163
|
fatal: options$1?.fatal ?? isFatalError(error),
|
|
19361
20164
|
error,
|
|
19362
|
-
callId: callInstance.id
|
|
20165
|
+
callId: callInstance.id,
|
|
20166
|
+
...options$1?.leg ? { leg: options$1.leg } : {},
|
|
20167
|
+
...options$1?.legId ? { legId: options$1.legId } : {}
|
|
19363
20168
|
};
|
|
19364
20169
|
callInstance.emitError(callError);
|
|
19365
20170
|
},
|
|
@@ -19831,6 +20636,30 @@ const logger$9 = getLogger();
|
|
|
19831
20636
|
function shouldAbortDial(callError) {
|
|
19832
20637
|
return callError.fatal || !(callError.error instanceof MediaAccessError);
|
|
19833
20638
|
}
|
|
20639
|
+
/**
|
|
20640
|
+
* Wait for a dialed call to be ready, or for the failure that stops it.
|
|
20641
|
+
*
|
|
20642
|
+
* Local media acquisition is deliberately unbounded: a permission prompt or a
|
|
20643
|
+
* device picker is human time, and `getUserMedia` cannot be cancelled anyway.
|
|
20644
|
+
* The clock starts only once acquisition settles, so a slow human never spends
|
|
20645
|
+
* the server's budget.
|
|
20646
|
+
*
|
|
20647
|
+
* `merge` rather than `race`, because the two legs settle asymmetrically. A
|
|
20648
|
+
* fatal acquisition failure reports the error and then destroys the call in the
|
|
20649
|
+
* same synchronous step; `errors$` defers delivery by a microtask while
|
|
20650
|
+
* `localMediaSettled$` completes immediately. Under `race` that bare completion
|
|
20651
|
+
* ended the wait first and `dial()` rejected with an RxJS `EmptyError`, burying
|
|
20652
|
+
* the `NotAllowedError` applications are told to inspect. Under `merge` the
|
|
20653
|
+
* completed leg is simply spent, and the queued error — enqueued before the
|
|
20654
|
+
* completion, so delivered before it — arrives to reject the wait. A dial
|
|
20655
|
+
* abandoned with no error at all still ends both legs, and the resulting
|
|
20656
|
+
* `EmptyError` remains the benign-cancel signal.
|
|
20657
|
+
*
|
|
20658
|
+
* Exported for unit testing.
|
|
20659
|
+
*/
|
|
20660
|
+
async function awaitDialReady(session, signalingTimeoutMs) {
|
|
20661
|
+
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)))));
|
|
20662
|
+
}
|
|
19834
20663
|
const getAddressSearchURI = (options) => {
|
|
19835
20664
|
const to = options.to?.split("?")[0];
|
|
19836
20665
|
const from$9 = options.from?.startsWith("subscriber://") ? options.from.replace("subscriber://", "") : options.from;
|
|
@@ -19847,7 +20676,6 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
19847
20676
|
this.authorizationStateKey = authorizationStateKey;
|
|
19848
20677
|
this.attachManager = attachManager;
|
|
19849
20678
|
this.dpopManager = dpopManager;
|
|
19850
|
-
this.callCreateTimeout = 6e3;
|
|
19851
20679
|
this.agent = `signalwire-js/4.0.0`;
|
|
19852
20680
|
this.eventAcks = true;
|
|
19853
20681
|
this.authorizationState$ = this.createReplaySubject(1);
|
|
@@ -19856,6 +20684,7 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
19856
20684
|
minor: 0,
|
|
19857
20685
|
revision: 0
|
|
19858
20686
|
};
|
|
20687
|
+
this.callControl = "routed";
|
|
19859
20688
|
this._authorization$ = this.createBehaviorSubject(void 0);
|
|
19860
20689
|
this._errors$ = this.createReplaySubject(1);
|
|
19861
20690
|
this._authState$ = this.createBehaviorSubject({ kind: "unauthenticated" });
|
|
@@ -20055,22 +20884,23 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
20055
20884
|
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 })));
|
|
20056
20885
|
}
|
|
20057
20886
|
async handleAuthenticationError(error) {
|
|
20887
|
+
if (error instanceof TransportConnectionError) {
|
|
20888
|
+
logger$9.debug("[Session] Socket closed during authentication, waiting for reconnect");
|
|
20889
|
+
return;
|
|
20890
|
+
}
|
|
20058
20891
|
logger$9.error("Authentication error:", error);
|
|
20059
|
-
const isRecoverableAuthError = error instanceof JSONRPCError && (error.code === RPC_ERROR_REQUESTER_VALIDATION_FAILED || error.code === RPC_ERROR_INVALID_PARAMS || error.code === RPC_ERROR_AUTHENTICATION_FAILED);
|
|
20892
|
+
const isRecoverableAuthError$1 = error instanceof JSONRPCError && (error.code === RPC_ERROR_REQUESTER_VALIDATION_FAILED || error.code === RPC_ERROR_INVALID_PARAMS || error.code === RPC_ERROR_AUTHENTICATION_FAILED);
|
|
20060
20893
|
const hasStoredState = await (0, import_cjs$7.firstValueFrom)(this.authorizationState$.pipe((0, import_cjs$7.take)(1))) !== void 0;
|
|
20061
|
-
if (isRecoverableAuthError && hasStoredState) {
|
|
20894
|
+
if (isRecoverableAuthError$1 && hasStoredState) {
|
|
20062
20895
|
logger$9.debug("[Session] Recoverable auth error — cleaning up stored state and reconnecting fresh");
|
|
20063
|
-
|
|
20064
|
-
await this.cleanupStoredConnectionParams();
|
|
20065
|
-
} catch (cleanupError) {
|
|
20066
|
-
logger$9.error("Failed to cleanup stored connection params:", cleanupError);
|
|
20067
|
-
} finally {
|
|
20068
|
-
this.transport.reconnect();
|
|
20069
|
-
}
|
|
20896
|
+
await this.discardResumeStateAndReconnect();
|
|
20070
20897
|
} else this._errors$.next(error);
|
|
20071
20898
|
}
|
|
20072
20899
|
/**
|
|
20073
|
-
* Clear the resume state (authorization_state + protocol)
|
|
20900
|
+
* Clear the resume state (authorization_state + protocol) and ask the
|
|
20901
|
+
* transport to reconnect. The `connected` event re-triggers
|
|
20902
|
+
* `authenticate()`, which now has no stored state and so performs a fresh
|
|
20903
|
+
* connect.
|
|
20074
20904
|
*
|
|
20075
20905
|
* This is the stale-auth-state recovery helper used by handleAuthError:
|
|
20076
20906
|
* the server rejected a reconnect, so the resume state is discarded and a
|
|
@@ -20078,9 +20908,24 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
20078
20908
|
* session lives on through the reconnect and reattachCalls() needs the
|
|
20079
20909
|
* stored call references afterwards. Do NOT add detachAll() here.
|
|
20080
20910
|
*
|
|
20911
|
+
* Connect-time recovery only. A *request* refused on an already
|
|
20912
|
+
* authenticated session is never healed here: dropping the resume state
|
|
20913
|
+
* destroys the association between the socket and the previous session,
|
|
20914
|
+
* which is what reattach depends on. That path mints a fresh credential and
|
|
20915
|
+
* reauthenticates instead (see `SignalWire.recoverAndRetry`).
|
|
20916
|
+
*
|
|
20081
20917
|
* For public teardown (disconnect/destroy), use {@link teardownSessionState}
|
|
20082
20918
|
* instead, which clears the attach records as well.
|
|
20083
20919
|
*/
|
|
20920
|
+
async discardResumeStateAndReconnect() {
|
|
20921
|
+
try {
|
|
20922
|
+
await this.cleanupStoredConnectionParams();
|
|
20923
|
+
} catch (cleanupError) {
|
|
20924
|
+
logger$9.error("Failed to cleanup stored connection params:", cleanupError);
|
|
20925
|
+
} finally {
|
|
20926
|
+
this.transport.reconnect();
|
|
20927
|
+
}
|
|
20928
|
+
}
|
|
20084
20929
|
async cleanupStoredConnectionParams() {
|
|
20085
20930
|
await this.transport.setProtocol(void 0);
|
|
20086
20931
|
await this.updateAuthorizationStateInStorage(void 0);
|
|
@@ -20154,11 +20999,15 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
20154
20999
|
const isReconnect = hasReconnectState && storedToken;
|
|
20155
21000
|
let dpopToken;
|
|
20156
21001
|
if (isReconnect) logger$9.debug("[Session] Reconnecting with stored jwt_token + authorization_state");
|
|
20157
|
-
else
|
|
20158
|
-
|
|
20159
|
-
|
|
21002
|
+
else {
|
|
21003
|
+
const credential = this.getCredential();
|
|
21004
|
+
const credentialExpired = credential.expiry_at !== void 0 && credential.expiry_at <= Date.now() + CREDENTIAL_EXPIRY_SKEW_MS;
|
|
21005
|
+
if (this.onBeforeReconnect && (this.clientBound || credentialExpired)) {
|
|
21006
|
+
logger$9.debug("[Session] Refreshing credentials before fresh connect");
|
|
21007
|
+
await this.onBeforeReconnect();
|
|
21008
|
+
}
|
|
20160
21009
|
}
|
|
20161
|
-
if (
|
|
21010
|
+
if (this.dpopManager?.initialized) try {
|
|
20162
21011
|
dpopToken = await this.dpopManager.createRpcProof({ method: "signalwire.connect" });
|
|
20163
21012
|
} catch (error) {
|
|
20164
21013
|
if (this.clientBound) throw error;
|
|
@@ -20178,7 +21027,9 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
20178
21027
|
protocol: persistedParams.protocol
|
|
20179
21028
|
} : {}
|
|
20180
21029
|
});
|
|
20181
|
-
|
|
21030
|
+
if (this.transport.connectionStatus !== "connected") throw new TransportConnectionError("WebSocket closed during authentication");
|
|
21031
|
+
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"))));
|
|
21032
|
+
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)(() => {
|
|
20182
21033
|
logger$9.debug("[Session] Response passed filter, processing authentication result");
|
|
20183
21034
|
}), (0, import_cjs$7.take)(1), (0, import_cjs$7.catchError)((err) => {
|
|
20184
21035
|
logger$9.error("[Session] Authentication RPC failed:", err);
|
|
@@ -20191,7 +21042,9 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
20191
21042
|
});
|
|
20192
21043
|
if (response.protocol) await this.transport.setProtocol(response.protocol);
|
|
20193
21044
|
this._authorization$.next(response.authorization);
|
|
21045
|
+
if (response.authorization.cnf?.jkt) this._wasClientBound = true;
|
|
20194
21046
|
this._iceServers$.next(response.ice_servers ?? []);
|
|
21047
|
+
this.transport.setAuthenticated();
|
|
20195
21048
|
this._authState$.next({ kind: "authenticated" });
|
|
20196
21049
|
logger$9.debug("[Session] Authentication completed successfully");
|
|
20197
21050
|
}
|
|
@@ -20257,7 +21110,7 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
20257
21110
|
to: destinationURI,
|
|
20258
21111
|
...options
|
|
20259
21112
|
});
|
|
20260
|
-
await (
|
|
21113
|
+
await awaitDialReady(callSession, DEFAULT_CALL_SIGNALING_TIMEOUT_MS);
|
|
20261
21114
|
this._calls$.next({
|
|
20262
21115
|
[`${callSession.id}`]: callSession,
|
|
20263
21116
|
...this._calls$.value
|
|
@@ -20310,12 +21163,23 @@ var ClientSessionWrapper = class {
|
|
|
20310
21163
|
get authenticated() {
|
|
20311
21164
|
return this.clientSessionManager.authenticated;
|
|
20312
21165
|
}
|
|
21166
|
+
/**
|
|
21167
|
+
* Whether the session is using a Client Bound SAT (DPoP). Sticky — set
|
|
21168
|
+
* when the binding is established or restored from a resumed session's
|
|
21169
|
+
* server authorization.
|
|
21170
|
+
*/
|
|
21171
|
+
get clientBound() {
|
|
21172
|
+
return this.clientSessionManager.clientBound;
|
|
21173
|
+
}
|
|
20313
21174
|
get signalingEvent$() {
|
|
20314
21175
|
return this.clientSessionManager.signalingEvent$;
|
|
20315
21176
|
}
|
|
20316
21177
|
get iceServers() {
|
|
20317
21178
|
return this.clientSessionManager.iceServers;
|
|
20318
21179
|
}
|
|
21180
|
+
get callControl() {
|
|
21181
|
+
return this.clientSessionManager.callControl;
|
|
21182
|
+
}
|
|
20319
21183
|
async execute(request, options) {
|
|
20320
21184
|
return this.clientSessionManager.execute(request, options);
|
|
20321
21185
|
}
|
|
@@ -20512,7 +21376,7 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
20512
21376
|
await session.reauthenticate(tokenData.token, rpcProof, { clientBound: true });
|
|
20513
21377
|
updateCredential({ token: tokenData.token });
|
|
20514
21378
|
logger$7.info("[DeviceToken] Client Bound SAT activated successfully");
|
|
20515
|
-
this.
|
|
21379
|
+
this.emitCurrentToken(tokenData);
|
|
20516
21380
|
return { activated: true };
|
|
20517
21381
|
} catch (error) {
|
|
20518
21382
|
logger$7.error("[DeviceToken] Failed to activate Client Bound SAT:", error);
|
|
@@ -20524,6 +21388,21 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
20524
21388
|
}
|
|
20525
21389
|
}
|
|
20526
21390
|
/**
|
|
21391
|
+
* Emit a freshly received token to the reactive pipeline, stamping an
|
|
21392
|
+
* absolute `expires_at` when the response carried only `expires_in`.
|
|
21393
|
+
* Resolving the expiry at RECEIVE time (not at read time) is what lets
|
|
21394
|
+
* {@link refreshNowIfDue} detect due-ness on resume: a bare `expires_in`
|
|
21395
|
+
* re-resolved later would always compute a full TTL from "now" and never
|
|
21396
|
+
* cross the refresh buffer.
|
|
21397
|
+
*/
|
|
21398
|
+
emitCurrentToken(token) {
|
|
21399
|
+
const stamped = token.expires_at ? token : {
|
|
21400
|
+
...token,
|
|
21401
|
+
expires_at: resolveExpiresAt(token)
|
|
21402
|
+
};
|
|
21403
|
+
this._currentToken$.next(stamped);
|
|
21404
|
+
}
|
|
21405
|
+
/**
|
|
20527
21406
|
* Returns true when the cached token has enough headroom before expiry to
|
|
20528
21407
|
* be safely reused on reactivation. The headroom matches the refresh
|
|
20529
21408
|
* buffer, so a token within the refresh window is treated as stale (the
|
|
@@ -20541,7 +21420,7 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
20541
21420
|
method: "POST",
|
|
20542
21421
|
uri: DEVICE_TOKEN_ENDPOINT
|
|
20543
21422
|
});
|
|
20544
|
-
const response = await this.http.request({
|
|
21423
|
+
const response = await this.http().request({
|
|
20545
21424
|
url: DEVICE_TOKEN_ENDPOINT,
|
|
20546
21425
|
...POST_PARAMS,
|
|
20547
21426
|
body: JSON.stringify({
|
|
@@ -20568,7 +21447,7 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
20568
21447
|
uri: DEVICE_REFRESH_ENDPOINT,
|
|
20569
21448
|
accessToken: currentToken
|
|
20570
21449
|
});
|
|
20571
|
-
const response = await this.http.request({
|
|
21450
|
+
const response = await this.http().request({
|
|
20572
21451
|
url: DEVICE_REFRESH_ENDPOINT,
|
|
20573
21452
|
...POST_PARAMS,
|
|
20574
21453
|
body: JSON.stringify({
|
|
@@ -20616,7 +21495,7 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
20616
21495
|
const currentToken = this.getCredential().token;
|
|
20617
21496
|
if (!currentToken) throw new TokenRefreshError("No current token available for refresh");
|
|
20618
21497
|
const newTokenData = await this.retryRefresh(session, currentToken, updateCredential);
|
|
20619
|
-
this.
|
|
21498
|
+
this.emitCurrentToken(newTokenData);
|
|
20620
21499
|
} catch (error) {
|
|
20621
21500
|
logger$7.error("[DeviceToken] Automatic Client Bound SAT refresh failed:", error);
|
|
20622
21501
|
this.errorHandler(error instanceof TokenRefreshError ? error : new TokenRefreshError("Automatic token refresh failed", error));
|
|
@@ -20643,6 +21522,22 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
20643
21522
|
throw lastError instanceof Error ? lastError : new TokenRefreshError("All refresh retries exhausted", lastError);
|
|
20644
21523
|
}
|
|
20645
21524
|
/**
|
|
21525
|
+
* Force an immediate refresh when the cached Client Bound SAT is already
|
|
21526
|
+
* past its refresh window. Called on resume from suspension where
|
|
21527
|
+
* background-tab throttling can delay the reactive timer past the buffer.
|
|
21528
|
+
* A no-op when no token is cached or it still has headroom; the normal
|
|
21529
|
+
* {@link executeRefresh} guards (paused / in-progress / unauthenticated)
|
|
21530
|
+
* still apply.
|
|
21531
|
+
*/
|
|
21532
|
+
refreshNowIfDue() {
|
|
21533
|
+
const token = this._currentToken$.value;
|
|
21534
|
+
if (!token) return;
|
|
21535
|
+
if (resolveExpiresAt(token) * 1e3 - Date.now() <= DEVICE_TOKEN_REFRESH_BUFFER_MS) {
|
|
21536
|
+
logger$7.debug("[DeviceToken] Resume: cached SAT past refresh window; refreshing now");
|
|
21537
|
+
this.executeRefresh();
|
|
21538
|
+
}
|
|
21539
|
+
}
|
|
21540
|
+
/**
|
|
20646
21541
|
* Stops the reactive refresh pipeline from firing. Use when the underlying
|
|
20647
21542
|
* session is being torn down (e.g., during {@link SignalWire.disconnect})
|
|
20648
21543
|
* so a scheduled refresh cannot fire against a destroyed session.
|
|
@@ -20693,6 +21588,7 @@ var CredentialRefreshCoordinator = class extends Destroyable {
|
|
|
20693
21588
|
this.deps = deps;
|
|
20694
21589
|
this._activating = false;
|
|
20695
21590
|
this._activationGeneration = 0;
|
|
21591
|
+
this._developerRefreshInProgress = false;
|
|
20696
21592
|
if (dpopManager?.initialized) this._deviceTokenManager = (deps.deviceTokenManagerFactory ?? defaultDeviceTokenManagerFactory)(dpopManager, deps.http, (error) => deps.notifier.onError(error), () => deps.store.read());
|
|
20697
21593
|
}
|
|
20698
21594
|
/** True when the Client Bound SAT path is available (DPoP initialized). */
|
|
@@ -20711,28 +21607,120 @@ var CredentialRefreshCoordinator = class extends Destroyable {
|
|
|
20711
21607
|
* invokes `deps.onRefreshExhausted` so the orchestrator can disconnect.
|
|
20712
21608
|
*/
|
|
20713
21609
|
scheduleDeveloperRefresh(provider, expiresAt, attempt = 0) {
|
|
21610
|
+
this._activeProvider = provider;
|
|
20714
21611
|
if (this._developerTimerId !== void 0) clearTimeout(this._developerTimerId);
|
|
20715
21612
|
const refreshInterval = attempt === 0 ? Math.max(expiresAt - Date.now() - CREDENTIAL_REFRESH_BUFFER_MS, 1e3) : Math.min(CREDENTIAL_REFRESH_RETRY_BASE_MS * Math.pow(2, attempt) * (.5 + Math.random() * .5), CREDENTIAL_REFRESH_MAX_DELAY_MS);
|
|
20716
|
-
this._developerTimerId = setTimeout(
|
|
21613
|
+
this._developerTimerId = setTimeout(() => {
|
|
21614
|
+
this._developerTimerId = void 0;
|
|
21615
|
+
this.executeDeveloperRefresh(provider, expiresAt, attempt);
|
|
21616
|
+
}, refreshInterval);
|
|
21617
|
+
}
|
|
21618
|
+
/**
|
|
21619
|
+
* Runs the developer-provided refresh once: mints a new credential, stores
|
|
21620
|
+
* and persists it, reauthenticates the live session (via the notifier), and
|
|
21621
|
+
* reschedules against the new expiry. On failure retries with backoff up to
|
|
21622
|
+
* {@link CREDENTIAL_REFRESH_MAX_RETRIES}, then signals exhaustion.
|
|
21623
|
+
*
|
|
21624
|
+
* Shared by the scheduled timer tick and {@link forceRefreshIfDue}. The
|
|
21625
|
+
* `_developerRefreshInProgress` guard prevents the two from overlapping.
|
|
21626
|
+
*/
|
|
21627
|
+
async executeDeveloperRefresh(provider, expiresAt, attempt) {
|
|
21628
|
+
if (this._developerRefreshInProgress) {
|
|
21629
|
+
logger$6.debug("[Coordinator] Developer refresh already in progress; skipping");
|
|
21630
|
+
return;
|
|
21631
|
+
}
|
|
21632
|
+
this._developerRefreshInProgress = true;
|
|
21633
|
+
try {
|
|
21634
|
+
const newCredentials = await this.refreshCredential(provider);
|
|
21635
|
+
this.deps.store.write(newCredentials);
|
|
21636
|
+
this.deps.store.persist(newCredentials);
|
|
20717
21637
|
try {
|
|
20718
|
-
|
|
20719
|
-
|
|
20720
|
-
|
|
20721
|
-
this.deps.store.persist(newCredentials);
|
|
20722
|
-
logger$6.info("[Coordinator] Credentials refreshed successfully.");
|
|
20723
|
-
if (newCredentials.expiry_at) this.scheduleDeveloperRefresh(provider, newCredentials.expiry_at, 0);
|
|
20724
|
-
} catch (error) {
|
|
20725
|
-
const nextAttempt = attempt + 1;
|
|
20726
|
-
logger$6.error(`[Coordinator] Credential refresh failed (attempt ${nextAttempt}/${CREDENTIAL_REFRESH_MAX_RETRIES}):`, error);
|
|
20727
|
-
this.deps.notifier.onError(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
20728
|
-
if (nextAttempt < CREDENTIAL_REFRESH_MAX_RETRIES) this.scheduleDeveloperRefresh(provider, expiresAt, nextAttempt);
|
|
20729
|
-
else {
|
|
20730
|
-
logger$6.error("[Coordinator] Credential refresh exhausted all retries. Disconnecting.");
|
|
20731
|
-
this.deps.notifier.onError(new TokenRefreshError("Credential refresh failed after max retries"));
|
|
20732
|
-
this.deps.notifier.onRefreshExhausted();
|
|
20733
|
-
}
|
|
21638
|
+
await this.deps.notifier.onCredentialRefreshed(newCredentials);
|
|
21639
|
+
} catch (reauthError) {
|
|
21640
|
+
logger$6.warn("[Coordinator] onCredentialRefreshed rejected (non-fatal):", reauthError);
|
|
20734
21641
|
}
|
|
20735
|
-
|
|
21642
|
+
logger$6.info("[Coordinator] Credentials refreshed successfully.");
|
|
21643
|
+
if (newCredentials.expiry_at) this.scheduleDeveloperRefresh(provider, newCredentials.expiry_at, 0);
|
|
21644
|
+
} catch (error) {
|
|
21645
|
+
const nextAttempt = attempt + 1;
|
|
21646
|
+
logger$6.error(`[Coordinator] Credential refresh failed (attempt ${nextAttempt}/${CREDENTIAL_REFRESH_MAX_RETRIES}):`, error);
|
|
21647
|
+
this.deps.notifier.onError(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
21648
|
+
if (nextAttempt < CREDENTIAL_REFRESH_MAX_RETRIES) this.scheduleDeveloperRefresh(provider, expiresAt, nextAttempt);
|
|
21649
|
+
else {
|
|
21650
|
+
logger$6.error("[Coordinator] Credential refresh exhausted all retries. Disconnecting.");
|
|
21651
|
+
this.deps.notifier.onError(new TokenRefreshError("Credential refresh failed after max retries"));
|
|
21652
|
+
this.deps.notifier.onRefreshExhausted();
|
|
21653
|
+
}
|
|
21654
|
+
} finally {
|
|
21655
|
+
this._developerRefreshInProgress = false;
|
|
21656
|
+
}
|
|
21657
|
+
}
|
|
21658
|
+
/**
|
|
21659
|
+
* Force an immediate refresh when the current credential is already past its
|
|
21660
|
+
* scheduled refresh window. Called on resume from suspension, where
|
|
21661
|
+
* background-tab timer throttling can delay the armed refresh well past
|
|
21662
|
+
* expiry, leaving the live session stale.
|
|
21663
|
+
*
|
|
21664
|
+
* Routes to whichever mechanism is armed: the developer timer if armed,
|
|
21665
|
+
* otherwise the Client Bound SAT pipeline. A no-op when nothing is due.
|
|
21666
|
+
*/
|
|
21667
|
+
forceRefreshIfDue() {
|
|
21668
|
+
if (this._developerTimerId !== void 0 && this._activeProvider) {
|
|
21669
|
+
const expiry = this.deps.store.read().expiry_at;
|
|
21670
|
+
if (expiry !== void 0 && Date.now() >= expiry - CREDENTIAL_REFRESH_BUFFER_MS) {
|
|
21671
|
+
logger$6.debug("[Coordinator] Resume: credential past refresh window; forcing refresh");
|
|
21672
|
+
clearTimeout(this._developerTimerId);
|
|
21673
|
+
this._developerTimerId = void 0;
|
|
21674
|
+
this.executeDeveloperRefresh(this._activeProvider, expiry, 0);
|
|
21675
|
+
}
|
|
21676
|
+
return;
|
|
21677
|
+
}
|
|
21678
|
+
this._deviceTokenManager?.refreshNowIfDue();
|
|
21679
|
+
}
|
|
21680
|
+
/**
|
|
21681
|
+
* Sync the credential's expiry from the server-provided authorization (the
|
|
21682
|
+
* `signalwire.connect` result). SATs are opaque JWE, so
|
|
21683
|
+
* `fabric_subscriber.expires_at` is the authoritative expiry of the token
|
|
21684
|
+
* the session actually connected with — the provider-reported `expiry_at`
|
|
21685
|
+
* is only a hint (and may be wrong or absent). Corrects the stored
|
|
21686
|
+
* credential and re-arms the developer refresh timer against the real
|
|
21687
|
+
* deadline when the provider supports `refresh()`.
|
|
21688
|
+
*/
|
|
21689
|
+
syncExpiryFromAuthorization(authorization, provider) {
|
|
21690
|
+
const expiresAtSec = authorization?.fabric_subscriber?.expires_at;
|
|
21691
|
+
if (!expiresAtSec) return;
|
|
21692
|
+
const expiryAt = expiresAtSec * 1e3;
|
|
21693
|
+
const credential = this.deps.store.read();
|
|
21694
|
+
if (credential.expiry_at === expiryAt) return;
|
|
21695
|
+
logger$6.debug(`[Coordinator] Correcting credential expiry from server authorization: ${new Date(expiryAt).toISOString()}`);
|
|
21696
|
+
const updated = {
|
|
21697
|
+
...credential,
|
|
21698
|
+
expiry_at: expiryAt
|
|
21699
|
+
};
|
|
21700
|
+
this.deps.store.write(updated);
|
|
21701
|
+
this.deps.store.persist(updated);
|
|
21702
|
+
if (provider?.refresh) this.scheduleDeveloperRefresh(provider, expiryAt);
|
|
21703
|
+
}
|
|
21704
|
+
/**
|
|
21705
|
+
* Invoke `provider.refresh()` deduped against any concurrent developer
|
|
21706
|
+
* refresh. Concurrent callers — the scheduled tick, a resume-forced refresh,
|
|
21707
|
+
* and the orchestrator's -32003 recovery / reconnect re-mint — share one
|
|
21708
|
+
* in-flight promise, so a provider backed by one-time-use rotating refresh
|
|
21709
|
+
* tokens is never invoked twice in parallel.
|
|
21710
|
+
*
|
|
21711
|
+
* The caller owns applying the returned credential (store write, session
|
|
21712
|
+
* reauth, rescheduling); this method only serializes the network call.
|
|
21713
|
+
*/
|
|
21714
|
+
async refreshCredential(provider) {
|
|
21715
|
+
if (this._refreshInFlight) return this._refreshInFlight;
|
|
21716
|
+
if (!provider.refresh) throw new InvalidCredentialsError("Credential provider does not support refresh");
|
|
21717
|
+
const run = provider.refresh();
|
|
21718
|
+
this._refreshInFlight = run;
|
|
21719
|
+
const clear = () => {
|
|
21720
|
+
if (this._refreshInFlight === run) this._refreshInFlight = void 0;
|
|
21721
|
+
};
|
|
21722
|
+
run.then(clear, clear);
|
|
21723
|
+
return run;
|
|
20736
21724
|
}
|
|
20737
21725
|
/**
|
|
20738
21726
|
* Cancels any scheduled developer-provided refresh. Idempotent.
|
|
@@ -21253,6 +22241,7 @@ var WebSocketController = class WebSocketController extends Destroyable {
|
|
|
21253
22241
|
}
|
|
21254
22242
|
handleClose(_event) {
|
|
21255
22243
|
this.clearConnectionTimeout();
|
|
22244
|
+
this.messageQueue = [];
|
|
21256
22245
|
if (this.shouldReconnect) {
|
|
21257
22246
|
this._status$.next("reconnecting");
|
|
21258
22247
|
this.scheduleReconnection();
|
|
@@ -21322,6 +22311,9 @@ var WebSocketController = class WebSocketController extends Destroyable {
|
|
|
21322
22311
|
function isSignalwirePingRequest(value) {
|
|
21323
22312
|
return isJSONRPCRequest(value) && value.method === "signalwire.ping";
|
|
21324
22313
|
}
|
|
22314
|
+
function isSignalwireConnectRequest(value) {
|
|
22315
|
+
return isJSONRPCRequest(value) && value.method === "signalwire.connect";
|
|
22316
|
+
}
|
|
21325
22317
|
|
|
21326
22318
|
//#endregion
|
|
21327
22319
|
//#region src/managers/TransportManager.ts
|
|
@@ -21336,6 +22328,10 @@ var TransportManager = class extends Destroyable {
|
|
|
21336
22328
|
this.protocol$ = this.createReplaySubject(1);
|
|
21337
22329
|
this.isConnecting = false;
|
|
21338
22330
|
this.isConnected = false;
|
|
22331
|
+
this.socketStatus = "disconnected";
|
|
22332
|
+
this.sessionAuthenticated = false;
|
|
22333
|
+
this.heldRequests = [];
|
|
22334
|
+
this.serverPingWatch = 0;
|
|
21339
22335
|
this.ackEvent = () => {
|
|
21340
22336
|
return (0, import_cjs$2.tap)((message) => {
|
|
21341
22337
|
if (isSignalwireRequest(message)) try {
|
|
@@ -21351,6 +22347,7 @@ var TransportManager = class extends Destroyable {
|
|
|
21351
22347
|
if (isSignalwirePingRequest(message)) {
|
|
21352
22348
|
try {
|
|
21353
22349
|
logger$2.debug("[Transport] Received ping, sending pong", { pingId: message.id });
|
|
22350
|
+
if (this.sessionAuthenticated) this.armServerPingWatchdog();
|
|
21354
22351
|
this.send(RPCPingResponse(message.id));
|
|
21355
22352
|
} catch (error) {
|
|
21356
22353
|
logger$2.error("[Transport] Failed to send ping response:", error);
|
|
@@ -21388,6 +22385,14 @@ var TransportManager = class extends Destroyable {
|
|
|
21388
22385
|
this.subscribeTo(this._webSocketConnections.errors$, (error) => {
|
|
21389
22386
|
this.onError?.(error);
|
|
21390
22387
|
});
|
|
22388
|
+
this.subscribeTo(this._webSocketConnections.status$, (status) => {
|
|
22389
|
+
this.socketStatus = status;
|
|
22390
|
+
if (status !== "connected") {
|
|
22391
|
+
this.sessionAuthenticated = false;
|
|
22392
|
+
this.clearServerPingWatchdog();
|
|
22393
|
+
}
|
|
22394
|
+
if (status === "disconnected") this.heldRequests = [];
|
|
22395
|
+
});
|
|
21391
22396
|
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$));
|
|
21392
22397
|
this._jsonRPCMessage$ = this._webSocketConnections.incomingMessages$.pipe((0, import_cjs$2.map)((event) => {
|
|
21393
22398
|
try {
|
|
@@ -21416,6 +22421,9 @@ var TransportManager = class extends Destroyable {
|
|
|
21416
22421
|
get connectionStatus$() {
|
|
21417
22422
|
return this._webSocketConnections.status$;
|
|
21418
22423
|
}
|
|
22424
|
+
get connectionStatus() {
|
|
22425
|
+
return this.socketStatus;
|
|
22426
|
+
}
|
|
21419
22427
|
async connect() {
|
|
21420
22428
|
if (this.isConnecting || this.isConnected) {
|
|
21421
22429
|
logger$2.warn("[Transport] Already connecting or connected");
|
|
@@ -21460,16 +22468,45 @@ var TransportManager = class extends Destroyable {
|
|
|
21460
22468
|
}
|
|
21461
22469
|
async execute(request, options) {
|
|
21462
22470
|
this.send(request);
|
|
21463
|
-
|
|
22471
|
+
const pending = new PendingRPC(request, this._jsonRPCResponse$, options).promise;
|
|
22472
|
+
pending.catch(() => {
|
|
22473
|
+
this.heldRequests = this.heldRequests.filter((held) => held.id !== request.id);
|
|
22474
|
+
});
|
|
22475
|
+
return pending;
|
|
21464
22476
|
}
|
|
21465
22477
|
send(message) {
|
|
21466
22478
|
const payload = JSON.stringify(message);
|
|
22479
|
+
if (isJSONRPCResponse(message)) {
|
|
22480
|
+
if (this.socketStatus !== "connected") {
|
|
22481
|
+
logger$2.debug("[Transport] Dropping response while disconnected", { id: message.id });
|
|
22482
|
+
return;
|
|
22483
|
+
}
|
|
22484
|
+
} else if (!this.sessionAuthenticated && !isSignalwireConnectRequest(message)) {
|
|
22485
|
+
this.heldRequests.push({
|
|
22486
|
+
id: message.id,
|
|
22487
|
+
payload
|
|
22488
|
+
});
|
|
22489
|
+
return;
|
|
22490
|
+
}
|
|
21467
22491
|
this._outgoingMessages$.next(payload);
|
|
21468
22492
|
}
|
|
22493
|
+
/**
|
|
22494
|
+
* Called by the session when signalwire.connect succeeds on the current
|
|
22495
|
+
* socket. Sends the requests held since the socket opened, in order.
|
|
22496
|
+
*/
|
|
22497
|
+
setAuthenticated() {
|
|
22498
|
+
if (this.socketStatus !== "connected") return;
|
|
22499
|
+
this.sessionAuthenticated = true;
|
|
22500
|
+
this.armServerPingWatchdog();
|
|
22501
|
+
const held = this.heldRequests;
|
|
22502
|
+
this.heldRequests = [];
|
|
22503
|
+
held.forEach(({ payload }) => this._outgoingMessages$.next(payload));
|
|
22504
|
+
}
|
|
21469
22505
|
disconnect() {
|
|
21470
22506
|
logger$2.debug("[Transport] Disconnecting");
|
|
21471
22507
|
this.isConnected = false;
|
|
21472
22508
|
this.isConnecting = false;
|
|
22509
|
+
this.clearServerPingWatchdog();
|
|
21473
22510
|
this._webSocketConnections.disconnect();
|
|
21474
22511
|
}
|
|
21475
22512
|
destroy() {
|
|
@@ -21478,6 +22515,28 @@ var TransportManager = class extends Destroyable {
|
|
|
21478
22515
|
super.destroy();
|
|
21479
22516
|
this._webSocketConnections.destroy();
|
|
21480
22517
|
}
|
|
22518
|
+
armServerPingWatchdog() {
|
|
22519
|
+
this.clearServerPingWatchdog();
|
|
22520
|
+
const watch = this.serverPingWatch;
|
|
22521
|
+
this.serverPingTimer = setTimeout(() => void this.probeServer(watch), SERVER_PING_TIMEOUT_MS);
|
|
22522
|
+
}
|
|
22523
|
+
/** Bumps the watch so a probe still in flight no longer acts on its result. */
|
|
22524
|
+
clearServerPingWatchdog() {
|
|
22525
|
+
clearTimeout(this.serverPingTimer);
|
|
22526
|
+
this.serverPingTimer = void 0;
|
|
22527
|
+
this.serverPingWatch += 1;
|
|
22528
|
+
}
|
|
22529
|
+
async probeServer(watch) {
|
|
22530
|
+
logger$2.debug(`[Transport] No signalwire.ping for ${SERVER_PING_TIMEOUT_MS}ms, probing the server`);
|
|
22531
|
+
try {
|
|
22532
|
+
await this.execute(RPCPing(), { timeoutMs: SERVER_PING_PROBE_TIMEOUT_MS });
|
|
22533
|
+
if (watch === this.serverPingWatch) this.armServerPingWatchdog();
|
|
22534
|
+
} catch (error) {
|
|
22535
|
+
if (watch !== this.serverPingWatch) return;
|
|
22536
|
+
logger$2.warn("[Transport] Server did not answer the ping probe, replacing the socket", error);
|
|
22537
|
+
this.reconnect();
|
|
22538
|
+
}
|
|
22539
|
+
}
|
|
21481
22540
|
async _loadProtocolFromStorage() {
|
|
21482
22541
|
try {
|
|
21483
22542
|
const storedProtocol = await this.storage.getItem(this.protocolKey);
|
|
@@ -21516,6 +22575,13 @@ var TransportManager = class extends Destroyable {
|
|
|
21516
22575
|
//#region src/clients/SignalWire.ts
|
|
21517
22576
|
var import_cjs$1 = require_cjs();
|
|
21518
22577
|
const logger$1 = getLogger();
|
|
22578
|
+
/**
|
|
22579
|
+
* Storage key for the client-bound marker. The SAT and authorization_state are
|
|
22580
|
+
* both opaque to the SDK, so on a page reload the preflight recovery — which
|
|
22581
|
+
* runs before any session exists — has no other way to know the session was
|
|
22582
|
+
* client-bound. See {@link SignalWire.persistClientBoundMarker}.
|
|
22583
|
+
*/
|
|
22584
|
+
const CLIENT_BOUND_STORAGE_KEY = "sw:client_bound";
|
|
21519
22585
|
const buildOptionsFromDestination = (destination) => {
|
|
21520
22586
|
if (typeof destination === "string") {
|
|
21521
22587
|
const queryStartIndex = destination.indexOf("?");
|
|
@@ -21559,6 +22625,7 @@ var SignalWire = class extends Destroyable {
|
|
|
21559
22625
|
this.preferences = new ClientPreferences();
|
|
21560
22626
|
this._user$ = this.createBehaviorSubject(void 0);
|
|
21561
22627
|
this._directory$ = this.createBehaviorSubject(void 0);
|
|
22628
|
+
this._credentialRecovered = false;
|
|
21562
22629
|
this._isConnected$ = this.createBehaviorSubject(false);
|
|
21563
22630
|
this._isRegistered$ = this.createBehaviorSubject(false);
|
|
21564
22631
|
this._errors$ = this.createReplaySubject(1);
|
|
@@ -21597,6 +22664,16 @@ var SignalWire = class extends Destroyable {
|
|
|
21597
22664
|
});
|
|
21598
22665
|
}
|
|
21599
22666
|
/**
|
|
22667
|
+
* Build the refresh path's own HTTP controller, against whatever host is current.
|
|
22668
|
+
*
|
|
22669
|
+
* Called on first use rather than up front, so `apiHost` already reflects the
|
|
22670
|
+
* token's `ch` claim. Same credential source as the container's controller — only
|
|
22671
|
+
* the instance, and therefore its observable streams, is separate.
|
|
22672
|
+
*/
|
|
22673
|
+
createRefreshHttpController() {
|
|
22674
|
+
return new HTTPRequestController(this._deps.apiHost, () => this._deps.credential);
|
|
22675
|
+
}
|
|
22676
|
+
/**
|
|
21600
22677
|
* Initializes DPoP if not already set up. Returns the fingerprint on success.
|
|
21601
22678
|
*/
|
|
21602
22679
|
async initDPoP() {
|
|
@@ -21623,11 +22700,19 @@ var SignalWire = class extends Destroyable {
|
|
|
21623
22700
|
async resolveCredentials() {
|
|
21624
22701
|
const fingerprint = await this.initDPoP();
|
|
21625
22702
|
this._refreshCoordinator = new CredentialRefreshCoordinator(this._dpopManager, {
|
|
21626
|
-
http:
|
|
22703
|
+
http: () => {
|
|
22704
|
+
if (!this._refreshHttp || this._refreshHttpHost !== this._deps.apiHost) {
|
|
22705
|
+
this._refreshHttp?.destroy();
|
|
22706
|
+
this._refreshHttp = this.createRefreshHttpController();
|
|
22707
|
+
this._refreshHttpHost = this._deps.apiHost;
|
|
22708
|
+
}
|
|
22709
|
+
return this._refreshHttp;
|
|
22710
|
+
},
|
|
21627
22711
|
notifier: {
|
|
21628
22712
|
onError: (error) => this._errors$.next(error),
|
|
21629
22713
|
onWarning: (warning) => this._warnings$.next(warning),
|
|
21630
|
-
onRefreshExhausted: () => void this.disconnect()
|
|
22714
|
+
onRefreshExhausted: () => void this.disconnect(),
|
|
22715
|
+
onCredentialRefreshed: async (credential) => this.reauthenticateLiveSession(credential)
|
|
21631
22716
|
},
|
|
21632
22717
|
store: {
|
|
21633
22718
|
read: () => this._deps.credential,
|
|
@@ -21639,6 +22724,7 @@ var SignalWire = class extends Destroyable {
|
|
|
21639
22724
|
...this._deps.credential,
|
|
21640
22725
|
...partial
|
|
21641
22726
|
};
|
|
22727
|
+
this.persistCredential(this._deps.credential);
|
|
21642
22728
|
},
|
|
21643
22729
|
persist: (credential) => this.persistCredential(credential)
|
|
21644
22730
|
}
|
|
@@ -21684,20 +22770,181 @@ var SignalWire = class extends Destroyable {
|
|
|
21684
22770
|
}
|
|
21685
22771
|
this._deps.credential = _credentials;
|
|
21686
22772
|
this.persistCredential(_credentials);
|
|
21687
|
-
|
|
21688
|
-
|
|
22773
|
+
await this.reauthenticateLiveSession(_credentials);
|
|
22774
|
+
}
|
|
22775
|
+
/**
|
|
22776
|
+
* Reauthenticate the currently-open session with a freshly obtained
|
|
22777
|
+
* credential so the new token takes effect on the live socket immediately —
|
|
22778
|
+
* not just on the next reconnect. No-op when the session is not
|
|
22779
|
+
* connected/authenticated or the credential carries no token (e.g. an
|
|
22780
|
+
* authorization-state-only refresh). Non-fatal: reauth failures surface on
|
|
22781
|
+
* `errors$` without aborting the refresh that triggered this.
|
|
22782
|
+
*/
|
|
22783
|
+
async reauthenticateLiveSession(credential) {
|
|
22784
|
+
if (!this.isConnected || !this._clientSession.authenticated || !credential.token) return;
|
|
22785
|
+
try {
|
|
22786
|
+
await this._clientSession.reauthenticate(credential.token);
|
|
21689
22787
|
logger$1.info("[SignalWire] Session refreshed with new credentials.");
|
|
21690
22788
|
} catch (error) {
|
|
21691
22789
|
logger$1.error("[SignalWire] Failed to refresh session with new credentials:", error);
|
|
21692
22790
|
this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
21693
22791
|
}
|
|
21694
22792
|
}
|
|
22793
|
+
/**
|
|
22794
|
+
* Recover a session the server is refusing: mint a fresh credential,
|
|
22795
|
+
* reauthenticate the live session with it, and retry the operation.
|
|
22796
|
+
*
|
|
22797
|
+
* The connection is deliberately kept. A reload authenticates the new socket
|
|
22798
|
+
* against the persisted `authorization_state`, and that handshake is what
|
|
22799
|
+
* associates the socket with the previous session — the association reattach
|
|
22800
|
+
* depends on. `signalwire.reauthenticate` swaps the credential *on that same
|
|
22801
|
+
* session*, so recovery never touches the resume state. Discarding it would
|
|
22802
|
+
* heal the credential by destroying the very thing the caller is trying to
|
|
22803
|
+
* get back to.
|
|
22804
|
+
*
|
|
22805
|
+
* The operation is still the verdict, never the RPC. Reauthenticating with
|
|
22806
|
+
* the in-memory token is accepted by a resume even while requests stay
|
|
22807
|
+
* refused, because the persisted `authorization_state` short-circuits token
|
|
22808
|
+
* validation — and `signalwire.reauthenticate` with a *freshly minted* token
|
|
22809
|
+
* has also been observed accepted while `subscriber.online` keeps being
|
|
22810
|
+
* refused (staging run 33826974634). Both look like success and are not.
|
|
22811
|
+
*
|
|
22812
|
+
* @returns the operation's value, or the reason recovery could not deliver
|
|
22813
|
+
* one. `error` is undefined when there was no way to mint at all.
|
|
22814
|
+
*/
|
|
22815
|
+
async recoverAndRetry(operation) {
|
|
22816
|
+
if (!await this.remintAndReauthenticate()) return { ok: false };
|
|
22817
|
+
try {
|
|
22818
|
+
const value = await operation();
|
|
22819
|
+
this._credentialRecovered = true;
|
|
22820
|
+
return {
|
|
22821
|
+
ok: true,
|
|
22822
|
+
value
|
|
22823
|
+
};
|
|
22824
|
+
} catch (error) {
|
|
22825
|
+
logger$1.warn("[SignalWire] Reauthentication was accepted but the operation is still refused:", error);
|
|
22826
|
+
return {
|
|
22827
|
+
ok: false,
|
|
22828
|
+
error
|
|
22829
|
+
};
|
|
22830
|
+
}
|
|
22831
|
+
}
|
|
22832
|
+
/**
|
|
22833
|
+
* Re-mint a credential and adopt it only if the live session accepts it.
|
|
22834
|
+
*
|
|
22835
|
+
* The mechanism follows the binding: a client-bound session re-mints a bound
|
|
22836
|
+
* base SAT through `authenticate()` with the DPoP fingerprint, because the
|
|
22837
|
+
* developer refresh handler would hand back an unbound token and silently
|
|
22838
|
+
* degrade the session. An unbound session uses the refresh handler. Rotation
|
|
22839
|
+
* cost is not a reason to skip this — the only reason is having no mechanism.
|
|
22840
|
+
*
|
|
22841
|
+
* @returns whether the session is now running on a freshly accepted credential.
|
|
22842
|
+
*/
|
|
22843
|
+
async remintAndReauthenticate() {
|
|
22844
|
+
const provider = this._credentialProvider;
|
|
22845
|
+
if (!provider) return false;
|
|
22846
|
+
const { clientBound } = this._clientSession;
|
|
22847
|
+
if (!clientBound && !provider.refresh) {
|
|
22848
|
+
logger$1.debug("[SignalWire] [SW-NO-REFRESH-HANDLER] Unbound session with no refresh handler; cannot re-mint.");
|
|
22849
|
+
return false;
|
|
22850
|
+
}
|
|
22851
|
+
try {
|
|
22852
|
+
const newCredentials = clientBound ? await provider.authenticate(this._dpopManager?.initialized ? { fingerprint: this._dpopManager.fingerprint } : void 0) : await this.remintCredential(provider);
|
|
22853
|
+
if (!newCredentials.token) {
|
|
22854
|
+
logger$1.warn("[SignalWire] Re-minted credential has no token; keeping the current one.");
|
|
22855
|
+
return false;
|
|
22856
|
+
}
|
|
22857
|
+
await this._clientSession.reauthenticate(newCredentials.token);
|
|
22858
|
+
this._deps.credential = newCredentials;
|
|
22859
|
+
this.persistCredential(newCredentials);
|
|
22860
|
+
if (newCredentials.expiry_at && provider.refresh) this._refreshCoordinator?.scheduleDeveloperRefresh(provider, newCredentials.expiry_at);
|
|
22861
|
+
return true;
|
|
22862
|
+
} catch (error) {
|
|
22863
|
+
logger$1.warn("[SignalWire] Re-mint recovery failed:", error);
|
|
22864
|
+
return false;
|
|
22865
|
+
}
|
|
22866
|
+
}
|
|
22867
|
+
/**
|
|
22868
|
+
* Re-mint a credential via `provider.refresh()`, routed through the
|
|
22869
|
+
* coordinator's shared in-flight guard so concurrent re-mint paths (a
|
|
22870
|
+
* scheduled/resume refresh, -32003 recovery, and reconnect) never fire a
|
|
22871
|
+
* second `provider.refresh()` in parallel — which rotating one-time-use
|
|
22872
|
+
* refresh tokens reject. Falls back to a direct call only if the coordinator
|
|
22873
|
+
* has not been constructed yet.
|
|
22874
|
+
*/
|
|
22875
|
+
async remintCredential(provider) {
|
|
22876
|
+
if (this._refreshCoordinator) return this._refreshCoordinator.refreshCredential(provider);
|
|
22877
|
+
if (!provider.refresh) throw new InvalidCredentialsError("Credential provider does not support refresh");
|
|
22878
|
+
return provider.refresh();
|
|
22879
|
+
}
|
|
22880
|
+
/**
|
|
22881
|
+
* Re-mint credentials before a fresh (re)connect (`onBeforeReconnect` hook).
|
|
22882
|
+
* The session invokes this only when it is client-bound OR the in-memory
|
|
22883
|
+
* token is expired. The re-mint mechanism depends on the binding:
|
|
22884
|
+
* - Client-bound: `authenticate()` with the DPoP fingerprint to obtain a
|
|
22885
|
+
* fresh base SAT the upcoming reconnect can re-bind (the
|
|
22886
|
+
* DeviceTokenManager re-activates afterwards).
|
|
22887
|
+
* - Unbound: the developer's non-interactive `refresh()` handler.
|
|
22888
|
+
* `authenticate()` is deliberately NOT used here — it may be interactive
|
|
22889
|
+
* (a login prompt) and must not fire on a background reconnect.
|
|
22890
|
+
*
|
|
22891
|
+
* Rejects on failure so the session aborts the reconnect rather than
|
|
22892
|
+
* replaying a stale token.
|
|
22893
|
+
*/
|
|
22894
|
+
async refreshCredentialForReconnect() {
|
|
22895
|
+
if (!this._credentialProvider) return;
|
|
22896
|
+
try {
|
|
22897
|
+
let newCredentials;
|
|
22898
|
+
if (this._clientSession?.clientBound ?? await this.wasClientBound()) {
|
|
22899
|
+
logger$1.debug("[SignalWire] Re-minting client-bound base SAT before reconnect");
|
|
22900
|
+
newCredentials = await this._credentialProvider.authenticate(this._dpopManager?.initialized ? { fingerprint: this._dpopManager.fingerprint } : void 0);
|
|
22901
|
+
} else if (this._credentialProvider.refresh) {
|
|
22902
|
+
logger$1.debug("[SignalWire] Refreshing unbound credential before reconnect");
|
|
22903
|
+
newCredentials = await this.remintCredential(this._credentialProvider);
|
|
22904
|
+
} else {
|
|
22905
|
+
logger$1.warn("[SignalWire] [SW-NO-REFRESH-HANDLER] Token expired on reconnect but no refresh handler; reconnecting with the existing token.");
|
|
22906
|
+
return;
|
|
22907
|
+
}
|
|
22908
|
+
if (!newCredentials.token) {
|
|
22909
|
+
logger$1.warn("[SignalWire] Re-minted credential has no token; keeping the existing credential for reconnect.");
|
|
22910
|
+
return;
|
|
22911
|
+
}
|
|
22912
|
+
this._deps.credential = newCredentials;
|
|
22913
|
+
this.persistCredential(newCredentials);
|
|
22914
|
+
if (newCredentials.expiry_at && this._credentialProvider.refresh) this._refreshCoordinator?.scheduleDeveloperRefresh(this._credentialProvider, newCredentials.expiry_at);
|
|
22915
|
+
logger$1.debug("[SignalWire] Credential refreshed successfully for reconnect");
|
|
22916
|
+
} catch (error) {
|
|
22917
|
+
logger$1.error("[SignalWire] Failed to refresh credentials for reconnect:", error);
|
|
22918
|
+
this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
22919
|
+
throw error;
|
|
22920
|
+
}
|
|
22921
|
+
}
|
|
21695
22922
|
/** Persist credential to localStorage when persistSession is enabled. */
|
|
21696
22923
|
persistCredential(credential) {
|
|
21697
22924
|
if (!credential.token) return;
|
|
21698
22925
|
this._deps.storage.setItem("sw:cached_credential", credential);
|
|
21699
22926
|
if (this._deps.persistSession) this._deps.storage.setItem("sw:cached_credential", credential, "local");
|
|
21700
22927
|
}
|
|
22928
|
+
/**
|
|
22929
|
+
* Persist whether the session is client-bound, mirroring the credential's
|
|
22930
|
+
* storage scopes so it survives a reload. The preflight recovery reads it
|
|
22931
|
+
* before any session exists to decide whether to re-bind via `authenticate()`
|
|
22932
|
+
* or refresh an unbound token; the marker tracks the latest binding, so an
|
|
22933
|
+
* unbound reconnect clears a stale marker from an earlier client-bound login.
|
|
22934
|
+
*/
|
|
22935
|
+
persistClientBoundMarker(bound) {
|
|
22936
|
+
const scopes = this._deps.persistSession ? ["session", "local"] : ["session"];
|
|
22937
|
+
for (const scope of scopes) if (bound) this._deps.storage.setItem(CLIENT_BOUND_STORAGE_KEY, true, scope);
|
|
22938
|
+
else this._deps.storage.removeItem(CLIENT_BOUND_STORAGE_KEY, scope);
|
|
22939
|
+
}
|
|
22940
|
+
/** Read the persisted client-bound marker (see {@link persistClientBoundMarker}). */
|
|
22941
|
+
async wasClientBound() {
|
|
22942
|
+
const scopes = this._deps.persistSession ? ["local", "session"] : ["session"];
|
|
22943
|
+
for (const scope of scopes) try {
|
|
22944
|
+
if (await this._deps.storage.getItem(CLIENT_BOUND_STORAGE_KEY, scope)) return true;
|
|
22945
|
+
} catch {}
|
|
22946
|
+
return false;
|
|
22947
|
+
}
|
|
21701
22948
|
async init() {
|
|
21702
22949
|
this._user$.next(new User(this._deps.http));
|
|
21703
22950
|
if (!this._options.skipConnection) await this.connect();
|
|
@@ -21721,6 +22968,42 @@ var SignalWire = class extends Destroyable {
|
|
|
21721
22968
|
} catch (error) {
|
|
21722
22969
|
logger$1.error("[SignalWire] Failed to reattach calls:", error);
|
|
21723
22970
|
this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
22971
|
+
} finally {
|
|
22972
|
+
this._credentialRecovered = false;
|
|
22973
|
+
}
|
|
22974
|
+
}
|
|
22975
|
+
/**
|
|
22976
|
+
* Fetch the authenticated user profile, recovering a stale credential.
|
|
22977
|
+
*
|
|
22978
|
+
* On a reload the persisted credential can be expired. Unlike the WS resume —
|
|
22979
|
+
* which the server accepts against the persisted `authorization_state` even
|
|
22980
|
+
* with an expired token — this REST preflight has no such short-circuit and is
|
|
22981
|
+
* refused (401). There is no session yet to reauthenticate, so recovery
|
|
22982
|
+
* re-mints the credential through the provider ({@link refreshCredentialForReconnect})
|
|
22983
|
+
* and retries with a FRESH {@link User}: Fetchable memoizes its result
|
|
22984
|
+
* (shareReplay), so reusing the instance would replay the 401 instead of
|
|
22985
|
+
* re-fetching with the new token. Without the user id the transport/session —
|
|
22986
|
+
* and the reattach a reload is trying to preserve — cannot even be addressed.
|
|
22987
|
+
*/
|
|
22988
|
+
async fetchUserOrRecover() {
|
|
22989
|
+
const fetchUser = async (user$1) => {
|
|
22990
|
+
if (!await (0, import_cjs$1.firstValueFrom)(user$1.fetched$)) throw new UnexpectedError("Failed to fetch user information - fetched$ emitted false");
|
|
22991
|
+
this._deps.user = user$1;
|
|
22992
|
+
};
|
|
22993
|
+
const user = this._user$.value;
|
|
22994
|
+
if (!user) throw new UnexpectedError("User not initialized before connect");
|
|
22995
|
+
try {
|
|
22996
|
+
await fetchUser(user);
|
|
22997
|
+
} catch (firstError) {
|
|
22998
|
+
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.`);
|
|
22999
|
+
try {
|
|
23000
|
+
await this.refreshCredentialForReconnect();
|
|
23001
|
+
const refetched = new User(this._deps.http);
|
|
23002
|
+
await fetchUser(refetched);
|
|
23003
|
+
this._user$.next(refetched);
|
|
23004
|
+
} catch (retryError) {
|
|
23005
|
+
throw new UnexpectedError("Error fetching user information", { cause: retryError });
|
|
23006
|
+
}
|
|
21724
23007
|
}
|
|
21725
23008
|
}
|
|
21726
23009
|
/**
|
|
@@ -21762,40 +23045,23 @@ var SignalWire = class extends Destroyable {
|
|
|
21762
23045
|
*/
|
|
21763
23046
|
async connect() {
|
|
21764
23047
|
await this.teardownTransportAndSession();
|
|
21765
|
-
|
|
21766
|
-
const user = this._user$.value;
|
|
21767
|
-
if (!user) throw new UnexpectedError("User not initialized before connect");
|
|
21768
|
-
if (!await (0, import_cjs$1.firstValueFrom)(user.fetched$)) throw new UnexpectedError("Failed to fetch user information - fetched$ emitted false");
|
|
21769
|
-
this._deps.user = user;
|
|
21770
|
-
} catch (error) {
|
|
21771
|
-
logger$1.error(`[SignalWire] Failed to fetch user information: ${error instanceof Error ? error.message : "Unknown error"}. This usually means the user token is invalid or expired.`);
|
|
21772
|
-
throw new UnexpectedError("Error fetching user information", { cause: error });
|
|
21773
|
-
}
|
|
23048
|
+
await this.fetchUserOrRecover();
|
|
21774
23049
|
const errorHandler = (error) => {
|
|
21775
23050
|
this._errors$.next(error);
|
|
21776
23051
|
};
|
|
21777
23052
|
this._transport = new TransportManager(this._deps.storage, this._deps.protocolKey, this._deps.WebSocket, PreferencesContainer.instance.relayHost ?? this._deps.relayHost, errorHandler);
|
|
21778
|
-
this._attachManager = new AttachManager(this._deps.storage, this._deps.deviceController, PreferencesContainer.instance.reconnectCallsTimeout, this._deps.attachedCallsKey);
|
|
23053
|
+
this._attachManager = new AttachManager(this._deps.storage, this._deps.deviceController, PreferencesContainer.instance.reconnectCallsTimeout, this._deps.attachedCallsKey, () => this._credentialRecovered);
|
|
21779
23054
|
this._clientSession = new ClientSessionManager(() => this._deps.credential, this._transport, this._deps.storage, this._deps.authorizationStateKey, this._deps.deviceController, this._attachManager, this._deps.webRTCApiProvider, this._dpopManager, this._networkMonitor?.networkChange$);
|
|
21780
23055
|
this._publicSession = new ClientSessionWrapper(this._clientSession);
|
|
21781
|
-
this._clientSession.
|
|
21782
|
-
|
|
21783
|
-
try {
|
|
21784
|
-
const fingerprint = this._dpopManager?.initialized ? this._dpopManager.fingerprint : void 0;
|
|
21785
|
-
logger$1.debug("[SignalWire] Credential expired, refreshing before reconnect");
|
|
21786
|
-
const newCredentials = await this._credentialProvider.authenticate(fingerprint ? { fingerprint } : void 0);
|
|
21787
|
-
this._deps.credential = newCredentials;
|
|
21788
|
-
if (newCredentials.expiry_at && this._credentialProvider.refresh) this._refreshCoordinator?.scheduleDeveloperRefresh(this._credentialProvider, newCredentials.expiry_at);
|
|
21789
|
-
logger$1.debug("[SignalWire] Credential refreshed successfully for reconnect");
|
|
21790
|
-
} catch (error) {
|
|
21791
|
-
logger$1.error("[SignalWire] Failed to refresh credentials for reconnect:", error);
|
|
21792
|
-
this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
21793
|
-
throw error;
|
|
21794
|
-
}
|
|
21795
|
-
};
|
|
23056
|
+
this._clientSession.callControl = this._options.callControl ?? "routed";
|
|
23057
|
+
this._clientSession.onBeforeReconnect = async () => this.refreshCredentialForReconnect();
|
|
21796
23058
|
this.subscribeTo(this._clientSession.errors$, (error) => {
|
|
21797
23059
|
this._errors$.next(error);
|
|
21798
23060
|
});
|
|
23061
|
+
this.subscribeTo(this._clientSession.authorization$, (authorization) => {
|
|
23062
|
+
this._refreshCoordinator?.syncExpiryFromAuthorization(authorization, this._credentialProvider);
|
|
23063
|
+
this.persistClientBoundMarker(Boolean(authorization?.cnf?.jkt));
|
|
23064
|
+
});
|
|
21799
23065
|
await this._clientSession.connect();
|
|
21800
23066
|
await this._refreshCoordinator?.activate(this._deps.user, this._clientSession);
|
|
21801
23067
|
this.subscribeTo(this._clientSession.authenticated$.pipe((0, import_cjs$1.skip)(1), (0, import_cjs$1.filter)(Boolean)), async () => {
|
|
@@ -21958,6 +23224,13 @@ var SignalWire = class extends Destroyable {
|
|
|
21958
23224
|
}
|
|
21959
23225
|
try {
|
|
21960
23226
|
this._visibilityController = new VisibilityController();
|
|
23227
|
+
this.subscribeTo(this._visibilityController.visibilityChange$.pipe((0, import_cjs$1.filter)((event) => event.to === "visible")), () => {
|
|
23228
|
+
try {
|
|
23229
|
+
this._refreshCoordinator?.forceRefreshIfDue();
|
|
23230
|
+
} catch (error) {
|
|
23231
|
+
logger$1.warn("[SignalWire] Resume credential revalidation failed (non-fatal):", error);
|
|
23232
|
+
}
|
|
23233
|
+
});
|
|
21961
23234
|
this.subscribeTo(this._visibilityController.visibilityChange$.pipe((0, import_cjs$1.filter)((event) => event.to === "visible" && PreferencesContainer.instance.refreshDevicesOnVisible)), () => {
|
|
21962
23235
|
logger$1.debug("[SignalWire] Page visible, re-enumerating devices");
|
|
21963
23236
|
try {
|
|
@@ -21969,7 +23242,7 @@ var SignalWire = class extends Destroyable {
|
|
|
21969
23242
|
logger$1.warn("[SignalWire] Failed to initialize VisibilityController:", error);
|
|
21970
23243
|
}
|
|
21971
23244
|
try {
|
|
21972
|
-
this._diagnosticsCollector = new DiagnosticsCollector({ sdkVersion: "4.0.0-rc.
|
|
23245
|
+
this._diagnosticsCollector = new DiagnosticsCollector({ sdkVersion: "4.0.0-rc.4" });
|
|
21973
23246
|
} catch (error) {
|
|
21974
23247
|
logger$1.warn("[SignalWire] Failed to initialize DiagnosticsCollector:", error);
|
|
21975
23248
|
}
|
|
@@ -22040,21 +23313,22 @@ var SignalWire = class extends Destroyable {
|
|
|
22040
23313
|
this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
22041
23314
|
throw error;
|
|
22042
23315
|
}
|
|
22043
|
-
logger$1.debug("[SignalWire] Failed to register user,
|
|
22044
|
-
|
|
22045
|
-
await this._clientSession.reauthenticate(this._deps.credential.token);
|
|
22046
|
-
logger$1.debug("[SignalWire] Reauthentication successful, retrying register()");
|
|
23316
|
+
logger$1.debug("[SignalWire] Failed to register user, attempting credential recovery...");
|
|
23317
|
+
const outcome = await this.recoverAndRetry(async () => {
|
|
22047
23318
|
await this._transport.execute(RPCExecute({
|
|
22048
23319
|
method: "subscriber.online",
|
|
22049
23320
|
params: {}
|
|
22050
23321
|
}));
|
|
23322
|
+
});
|
|
23323
|
+
if (outcome.ok) {
|
|
23324
|
+
logger$1.debug("[SignalWire] Recovery restored registration");
|
|
22051
23325
|
this._isRegistered$.next(true);
|
|
22052
|
-
|
|
22053
|
-
logger$1.error("[SignalWire] Reauthentication failed during register():", reauthError);
|
|
22054
|
-
const registerError = new InvalidCredentialsError("Failed to register user, and reauthentication attempt also failed. Please check your credentials.", { cause: reauthError instanceof Error ? reauthError : new Error(String(reauthError), { cause: reauthError }) });
|
|
22055
|
-
this._errors$.next(registerError);
|
|
22056
|
-
throw registerError;
|
|
23326
|
+
return;
|
|
22057
23327
|
}
|
|
23328
|
+
const failureCause = outcome.error ?? error;
|
|
23329
|
+
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 }) });
|
|
23330
|
+
this._errors$.next(registerError);
|
|
23331
|
+
throw registerError;
|
|
22058
23332
|
}
|
|
22059
23333
|
}
|
|
22060
23334
|
/**
|
|
@@ -22085,6 +23359,11 @@ var SignalWire = class extends Destroyable {
|
|
|
22085
23359
|
* Returns a {@link Call} in `'ringing'` state. Subscribe to {@link Call.status$}
|
|
22086
23360
|
* to track progression through `'connected'` → `'disconnected'`.
|
|
22087
23361
|
*
|
|
23362
|
+
* Local media acquisition is deliberately unbounded: an unanswered permission
|
|
23363
|
+
* prompt leaves this promise pending indefinitely, so apply your own bound if
|
|
23364
|
+
* your UI needs one. The 12 s signaling budget starts only once acquisition
|
|
23365
|
+
* settles.
|
|
23366
|
+
*
|
|
22088
23367
|
* @param destination - Address URI string (e.g. `'/public/my-room'`) or {@link Address} instance.
|
|
22089
23368
|
* @param options - Media and dial options (audio/video, device constraints). Overrides defaults.
|
|
22090
23369
|
* @returns The created {@link Call} instance.
|
|
@@ -22107,7 +23386,18 @@ var SignalWire = class extends Destroyable {
|
|
|
22107
23386
|
};
|
|
22108
23387
|
await this.waitAuthentication();
|
|
22109
23388
|
logger$1.debug("[SignalWire] Dialing with options:", computed_options);
|
|
22110
|
-
|
|
23389
|
+
try {
|
|
23390
|
+
return await this._clientSession.createOutboundCall(destination, computed_options);
|
|
23391
|
+
} catch (error) {
|
|
23392
|
+
if (!isRecoverableAuthError(error)) throw error;
|
|
23393
|
+
logger$1.debug("[SignalWire] Dial hit a recoverable auth error; recovering the session and retrying");
|
|
23394
|
+
const outcome = await this.recoverAndRetry(async () => {
|
|
23395
|
+
await this.waitAuthentication();
|
|
23396
|
+
return this._clientSession.createOutboundCall(destination, computed_options);
|
|
23397
|
+
});
|
|
23398
|
+
if (outcome.ok) return outcome.value;
|
|
23399
|
+
throw outcome.error ?? error;
|
|
23400
|
+
}
|
|
22111
23401
|
}
|
|
22112
23402
|
/**
|
|
22113
23403
|
* Runs a multi-phase connectivity test against the given destination.
|
|
@@ -22388,10 +23678,13 @@ var SignalWire = class extends Destroyable {
|
|
|
22388
23678
|
destroy() {
|
|
22389
23679
|
this._refreshCoordinator?.destroy();
|
|
22390
23680
|
this._refreshCoordinator = void 0;
|
|
23681
|
+
this._refreshHttp?.destroy();
|
|
23682
|
+
this._refreshHttp = void 0;
|
|
22391
23683
|
this._dpopManager?.destroy();
|
|
22392
|
-
this._clientSession
|
|
22393
|
-
|
|
22394
|
-
this.
|
|
23684
|
+
const session = this._clientSession;
|
|
23685
|
+
session?.teardownSessionState();
|
|
23686
|
+
this._transport?.destroy();
|
|
23687
|
+
session?.destroy();
|
|
22395
23688
|
try {
|
|
22396
23689
|
this._networkMonitor?.destroy();
|
|
22397
23690
|
} catch {}
|
|
@@ -22504,7 +23797,7 @@ var StaticCredentialProvider = class {
|
|
|
22504
23797
|
/**
|
|
22505
23798
|
* Library version from package.json, injected at build time.
|
|
22506
23799
|
*/
|
|
22507
|
-
const version = "4.0.0-rc.
|
|
23800
|
+
const version = "4.0.0-rc.4";
|
|
22508
23801
|
/**
|
|
22509
23802
|
* Flag indicating the library has been loaded and is ready to use.
|
|
22510
23803
|
* For UMD builds: `window.SignalWire.ready`
|
|
@@ -22526,7 +23819,7 @@ const ready = true;
|
|
|
22526
23819
|
*/
|
|
22527
23820
|
const emitReadyEvent = () => {
|
|
22528
23821
|
if (typeof window !== "undefined") {
|
|
22529
|
-
const event = new CustomEvent("signalwire:js:ready", { detail: { version: "4.0.0-rc.
|
|
23822
|
+
const event = new CustomEvent("signalwire:js:ready", { detail: { version: "4.0.0-rc.4" } });
|
|
22530
23823
|
window.dispatchEvent(event);
|
|
22531
23824
|
}
|
|
22532
23825
|
};
|
|
@@ -22550,5 +23843,5 @@ emitReadyEvent();
|
|
|
22550
23843
|
if (typeof process === "undefined") globalThis.process = { env: { NODE_ENV: "production" } };
|
|
22551
23844
|
|
|
22552
23845
|
//#endregion
|
|
22553
|
-
export { Address, CallCreateError, ClientPreferences, CollectionFetchError, DPoPInitError, DeviceTokenError, EmbedTokenCredentialProvider, InvalidCredentialsError, MediaAccessError, MediaTrackError, MessageParseError, OverconstrainedFallbackError, Participant, PreflightError, RecoveryError, SelfCapabilities, SelfParticipant, SignalWire, StaticCredentialProvider, TokenRefreshError, UnexpectedError, User, VertoPongError, WebRTCCall, embeddableCall, getLogger, isSelfParticipant, ready, setDebugOptions, setLogLevel, setLogger, version };
|
|
23846
|
+
export { Address, AuxiliaryLegCancelledError, AuxiliaryLegTimeoutError, CallCreateError, CallNotReadyError, ClientPreferences, CollectionFetchError, DPoPInitError, DeviceTokenError, EmbedTokenCredentialProvider, InvalidCredentialsError, MediaAccessError, MediaTrackError, MessageParseError, OverconstrainedFallbackError, Participant, ParticipantNotReadyError, PreflightError, RecoveryError, ScreenShareAlreadyActiveError, SelfCapabilities, SelfParticipant, SignalWire, StaticCredentialProvider, TokenRefreshError, UnexpectedError, User, VertoPongError, WebRTCCall, embeddableCall, getLogger, isSelfParticipant, ready, setDebugOptions, setLogLevel, setLogger, version };
|
|
22554
23847
|
//# sourceMappingURL=browser.mjs.map
|