@signalwire/js 4.0.0-rc.1 → 4.0.0-rc.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser.mjs +1905 -586
- package/dist/browser.mjs.map +1 -1
- package/dist/browser.umd.js +1910 -585
- package/dist/browser.umd.js.map +1 -1
- package/dist/index.cjs +1714 -642
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2360 -1817
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +2360 -1817
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1637 -571
- package/dist/index.mjs.map +1 -1
- package/dist/operators/index.cjs +1 -1
- package/dist/operators/index.mjs +1 -1
- package/dist/operators-BJ6QW1VP.cjs +1466 -0
- package/dist/operators-BJ6QW1VP.cjs.map +1 -0
- package/dist/operators-BVxi3KpN.mjs +731 -0
- package/dist/operators-BVxi3KpN.mjs.map +1 -0
- package/package.json +1 -1
- package/dist/operators-CX_lCCJm.mjs +0 -478
- package/dist/operators-CX_lCCJm.mjs.map +0 -1
- package/dist/operators-D6a2J1KA.cjs +0 -763
- package/dist/operators-D6a2J1KA.cjs.map +0 -1
package/dist/browser.umd.js
CHANGED
|
@@ -4192,7 +4192,7 @@ var require_merge$1 = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
4192
4192
|
var empty_1$7 = require_empty();
|
|
4193
4193
|
var args_1$8 = require_args();
|
|
4194
4194
|
var from_1$4 = require_from();
|
|
4195
|
-
function merge$
|
|
4195
|
+
function merge$7() {
|
|
4196
4196
|
var args = [];
|
|
4197
4197
|
for (var _i = 0; _i < arguments.length; _i++) args[_i] = arguments[_i];
|
|
4198
4198
|
var scheduler = args_1$8.popScheduler(args);
|
|
@@ -4200,7 +4200,7 @@ var require_merge$1 = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
4200
4200
|
var sources = args;
|
|
4201
4201
|
return !sources.length ? empty_1$7.EMPTY : sources.length === 1 ? innerFrom_1$31.innerFrom(sources[0]) : mergeAll_1$3.mergeAll(concurrent)(from_1$4.from(sources, scheduler));
|
|
4202
4202
|
}
|
|
4203
|
-
exports.merge = merge$
|
|
4203
|
+
exports.merge = merge$7;
|
|
4204
4204
|
}));
|
|
4205
4205
|
|
|
4206
4206
|
//#endregion
|
|
@@ -4331,13 +4331,13 @@ var require_race$1 = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
4331
4331
|
var innerFrom_1$28 = require_innerFrom();
|
|
4332
4332
|
var argsOrArgArray_1$4 = require_argsOrArgArray();
|
|
4333
4333
|
var OperatorSubscriber_1$48 = require_OperatorSubscriber();
|
|
4334
|
-
function race$
|
|
4334
|
+
function race$4() {
|
|
4335
4335
|
var sources = [];
|
|
4336
4336
|
for (var _i = 0; _i < arguments.length; _i++) sources[_i] = arguments[_i];
|
|
4337
4337
|
sources = argsOrArgArray_1$4.argsOrArgArray(sources);
|
|
4338
4338
|
return sources.length === 1 ? innerFrom_1$28.innerFrom(sources[0]) : new Observable_1$6.Observable(raceInit(sources));
|
|
4339
4339
|
}
|
|
4340
|
-
exports.race = race$
|
|
4340
|
+
exports.race = race$4;
|
|
4341
4341
|
function raceInit(sources) {
|
|
4342
4342
|
return function(subscriber) {
|
|
4343
4343
|
var subscriptions = [];
|
|
@@ -6082,7 +6082,7 @@ var require_merge = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
6082
6082
|
var mergeAll_1$2 = require_mergeAll();
|
|
6083
6083
|
var args_1$3 = require_args();
|
|
6084
6084
|
var from_1$1 = require_from();
|
|
6085
|
-
function merge$
|
|
6085
|
+
function merge$6() {
|
|
6086
6086
|
var args = [];
|
|
6087
6087
|
for (var _i = 0; _i < arguments.length; _i++) args[_i] = arguments[_i];
|
|
6088
6088
|
var scheduler = args_1$3.popScheduler(args);
|
|
@@ -6091,7 +6091,7 @@ var require_merge = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
6091
6091
|
mergeAll_1$2.mergeAll(concurrent)(from_1$1.from(__spreadArray$8([source], __read$8(args)), scheduler)).subscribe(subscriber);
|
|
6092
6092
|
});
|
|
6093
6093
|
}
|
|
6094
|
-
exports.merge = merge$
|
|
6094
|
+
exports.merge = merge$6;
|
|
6095
6095
|
}));
|
|
6096
6096
|
|
|
6097
6097
|
//#endregion
|
|
@@ -6938,7 +6938,7 @@ var require_switchMap = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
6938
6938
|
var innerFrom_1$6 = require_innerFrom();
|
|
6939
6939
|
var lift_1$13 = require_lift();
|
|
6940
6940
|
var OperatorSubscriber_1$11 = require_OperatorSubscriber();
|
|
6941
|
-
function switchMap$
|
|
6941
|
+
function switchMap$8(project, resultSelector) {
|
|
6942
6942
|
return lift_1$13.operate(function(source, subscriber) {
|
|
6943
6943
|
var innerSubscriber = null;
|
|
6944
6944
|
var index = 0;
|
|
@@ -6962,7 +6962,7 @@ var require_switchMap = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
6962
6962
|
}));
|
|
6963
6963
|
});
|
|
6964
6964
|
}
|
|
6965
|
-
exports.switchMap = switchMap$
|
|
6965
|
+
exports.switchMap = switchMap$8;
|
|
6966
6966
|
}));
|
|
6967
6967
|
|
|
6968
6968
|
//#endregion
|
|
@@ -9021,6 +9021,172 @@ var Destroyable = class {
|
|
|
9021
9021
|
}
|
|
9022
9022
|
};
|
|
9023
9023
|
|
|
9024
|
+
//#endregion
|
|
9025
|
+
//#region src/core/constants.ts
|
|
9026
|
+
const INVITE_VERSION = 1e3;
|
|
9027
|
+
const DEFAULT_ICE_CANDIDATE_TIMEOUT_MS = 600;
|
|
9028
|
+
const DEFAULT_ICE_GATHERING_TIMEOUT_MS = 6e3;
|
|
9029
|
+
const DEFAULT_RECONNECT_CALLS_TIMEOUT_MS = 300 * 1e3;
|
|
9030
|
+
const DEFAULT_CONNECTION_TIMEOUT_MS = 1e4;
|
|
9031
|
+
/**
|
|
9032
|
+
* How long call setup may wait for the network, measured from local media
|
|
9033
|
+
* settling until the member id arrives.
|
|
9034
|
+
*
|
|
9035
|
+
* Local media acquisition is deliberately NOT inside this: a permission prompt
|
|
9036
|
+
* or a device picker is human time, and a human may take as long as they like
|
|
9037
|
+
* without spending the server's budget. The clock starts when acquisition ends.
|
|
9038
|
+
*
|
|
9039
|
+
* Must exceed `iceGatheringTimeout + the RPC timeout`, which both run inside it.
|
|
9040
|
+
*/
|
|
9041
|
+
const DEFAULT_CALL_SIGNALING_TIMEOUT_MS = 12e3;
|
|
9042
|
+
/**
|
|
9043
|
+
* How long an auxiliary leg (screen share, additional device) may take to
|
|
9044
|
+
* connect once its media is in hand.
|
|
9045
|
+
*
|
|
9046
|
+
* One bound for both: the picker is human time and sits outside it. What remains
|
|
9047
|
+
* — offer, ICE, invite, answer, DTLS — is the same work for either leg kind.
|
|
9048
|
+
*/
|
|
9049
|
+
const DEFAULT_AUX_LEG_CONNECT_TIMEOUT_MS = 15e3;
|
|
9050
|
+
const DEFAULT_RECONNECT_DELAY_MIN_MS = 100;
|
|
9051
|
+
const DEFAULT_RECONNECT_DELAY_MAX_MS = 3e3;
|
|
9052
|
+
const DEFAULT_DEVICE_DEBOUNCE_TIME_MS = 1500;
|
|
9053
|
+
const DEFAULT_DEVICE_POLLING_INTERVAL_MS = 0;
|
|
9054
|
+
const PREFERENCES_STORAGE_KEY = "sw:preferences";
|
|
9055
|
+
/** Scope value that enables automatic token refresh. */
|
|
9056
|
+
const SAT_REFRESH_SCOPE = "sat:refresh";
|
|
9057
|
+
/** API endpoints for device token operations. */
|
|
9058
|
+
const DEVICE_TOKEN_ENDPOINT = "/api/fabric/subscriber/devices/token";
|
|
9059
|
+
const DEVICE_REFRESH_ENDPOINT = "/api/fabric/subscriber/devices/refresh";
|
|
9060
|
+
/** Default device token TTL in seconds (15 minutes). */
|
|
9061
|
+
const DEVICE_TOKEN_DEFAULT_EXPIRE_IN = 900;
|
|
9062
|
+
/** Buffer time in milliseconds before expiry to trigger refresh. */
|
|
9063
|
+
const DEVICE_TOKEN_REFRESH_BUFFER_MS = 3e4;
|
|
9064
|
+
/** Maximum retry attempts for device token refresh on transient failure. */
|
|
9065
|
+
const DEVICE_TOKEN_REFRESH_MAX_RETRIES = 3;
|
|
9066
|
+
/** Base delay in milliseconds for exponential backoff on refresh retry. */
|
|
9067
|
+
const DEVICE_TOKEN_REFRESH_RETRY_BASE_MS = 1e3;
|
|
9068
|
+
/** Maximum retry attempts for developer credential refresh on transient failure. */
|
|
9069
|
+
const CREDENTIAL_REFRESH_MAX_RETRIES = 5;
|
|
9070
|
+
/** Base delay in milliseconds for exponential backoff on credential refresh retry. */
|
|
9071
|
+
const CREDENTIAL_REFRESH_RETRY_BASE_MS = 1e3;
|
|
9072
|
+
/** Maximum delay in milliseconds for credential refresh backoff. */
|
|
9073
|
+
const CREDENTIAL_REFRESH_MAX_DELAY_MS = 3e4;
|
|
9074
|
+
/** Buffer in milliseconds before token expiry to trigger refresh. */
|
|
9075
|
+
const CREDENTIAL_REFRESH_BUFFER_MS = 5e3;
|
|
9076
|
+
/**
|
|
9077
|
+
* Clock-skew allowance (ms) for treating an in-memory credential as expired
|
|
9078
|
+
* when deciding whether a fresh (re)connect must re-mint the token before
|
|
9079
|
+
* authenticating. A token within this window of its `expiry_at` is treated as
|
|
9080
|
+
* stale so the reconnect re-mints via the credential provider instead of
|
|
9081
|
+
* replaying a dead token (which the server rejects with -32003).
|
|
9082
|
+
*/
|
|
9083
|
+
const CREDENTIAL_EXPIRY_SKEW_MS = 3e4;
|
|
9084
|
+
/**
|
|
9085
|
+
* Maximum time the coordinator will wait for `DeviceTokenManager.activate()`
|
|
9086
|
+
* to resolve before treating the activation as failed and falling back to
|
|
9087
|
+
* the developer-provided refresh path. Prevents a wedged HTTP layer from
|
|
9088
|
+
* leaving the session with no active refresh mechanism.
|
|
9089
|
+
*/
|
|
9090
|
+
const CREDENTIAL_ACTIVATE_TIMEOUT_MS = 3e4;
|
|
9091
|
+
/** JSON-RPC error code for requester validation failure (corrupted auth state). */
|
|
9092
|
+
const RPC_ERROR_REQUESTER_VALIDATION_FAILED = -32003;
|
|
9093
|
+
/** JSON-RPC error code for invalid params (e.g., missing authentication block). */
|
|
9094
|
+
const RPC_ERROR_INVALID_PARAMS = -32602;
|
|
9095
|
+
/** JSON-RPC error code for authentication failure (invalid token, missing DPoP, etc.). */
|
|
9096
|
+
const RPC_ERROR_AUTHENTICATION_FAILED = -32002;
|
|
9097
|
+
/** Error names browsers use for a media permission denial (user or policy). */
|
|
9098
|
+
const MEDIA_ACCESS_DENIAL_NAMES = [
|
|
9099
|
+
"NotAllowedError",
|
|
9100
|
+
"SecurityError",
|
|
9101
|
+
"PermissionDeniedError"
|
|
9102
|
+
];
|
|
9103
|
+
/** Error names browsers use when the capture hardware is already held exclusively. */
|
|
9104
|
+
const MEDIA_DEVICE_IN_USE_NAMES = ["NotReadableError", "TrackStartError"];
|
|
9105
|
+
/** Default polling interval for RTCPeerConnection.getStats() in milliseconds. */
|
|
9106
|
+
const DEFAULT_STATS_POLLING_INTERVAL_MS = 1e3;
|
|
9107
|
+
/** Number of initial samples used to build a baseline for spike detection. */
|
|
9108
|
+
const DEFAULT_STATS_BASELINE_SAMPLES = 10;
|
|
9109
|
+
/** Duration in ms with no inbound audio packets before emitting a critical issue. */
|
|
9110
|
+
const DEFAULT_STATS_NO_PACKET_THRESHOLD_MS = 2e3;
|
|
9111
|
+
/** Multiplier applied to baseline RTT to detect a warning-level RTT spike. */
|
|
9112
|
+
const DEFAULT_STATS_RTT_SPIKE_MULTIPLIER = 3;
|
|
9113
|
+
/** Packet loss fraction (0-1) above which a warning is emitted. */
|
|
9114
|
+
const DEFAULT_STATS_PACKET_LOSS_THRESHOLD = .05;
|
|
9115
|
+
/** Multiplier applied to baseline jitter to detect a jitter spike. */
|
|
9116
|
+
const DEFAULT_STATS_JITTER_SPIKE_MULTIPLIER = 4;
|
|
9117
|
+
/** Number of seconds of metrics history to retain. */
|
|
9118
|
+
const DEFAULT_STATS_HISTORY_SIZE = 30;
|
|
9119
|
+
/** Maximum keyframe requests allowed within a single burst window. */
|
|
9120
|
+
const DEFAULT_KEYFRAME_MAX_BURST$1 = 3;
|
|
9121
|
+
/** Duration of the keyframe burst window in milliseconds. */
|
|
9122
|
+
const DEFAULT_KEYFRAME_BURST_WINDOW_MS$1 = 3e3;
|
|
9123
|
+
/** Cooldown period in ms after burst limit is reached before allowing more keyframes. */
|
|
9124
|
+
const DEFAULT_KEYFRAME_COOLDOWN_MS$1 = 1e4;
|
|
9125
|
+
/** Minimum time between re-INVITE attempts in milliseconds. */
|
|
9126
|
+
const DEFAULT_REINVITE_DEBOUNCE_TIME_MS = 1e4;
|
|
9127
|
+
/** Maximum number of re-INVITE attempts per call. */
|
|
9128
|
+
const DEFAULT_REINVITE_MAX_ATTEMPTS = 3;
|
|
9129
|
+
/** Timeout for a single re-INVITE attempt in milliseconds. */
|
|
9130
|
+
const DEFAULT_REINVITE_TIMEOUT_MS = 5e3;
|
|
9131
|
+
/** Debounce window in ms to collapse multiple detection signals into one trigger. */
|
|
9132
|
+
const DEFAULT_RECOVERY_DEBOUNCE_TIME_MS = 2e3;
|
|
9133
|
+
/** Cooldown period in ms between recovery attempts. */
|
|
9134
|
+
const DEFAULT_RECOVERY_COOLDOWN_MS = 1e4;
|
|
9135
|
+
/** Grace period in ms before treating ICE 'disconnected' as a failure. */
|
|
9136
|
+
const DEFAULT_ICE_DISCONNECTED_GRACE_PERIOD_MS = 3e3;
|
|
9137
|
+
/** Timeout for a single ICE restart attempt in milliseconds. */
|
|
9138
|
+
const DEFAULT_ICE_RESTART_TIMEOUT_MS$1 = 5e3;
|
|
9139
|
+
/** Maximum recovery attempts before emitting 'max_attempts_reached'. */
|
|
9140
|
+
const DEFAULT_MAX_RECOVERY_ATTEMPTS = 3;
|
|
9141
|
+
/** Upper bound in ms for waiting on iceGatheringState === 'complete' after an ICE restart. */
|
|
9142
|
+
const ICE_GATHERING_COMPLETE_TIMEOUT_MS = 1e4;
|
|
9143
|
+
/** Upper bound in ms for waiting on RTCPeerConnection.connectionState === 'connected' after a recovery ICE restart. */
|
|
9144
|
+
const PEER_CONNECTION_RECOVERY_WAIT_MS = 5e3;
|
|
9145
|
+
/** Polling interval in ms while waiting for RTCPeerConnection.connectionState to transition. */
|
|
9146
|
+
const PEER_CONNECTION_RECOVERY_POLL_MS = 100;
|
|
9147
|
+
/** Polling interval for LocalAudioPipeline.level$ (ms). ~30fps is smooth for meters. */
|
|
9148
|
+
const AUDIO_LEVEL_POLL_INTERVAL_MS = 33;
|
|
9149
|
+
/** RMS level threshold (0..1) above which the local participant is considered speaking. */
|
|
9150
|
+
const VAD_THRESHOLD = .03;
|
|
9151
|
+
/** Hold window in ms below the threshold before speaking$ flips back to false. */
|
|
9152
|
+
const VAD_HOLD_MS = 250;
|
|
9153
|
+
/** Whether to persist device selections to storage by default. */
|
|
9154
|
+
const DEFAULT_PERSIST_DEVICE_SELECTION = true;
|
|
9155
|
+
/** Whether to auto-apply device changes to active calls by default. */
|
|
9156
|
+
const DEFAULT_SYNC_DEVICES_TO_ACTIVE_CALLS = true;
|
|
9157
|
+
/** Storage keys for persisted device selections. */
|
|
9158
|
+
const DEVICE_STORAGE_KEY_AUDIO_INPUT = "sw:device:audioinput";
|
|
9159
|
+
const DEVICE_STORAGE_KEY_AUDIO_OUTPUT = "sw:device:audiooutput";
|
|
9160
|
+
const DEVICE_STORAGE_KEY_VIDEO_INPUT = "sw:device:videoinput";
|
|
9161
|
+
/** Whether to auto-mute video when the tab becomes hidden. */
|
|
9162
|
+
const DEFAULT_AUTO_MUTE_VIDEO_ON_HIDDEN = false;
|
|
9163
|
+
/** Whether to re-enumerate devices when the page becomes visible. */
|
|
9164
|
+
const DEFAULT_REFRESH_DEVICES_ON_VISIBLE = true;
|
|
9165
|
+
/** Whether to check peer connection health when the page becomes visible. */
|
|
9166
|
+
const DEFAULT_CHECK_CONNECTION_ON_VISIBLE = true;
|
|
9167
|
+
/** Whether automatic video degradation on low bandwidth is enabled. */
|
|
9168
|
+
const DEFAULT_ENABLE_AUTO_DEGRADATION = true;
|
|
9169
|
+
/** Bitrate in kbps below which video is automatically disabled. */
|
|
9170
|
+
const DEFAULT_DEGRADATION_BITRATE_THRESHOLD_KBPS = 150;
|
|
9171
|
+
/** Bitrate in kbps above which video is automatically re-enabled (hysteresis). */
|
|
9172
|
+
const DEFAULT_DEGRADATION_RECOVERY_THRESHOLD_KBPS = 300;
|
|
9173
|
+
/** Whether relay-only escalation is enabled as a last-resort recovery tier. */
|
|
9174
|
+
const DEFAULT_ENABLE_RELAY_FALLBACK = true;
|
|
9175
|
+
/** Whether to listen for browser online/offline/connection events. */
|
|
9176
|
+
const DEFAULT_ENABLE_NETWORK_CHANGE_DETECTION = true;
|
|
9177
|
+
/** Whether to intercept server-sent media-timeout hangups and attempt recovery. */
|
|
9178
|
+
const DEFAULT_ENABLE_SERVER_HANGUP_INTERCEPTION = true;
|
|
9179
|
+
/** Default video track constraints applied when video is enabled without explicit constraints. */
|
|
9180
|
+
const DEFAULT_VIDEO_CONSTRAINTS = {
|
|
9181
|
+
width: { ideal: 1280 },
|
|
9182
|
+
height: { ideal: 720 },
|
|
9183
|
+
aspectRatio: 16 / 9
|
|
9184
|
+
};
|
|
9185
|
+
/** Whether stereo Opus is enabled by default. */
|
|
9186
|
+
const DEFAULT_STEREO_AUDIO = false;
|
|
9187
|
+
/** Max average bitrate for stereo Opus in bits per second. */
|
|
9188
|
+
const DEFAULT_STEREO_MAX_AVERAGE_BITRATE = 51e4;
|
|
9189
|
+
|
|
9024
9190
|
//#endregion
|
|
9025
9191
|
//#region src/core/errors.ts
|
|
9026
9192
|
var UnexpectedError = class extends Error {
|
|
@@ -9150,6 +9316,20 @@ var CallCreateError = class extends Error {
|
|
|
9150
9316
|
this.name = "CallCreateError";
|
|
9151
9317
|
}
|
|
9152
9318
|
};
|
|
9319
|
+
var CallNotReadyError = class extends Error {
|
|
9320
|
+
constructor(callId, options) {
|
|
9321
|
+
super(`Call "${callId}" has no self member context yet: selfId/nodeId have not been received from the server`, options);
|
|
9322
|
+
this.callId = callId;
|
|
9323
|
+
this.name = "CallNotReadyError";
|
|
9324
|
+
}
|
|
9325
|
+
};
|
|
9326
|
+
var ParticipantNotReadyError = class extends Error {
|
|
9327
|
+
constructor(memberId, options) {
|
|
9328
|
+
super(`Participant "${memberId}" has no call context yet: its member state (call_id/node_id) has not been received from the server`, options);
|
|
9329
|
+
this.memberId = memberId;
|
|
9330
|
+
this.name = "ParticipantNotReadyError";
|
|
9331
|
+
}
|
|
9332
|
+
};
|
|
9153
9333
|
var JSONRPCError = class extends Error {
|
|
9154
9334
|
constructor(code, message, data, options, requestId) {
|
|
9155
9335
|
super(message, options);
|
|
@@ -9221,6 +9401,32 @@ var CollectionFetchError = class extends Error {
|
|
|
9221
9401
|
this.name = "CollectionFetchError";
|
|
9222
9402
|
}
|
|
9223
9403
|
};
|
|
9404
|
+
/**
|
|
9405
|
+
* An auxiliary leg did not connect within its budget. Typed rather than a bare
|
|
9406
|
+
* RxJS `TimeoutError` so the leg and cause survive.
|
|
9407
|
+
*/
|
|
9408
|
+
var AuxiliaryLegTimeoutError = class extends Error {
|
|
9409
|
+
constructor(leg, originalError) {
|
|
9410
|
+
super(`Timed out waiting for the ${leg} connection to be established`, { cause: originalError });
|
|
9411
|
+
this.leg = leg;
|
|
9412
|
+
this.originalError = originalError;
|
|
9413
|
+
this.name = "AuxiliaryLegTimeoutError";
|
|
9414
|
+
}
|
|
9415
|
+
};
|
|
9416
|
+
/**
|
|
9417
|
+
* An auxiliary leg was removed before it finished connecting.
|
|
9418
|
+
*
|
|
9419
|
+
* Typed rather than a bare resolve so a caller awaiting the start can tell a
|
|
9420
|
+
* cancel apart from a share that actually came up — the public methods return
|
|
9421
|
+
* `void`, so the promise is the only signal they have.
|
|
9422
|
+
*/
|
|
9423
|
+
var AuxiliaryLegCancelledError = class extends Error {
|
|
9424
|
+
constructor(leg) {
|
|
9425
|
+
super(`The ${leg} leg was removed before it finished connecting`);
|
|
9426
|
+
this.leg = leg;
|
|
9427
|
+
this.name = "AuxiliaryLegCancelledError";
|
|
9428
|
+
}
|
|
9429
|
+
};
|
|
9224
9430
|
var MediaTrackError = class extends Error {
|
|
9225
9431
|
constructor(operation, kind, originalError) {
|
|
9226
9432
|
super(`Media track ${operation} failed for ${kind}`, { cause: originalError instanceof Error ? originalError : void 0 });
|
|
@@ -9230,6 +9436,53 @@ var MediaTrackError = class extends Error {
|
|
|
9230
9436
|
this.name = "MediaTrackError";
|
|
9231
9437
|
}
|
|
9232
9438
|
};
|
|
9439
|
+
/** True when a `getUserMedia`/`getDisplayMedia` rejection is a permission denial. */
|
|
9440
|
+
function isMediaAccessDenial(originalError) {
|
|
9441
|
+
return originalError instanceof Error && MEDIA_ACCESS_DENIAL_NAMES.includes(originalError.name);
|
|
9442
|
+
}
|
|
9443
|
+
/** True when a `getUserMedia` rejection means the hardware is already held exclusively. */
|
|
9444
|
+
function isMediaDeviceInUse(originalError) {
|
|
9445
|
+
return originalError instanceof Error && MEDIA_DEVICE_IN_USE_NAMES.includes(originalError.name);
|
|
9446
|
+
}
|
|
9447
|
+
/**
|
|
9448
|
+
* Failure to acquire local media (camera, microphone, or screen capture)
|
|
9449
|
+
* via `getUserMedia`/`getDisplayMedia`.
|
|
9450
|
+
*
|
|
9451
|
+
* Non-fatal by default: screenshare and additional-device failures never
|
|
9452
|
+
* end the call, and main-connection failures degrade to receive-only when
|
|
9453
|
+
* possible. The wrapping site sets `fatal` to `true` only when the call
|
|
9454
|
+
* cannot continue (receive-only fallback disabled or no receive intent).
|
|
9455
|
+
*/
|
|
9456
|
+
var MediaAccessError = class extends Error {
|
|
9457
|
+
constructor(operation, media, originalError, fatal = false) {
|
|
9458
|
+
super(`Media access ${isMediaAccessDenial(originalError) ? "denied" : "failed"} for ${operation} (${media})`, { cause: originalError instanceof Error ? originalError : void 0 });
|
|
9459
|
+
this.operation = operation;
|
|
9460
|
+
this.media = media;
|
|
9461
|
+
this.originalError = originalError;
|
|
9462
|
+
this.fatal = fatal;
|
|
9463
|
+
this.name = "MediaAccessError";
|
|
9464
|
+
}
|
|
9465
|
+
/** True when the underlying failure is a permission denial (user or policy). */
|
|
9466
|
+
get denied() {
|
|
9467
|
+
return isMediaAccessDenial(this.originalError);
|
|
9468
|
+
}
|
|
9469
|
+
};
|
|
9470
|
+
/**
|
|
9471
|
+
* Thrown by `startScreenShare()` when the call is already sharing a screen.
|
|
9472
|
+
*
|
|
9473
|
+
* A call carries at most one screen share. Accepting a second one would
|
|
9474
|
+
* overwrite the only reference the SDK holds to the first, leaving it
|
|
9475
|
+
* capturing and sending with no way to stop it — so the second request is
|
|
9476
|
+
* rejected and the live share is left untouched. Call `stopScreenShare()`
|
|
9477
|
+
* first to replace it.
|
|
9478
|
+
*/
|
|
9479
|
+
var ScreenShareAlreadyActiveError = class extends Error {
|
|
9480
|
+
constructor(screenShareId, options) {
|
|
9481
|
+
super(`A screen share is already active on this call (${screenShareId}). Call stopScreenShare() before starting another one.`, options);
|
|
9482
|
+
this.screenShareId = screenShareId;
|
|
9483
|
+
this.name = "ScreenShareAlreadyActiveError";
|
|
9484
|
+
}
|
|
9485
|
+
};
|
|
9233
9486
|
var DPoPInitError = class extends Error {
|
|
9234
9487
|
constructor(originalError, message = "Failed to initialize DPoP key pair") {
|
|
9235
9488
|
super(message, { cause: originalError instanceof Error ? originalError : void 0 });
|
|
@@ -9470,9 +9723,9 @@ var require_loglevel = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
9470
9723
|
defaultLogger$1 = new Logger();
|
|
9471
9724
|
defaultLogger$1.getLogger = function getLogger$1(name) {
|
|
9472
9725
|
if (typeof name !== "symbol" && typeof name !== "string" || name === "") throw new TypeError("You must supply a name when creating a logger.");
|
|
9473
|
-
var logger$
|
|
9474
|
-
if (!logger$
|
|
9475
|
-
return logger$
|
|
9726
|
+
var logger$34 = _loggersByName[name];
|
|
9727
|
+
if (!logger$34) logger$34 = _loggersByName[name] = new Logger(name, defaultLogger$1.methodFactory);
|
|
9728
|
+
return logger$34;
|
|
9476
9729
|
};
|
|
9477
9730
|
var _log = typeof window !== undefinedType ? window.log : void 0;
|
|
9478
9731
|
defaultLogger$1.noConflict = function() {
|
|
@@ -9508,8 +9761,8 @@ const defaultLoggerLevel = defaultLogger.levels.WARN;
|
|
|
9508
9761
|
defaultLogger.setLevel(defaultLoggerLevel);
|
|
9509
9762
|
let userLogger = null;
|
|
9510
9763
|
/** Replace the built-in logger with a custom implementation. Pass `null` to restore defaults. */
|
|
9511
|
-
const setLogger = (logger$
|
|
9512
|
-
userLogger = logger$
|
|
9764
|
+
const setLogger = (logger$34) => {
|
|
9765
|
+
userLogger = logger$34;
|
|
9513
9766
|
};
|
|
9514
9767
|
let debugOptions = {};
|
|
9515
9768
|
/** Configure debug options (e.g., `{ logWsTraffic: true }`). */
|
|
@@ -9553,8 +9806,8 @@ const wsTraffic = (options) => {
|
|
|
9553
9806
|
loggerInstance.debug(`${options.type.toUpperCase()}: \n`, msg, "\n");
|
|
9554
9807
|
};
|
|
9555
9808
|
const getLogger = () => {
|
|
9556
|
-
const logger$
|
|
9557
|
-
return new Proxy(logger$
|
|
9809
|
+
const logger$34 = getLoggerInstance();
|
|
9810
|
+
return new Proxy(logger$34, { get(_target, prop, _receiver) {
|
|
9558
9811
|
if (prop === "wsTraffic") return wsTraffic;
|
|
9559
9812
|
const instance = getLoggerInstance();
|
|
9560
9813
|
const value = Reflect.get(instance, prop);
|
|
@@ -9606,7 +9859,7 @@ const asyncRetry = async ({ asyncCallable, maxRetries: retries = DEFAULT_MAX_RET
|
|
|
9606
9859
|
|
|
9607
9860
|
//#endregion
|
|
9608
9861
|
//#region src/controllers/HTTPRequestController.ts
|
|
9609
|
-
const logger$
|
|
9862
|
+
const logger$33 = getLogger();
|
|
9610
9863
|
const GET_PARAMS = {
|
|
9611
9864
|
method: "GET",
|
|
9612
9865
|
headers: { Accept: "application/json" }
|
|
@@ -9670,7 +9923,7 @@ var HTTPRequestController = class HTTPRequestController extends Destroyable {
|
|
|
9670
9923
|
this._responses$.next(response);
|
|
9671
9924
|
return response;
|
|
9672
9925
|
} catch (error) {
|
|
9673
|
-
logger$
|
|
9926
|
+
logger$33.error("[HTTPRequestController] Request error:", error);
|
|
9674
9927
|
this._status$.next("error");
|
|
9675
9928
|
const err = error instanceof Error ? error : new Error("HTTP request failed", { cause: error });
|
|
9676
9929
|
this._errors$.next(err);
|
|
@@ -9697,7 +9950,7 @@ var HTTPRequestController = class HTTPRequestController extends Destroyable {
|
|
|
9697
9950
|
const url = this.buildURL(request.url);
|
|
9698
9951
|
const headers = this.buildHeaders(request.headers);
|
|
9699
9952
|
const timeout$5 = request.timeout ?? this.requestTimeout;
|
|
9700
|
-
logger$
|
|
9953
|
+
logger$33.debug("[HTTPRequestController] Executing request:", {
|
|
9701
9954
|
method: request.method,
|
|
9702
9955
|
url,
|
|
9703
9956
|
headers: Object.keys(headers).reduce((acc, key) => {
|
|
@@ -9717,7 +9970,7 @@ var HTTPRequestController = class HTTPRequestController extends Destroyable {
|
|
|
9717
9970
|
});
|
|
9718
9971
|
clearTimeout(timeoutId);
|
|
9719
9972
|
const httpResponse = await this.convertResponse(response);
|
|
9720
|
-
logger$
|
|
9973
|
+
logger$33.debug("[HTTPRequestController] Response received:", {
|
|
9721
9974
|
status: response.status,
|
|
9722
9975
|
statusText: response.statusText,
|
|
9723
9976
|
headers: [...response.headers.entries()],
|
|
@@ -9727,7 +9980,7 @@ var HTTPRequestController = class HTTPRequestController extends Destroyable {
|
|
|
9727
9980
|
} catch (error) {
|
|
9728
9981
|
clearTimeout(timeoutId);
|
|
9729
9982
|
if (error instanceof Error && error.name === "AbortError") throw new RequestTimeoutError(`Request timeout after ${timeout$5}ms`, { cause: error });
|
|
9730
|
-
logger$
|
|
9983
|
+
logger$33.error("[HTTPRequestController] Request failed:", error);
|
|
9731
9984
|
throw error;
|
|
9732
9985
|
}
|
|
9733
9986
|
}
|
|
@@ -9741,8 +9994,8 @@ var HTTPRequestController = class HTTPRequestController extends Destroyable {
|
|
|
9741
9994
|
const credential = this.getCredential();
|
|
9742
9995
|
if (credential.token) {
|
|
9743
9996
|
headers.Authorization = `Bearer ${credential.token}`;
|
|
9744
|
-
logger$
|
|
9745
|
-
} else logger$
|
|
9997
|
+
logger$33.debug("[HTTPRequestController] Using Bearer token auth, token length:", credential.token.length);
|
|
9998
|
+
} else logger$33.warn("[HTTPRequestController] No credentials available for authentication");
|
|
9746
9999
|
return headers;
|
|
9747
10000
|
}
|
|
9748
10001
|
/**
|
|
@@ -9879,137 +10132,6 @@ var DeviceHistoryManager = class {
|
|
|
9879
10132
|
}
|
|
9880
10133
|
};
|
|
9881
10134
|
|
|
9882
|
-
//#endregion
|
|
9883
|
-
//#region src/core/constants.ts
|
|
9884
|
-
const INVITE_VERSION = 1e3;
|
|
9885
|
-
const DEFAULT_ICE_CANDIDATE_TIMEOUT_MS = 600;
|
|
9886
|
-
const DEFAULT_ICE_GATHERING_TIMEOUT_MS = 6e3;
|
|
9887
|
-
const DEFAULT_RECONNECT_CALLS_TIMEOUT_MS = 300 * 1e3;
|
|
9888
|
-
const DEFAULT_CONNECTION_TIMEOUT_MS = 1e4;
|
|
9889
|
-
const DEFAULT_RECONNECT_DELAY_MIN_MS = 100;
|
|
9890
|
-
const DEFAULT_RECONNECT_DELAY_MAX_MS = 3e3;
|
|
9891
|
-
const DEFAULT_DEVICE_DEBOUNCE_TIME_MS = 1500;
|
|
9892
|
-
const DEFAULT_DEVICE_POLLING_INTERVAL_MS = 0;
|
|
9893
|
-
const PREFERENCES_STORAGE_KEY = "sw:preferences";
|
|
9894
|
-
/** Scope value that enables automatic token refresh. */
|
|
9895
|
-
const SAT_REFRESH_SCOPE = "sat:refresh";
|
|
9896
|
-
/** API endpoints for device token operations. */
|
|
9897
|
-
const DEVICE_TOKEN_ENDPOINT = "/api/fabric/subscriber/devices/token";
|
|
9898
|
-
const DEVICE_REFRESH_ENDPOINT = "/api/fabric/subscriber/devices/refresh";
|
|
9899
|
-
/** Default device token TTL in seconds (15 minutes). */
|
|
9900
|
-
const DEVICE_TOKEN_DEFAULT_EXPIRE_IN = 900;
|
|
9901
|
-
/** Buffer time in milliseconds before expiry to trigger refresh. */
|
|
9902
|
-
const DEVICE_TOKEN_REFRESH_BUFFER_MS = 3e4;
|
|
9903
|
-
/** Maximum retry attempts for device token refresh on transient failure. */
|
|
9904
|
-
const DEVICE_TOKEN_REFRESH_MAX_RETRIES = 3;
|
|
9905
|
-
/** Base delay in milliseconds for exponential backoff on refresh retry. */
|
|
9906
|
-
const DEVICE_TOKEN_REFRESH_RETRY_BASE_MS = 1e3;
|
|
9907
|
-
/** Maximum retry attempts for developer credential refresh on transient failure. */
|
|
9908
|
-
const CREDENTIAL_REFRESH_MAX_RETRIES = 5;
|
|
9909
|
-
/** Base delay in milliseconds for exponential backoff on credential refresh retry. */
|
|
9910
|
-
const CREDENTIAL_REFRESH_RETRY_BASE_MS = 1e3;
|
|
9911
|
-
/** Maximum delay in milliseconds for credential refresh backoff. */
|
|
9912
|
-
const CREDENTIAL_REFRESH_MAX_DELAY_MS = 3e4;
|
|
9913
|
-
/** Buffer in milliseconds before token expiry to trigger refresh. */
|
|
9914
|
-
const CREDENTIAL_REFRESH_BUFFER_MS = 5e3;
|
|
9915
|
-
/**
|
|
9916
|
-
* Maximum time the coordinator will wait for `DeviceTokenManager.activate()`
|
|
9917
|
-
* to resolve before treating the activation as failed and falling back to
|
|
9918
|
-
* the developer-provided refresh path. Prevents a wedged HTTP layer from
|
|
9919
|
-
* leaving the session with no active refresh mechanism.
|
|
9920
|
-
*/
|
|
9921
|
-
const CREDENTIAL_ACTIVATE_TIMEOUT_MS = 3e4;
|
|
9922
|
-
/** JSON-RPC error code for requester validation failure (corrupted auth state). */
|
|
9923
|
-
const RPC_ERROR_REQUESTER_VALIDATION_FAILED = -32003;
|
|
9924
|
-
/** JSON-RPC error code for invalid params (e.g., missing authentication block). */
|
|
9925
|
-
const RPC_ERROR_INVALID_PARAMS = -32602;
|
|
9926
|
-
/** JSON-RPC error code for authentication failure (invalid token, missing DPoP, etc.). */
|
|
9927
|
-
const RPC_ERROR_AUTHENTICATION_FAILED = -32002;
|
|
9928
|
-
/** Default polling interval for RTCPeerConnection.getStats() in milliseconds. */
|
|
9929
|
-
const DEFAULT_STATS_POLLING_INTERVAL_MS = 1e3;
|
|
9930
|
-
/** Number of initial samples used to build a baseline for spike detection. */
|
|
9931
|
-
const DEFAULT_STATS_BASELINE_SAMPLES = 10;
|
|
9932
|
-
/** Duration in ms with no inbound audio packets before emitting a critical issue. */
|
|
9933
|
-
const DEFAULT_STATS_NO_PACKET_THRESHOLD_MS = 2e3;
|
|
9934
|
-
/** Multiplier applied to baseline RTT to detect a warning-level RTT spike. */
|
|
9935
|
-
const DEFAULT_STATS_RTT_SPIKE_MULTIPLIER = 3;
|
|
9936
|
-
/** Packet loss fraction (0-1) above which a warning is emitted. */
|
|
9937
|
-
const DEFAULT_STATS_PACKET_LOSS_THRESHOLD = .05;
|
|
9938
|
-
/** Multiplier applied to baseline jitter to detect a jitter spike. */
|
|
9939
|
-
const DEFAULT_STATS_JITTER_SPIKE_MULTIPLIER = 4;
|
|
9940
|
-
/** Number of seconds of metrics history to retain. */
|
|
9941
|
-
const DEFAULT_STATS_HISTORY_SIZE = 30;
|
|
9942
|
-
/** Maximum keyframe requests allowed within a single burst window. */
|
|
9943
|
-
const DEFAULT_KEYFRAME_MAX_BURST$1 = 3;
|
|
9944
|
-
/** Duration of the keyframe burst window in milliseconds. */
|
|
9945
|
-
const DEFAULT_KEYFRAME_BURST_WINDOW_MS$1 = 3e3;
|
|
9946
|
-
/** Cooldown period in ms after burst limit is reached before allowing more keyframes. */
|
|
9947
|
-
const DEFAULT_KEYFRAME_COOLDOWN_MS$1 = 1e4;
|
|
9948
|
-
/** Minimum time between re-INVITE attempts in milliseconds. */
|
|
9949
|
-
const DEFAULT_REINVITE_DEBOUNCE_TIME_MS = 1e4;
|
|
9950
|
-
/** Maximum number of re-INVITE attempts per call. */
|
|
9951
|
-
const DEFAULT_REINVITE_MAX_ATTEMPTS = 3;
|
|
9952
|
-
/** Timeout for a single re-INVITE attempt in milliseconds. */
|
|
9953
|
-
const DEFAULT_REINVITE_TIMEOUT_MS = 5e3;
|
|
9954
|
-
/** Debounce window in ms to collapse multiple detection signals into one trigger. */
|
|
9955
|
-
const DEFAULT_RECOVERY_DEBOUNCE_TIME_MS = 2e3;
|
|
9956
|
-
/** Cooldown period in ms between recovery attempts. */
|
|
9957
|
-
const DEFAULT_RECOVERY_COOLDOWN_MS = 1e4;
|
|
9958
|
-
/** Grace period in ms before treating ICE 'disconnected' as a failure. */
|
|
9959
|
-
const DEFAULT_ICE_DISCONNECTED_GRACE_PERIOD_MS = 3e3;
|
|
9960
|
-
/** Timeout for a single ICE restart attempt in milliseconds. */
|
|
9961
|
-
const DEFAULT_ICE_RESTART_TIMEOUT_MS$1 = 5e3;
|
|
9962
|
-
/** Maximum recovery attempts before emitting 'max_attempts_reached'. */
|
|
9963
|
-
const DEFAULT_MAX_RECOVERY_ATTEMPTS = 3;
|
|
9964
|
-
/** Upper bound in ms for waiting on iceGatheringState === 'complete' after an ICE restart. */
|
|
9965
|
-
const ICE_GATHERING_COMPLETE_TIMEOUT_MS = 1e4;
|
|
9966
|
-
/** Upper bound in ms for waiting on RTCPeerConnection.connectionState === 'connected' after a recovery ICE restart. */
|
|
9967
|
-
const PEER_CONNECTION_RECOVERY_WAIT_MS = 5e3;
|
|
9968
|
-
/** Polling interval in ms while waiting for RTCPeerConnection.connectionState to transition. */
|
|
9969
|
-
const PEER_CONNECTION_RECOVERY_POLL_MS = 100;
|
|
9970
|
-
/** Polling interval for LocalAudioPipeline.level$ (ms). ~30fps is smooth for meters. */
|
|
9971
|
-
const AUDIO_LEVEL_POLL_INTERVAL_MS = 33;
|
|
9972
|
-
/** RMS level threshold (0..1) above which the local participant is considered speaking. */
|
|
9973
|
-
const VAD_THRESHOLD = .03;
|
|
9974
|
-
/** Hold window in ms below the threshold before speaking$ flips back to false. */
|
|
9975
|
-
const VAD_HOLD_MS = 250;
|
|
9976
|
-
/** Whether to persist device selections to storage by default. */
|
|
9977
|
-
const DEFAULT_PERSIST_DEVICE_SELECTION = true;
|
|
9978
|
-
/** Whether to auto-apply device changes to active calls by default. */
|
|
9979
|
-
const DEFAULT_SYNC_DEVICES_TO_ACTIVE_CALLS = true;
|
|
9980
|
-
/** Storage keys for persisted device selections. */
|
|
9981
|
-
const DEVICE_STORAGE_KEY_AUDIO_INPUT = "sw:device:audioinput";
|
|
9982
|
-
const DEVICE_STORAGE_KEY_AUDIO_OUTPUT = "sw:device:audiooutput";
|
|
9983
|
-
const DEVICE_STORAGE_KEY_VIDEO_INPUT = "sw:device:videoinput";
|
|
9984
|
-
/** Whether to auto-mute video when the tab becomes hidden. */
|
|
9985
|
-
const DEFAULT_AUTO_MUTE_VIDEO_ON_HIDDEN = false;
|
|
9986
|
-
/** Whether to re-enumerate devices when the page becomes visible. */
|
|
9987
|
-
const DEFAULT_REFRESH_DEVICES_ON_VISIBLE = true;
|
|
9988
|
-
/** Whether to check peer connection health when the page becomes visible. */
|
|
9989
|
-
const DEFAULT_CHECK_CONNECTION_ON_VISIBLE = true;
|
|
9990
|
-
/** Whether automatic video degradation on low bandwidth is enabled. */
|
|
9991
|
-
const DEFAULT_ENABLE_AUTO_DEGRADATION = true;
|
|
9992
|
-
/** Bitrate in kbps below which video is automatically disabled. */
|
|
9993
|
-
const DEFAULT_DEGRADATION_BITRATE_THRESHOLD_KBPS = 150;
|
|
9994
|
-
/** Bitrate in kbps above which video is automatically re-enabled (hysteresis). */
|
|
9995
|
-
const DEFAULT_DEGRADATION_RECOVERY_THRESHOLD_KBPS = 300;
|
|
9996
|
-
/** Whether relay-only escalation is enabled as a last-resort recovery tier. */
|
|
9997
|
-
const DEFAULT_ENABLE_RELAY_FALLBACK = true;
|
|
9998
|
-
/** Whether to listen for browser online/offline/connection events. */
|
|
9999
|
-
const DEFAULT_ENABLE_NETWORK_CHANGE_DETECTION = true;
|
|
10000
|
-
/** Whether to intercept server-sent media-timeout hangups and attempt recovery. */
|
|
10001
|
-
const DEFAULT_ENABLE_SERVER_HANGUP_INTERCEPTION = true;
|
|
10002
|
-
/** Default video track constraints applied when video is enabled without explicit constraints. */
|
|
10003
|
-
const DEFAULT_VIDEO_CONSTRAINTS = {
|
|
10004
|
-
width: { ideal: 1280 },
|
|
10005
|
-
height: { ideal: 720 },
|
|
10006
|
-
aspectRatio: 16 / 9
|
|
10007
|
-
};
|
|
10008
|
-
/** Whether stereo Opus is enabled by default. */
|
|
10009
|
-
const DEFAULT_STEREO_AUDIO = false;
|
|
10010
|
-
/** Max average bitrate for stereo Opus in bits per second. */
|
|
10011
|
-
const DEFAULT_STEREO_MAX_AVERAGE_BITRATE = 51e4;
|
|
10012
|
-
|
|
10013
10135
|
//#endregion
|
|
10014
10136
|
//#region src/utils/time.ts
|
|
10015
10137
|
function fromSecToMs(seconds) {
|
|
@@ -10021,7 +10143,7 @@ function fromMsToSec(milliseconds) {
|
|
|
10021
10143
|
|
|
10022
10144
|
//#endregion
|
|
10023
10145
|
//#region src/containers/PreferencesContainer.ts
|
|
10024
|
-
const logger$
|
|
10146
|
+
const logger$32 = getLogger();
|
|
10025
10147
|
var PreferencesContainer = class PreferencesContainer {
|
|
10026
10148
|
static get instance() {
|
|
10027
10149
|
this._instance ??= new PreferencesContainer();
|
|
@@ -10683,7 +10805,7 @@ var ClientPreferences = class {
|
|
|
10683
10805
|
if (!this._storage) return;
|
|
10684
10806
|
const data = collectStoredPreferences();
|
|
10685
10807
|
this._storage.setItem(PREFERENCES_STORAGE_KEY, data, "local").catch((error) => {
|
|
10686
|
-
logger$
|
|
10808
|
+
logger$32.error(`[ClientPreferences] Failed to save preferences: ${String(error)}`);
|
|
10687
10809
|
});
|
|
10688
10810
|
}
|
|
10689
10811
|
/** Loads preferences from storage and applies them to the container. */
|
|
@@ -10692,7 +10814,7 @@ var ClientPreferences = class {
|
|
|
10692
10814
|
this._storage.getItem(PREFERENCES_STORAGE_KEY, "local").then((stored) => {
|
|
10693
10815
|
if (stored) applyStoredPreferences(stored);
|
|
10694
10816
|
}).catch((error) => {
|
|
10695
|
-
logger$
|
|
10817
|
+
logger$32.error(`[ClientPreferences] Failed to load preferences: ${String(error)}`);
|
|
10696
10818
|
});
|
|
10697
10819
|
}
|
|
10698
10820
|
};
|
|
@@ -10714,7 +10836,7 @@ function toError(value) {
|
|
|
10714
10836
|
//#endregion
|
|
10715
10837
|
//#region src/controllers/NavigatorDeviceController.ts
|
|
10716
10838
|
var import_cjs$29 = require_cjs();
|
|
10717
|
-
const logger$
|
|
10839
|
+
const logger$31 = getLogger();
|
|
10718
10840
|
/** Maps a device kind to its storage key. */
|
|
10719
10841
|
const DEVICE_STORAGE_KEYS = {
|
|
10720
10842
|
audioinput: DEVICE_STORAGE_KEY_AUDIO_INPUT,
|
|
@@ -10736,7 +10858,7 @@ var NavigatorDeviceController = class extends Destroyable {
|
|
|
10736
10858
|
super();
|
|
10737
10859
|
this.webRTCApiProvider = webRTCApiProvider;
|
|
10738
10860
|
this.deviceChangeHandler = () => {
|
|
10739
|
-
logger$
|
|
10861
|
+
logger$31.debug("[DeviceController] Device change detected");
|
|
10740
10862
|
this.enumerateDevices();
|
|
10741
10863
|
};
|
|
10742
10864
|
this._devicesState$ = this.createBehaviorSubject(initialDevicesState);
|
|
@@ -10801,13 +10923,13 @@ var NavigatorDeviceController = class extends Destroyable {
|
|
|
10801
10923
|
return this.cachedObservable("videoInputDevices$", () => this._devicesState$.pipe((0, import_cjs$29.map)((state) => state.videoinput), (0, import_cjs$29.distinctUntilChanged)(), (0, import_cjs$29.takeUntil)(this.destroyed$)));
|
|
10802
10924
|
}
|
|
10803
10925
|
get selectedAudioInputDevice$() {
|
|
10804
|
-
return this.cachedObservable("selectedAudioInputDevice$", () => this._selectedDevicesState$.asObservable().pipe((0, import_cjs$29.map)((state) => state.audioinput), (0, import_cjs$29.distinctUntilChanged)(), (0, import_cjs$29.takeUntil)(this.destroyed$), (0, import_cjs$29.tap)((info) => logger$
|
|
10926
|
+
return this.cachedObservable("selectedAudioInputDevice$", () => this._selectedDevicesState$.asObservable().pipe((0, import_cjs$29.map)((state) => state.audioinput), (0, import_cjs$29.distinctUntilChanged)(), (0, import_cjs$29.takeUntil)(this.destroyed$), (0, import_cjs$29.tap)((info) => logger$31.debug("[DeviceController] Selected audio input device changed:", info))));
|
|
10805
10927
|
}
|
|
10806
10928
|
get selectedAudioOutputDevice$() {
|
|
10807
|
-
return this.cachedObservable("selectedAudioOutputDevice$", () => this._selectedDevicesState$.asObservable().pipe((0, import_cjs$29.map)((state) => state.audiooutput), (0, import_cjs$29.distinctUntilChanged)(), (0, import_cjs$29.takeUntil)(this.destroyed$), (0, import_cjs$29.tap)((info) => logger$
|
|
10929
|
+
return this.cachedObservable("selectedAudioOutputDevice$", () => this._selectedDevicesState$.asObservable().pipe((0, import_cjs$29.map)((state) => state.audiooutput), (0, import_cjs$29.distinctUntilChanged)(), (0, import_cjs$29.takeUntil)(this.destroyed$), (0, import_cjs$29.tap)((info) => logger$31.debug("[DeviceController] Selected audio output device changed:", info))));
|
|
10808
10930
|
}
|
|
10809
10931
|
get selectedVideoInputDevice$() {
|
|
10810
|
-
return this.cachedObservable("selectedVideoInputDevice$", () => this._selectedDevicesState$.asObservable().pipe((0, import_cjs$29.map)((state) => state.videoinput), (0, import_cjs$29.distinctUntilChanged)(), (0, import_cjs$29.takeUntil)(this.destroyed$), (0, import_cjs$29.tap)((info) => logger$
|
|
10932
|
+
return this.cachedObservable("selectedVideoInputDevice$", () => this._selectedDevicesState$.asObservable().pipe((0, import_cjs$29.map)((state) => state.videoinput), (0, import_cjs$29.distinctUntilChanged)(), (0, import_cjs$29.takeUntil)(this.destroyed$), (0, import_cjs$29.tap)((info) => logger$31.debug("[DeviceController] Selected video input device changed:", info))));
|
|
10811
10933
|
}
|
|
10812
10934
|
get selectedAudioInputDevice() {
|
|
10813
10935
|
if (this._audioInputDisabled$.value) return null;
|
|
@@ -10882,7 +11004,7 @@ var NavigatorDeviceController = class extends Destroyable {
|
|
|
10882
11004
|
if (device) this.persistDeviceSelection("audioinput", device);
|
|
10883
11005
|
}
|
|
10884
11006
|
selectVideoInputDevice(device) {
|
|
10885
|
-
logger$
|
|
11007
|
+
logger$31.debug("[DeviceController] Setting selected video input device:", device);
|
|
10886
11008
|
if (this._videoInputDisabled$.value && device) this._videoInputDisabled$.next(false);
|
|
10887
11009
|
const previous = this._selectedDevicesState$.value.videoinput;
|
|
10888
11010
|
if (previous && previous.deviceId !== device?.deviceId) this._deviceHistory.push("videoinput", previous);
|
|
@@ -10939,7 +11061,7 @@ var NavigatorDeviceController = class extends Destroyable {
|
|
|
10939
11061
|
}
|
|
10940
11062
|
const fromHistory = this._deviceHistory.findInHistory(kind, devices);
|
|
10941
11063
|
if (fromHistory) {
|
|
10942
|
-
logger$
|
|
11064
|
+
logger$31.debug(`[DeviceController] Device disappeared, falling back to history: ${fromHistory.label}`);
|
|
10943
11065
|
this.emitDeviceRecovered(kind, selected, fromHistory, "device_disconnected");
|
|
10944
11066
|
return fromHistory;
|
|
10945
11067
|
}
|
|
@@ -10992,7 +11114,7 @@ var NavigatorDeviceController = class extends Destroyable {
|
|
|
10992
11114
|
try {
|
|
10993
11115
|
await this._storageManager.setItem(DEVICE_STORAGE_KEYS[kind], stored, "local");
|
|
10994
11116
|
} catch (error) {
|
|
10995
|
-
logger$
|
|
11117
|
+
logger$31.error(`[DeviceController] Failed to persist device selection for ${kind}:`, error);
|
|
10996
11118
|
}
|
|
10997
11119
|
}
|
|
10998
11120
|
async loadPersistedDevices() {
|
|
@@ -11008,7 +11130,7 @@ var NavigatorDeviceController = class extends Destroyable {
|
|
|
11008
11130
|
[kind]: stored
|
|
11009
11131
|
};
|
|
11010
11132
|
} catch (error) {
|
|
11011
|
-
logger$
|
|
11133
|
+
logger$31.error(`[DeviceController] Failed to load persisted device for ${kind}:`, error);
|
|
11012
11134
|
}
|
|
11013
11135
|
}
|
|
11014
11136
|
/** Clears device history, persisted selections, and re-enumerates devices. */
|
|
@@ -11026,7 +11148,7 @@ var NavigatorDeviceController = class extends Destroyable {
|
|
|
11026
11148
|
this.disableDeviceMonitoring();
|
|
11027
11149
|
this.webRTCApiProvider.mediaDevices.addEventListener("devicechange", this.deviceChangeHandler);
|
|
11028
11150
|
if (PreferencesContainer.instance.devicePollingInterval > 0) this._devicesPoolingSubscription = (0, import_cjs$29.interval)(PreferencesContainer.instance.devicePollingInterval).subscribe(() => {
|
|
11029
|
-
logger$
|
|
11151
|
+
logger$31.debug("[DeviceController] Polling devices due to interval");
|
|
11030
11152
|
this.enumerateDevices();
|
|
11031
11153
|
});
|
|
11032
11154
|
this.enumerateDevices();
|
|
@@ -11052,13 +11174,13 @@ var NavigatorDeviceController = class extends Destroyable {
|
|
|
11052
11174
|
videoinput: []
|
|
11053
11175
|
});
|
|
11054
11176
|
this._devicesState$.next(devicesByKind);
|
|
11055
|
-
logger$
|
|
11177
|
+
logger$31.debug("[DeviceController] Devices enumerated:", {
|
|
11056
11178
|
audioInputs: devicesByKind.audioinput.length,
|
|
11057
11179
|
audioOutputs: devicesByKind.audiooutput.length,
|
|
11058
11180
|
videoInputs: devicesByKind.videoinput.length
|
|
11059
11181
|
});
|
|
11060
11182
|
} catch (error) {
|
|
11061
|
-
logger$
|
|
11183
|
+
logger$31.error("[DeviceController] Failed to enumerate devices:", error);
|
|
11062
11184
|
this._errors$.next(toError(error));
|
|
11063
11185
|
}
|
|
11064
11186
|
}
|
|
@@ -11074,7 +11196,7 @@ var NavigatorDeviceController = class extends Destroyable {
|
|
|
11074
11196
|
stream.getTracks().forEach((t) => t.stop());
|
|
11075
11197
|
return capabilities;
|
|
11076
11198
|
} catch (error) {
|
|
11077
|
-
logger$
|
|
11199
|
+
logger$31.error("[DeviceController] Failed to get device capabilities:", error);
|
|
11078
11200
|
this._errors$.next(toError(error));
|
|
11079
11201
|
throw error;
|
|
11080
11202
|
}
|
|
@@ -11325,7 +11447,7 @@ var DependencyContainer = class {
|
|
|
11325
11447
|
|
|
11326
11448
|
//#endregion
|
|
11327
11449
|
//#region src/controllers/CryptoController.ts
|
|
11328
|
-
const logger$
|
|
11450
|
+
const logger$30 = getLogger();
|
|
11329
11451
|
const DPOP_DB_NAME = "sw-dpop";
|
|
11330
11452
|
const DPOP_DB_VERSION = 1;
|
|
11331
11453
|
const DPOP_STORE_NAME = "keys";
|
|
@@ -11384,7 +11506,7 @@ async function loadKeyPairFromDB() {
|
|
|
11384
11506
|
tx.oncomplete = () => db.close();
|
|
11385
11507
|
});
|
|
11386
11508
|
} catch (error) {
|
|
11387
|
-
logger$
|
|
11509
|
+
logger$30.warn("[DPoP] Failed to load key pair from IndexedDB:", error);
|
|
11388
11510
|
return null;
|
|
11389
11511
|
}
|
|
11390
11512
|
}
|
|
@@ -11404,7 +11526,7 @@ async function saveKeyPairToDB(keyPair) {
|
|
|
11404
11526
|
};
|
|
11405
11527
|
});
|
|
11406
11528
|
} catch (error) {
|
|
11407
|
-
logger$
|
|
11529
|
+
logger$30.warn("[DPoP] Failed to save key pair to IndexedDB:", error);
|
|
11408
11530
|
}
|
|
11409
11531
|
}
|
|
11410
11532
|
async function deleteKeyPairFromDB() {
|
|
@@ -11423,7 +11545,7 @@ async function deleteKeyPairFromDB() {
|
|
|
11423
11545
|
};
|
|
11424
11546
|
});
|
|
11425
11547
|
} catch (error) {
|
|
11426
|
-
logger$
|
|
11548
|
+
logger$30.warn("[DPoP] Failed to delete key pair from IndexedDB:", error);
|
|
11427
11549
|
}
|
|
11428
11550
|
}
|
|
11429
11551
|
/**
|
|
@@ -11483,13 +11605,13 @@ var CryptoController = class {
|
|
|
11483
11605
|
this._publicJwk = await crypto.subtle.exportKey("jwk", stored.publicKey);
|
|
11484
11606
|
this._fingerprint = await computeJwkThumbprint(this._publicJwk);
|
|
11485
11607
|
this._initialized = true;
|
|
11486
|
-
logger$
|
|
11608
|
+
logger$30.debug("[DPoP] Key pair restored from IndexedDB, fingerprint:", this._fingerprint);
|
|
11487
11609
|
return this._fingerprint;
|
|
11488
11610
|
} catch (error) {
|
|
11489
|
-
logger$
|
|
11611
|
+
logger$30.warn("[DPoP] Stored key pair unusable, generating new one:", error);
|
|
11490
11612
|
await deleteKeyPairFromDB();
|
|
11491
11613
|
}
|
|
11492
|
-
logger$
|
|
11614
|
+
logger$30.debug("[DPoP] Generating RSA key pair");
|
|
11493
11615
|
this._keyPair = await crypto.subtle.generateKey({
|
|
11494
11616
|
name: "RSASSA-PKCS1-v1_5",
|
|
11495
11617
|
modulusLength: 2048,
|
|
@@ -11504,7 +11626,7 @@ var CryptoController = class {
|
|
|
11504
11626
|
this._fingerprint = await computeJwkThumbprint(this._publicJwk);
|
|
11505
11627
|
this._initialized = true;
|
|
11506
11628
|
await saveKeyPairToDB(this._keyPair);
|
|
11507
|
-
logger$
|
|
11629
|
+
logger$30.debug("[DPoP] Key pair generated and persisted, fingerprint:", this._fingerprint);
|
|
11508
11630
|
return this._fingerprint;
|
|
11509
11631
|
}
|
|
11510
11632
|
/**
|
|
@@ -11570,7 +11692,7 @@ var CryptoController = class {
|
|
|
11570
11692
|
this._fingerprint = null;
|
|
11571
11693
|
this._initialized = false;
|
|
11572
11694
|
deleteKeyPairFromDB();
|
|
11573
|
-
logger$
|
|
11695
|
+
logger$30.debug("[DPoP] Controller destroyed");
|
|
11574
11696
|
}
|
|
11575
11697
|
get publicJwk() {
|
|
11576
11698
|
if (!this._publicJwk) throw new DPoPInitError("CryptoController not initialized. Call init() first.");
|
|
@@ -11594,7 +11716,7 @@ var CryptoController = class {
|
|
|
11594
11716
|
//#endregion
|
|
11595
11717
|
//#region src/controllers/NetworkMonitor.ts
|
|
11596
11718
|
var import_cjs$28 = require_cjs();
|
|
11597
|
-
const logger$
|
|
11719
|
+
const logger$29 = getLogger();
|
|
11598
11720
|
/**
|
|
11599
11721
|
* Safely check whether we are running in a browser environment
|
|
11600
11722
|
* with `window` and the relevant event targets.
|
|
@@ -11651,7 +11773,7 @@ var NetworkMonitor = class extends Destroyable {
|
|
|
11651
11773
|
}
|
|
11652
11774
|
attachListeners() {
|
|
11653
11775
|
if (!hasBrowserNetworkEvents()) {
|
|
11654
|
-
logger$
|
|
11776
|
+
logger$29.debug("NetworkMonitor: no browser environment detected, skipping event listeners");
|
|
11655
11777
|
return;
|
|
11656
11778
|
}
|
|
11657
11779
|
window.addEventListener("online", this._onOnline);
|
|
@@ -11659,7 +11781,7 @@ var NetworkMonitor = class extends Destroyable {
|
|
|
11659
11781
|
const connection = getNetworkConnection();
|
|
11660
11782
|
if (connection) connection.addEventListener("change", this._onConnectionChange);
|
|
11661
11783
|
this._listenersAttached = true;
|
|
11662
|
-
logger$
|
|
11784
|
+
logger$29.debug("NetworkMonitor: event listeners attached");
|
|
11663
11785
|
}
|
|
11664
11786
|
removeListeners() {
|
|
11665
11787
|
if (!this._listenersAttached) return;
|
|
@@ -11670,10 +11792,10 @@ var NetworkMonitor = class extends Destroyable {
|
|
|
11670
11792
|
if (connection) connection.removeEventListener("change", this._onConnectionChange);
|
|
11671
11793
|
}
|
|
11672
11794
|
this._listenersAttached = false;
|
|
11673
|
-
logger$
|
|
11795
|
+
logger$29.debug("NetworkMonitor: event listeners removed");
|
|
11674
11796
|
}
|
|
11675
11797
|
handleOnline() {
|
|
11676
|
-
logger$
|
|
11798
|
+
logger$29.info("NetworkMonitor: browser went online");
|
|
11677
11799
|
this._isOnline$.next(true);
|
|
11678
11800
|
this._networkChange$.next({
|
|
11679
11801
|
type: "online",
|
|
@@ -11682,7 +11804,7 @@ var NetworkMonitor = class extends Destroyable {
|
|
|
11682
11804
|
});
|
|
11683
11805
|
}
|
|
11684
11806
|
handleOffline() {
|
|
11685
|
-
logger$
|
|
11807
|
+
logger$29.info("NetworkMonitor: browser went offline");
|
|
11686
11808
|
this._isOnline$.next(false);
|
|
11687
11809
|
this._networkChange$.next({
|
|
11688
11810
|
type: "offline",
|
|
@@ -11691,7 +11813,7 @@ var NetworkMonitor = class extends Destroyable {
|
|
|
11691
11813
|
}
|
|
11692
11814
|
handleConnectionChange() {
|
|
11693
11815
|
const networkType = getNetworkType();
|
|
11694
|
-
logger$
|
|
11816
|
+
logger$29.info(`NetworkMonitor: connection changed — effectiveType=${networkType ?? "unknown"}`);
|
|
11695
11817
|
this._networkChange$.next({
|
|
11696
11818
|
type: "connection_change",
|
|
11697
11819
|
timestamp: Date.now(),
|
|
@@ -11807,7 +11929,7 @@ function getNavigatorMediaDevices() {
|
|
|
11807
11929
|
//#endregion
|
|
11808
11930
|
//#region src/controllers/PreflightRunner.ts
|
|
11809
11931
|
var import_cjs$27 = require_cjs();
|
|
11810
|
-
const logger$
|
|
11932
|
+
const logger$28 = getLogger();
|
|
11811
11933
|
const DEFAULT_MEDIA_TEST_DURATION_S = 10;
|
|
11812
11934
|
const ICE_GATHERING_TIMEOUT_MS = 1e4;
|
|
11813
11935
|
const SIGNALING_RTT_TIMEOUT_MS = 5e3;
|
|
@@ -11856,7 +11978,7 @@ var PreflightRunner = class extends Destroyable {
|
|
|
11856
11978
|
if (!this._options.skipMediaTest) try {
|
|
11857
11979
|
bandwidth = await this.testMediaBandwidth(destination);
|
|
11858
11980
|
} catch (error) {
|
|
11859
|
-
logger$
|
|
11981
|
+
logger$28.warn("[PreflightRunner] Media bandwidth test failed:", error);
|
|
11860
11982
|
warnings.push("Media bandwidth test failed");
|
|
11861
11983
|
}
|
|
11862
11984
|
return {
|
|
@@ -11868,7 +11990,7 @@ var PreflightRunner = class extends Destroyable {
|
|
|
11868
11990
|
warnings
|
|
11869
11991
|
};
|
|
11870
11992
|
} catch (error) {
|
|
11871
|
-
logger$
|
|
11993
|
+
logger$28.error("[PreflightRunner] Preflight test failed:", error);
|
|
11872
11994
|
throw new PreflightError("preflight", error instanceof Error ? error : new Error(String(error)));
|
|
11873
11995
|
} finally {
|
|
11874
11996
|
this.destroy();
|
|
@@ -11899,7 +12021,7 @@ var PreflightRunner = class extends Destroyable {
|
|
|
11899
12021
|
if (track.kind === "video" && track.readyState === "live") videoWorking = true;
|
|
11900
12022
|
}
|
|
11901
12023
|
} catch (error) {
|
|
11902
|
-
logger$
|
|
12024
|
+
logger$28.warn("[PreflightRunner] Device test failed:", error);
|
|
11903
12025
|
} finally {
|
|
11904
12026
|
if (audioStream) audioStream.getTracks().forEach((t) => t.stop());
|
|
11905
12027
|
}
|
|
@@ -11957,7 +12079,7 @@ var PreflightRunner = class extends Destroyable {
|
|
|
11957
12079
|
rttMs
|
|
11958
12080
|
};
|
|
11959
12081
|
} catch (error) {
|
|
11960
|
-
logger$
|
|
12082
|
+
logger$28.warn("[PreflightRunner] ICE connectivity test failed:", error);
|
|
11961
12083
|
return {
|
|
11962
12084
|
type: "failed",
|
|
11963
12085
|
turnReachable: false,
|
|
@@ -12005,7 +12127,7 @@ var PreflightRunner = class extends Destroyable {
|
|
|
12005
12127
|
//#endregion
|
|
12006
12128
|
//#region src/controllers/VisibilityController.ts
|
|
12007
12129
|
var import_cjs$26 = require_cjs();
|
|
12008
|
-
const logger$
|
|
12130
|
+
const logger$27 = getLogger();
|
|
12009
12131
|
/**
|
|
12010
12132
|
* Checks whether the document visibility API is available.
|
|
12011
12133
|
*/
|
|
@@ -12042,8 +12164,8 @@ var VisibilityController = class extends Destroyable {
|
|
|
12042
12164
|
this._boundHandler = this._handleVisibilityChange.bind(this);
|
|
12043
12165
|
if (this._hasVisibilityApi) {
|
|
12044
12166
|
document.addEventListener("visibilitychange", this._boundHandler);
|
|
12045
|
-
logger$
|
|
12046
|
-
} else logger$
|
|
12167
|
+
logger$27.debug("VisibilityController: listening for visibilitychange events");
|
|
12168
|
+
} else logger$27.debug("VisibilityController: document visibility API not available, defaulting to visible");
|
|
12047
12169
|
}
|
|
12048
12170
|
/**
|
|
12049
12171
|
* Observable of the current visibility state.
|
|
@@ -12068,7 +12190,7 @@ var VisibilityController = class extends Destroyable {
|
|
|
12068
12190
|
destroy() {
|
|
12069
12191
|
if (this._hasVisibilityApi) {
|
|
12070
12192
|
document.removeEventListener("visibilitychange", this._boundHandler);
|
|
12071
|
-
logger$
|
|
12193
|
+
logger$27.debug("VisibilityController: removed visibilitychange listener");
|
|
12072
12194
|
}
|
|
12073
12195
|
super.destroy();
|
|
12074
12196
|
}
|
|
@@ -12086,7 +12208,7 @@ var VisibilityController = class extends Destroyable {
|
|
|
12086
12208
|
timestamp: Date.now()
|
|
12087
12209
|
};
|
|
12088
12210
|
this._visibilityChange$.next(changeEvent);
|
|
12089
|
-
logger$
|
|
12211
|
+
logger$27.debug("VisibilityController: visibility changed", {
|
|
12090
12212
|
from: previousState,
|
|
12091
12213
|
to: newState
|
|
12092
12214
|
});
|
|
@@ -12325,15 +12447,57 @@ const RPCEventAckResponse = (id) => makeRPCResponse({
|
|
|
12325
12447
|
result: {}
|
|
12326
12448
|
});
|
|
12327
12449
|
|
|
12450
|
+
//#endregion
|
|
12451
|
+
//#region src/utils/authRecovery.ts
|
|
12452
|
+
/**
|
|
12453
|
+
* Walk an error's `error`/`cause` chain looking for a {@link JSONRPCError}.
|
|
12454
|
+
* Errors thrown by call creation are wrapped (e.g. `CallCreateError`), so the
|
|
12455
|
+
* underlying signaling error is nested. Bounded by a visited set to guard
|
|
12456
|
+
* against cyclic causes.
|
|
12457
|
+
*/
|
|
12458
|
+
function findJSONRPCError(error) {
|
|
12459
|
+
const seen = /* @__PURE__ */ new Set();
|
|
12460
|
+
let current = error;
|
|
12461
|
+
while (current instanceof Error && !seen.has(current)) {
|
|
12462
|
+
seen.add(current);
|
|
12463
|
+
if (current instanceof JSONRPCError) return current;
|
|
12464
|
+
current = current.error ?? current.cause;
|
|
12465
|
+
}
|
|
12466
|
+
}
|
|
12467
|
+
/**
|
|
12468
|
+
* Whether an error is a session-recoverable authentication failure
|
|
12469
|
+
* (`-32002` authentication failed or `-32003` requester validation failed)
|
|
12470
|
+
* that a credential re-mint + retry can heal.
|
|
12471
|
+
*/
|
|
12472
|
+
function isRecoverableAuthError(error) {
|
|
12473
|
+
const rpcError = findJSONRPCError(error);
|
|
12474
|
+
return rpcError !== void 0 && (rpcError.code === RPC_ERROR_REQUESTER_VALIDATION_FAILED || rpcError.code === RPC_ERROR_AUTHENTICATION_FAILED);
|
|
12475
|
+
}
|
|
12476
|
+
/**
|
|
12477
|
+
* Whether an error is specifically a requester-validation rejection
|
|
12478
|
+
* (`-32003`) — the server refusing the session's credential.
|
|
12479
|
+
*
|
|
12480
|
+
* Narrower than {@link isRecoverableAuthError} on purpose. `-32002` is
|
|
12481
|
+
* overloaded server-side: a rejected reattach arrives as `-32002` with
|
|
12482
|
+
* `cause: INVALID_MSG_UNSPECIFIED` and message `CALL ERROR`, which is a
|
|
12483
|
+
* call-level rejection and says nothing about the credential. Use this where
|
|
12484
|
+
* the decision must not be fooled by that, such as deciding whether retrying
|
|
12485
|
+
* an operation could possibly succeed.
|
|
12486
|
+
*/
|
|
12487
|
+
function isRequesterValidationError(error) {
|
|
12488
|
+
return findJSONRPCError(error)?.code === RPC_ERROR_REQUESTER_VALIDATION_FAILED;
|
|
12489
|
+
}
|
|
12490
|
+
|
|
12328
12491
|
//#endregion
|
|
12329
12492
|
//#region src/managers/AttachManager.ts
|
|
12330
|
-
const logger$
|
|
12493
|
+
const logger$26 = getLogger();
|
|
12331
12494
|
var AttachManager = class {
|
|
12332
|
-
constructor(storage, deviceController, reconnectCallsTimeout, attachKey) {
|
|
12495
|
+
constructor(storage, deviceController, reconnectCallsTimeout, attachKey, credentialRecovered) {
|
|
12333
12496
|
this.storage = storage;
|
|
12334
12497
|
this.deviceController = deviceController;
|
|
12335
12498
|
this.reconnectCallsTimeout = reconnectCallsTimeout;
|
|
12336
12499
|
this.attachKey = attachKey;
|
|
12500
|
+
this.credentialRecovered = credentialRecovered;
|
|
12337
12501
|
this.writeQueue = Promise.resolve();
|
|
12338
12502
|
}
|
|
12339
12503
|
async detachAll() {
|
|
@@ -12348,7 +12512,7 @@ var AttachManager = class {
|
|
|
12348
12512
|
try {
|
|
12349
12513
|
return await this.storage.getItem(this.attachKey) ?? {};
|
|
12350
12514
|
} catch (error) {
|
|
12351
|
-
logger$
|
|
12515
|
+
logger$26.warn("[AttachManager] Failed to retrieve attached calls from storage", error);
|
|
12352
12516
|
return {};
|
|
12353
12517
|
}
|
|
12354
12518
|
}
|
|
@@ -12356,7 +12520,7 @@ var AttachManager = class {
|
|
|
12356
12520
|
try {
|
|
12357
12521
|
await this.storage.setItem(this.attachKey, attached);
|
|
12358
12522
|
} catch (error) {
|
|
12359
|
-
logger$
|
|
12523
|
+
logger$26.warn("[AttachManager] Failed to write attached calls to storage", error);
|
|
12360
12524
|
}
|
|
12361
12525
|
}
|
|
12362
12526
|
/**
|
|
@@ -12375,11 +12539,39 @@ var AttachManager = class {
|
|
|
12375
12539
|
}
|
|
12376
12540
|
async attach(call) {
|
|
12377
12541
|
if (!call.to) {
|
|
12378
|
-
logger$
|
|
12542
|
+
logger$26.warn("[AttachManager] Skip attach for calls with no destination");
|
|
12379
12543
|
return;
|
|
12380
12544
|
}
|
|
12545
|
+
const attachment = this.buildAttachment(call, call.to);
|
|
12546
|
+
await this.mutate((attached) => ({
|
|
12547
|
+
...attached,
|
|
12548
|
+
[call.id]: attachment
|
|
12549
|
+
}));
|
|
12550
|
+
}
|
|
12551
|
+
/**
|
|
12552
|
+
* Keep an already-stored call's reference alive and current — the periodic
|
|
12553
|
+
* refresh the `verto.ping` keepalive drives.
|
|
12554
|
+
*
|
|
12555
|
+
* Only ever updates: a call with no record is one nothing wants reattached,
|
|
12556
|
+
* and re-creating it here would undo a `detach`. That matters because a ping
|
|
12557
|
+
* can land in the window between `bye()` detaching and the call being torn
|
|
12558
|
+
* down, and a record revived there survives the hangup — so the next page
|
|
12559
|
+
* load dials a call nobody is on. The existence check and the write share
|
|
12560
|
+
* one {@link mutate} turn, so a concurrent detach cannot slip between them.
|
|
12561
|
+
*/
|
|
12562
|
+
async refresh(call) {
|
|
12563
|
+
if (!call.to) return;
|
|
12381
12564
|
const destination = call.to;
|
|
12382
|
-
|
|
12565
|
+
await this.mutate((attached) => {
|
|
12566
|
+
if (!Object.hasOwn(attached, call.id)) return attached;
|
|
12567
|
+
return {
|
|
12568
|
+
...attached,
|
|
12569
|
+
[call.id]: this.buildAttachment(call, destination)
|
|
12570
|
+
};
|
|
12571
|
+
});
|
|
12572
|
+
}
|
|
12573
|
+
buildAttachment(call, destination) {
|
|
12574
|
+
return {
|
|
12383
12575
|
nodeId: call.nodeId,
|
|
12384
12576
|
destination,
|
|
12385
12577
|
mediaDirections: call.mediaDirections,
|
|
@@ -12387,10 +12579,6 @@ var AttachManager = class {
|
|
|
12387
12579
|
videoInputDevice: call.mediaDirections.video !== "inactive" ? this.deviceController.selectedVideoInputDevice : null,
|
|
12388
12580
|
attachedAt: Date.now()
|
|
12389
12581
|
};
|
|
12390
|
-
await this.mutate((attached) => ({
|
|
12391
|
-
...attached,
|
|
12392
|
-
[call.id]: attachment
|
|
12393
|
-
}));
|
|
12394
12582
|
}
|
|
12395
12583
|
async detach(call) {
|
|
12396
12584
|
await this.mutate((attached) => {
|
|
@@ -12413,8 +12601,14 @@ var AttachManager = class {
|
|
|
12413
12601
|
* rejecting. Once that fix is deployed, this will work for both
|
|
12414
12602
|
* page reloads and WebSocket reconnects.
|
|
12415
12603
|
*
|
|
12416
|
-
*
|
|
12417
|
-
*
|
|
12604
|
+
* A failed reattach does NOT generally cost the stored reference. It is
|
|
12605
|
+
* discarded only when the server denied the reattach on a session whose
|
|
12606
|
+
* credential it had already accepted — a verified reauthentication followed
|
|
12607
|
+
* by a refusal is the server saying the call is gone, and that is the one
|
|
12608
|
+
* refusal worth acting on. Until then the credential may be what is being
|
|
12609
|
+
* refused, and the record is the only way a later reload can try again;
|
|
12610
|
+
* keeping it costs nothing, since `detachExpired` reaps it once it is older
|
|
12611
|
+
* than `reconnectCallsTimeout`.
|
|
12418
12612
|
*/
|
|
12419
12613
|
async reattachCalls() {
|
|
12420
12614
|
const attached = await this.readAttached();
|
|
@@ -12423,25 +12617,31 @@ var AttachManager = class {
|
|
|
12423
12617
|
const { destination } = attachment;
|
|
12424
12618
|
const options = this.buildCallOptions(attachment);
|
|
12425
12619
|
let succeeded = false;
|
|
12620
|
+
let refusedOnCredentials = false;
|
|
12426
12621
|
for (let attempt = 1; attempt <= 3; attempt++) try {
|
|
12427
12622
|
await this.session.createOutboundCall(destination, {
|
|
12428
12623
|
callId,
|
|
12429
12624
|
...options
|
|
12430
12625
|
});
|
|
12431
|
-
logger$
|
|
12626
|
+
logger$26.info(`[AttachManager] Reattached call ${callId} (attempt ${attempt})`);
|
|
12432
12627
|
succeeded = true;
|
|
12433
12628
|
break;
|
|
12434
12629
|
} catch (error) {
|
|
12435
|
-
logger$
|
|
12630
|
+
logger$26.warn(`[AttachManager] Reattach attempt ${attempt}/3 failed for call ${callId}:`, error);
|
|
12631
|
+
if (isRequesterValidationError(error)) {
|
|
12632
|
+
refusedOnCredentials = true;
|
|
12633
|
+
logger$26.warn(`[AttachManager] Reattach of ${callId} was refused on credentials; not retrying.`);
|
|
12634
|
+
break;
|
|
12635
|
+
}
|
|
12436
12636
|
if (attempt < 3) await new Promise((r) => setTimeout(r, (attempt + 1) * 1e3));
|
|
12437
12637
|
}
|
|
12438
|
-
if (!succeeded) {
|
|
12439
|
-
logger$
|
|
12638
|
+
if (!succeeded) if (this.credentialRecovered() && !refusedOnCredentials) {
|
|
12639
|
+
logger$26.warn(`[AttachManager] Reattach of ${callId} was denied after a verified reauthentication, removing reference`);
|
|
12440
12640
|
await this.detach({
|
|
12441
12641
|
id: callId,
|
|
12442
12642
|
mediaDirections: attachment.mediaDirections
|
|
12443
12643
|
});
|
|
12444
|
-
}
|
|
12644
|
+
} else logger$26.warn(`[AttachManager] Reattach failed for call ${callId}; keeping the reference (credential refused or never proven good)`);
|
|
12445
12645
|
}
|
|
12446
12646
|
}
|
|
12447
12647
|
/**
|
|
@@ -12542,12 +12742,12 @@ var require_race = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
12542
12742
|
exports.race = void 0;
|
|
12543
12743
|
var argsOrArgArray_1 = require_argsOrArgArray();
|
|
12544
12744
|
var raceWith_1$1 = require_raceWith();
|
|
12545
|
-
function race$
|
|
12745
|
+
function race$3() {
|
|
12546
12746
|
var args = [];
|
|
12547
12747
|
for (var _i = 0; _i < arguments.length; _i++) args[_i] = arguments[_i];
|
|
12548
12748
|
return raceWith_1$1.raceWith.apply(void 0, __spreadArray([], __read(argsOrArgArray_1.argsOrArgArray(args))));
|
|
12549
12749
|
}
|
|
12550
|
-
exports.race = race$
|
|
12750
|
+
exports.race = race$3;
|
|
12551
12751
|
}));
|
|
12552
12752
|
|
|
12553
12753
|
//#endregion
|
|
@@ -13607,7 +13807,7 @@ function toggleHandraiseMethod(is) {
|
|
|
13607
13807
|
|
|
13608
13808
|
//#endregion
|
|
13609
13809
|
//#region src/core/entities/Participant.ts
|
|
13610
|
-
const logger$
|
|
13810
|
+
const logger$25 = getLogger();
|
|
13611
13811
|
const initialState = {};
|
|
13612
13812
|
/**
|
|
13613
13813
|
* Represents a participant in a call.
|
|
@@ -13617,9 +13817,9 @@ const initialState = {};
|
|
|
13617
13817
|
* the local participant with additional device control.
|
|
13618
13818
|
*/
|
|
13619
13819
|
var Participant = class extends Destroyable {
|
|
13620
|
-
constructor(id,
|
|
13820
|
+
constructor(id, callExecuteMethod, deviceController) {
|
|
13621
13821
|
super();
|
|
13622
|
-
this.
|
|
13822
|
+
this.callExecuteMethod = callExecuteMethod;
|
|
13623
13823
|
this.deviceController = deviceController;
|
|
13624
13824
|
this._state$ = this.createBehaviorSubject(initialState);
|
|
13625
13825
|
this.id = id;
|
|
@@ -13841,22 +14041,55 @@ var Participant = class extends Destroyable {
|
|
|
13841
14041
|
get value() {
|
|
13842
14042
|
return this._state$.value;
|
|
13843
14043
|
}
|
|
14044
|
+
/**
|
|
14045
|
+
* Target triple for member RPCs, built from the participant's own state.
|
|
14046
|
+
* The backend locates the member's session by the target `call_id`/`node_id`,
|
|
14047
|
+
* so this must always be the participant's own call context — never the
|
|
14048
|
+
* local call's id (issue #19400).
|
|
14049
|
+
*
|
|
14050
|
+
* Reading it doubles as a readiness probe: it throws until the first full
|
|
14051
|
+
* member event (`member.joined`/`member.updated` or the `call.joined`
|
|
14052
|
+
* roster) arrives, and never regresses afterwards.
|
|
14053
|
+
*
|
|
14054
|
+
* @throws {ParticipantNotReadyError} If the member state has not been
|
|
14055
|
+
* received yet (e.g. a participant first seen via `member.talking`) — an
|
|
14056
|
+
* empty call context can never address the member, so fail fast instead of
|
|
14057
|
+
* sending a doomed RPC.
|
|
14058
|
+
*/
|
|
14059
|
+
get target() {
|
|
14060
|
+
const { call_id, node_id } = this._state$.value;
|
|
14061
|
+
if (!call_id || !node_id) throw new ParticipantNotReadyError(this.id);
|
|
14062
|
+
return {
|
|
14063
|
+
member_id: this.id,
|
|
14064
|
+
call_id,
|
|
14065
|
+
node_id
|
|
14066
|
+
};
|
|
14067
|
+
}
|
|
14068
|
+
/**
|
|
14069
|
+
* Executes a member RPC against this participant, injecting its own
|
|
14070
|
+
* {@link target} as the target.
|
|
14071
|
+
*
|
|
14072
|
+
* @throws {ParticipantNotReadyError} Via {@link target}, when the
|
|
14073
|
+
* member state has not been received yet.
|
|
14074
|
+
*/
|
|
14075
|
+
async executeMethod(method, args) {
|
|
14076
|
+
return this.callExecuteMethod(this.target, method, args);
|
|
14077
|
+
}
|
|
13844
14078
|
/** Toggles the deafened state (mutes/unmutes incoming audio). */
|
|
13845
14079
|
async toggleDeaf() {
|
|
13846
|
-
|
|
13847
|
-
await this.executeMethod(this.id, method, {});
|
|
14080
|
+
await this.executeMethod(toggleDeafMethod(this.deaf), {});
|
|
13848
14081
|
}
|
|
13849
14082
|
/** Toggles the hand-raised state. */
|
|
13850
14083
|
async toggleHandraise() {
|
|
13851
|
-
await this.executeMethod(
|
|
14084
|
+
await this.executeMethod(toggleHandraiseMethod(this.handraised), {});
|
|
13852
14085
|
}
|
|
13853
14086
|
/** Mutes the participant's audio. */
|
|
13854
14087
|
async mute() {
|
|
13855
|
-
await this.executeMethod(
|
|
14088
|
+
await this.executeMethod("call.mute", { channels: ["audio"] });
|
|
13856
14089
|
}
|
|
13857
14090
|
/** Unmutes the participant's audio. */
|
|
13858
14091
|
async unmute() {
|
|
13859
|
-
await this.executeMethod(
|
|
14092
|
+
await this.executeMethod("call.unmute", { channels: ["audio"] });
|
|
13860
14093
|
}
|
|
13861
14094
|
/** Toggles the participant's audio mute state. */
|
|
13862
14095
|
async toggleMute() {
|
|
@@ -13864,11 +14097,11 @@ var Participant = class extends Destroyable {
|
|
|
13864
14097
|
}
|
|
13865
14098
|
/** Mutes the participant's video. */
|
|
13866
14099
|
async muteVideo() {
|
|
13867
|
-
await this.executeMethod(
|
|
14100
|
+
await this.executeMethod("call.mute", { channels: ["video"] });
|
|
13868
14101
|
}
|
|
13869
14102
|
/** Unmutes the participant's video. */
|
|
13870
14103
|
async unmuteVideo() {
|
|
13871
|
-
await this.executeMethod(
|
|
14104
|
+
await this.executeMethod("call.unmute", { channels: ["video"] });
|
|
13872
14105
|
}
|
|
13873
14106
|
/** Toggles the participant's video mute state. */
|
|
13874
14107
|
async toggleMuteVideo() {
|
|
@@ -13876,7 +14109,7 @@ var Participant = class extends Destroyable {
|
|
|
13876
14109
|
}
|
|
13877
14110
|
/** Toggles echo cancellation on the audio input. */
|
|
13878
14111
|
async toggleEchoCancellation() {
|
|
13879
|
-
await this.executeMethod(
|
|
14112
|
+
await this.executeMethod("call.audioflags.set", {
|
|
13880
14113
|
echo_cancellation: !this.echoCancellation,
|
|
13881
14114
|
auto_gain: this.autoGain,
|
|
13882
14115
|
noise_suppression: this.noiseSuppression
|
|
@@ -13884,7 +14117,7 @@ var Participant = class extends Destroyable {
|
|
|
13884
14117
|
}
|
|
13885
14118
|
/** Toggles automatic gain control on the audio input. */
|
|
13886
14119
|
async toggleAudioInputAutoGain() {
|
|
13887
|
-
await this.executeMethod(
|
|
14120
|
+
await this.executeMethod("call.audioflags.set", {
|
|
13888
14121
|
echo_cancellation: this.echoCancellation,
|
|
13889
14122
|
auto_gain: !this.autoGain,
|
|
13890
14123
|
noise_suppression: this.noiseSuppression
|
|
@@ -13892,7 +14125,7 @@ var Participant = class extends Destroyable {
|
|
|
13892
14125
|
}
|
|
13893
14126
|
/** Toggles noise suppression on the audio input. */
|
|
13894
14127
|
async toggleNoiseSuppression() {
|
|
13895
|
-
await this.executeMethod(
|
|
14128
|
+
await this.executeMethod("call.audioflags.set", {
|
|
13896
14129
|
echo_cancellation: this.echoCancellation,
|
|
13897
14130
|
auto_gain: this.autoGain,
|
|
13898
14131
|
noise_suppression: !this.noiseSuppression
|
|
@@ -13900,7 +14133,7 @@ var Participant = class extends Destroyable {
|
|
|
13900
14133
|
}
|
|
13901
14134
|
/** Toggles low-bitrate mode for this participant's media. */
|
|
13902
14135
|
async toggleLowbitrate() {
|
|
13903
|
-
await this.executeMethod(
|
|
14136
|
+
await this.executeMethod("call.lowbitrate.set", { lowbitrate: !this.lowbitrate });
|
|
13904
14137
|
}
|
|
13905
14138
|
/**
|
|
13906
14139
|
* Adjusts the **conference-only** microphone energy gate / sensitivity level
|
|
@@ -13916,7 +14149,7 @@ var Participant = class extends Destroyable {
|
|
|
13916
14149
|
* (integer, larger values are more sensitive).
|
|
13917
14150
|
*/
|
|
13918
14151
|
async setAudioInputSensitivity(value) {
|
|
13919
|
-
await this.executeMethod(
|
|
14152
|
+
await this.executeMethod("call.microphone.sensitivity.set", { sensitivity: value });
|
|
13920
14153
|
}
|
|
13921
14154
|
/**
|
|
13922
14155
|
* Sets the **server-side** microphone volume on this participant's bridged
|
|
@@ -13929,7 +14162,7 @@ var Participant = class extends Destroyable {
|
|
|
13929
14162
|
* @param value - Volume level (0-100).
|
|
13930
14163
|
*/
|
|
13931
14164
|
async setAudioInputVolume(value) {
|
|
13932
|
-
await this.executeMethod(
|
|
14165
|
+
await this.executeMethod("call.microphone.volume.set", { volume: value });
|
|
13933
14166
|
}
|
|
13934
14167
|
/**
|
|
13935
14168
|
* Sets the **server-side** speaker volume on this participant's bridged call
|
|
@@ -13943,45 +14176,31 @@ var Participant = class extends Destroyable {
|
|
|
13943
14176
|
* @param value - Volume level (0-100).
|
|
13944
14177
|
*/
|
|
13945
14178
|
async setAudioOutputVolume(value) {
|
|
13946
|
-
await this.executeMethod(
|
|
14179
|
+
await this.executeMethod("call.speaker.volume.set", { volume: value });
|
|
13947
14180
|
}
|
|
13948
14181
|
/**
|
|
13949
14182
|
* Sets the participant's position in the video layout.
|
|
13950
14183
|
*
|
|
13951
|
-
* Requires the `member.position` capability. The gateway
|
|
13952
|
-
*
|
|
13953
|
-
*
|
|
13954
|
-
*
|
|
13955
|
-
* guarantee a visible change: the backend silently returns `200` (no-op) for
|
|
13956
|
-
* non-conference targets.
|
|
14184
|
+
* Requires the `member.position` capability. The gateway requires a
|
|
14185
|
+
* `targets` array of `{ target, position }` entries (issue #19400). A
|
|
14186
|
+
* resolved promise does not guarantee a visible change: the backend silently
|
|
14187
|
+
* returns `200` (no-op) for non-conference targets.
|
|
13957
14188
|
*
|
|
13958
14189
|
* @param value - The {@link VideoPosition} to assign (e.g. `'auto'`, `'reserved-0'`).
|
|
13959
14190
|
*/
|
|
13960
14191
|
async setPosition(value) {
|
|
13961
|
-
|
|
13962
|
-
|
|
13963
|
-
member_id: this.id,
|
|
13964
|
-
call_id: state.call_id ?? "",
|
|
13965
|
-
node_id: state.node_id ?? ""
|
|
13966
|
-
};
|
|
13967
|
-
await this.executeMethod(target, "call.member.position.set", { targets: [{
|
|
13968
|
-
target,
|
|
14192
|
+
await this.executeMethod("call.member.position.set", { targets: [{
|
|
14193
|
+
target: this.target,
|
|
13969
14194
|
position: value
|
|
13970
14195
|
}] });
|
|
13971
14196
|
}
|
|
13972
14197
|
/** Removes this participant from the call. */
|
|
13973
14198
|
async remove() {
|
|
13974
|
-
|
|
13975
|
-
const target = {
|
|
13976
|
-
member_id: this.id,
|
|
13977
|
-
call_id: state.call_id ?? "",
|
|
13978
|
-
node_id: state.node_id ?? ""
|
|
13979
|
-
};
|
|
13980
|
-
await this.executeMethod(target, "call.member.remove", {});
|
|
14199
|
+
await this.executeMethod("call.member.remove", { targets: [this.target] });
|
|
13981
14200
|
}
|
|
13982
14201
|
/** Ends the call for this participant. */
|
|
13983
14202
|
async end() {
|
|
13984
|
-
await this.executeMethod(
|
|
14203
|
+
await this.executeMethod("call.end", {});
|
|
13985
14204
|
}
|
|
13986
14205
|
/**
|
|
13987
14206
|
* Replaces custom metadata for this participant.
|
|
@@ -14001,7 +14220,7 @@ var Participant = class extends Destroyable {
|
|
|
14001
14220
|
}
|
|
14002
14221
|
/** Destroys the participant, releasing all subscriptions and references. */
|
|
14003
14222
|
destroy() {
|
|
14004
|
-
this.
|
|
14223
|
+
this.callExecuteMethod = void 0;
|
|
14005
14224
|
super.destroy();
|
|
14006
14225
|
}
|
|
14007
14226
|
};
|
|
@@ -14013,8 +14232,8 @@ var Participant = class extends Destroyable {
|
|
|
14013
14232
|
*/
|
|
14014
14233
|
var SelfParticipant = class extends Participant {
|
|
14015
14234
|
/** @internal */
|
|
14016
|
-
constructor(id,
|
|
14017
|
-
super(id,
|
|
14235
|
+
constructor(id, callExecuteMethod, vertoManager, deviceController) {
|
|
14236
|
+
super(id, callExecuteMethod, deviceController);
|
|
14018
14237
|
this.vertoManager = vertoManager;
|
|
14019
14238
|
this._studioAudio$ = this.createBehaviorSubject(false);
|
|
14020
14239
|
this.capabilities = new SelfCapabilities();
|
|
@@ -14038,7 +14257,7 @@ var SelfParticipant = class extends Participant {
|
|
|
14038
14257
|
async enableStudioAudio() {
|
|
14039
14258
|
if (this._studioAudio$.value) return;
|
|
14040
14259
|
this._studioAudio$.next(true);
|
|
14041
|
-
await this.executeMethod(
|
|
14260
|
+
await this.executeMethod("call.audioflags.set", {
|
|
14042
14261
|
echo_cancellation: false,
|
|
14043
14262
|
auto_gain: false,
|
|
14044
14263
|
noise_suppression: false
|
|
@@ -14051,18 +14270,37 @@ var SelfParticipant = class extends Participant {
|
|
|
14051
14270
|
async disableStudioAudio() {
|
|
14052
14271
|
if (!this._studioAudio$.value) return;
|
|
14053
14272
|
this._studioAudio$.next(false);
|
|
14054
|
-
await this.executeMethod(
|
|
14273
|
+
await this.executeMethod("call.audioflags.set", {
|
|
14055
14274
|
echo_cancellation: true,
|
|
14056
14275
|
auto_gain: true,
|
|
14057
14276
|
noise_suppression: true
|
|
14058
14277
|
});
|
|
14059
14278
|
}
|
|
14060
|
-
/**
|
|
14061
|
-
|
|
14279
|
+
/**
|
|
14280
|
+
* Starts sharing the local screen.
|
|
14281
|
+
*
|
|
14282
|
+
* A call carries at most one screen share. Read `screenShareStatus` before
|
|
14283
|
+
* calling and treat `'starting'`/`'stopping'` as busy.
|
|
14284
|
+
*
|
|
14285
|
+
* The call is unaffected when acquisition fails.
|
|
14286
|
+
*
|
|
14287
|
+
* @param options - Pass `{ audio: true }` to also request the shared
|
|
14288
|
+
* surface's audio. Defaults to video only.
|
|
14289
|
+
* @throws {ScreenShareAlreadyActiveError} When this call is already
|
|
14290
|
+
* sharing a screen. Call {@link stopScreenShare} before starting another.
|
|
14291
|
+
* @throws {AuxiliaryLegCancelledError} When {@link stopScreenShare} removes
|
|
14292
|
+
* the share before its leg finishes connecting.
|
|
14293
|
+
* @throws The raw `getDisplayMedia` error. A dismissed picker or a
|
|
14294
|
+
* permission denial rejects with a `NotAllowedError` `DOMException` —
|
|
14295
|
+
* inspect `error.name` to tell benign cancels apart from real failures.
|
|
14296
|
+
*/
|
|
14297
|
+
async startScreenShare(options) {
|
|
14062
14298
|
try {
|
|
14063
|
-
await this.vertoManager.addScreenMedia();
|
|
14299
|
+
await this.vertoManager.addScreenMedia(options);
|
|
14064
14300
|
} catch (error) {
|
|
14065
|
-
logger$
|
|
14301
|
+
if (error instanceof AuxiliaryLegCancelledError) logger$25.debug("[Participant.startScreenShare] Screen share cancelled before connecting.");
|
|
14302
|
+
else logger$25.error("[Participant.startScreenShare] Screen share error:", error);
|
|
14303
|
+
throw error;
|
|
14066
14304
|
}
|
|
14067
14305
|
}
|
|
14068
14306
|
/** Observable of the current screen share status. */
|
|
@@ -14077,12 +14315,24 @@ var SelfParticipant = class extends Participant {
|
|
|
14077
14315
|
async stopScreenShare() {
|
|
14078
14316
|
return this.vertoManager.removeScreenMedia();
|
|
14079
14317
|
}
|
|
14080
|
-
/**
|
|
14318
|
+
/**
|
|
14319
|
+
* Adds an additional media input device to the call.
|
|
14320
|
+
*
|
|
14321
|
+
* The call is unaffected when acquisition fails.
|
|
14322
|
+
*
|
|
14323
|
+
* @throws {AuxiliaryLegCancelledError} When {@link removeAdditionalDevice}
|
|
14324
|
+
* removes the device before its leg finishes connecting.
|
|
14325
|
+
* @throws The raw `getUserMedia` error (e.g. `NotAllowedError` on
|
|
14326
|
+
* permission denial) — inspect `error.name` to decide how to react — or
|
|
14327
|
+
* `AuxiliaryLegTimeoutError` if the leg does not connect in time.
|
|
14328
|
+
*/
|
|
14081
14329
|
async addAdditionalDevice(options) {
|
|
14082
14330
|
try {
|
|
14083
14331
|
await this.vertoManager.addInputDevice(options);
|
|
14084
14332
|
} catch (error) {
|
|
14085
|
-
logger$
|
|
14333
|
+
if (error instanceof AuxiliaryLegCancelledError) logger$25.debug("[Participant.addAdditionalDevice] Device removed before connecting.");
|
|
14334
|
+
else logger$25.error("[Participant.addAdditionalDevice] Additional device error:", error);
|
|
14335
|
+
throw error;
|
|
14086
14336
|
}
|
|
14087
14337
|
}
|
|
14088
14338
|
/** Removes an additional media input device by ID. */
|
|
@@ -14116,22 +14366,31 @@ var SelfParticipant = class extends Participant {
|
|
|
14116
14366
|
this.deviceController.selectAudioInputDevice(device);
|
|
14117
14367
|
if (options.savePreference) PreferencesContainer.instance.preferredAudioInput = device;
|
|
14118
14368
|
}
|
|
14119
|
-
/**
|
|
14369
|
+
/**
|
|
14370
|
+
* Updates the audio input track constraints for the active call.
|
|
14371
|
+
* @returns whether the constraints reached the media the call is sending.
|
|
14372
|
+
*/
|
|
14120
14373
|
async setAudioInputDeviceConstraints(constraints) {
|
|
14121
|
-
|
|
14374
|
+
return this.vertoManager.updateMediaConstraints({ audio: constraints });
|
|
14122
14375
|
}
|
|
14123
|
-
/**
|
|
14376
|
+
/**
|
|
14377
|
+
* Updates both audio and video input track constraints for the active call.
|
|
14378
|
+
* @returns whether both kinds took the constraints.
|
|
14379
|
+
*/
|
|
14124
14380
|
async setInputDevicesConstraints(constraints) {
|
|
14125
|
-
|
|
14381
|
+
return this.vertoManager.updateMediaConstraints(constraints);
|
|
14126
14382
|
}
|
|
14127
14383
|
/** Selects the video input device for future calls. Optionally saves as a preference. */
|
|
14128
14384
|
selectVideoInputDevice(device, options = {}) {
|
|
14129
14385
|
this.deviceController.selectVideoInputDevice(device);
|
|
14130
14386
|
if (options.savePreference) PreferencesContainer.instance.preferredVideoInput = device;
|
|
14131
14387
|
}
|
|
14132
|
-
/**
|
|
14388
|
+
/**
|
|
14389
|
+
* Updates the video input track constraints for the active call.
|
|
14390
|
+
* @returns whether the constraints reached the media the call is sending.
|
|
14391
|
+
*/
|
|
14133
14392
|
async setVideoInputDeviceConstraints(constraints) {
|
|
14134
|
-
|
|
14393
|
+
return this.vertoManager.updateMediaConstraints({ video: constraints });
|
|
14135
14394
|
}
|
|
14136
14395
|
/** Selects the audio output device. Optionally saves as a preference. */
|
|
14137
14396
|
selectAudioOutputDevice(device, options = {}) {
|
|
@@ -14144,7 +14403,7 @@ var SelfParticipant = class extends Participant {
|
|
|
14144
14403
|
*/
|
|
14145
14404
|
exitStudioModeIfActive() {
|
|
14146
14405
|
if (this._studioAudio$.value) {
|
|
14147
|
-
logger$
|
|
14406
|
+
logger$25.debug("[SelfParticipant] Exiting studio audio mode due to individual flag toggle");
|
|
14148
14407
|
this._studioAudio$.next(false);
|
|
14149
14408
|
}
|
|
14150
14409
|
}
|
|
@@ -14168,7 +14427,7 @@ var SelfParticipant = class extends Participant {
|
|
|
14168
14427
|
try {
|
|
14169
14428
|
await super.mute();
|
|
14170
14429
|
} catch (error) {
|
|
14171
|
-
logger$
|
|
14430
|
+
logger$25.warn("[Participant.toggleAudioInput] Server Error while muting audio input, proceeding with local toggle anyway", error);
|
|
14172
14431
|
} finally {
|
|
14173
14432
|
this.vertoManager.muteMainAudioInputDevice();
|
|
14174
14433
|
}
|
|
@@ -14178,7 +14437,7 @@ var SelfParticipant = class extends Participant {
|
|
|
14178
14437
|
try {
|
|
14179
14438
|
await super.unmute();
|
|
14180
14439
|
} catch (error) {
|
|
14181
|
-
logger$
|
|
14440
|
+
logger$25.warn("[Participant.toggleAudioInput] Server Error while unmuting audio input, proceeding with local toggle anyway", error);
|
|
14182
14441
|
} finally {
|
|
14183
14442
|
await this.vertoManager.unmuteMainAudioInputDevice();
|
|
14184
14443
|
}
|
|
@@ -14188,7 +14447,7 @@ var SelfParticipant = class extends Participant {
|
|
|
14188
14447
|
try {
|
|
14189
14448
|
await super.muteVideo();
|
|
14190
14449
|
} catch (error) {
|
|
14191
|
-
logger$
|
|
14450
|
+
logger$25.warn("[Participant.toggleVideoInput] Server Error while muting video input, proceeding with local toggle anyway", error);
|
|
14192
14451
|
} finally {
|
|
14193
14452
|
this.vertoManager.muteMainVideoInputDevice();
|
|
14194
14453
|
}
|
|
@@ -14198,7 +14457,7 @@ var SelfParticipant = class extends Participant {
|
|
|
14198
14457
|
try {
|
|
14199
14458
|
await super.unmuteVideo();
|
|
14200
14459
|
} catch (error) {
|
|
14201
|
-
logger$
|
|
14460
|
+
logger$25.warn("[Participant.toggleVideoInput] Server Error while unmuting video input, proceeding with local toggle anyway", error);
|
|
14202
14461
|
} finally {
|
|
14203
14462
|
await this.vertoManager.unmuteMainVideoInputDevice();
|
|
14204
14463
|
}
|
|
@@ -14403,7 +14662,7 @@ function filterAs(predicate, resultPath) {
|
|
|
14403
14662
|
//#endregion
|
|
14404
14663
|
//#region src/operators/throwOnRPCError.ts
|
|
14405
14664
|
var import_cjs$21 = require_cjs();
|
|
14406
|
-
const logger$
|
|
14665
|
+
const logger$24 = getLogger();
|
|
14407
14666
|
/**
|
|
14408
14667
|
* RxJS operator that throws a {@link JSONRPCError} when the RPC response contains an error.
|
|
14409
14668
|
* Passes successful responses through unchanged.
|
|
@@ -14411,14 +14670,14 @@ const logger$23 = getLogger();
|
|
|
14411
14670
|
function throwOnRPCError() {
|
|
14412
14671
|
return (0, import_cjs$21.map)((response) => {
|
|
14413
14672
|
if (response.error) {
|
|
14414
|
-
logger$
|
|
14673
|
+
logger$24.error("[throwOnRPCError] RPC error response:", {
|
|
14415
14674
|
code: response.error.code,
|
|
14416
14675
|
message: response.error.message,
|
|
14417
14676
|
data: response.error.data
|
|
14418
14677
|
});
|
|
14419
14678
|
throw new JSONRPCError(response.error.code, response.error.message, response.error.data);
|
|
14420
14679
|
}
|
|
14421
|
-
logger$
|
|
14680
|
+
logger$24.debug("[throwOnRPCError] RPC successful response:", response);
|
|
14422
14681
|
return response;
|
|
14423
14682
|
});
|
|
14424
14683
|
}
|
|
@@ -14426,7 +14685,7 @@ function throwOnRPCError() {
|
|
|
14426
14685
|
//#endregion
|
|
14427
14686
|
//#region src/managers/CallEventsManager.ts
|
|
14428
14687
|
var import_cjs$20 = require_cjs();
|
|
14429
|
-
const logger$
|
|
14688
|
+
const logger$23 = getLogger();
|
|
14430
14689
|
const initialSessionState = {};
|
|
14431
14690
|
/** @internal */
|
|
14432
14691
|
var CallEventsManager = class extends Destroyable {
|
|
@@ -14530,7 +14789,7 @@ var CallEventsManager = class extends Destroyable {
|
|
|
14530
14789
|
}
|
|
14531
14790
|
initSubscriptions() {
|
|
14532
14791
|
this.subscribeTo(this.callJoinedEvent$, (callJoinedEvent) => {
|
|
14533
|
-
logger$
|
|
14792
|
+
logger$23.debug("[CallEventsManager] Handling call.joined event for call/session IDs:", {
|
|
14534
14793
|
callId: callJoinedEvent.call_id,
|
|
14535
14794
|
roomSessionId: callJoinedEvent.room_session_id
|
|
14536
14795
|
});
|
|
@@ -14557,19 +14816,19 @@ var CallEventsManager = class extends Destroyable {
|
|
|
14557
14816
|
if (this._self$.value?.capabilities.setLayout) this.updateLayouts();
|
|
14558
14817
|
});
|
|
14559
14818
|
this.subscribeTo(this.memberUpdates$, (member) => {
|
|
14560
|
-
logger$
|
|
14819
|
+
logger$23.debug("[CallEventsManager] Handling member update event for member ID:", member);
|
|
14561
14820
|
this.upsertParticipant(member);
|
|
14562
14821
|
});
|
|
14563
14822
|
this.subscribeTo(this.webRtcCallSession.memberLeft$, (memberLeftEvent) => {
|
|
14564
|
-
logger$
|
|
14823
|
+
logger$23.debug("[CallEventsManager] Handling member.left event for member ID:", memberLeftEvent.member.member_id);
|
|
14565
14824
|
const participants = { ...this._participants$.value };
|
|
14566
14825
|
if (memberLeftEvent.member.member_id in participants) {
|
|
14567
14826
|
delete participants[memberLeftEvent.member.member_id];
|
|
14568
14827
|
this._participants$.next(participants);
|
|
14569
|
-
} else logger$
|
|
14828
|
+
} else logger$23.warn(`[CallEventsManager] Received member.left event for unknown member ID: ${memberLeftEvent.member.member_id}`);
|
|
14570
14829
|
});
|
|
14571
14830
|
this.subscribeTo(this.webRtcCallSession.callUpdated$, (callUpdatedEvent) => {
|
|
14572
|
-
logger$
|
|
14831
|
+
logger$23.debug("[CallEventsManager] Handling call.updated event:", callUpdatedEvent);
|
|
14573
14832
|
const roomSession = callUpdatedEvent.room_session;
|
|
14574
14833
|
this._sessionState$.next({
|
|
14575
14834
|
...this._sessionState$.value,
|
|
@@ -14584,7 +14843,7 @@ var CallEventsManager = class extends Destroyable {
|
|
|
14584
14843
|
});
|
|
14585
14844
|
});
|
|
14586
14845
|
this.subscribeTo(this.layoutChangedEvent$, (layoutChangedEvent) => {
|
|
14587
|
-
logger$
|
|
14846
|
+
logger$23.debug("[CallEventsManager] Handling layout.changed event:", layoutChangedEvent);
|
|
14588
14847
|
this._sessionState$.next({
|
|
14589
14848
|
...this._sessionState$.value,
|
|
14590
14849
|
layout_name: layoutChangedEvent.id,
|
|
@@ -14594,10 +14853,10 @@ var CallEventsManager = class extends Destroyable {
|
|
|
14594
14853
|
});
|
|
14595
14854
|
}
|
|
14596
14855
|
updateParticipantPositions(layoutChangedEvent) {
|
|
14597
|
-
if (Object.keys(this._participants$.value).length > 0 && !layoutChangedEvent.layers.some((layer) => !!layer.member_id)) logger$
|
|
14856
|
+
if (Object.keys(this._participants$.value).length > 0 && !layoutChangedEvent.layers.some((layer) => !!layer.member_id)) logger$23.warn("[CallEventsManager] No layers with member_id found in layout.changed event. Nothing to update.");
|
|
14598
14857
|
layoutChangedEvent.layers.filter((layer) => !!layer.member_id).filter((layer) => {
|
|
14599
14858
|
if (!(layer.member_id in this._participants$.value)) {
|
|
14600
|
-
logger$
|
|
14859
|
+
logger$23.warn(`[CallEventsManager] Skipping layout layer for unknown member_id: ${layer.member_id}`);
|
|
14601
14860
|
return false;
|
|
14602
14861
|
}
|
|
14603
14862
|
return true;
|
|
@@ -14615,12 +14874,17 @@ var CallEventsManager = class extends Destroyable {
|
|
|
14615
14874
|
updateLayouts() {
|
|
14616
14875
|
if (!this.selfId) return;
|
|
14617
14876
|
this.webRtcCallSession.executeMethod(this.selfId, "call.layout.list", {}).then((response) => {
|
|
14877
|
+
const layouts = response.result?.layouts;
|
|
14878
|
+
if (!layouts) {
|
|
14879
|
+
logger$23.warn("[CallEventsManager] Layout list response carried no layouts; keeping current layouts");
|
|
14880
|
+
return;
|
|
14881
|
+
}
|
|
14618
14882
|
this._sessionState$.next({
|
|
14619
14883
|
...this._sessionState$.value,
|
|
14620
|
-
layouts
|
|
14884
|
+
layouts
|
|
14621
14885
|
});
|
|
14622
14886
|
}).catch((error) => {
|
|
14623
|
-
logger$
|
|
14887
|
+
logger$23.error("[CallEventsManager] Error fetching layouts:", error);
|
|
14624
14888
|
});
|
|
14625
14889
|
}
|
|
14626
14890
|
updateParticipants(members) {
|
|
@@ -14636,7 +14900,7 @@ var CallEventsManager = class extends Destroyable {
|
|
|
14636
14900
|
}
|
|
14637
14901
|
const participant = this._participants$.value[member.member_id];
|
|
14638
14902
|
const oldValue = participant.value;
|
|
14639
|
-
logger$
|
|
14903
|
+
logger$23.debug("[CallEventsManager] Updating participant:", member.member_id, {
|
|
14640
14904
|
oldValue,
|
|
14641
14905
|
newValue: member
|
|
14642
14906
|
});
|
|
@@ -14649,17 +14913,17 @@ var CallEventsManager = class extends Destroyable {
|
|
|
14649
14913
|
}
|
|
14650
14914
|
get callJoinedEvent$() {
|
|
14651
14915
|
return this.cachedObservable("callJoinedEvent$", () => this.webRtcCallSession.callEvent$.pipe((0, import_cjs$20.filter)(isCallJoinedPayload), (0, import_cjs$20.tap)((event) => {
|
|
14652
|
-
logger$
|
|
14916
|
+
logger$23.debug("[CallEventsManager] Call joined event:", event);
|
|
14653
14917
|
})));
|
|
14654
14918
|
}
|
|
14655
14919
|
get layoutChangedEvent$() {
|
|
14656
14920
|
return this.cachedObservable("layoutChangedEvent$", () => this.webRtcCallSession.callEvent$.pipe(filterAs(isLayoutChangedPayload, "layout"), (0, import_cjs$20.tap)((event) => {
|
|
14657
|
-
logger$
|
|
14921
|
+
logger$23.debug("[CallEventsManager] Layout changed event:", event);
|
|
14658
14922
|
})));
|
|
14659
14923
|
}
|
|
14660
14924
|
get memberUpdates$() {
|
|
14661
14925
|
return this.cachedObservable("memberUpdates$", () => (0, import_cjs$20.merge)(this.webRtcCallSession.memberJoined$, this.webRtcCallSession.memberUpdated$, this.webRtcCallSession.memberTalking$).pipe((0, import_cjs$20.map)((event) => event.member), (0, import_cjs$20.tap)((event) => {
|
|
14662
|
-
logger$
|
|
14926
|
+
logger$23.debug("[CallEventsManager] Member update event:", event);
|
|
14663
14927
|
})));
|
|
14664
14928
|
}
|
|
14665
14929
|
destroy() {
|
|
@@ -14678,6 +14942,111 @@ var CallEventsManager = class extends Destroyable {
|
|
|
14678
14942
|
}
|
|
14679
14943
|
};
|
|
14680
14944
|
|
|
14945
|
+
//#endregion
|
|
14946
|
+
//#region src/controllers/ConstraintFallbackHelper.ts
|
|
14947
|
+
/**
|
|
14948
|
+
* ConstraintFallbackHelper - Provides getUserMedia with automatic constraint
|
|
14949
|
+
* fallback on OverconstrainedError.
|
|
14950
|
+
*
|
|
14951
|
+
* When a specific device ID is requested, the helper tries progressively
|
|
14952
|
+
* looser constraints:
|
|
14953
|
+
* 1. `{ deviceId: { exact: deviceId } }` -- exact match
|
|
14954
|
+
* 2. `{ deviceId: deviceId }` -- preferred (browser may pick another)
|
|
14955
|
+
* 3. `{}` -- no constraint, browser default
|
|
14956
|
+
*
|
|
14957
|
+
* This prevents stale device IDs from blocking call setup.
|
|
14958
|
+
*
|
|
14959
|
+
* @see Section 5.8 and Section 11 of the Implementation Guide
|
|
14960
|
+
*/
|
|
14961
|
+
const logger$22 = getLogger();
|
|
14962
|
+
/**
|
|
14963
|
+
* Attempts getUserMedia with progressively looser constraints.
|
|
14964
|
+
*
|
|
14965
|
+
* The function tries three levels of constraint specificity for the given
|
|
14966
|
+
* device kind. Each level is only attempted if the previous one fails with
|
|
14967
|
+
* an OverconstrainedError. Non-OverconstrainedError failures (e.g.,
|
|
14968
|
+
* NotAllowedError) are thrown immediately without fallback.
|
|
14969
|
+
*
|
|
14970
|
+
* @param mediaDevices - Anything exposing `getUserMedia` (a full
|
|
14971
|
+
* `WebRTCMediaDevices`, or a shim wrapping one)
|
|
14972
|
+
* @param constraints - The full MediaStreamConstraints to use as a base
|
|
14973
|
+
* @param kind - Which track kind to apply fallback to ('audio' | 'video')
|
|
14974
|
+
* @param deviceId - The device ID to try (if undefined, calls getUserMedia as-is)
|
|
14975
|
+
* @returns The stream and the fallback level that succeeded
|
|
14976
|
+
* @throws When all fallback levels fail, or when a non-OverconstrainedError occurs
|
|
14977
|
+
*/
|
|
14978
|
+
async function getUserMediaWithFallback(mediaDevices, constraints, kind, deviceId) {
|
|
14979
|
+
if (!deviceId) return {
|
|
14980
|
+
stream: await mediaDevices.getUserMedia(constraints),
|
|
14981
|
+
fallbackLevel: "default"
|
|
14982
|
+
};
|
|
14983
|
+
const baseConstraints = typeof constraints[kind] === "object" ? constraints[kind] : {};
|
|
14984
|
+
try {
|
|
14985
|
+
const exactConstraints = {
|
|
14986
|
+
...constraints,
|
|
14987
|
+
[kind]: {
|
|
14988
|
+
...baseConstraints,
|
|
14989
|
+
deviceId: { exact: deviceId }
|
|
14990
|
+
}
|
|
14991
|
+
};
|
|
14992
|
+
return {
|
|
14993
|
+
stream: await mediaDevices.getUserMedia(exactConstraints),
|
|
14994
|
+
fallbackLevel: "exact"
|
|
14995
|
+
};
|
|
14996
|
+
} catch (error) {
|
|
14997
|
+
if (!isOverconstrainedError(error)) throw error;
|
|
14998
|
+
logger$22.debug(`[ConstraintFallbackHelper] Exact constraint failed for ${kind}, trying preferred`, { deviceId });
|
|
14999
|
+
}
|
|
15000
|
+
try {
|
|
15001
|
+
const preferredConstraints = {
|
|
15002
|
+
...constraints,
|
|
15003
|
+
[kind]: {
|
|
15004
|
+
...baseConstraints,
|
|
15005
|
+
deviceId
|
|
15006
|
+
}
|
|
15007
|
+
};
|
|
15008
|
+
return {
|
|
15009
|
+
stream: await mediaDevices.getUserMedia(preferredConstraints),
|
|
15010
|
+
fallbackLevel: "preferred"
|
|
15011
|
+
};
|
|
15012
|
+
} catch (error) {
|
|
15013
|
+
if (!isOverconstrainedError(error)) throw error;
|
|
15014
|
+
logger$22.debug(`[ConstraintFallbackHelper] Preferred constraint failed for ${kind}, trying default`, { deviceId });
|
|
15015
|
+
}
|
|
15016
|
+
try {
|
|
15017
|
+
const defaultConstraints = {
|
|
15018
|
+
...constraints,
|
|
15019
|
+
[kind]: { ...baseConstraints }
|
|
15020
|
+
};
|
|
15021
|
+
if (typeof defaultConstraints[kind] === "object") {
|
|
15022
|
+
const { deviceId: _removed, ...rest } = defaultConstraints[kind];
|
|
15023
|
+
defaultConstraints[kind] = rest;
|
|
15024
|
+
}
|
|
15025
|
+
const stream = await mediaDevices.getUserMedia(defaultConstraints);
|
|
15026
|
+
logger$22.warn(`[ConstraintFallbackHelper] Fell back to browser default for ${kind}`, { requestedDeviceId: deviceId });
|
|
15027
|
+
return {
|
|
15028
|
+
stream,
|
|
15029
|
+
fallbackLevel: "default"
|
|
15030
|
+
};
|
|
15031
|
+
} catch (error) {
|
|
15032
|
+
logger$22.error(`[ConstraintFallbackHelper] All fallback levels exhausted for ${kind}`, {
|
|
15033
|
+
deviceId,
|
|
15034
|
+
error
|
|
15035
|
+
});
|
|
15036
|
+
throw error;
|
|
15037
|
+
}
|
|
15038
|
+
}
|
|
15039
|
+
/**
|
|
15040
|
+
* Checks whether an error is an OverconstrainedError.
|
|
15041
|
+
*
|
|
15042
|
+
* Browsers may throw either a native OverconstrainedError or a DOMException
|
|
15043
|
+
* with a specific name.
|
|
15044
|
+
*/
|
|
15045
|
+
function isOverconstrainedError(error) {
|
|
15046
|
+
if (error instanceof Error) return error.name === "OverconstrainedError" || error.name === "ConstraintNotSatisfiedError";
|
|
15047
|
+
return false;
|
|
15048
|
+
}
|
|
15049
|
+
|
|
14681
15050
|
//#endregion
|
|
14682
15051
|
//#region src/helpers/SDPHelper.ts
|
|
14683
15052
|
/**
|
|
@@ -15236,6 +15605,7 @@ var LocalStreamController = class extends Destroyable {
|
|
|
15236
15605
|
this._localAudioTracks$ = this.createBehaviorSubject([]);
|
|
15237
15606
|
this._localVideoTracks$ = this.createBehaviorSubject([]);
|
|
15238
15607
|
this._mediaTrackEnded$ = this.createSubject();
|
|
15608
|
+
this._trackOrigins = /* @__PURE__ */ new WeakMap();
|
|
15239
15609
|
}
|
|
15240
15610
|
get localStream$() {
|
|
15241
15611
|
return this._localStream$.asObservable().pipe((0, import_cjs$17.takeUntil)(this.destroyed$));
|
|
@@ -15258,6 +15628,22 @@ var LocalStreamController = class extends Destroyable {
|
|
|
15258
15628
|
get localVideoTracks() {
|
|
15259
15629
|
return this._localVideoTracks$.value;
|
|
15260
15630
|
}
|
|
15631
|
+
tagTracks(tracks, origin) {
|
|
15632
|
+
for (const track of tracks) this._trackOrigins.set(track, origin);
|
|
15633
|
+
}
|
|
15634
|
+
setTrackOrigin(track, origin) {
|
|
15635
|
+
this._trackOrigins.set(track, origin);
|
|
15636
|
+
}
|
|
15637
|
+
getTrackOrigin(track) {
|
|
15638
|
+
return this._trackOrigins.get(track);
|
|
15639
|
+
}
|
|
15640
|
+
/**
|
|
15641
|
+
* Fail-safe: an unrecorded track reads as not-a-device-capture, so a missed
|
|
15642
|
+
* tagging site leaves media alone rather than destroying it.
|
|
15643
|
+
*/
|
|
15644
|
+
isDeviceCapture(track) {
|
|
15645
|
+
return this._trackOrigins.get(track) === "device";
|
|
15646
|
+
}
|
|
15261
15647
|
/**
|
|
15262
15648
|
* Build the local media stream based on the provided options.
|
|
15263
15649
|
*/
|
|
@@ -15267,13 +15653,16 @@ var LocalStreamController = class extends Destroyable {
|
|
|
15267
15653
|
if (this.options.inputAudioStream ?? this.options.inputVideoStream) {
|
|
15268
15654
|
const tracks = [...this.options.inputAudioStream?.getTracks() ?? [], ...this.options.inputVideoStream?.getTracks() ?? []];
|
|
15269
15655
|
stream = new MediaStream(tracks);
|
|
15656
|
+
this.tagTracks(tracks, "application");
|
|
15270
15657
|
} else if (this.options.propose === "screenshare") {
|
|
15271
|
-
|
|
15658
|
+
const audio = this.options.screenShareAudio ?? false;
|
|
15659
|
+
logger$19.debug("[LocalStreamController] Requesting display media for screen sharing with audio:", audio);
|
|
15272
15660
|
stream = await this.options.getDisplayMedia({
|
|
15273
15661
|
video: true,
|
|
15274
|
-
audio
|
|
15662
|
+
audio
|
|
15275
15663
|
});
|
|
15276
15664
|
logger$19.debug("[LocalStreamController] Screen share media obtained:", stream);
|
|
15665
|
+
this.tagTracks(stream.getTracks(), "display");
|
|
15277
15666
|
} else {
|
|
15278
15667
|
const constraints = {
|
|
15279
15668
|
audio: this.options.inputAudioDeviceConstraints,
|
|
@@ -15282,6 +15671,7 @@ var LocalStreamController = class extends Destroyable {
|
|
|
15282
15671
|
logger$19.debug("[LocalStreamController] Requesting user media with constraints:", constraints);
|
|
15283
15672
|
stream = await this.options.getUserMedia(constraints);
|
|
15284
15673
|
logger$19.debug("[LocalStreamController] User media obtained:", stream);
|
|
15674
|
+
this.tagTracks(stream.getTracks(), "device");
|
|
15285
15675
|
}
|
|
15286
15676
|
this._localStream$.next(stream);
|
|
15287
15677
|
this._localAudioTracks$.next(stream.getAudioTracks());
|
|
@@ -15291,10 +15681,13 @@ var LocalStreamController = class extends Destroyable {
|
|
|
15291
15681
|
/**
|
|
15292
15682
|
* Add a local media track to the local stream.
|
|
15293
15683
|
* @param track - The MediaStreamTrack to add
|
|
15684
|
+
* @param origin - Defaults to `'device'`; every internal caller passes a
|
|
15685
|
+
* fresh `getUserMedia` capture.
|
|
15294
15686
|
* @returns The MediaStream (either existing or newly created)
|
|
15295
15687
|
*/
|
|
15296
|
-
addTrack(track) {
|
|
15688
|
+
addTrack(track, origin = "device") {
|
|
15297
15689
|
const localStream = this._localStream$.value ?? new MediaStream();
|
|
15690
|
+
this._trackOrigins.set(track, origin);
|
|
15298
15691
|
track.addEventListener("ended", this.mediaTrackEndedHandler);
|
|
15299
15692
|
localStream.addTrack(track);
|
|
15300
15693
|
this._localStream$.next(localStream);
|
|
@@ -15564,15 +15957,27 @@ var TransceiverController = class extends Destroyable {
|
|
|
15564
15957
|
for (let i = 0; i < Number(msStreamsNumber); i++) this.peerConnection.addTransceiver("video", { direction: "recvonly" });
|
|
15565
15958
|
}
|
|
15566
15959
|
}
|
|
15960
|
+
/**
|
|
15961
|
+
* @returns whether every live sender of the kind took the constraints. A
|
|
15962
|
+
* skipped non-device sender, an exhausted fallback, and having no live sender
|
|
15963
|
+
* at all all report `false` — `mediaParamsUpdated.applied` is built from this,
|
|
15964
|
+
* and an application told `true` cannot tell a working push from a no-op.
|
|
15965
|
+
*/
|
|
15567
15966
|
async updateSendersConstraints(kind, constraints) {
|
|
15568
15967
|
if (!constraints) {
|
|
15569
15968
|
this.stopTrackSender(kind);
|
|
15570
|
-
return
|
|
15969
|
+
return false;
|
|
15571
15970
|
}
|
|
15572
15971
|
const senders = this.peerConnection.getSenders().filter((sender) => sender.track?.kind === kind && sender.track.readyState === "live");
|
|
15972
|
+
let applied = senders.length > 0;
|
|
15573
15973
|
for (const sender of senders) {
|
|
15574
15974
|
const { track } = sender;
|
|
15575
15975
|
if (track) {
|
|
15976
|
+
if (!this.options.localStreamController.isDeviceCapture(track)) {
|
|
15977
|
+
logger$18.debug(`[TransceiverController] Skipping ${kind} constraints for a non-device track (origin: ${this.options.localStreamController.getTrackOrigin(track) ?? "unrecorded"}), track ${track.id}`);
|
|
15978
|
+
applied = false;
|
|
15979
|
+
continue;
|
|
15980
|
+
}
|
|
15576
15981
|
const constraintsToApply = {
|
|
15577
15982
|
...track.getConstraints(),
|
|
15578
15983
|
...constraints
|
|
@@ -15588,33 +15993,39 @@ var TransceiverController = class extends Destroyable {
|
|
|
15588
15993
|
} catch (fallbackError) {
|
|
15589
15994
|
logger$18.warn(`[TransceiverController] Track replacement fallback also failed for ${kind} track:`, fallbackError);
|
|
15590
15995
|
this.options.onError?.(new MediaTrackError("updateSendersConstraints", kind, fallbackError));
|
|
15996
|
+
applied = false;
|
|
15591
15997
|
}
|
|
15592
15998
|
}
|
|
15593
15999
|
}
|
|
15594
16000
|
}
|
|
16001
|
+
return applied;
|
|
15595
16002
|
}
|
|
15596
16003
|
/**
|
|
15597
|
-
* Fallback when applyConstraints fails
|
|
15598
|
-
* one via getUserMedia with the merged constraints (preserving the current
|
|
15599
|
-
* deviceId), replace the sender track, and update the localStream.
|
|
16004
|
+
* Fallback when applyConstraints fails, which on iOS Safari it silently does.
|
|
15600
16005
|
*
|
|
15601
|
-
*
|
|
15602
|
-
*
|
|
16006
|
+
* Order matters: acquiring before stopping means a failed acquisition leaves
|
|
16007
|
+
* the existing media playing. The deviceId goes through the fallback ladder
|
|
16008
|
+
* rather than pinned `{ exact }`, so a stale id degrades instead of failing.
|
|
15603
16009
|
*/
|
|
15604
16010
|
async replaceTrackFallback(sender, oldTrack, kind, mergedConstraints) {
|
|
15605
16011
|
const { deviceId } = oldTrack.getSettings();
|
|
15606
|
-
const
|
|
15607
|
-
|
|
15608
|
-
|
|
15609
|
-
|
|
15610
|
-
|
|
16012
|
+
const { stream, fallbackLevel } = await getUserMediaWithFallback({ getUserMedia: this.options.getUserMedia }, { [kind]: mergedConstraints }, kind, deviceId);
|
|
16013
|
+
const newTrack = stream.getTracks().find((t) => t.kind === kind);
|
|
16014
|
+
if (!newTrack) {
|
|
16015
|
+
stream.getTracks().forEach((t) => t.stop());
|
|
16016
|
+
throw new MediaTrackError("replaceTrackFallback", kind, /* @__PURE__ */ new Error("getUserMedia returned no track of the requested kind"));
|
|
16017
|
+
}
|
|
16018
|
+
try {
|
|
16019
|
+
await sender.replaceTrack(newTrack);
|
|
16020
|
+
} catch (error) {
|
|
16021
|
+
stream.getTracks().forEach((t) => t.stop());
|
|
16022
|
+
throw error;
|
|
16023
|
+
}
|
|
16024
|
+
const oldTrackId = oldTrack.id;
|
|
16025
|
+
this.options.localStreamController.removeTrack(oldTrackId);
|
|
15611
16026
|
oldTrack.stop();
|
|
15612
|
-
this.options.localStreamController.removeTrack(trackId);
|
|
15613
|
-
const newTrack = (await this.options.getUserMedia({ [kind]: constraintsWithDevice })).getTracks().find((t) => t.kind === kind);
|
|
15614
|
-
if (!newTrack) throw new MediaTrackError("replaceTrackFallback", kind, /* @__PURE__ */ new Error("getUserMedia returned no track of the requested kind"));
|
|
15615
|
-
await sender.replaceTrack(newTrack);
|
|
15616
16027
|
this.options.localStreamController.addTrack(newTrack);
|
|
15617
|
-
logger$18.debug(`[TransceiverController] Track replacement fallback succeeded for ${kind}. New track: ${newTrack.id}`);
|
|
16028
|
+
logger$18.debug(`[TransceiverController] Track replacement fallback succeeded for ${kind} (deviceId fallback level: ${fallbackLevel}). New track: ${newTrack.id}`);
|
|
15618
16029
|
}
|
|
15619
16030
|
getMediaDirections() {
|
|
15620
16031
|
if (this.peerConnection.connectionState === "connected") return this.peerConnection.getTransceivers().reduce((acc, transceiver) => {
|
|
@@ -15684,48 +16095,44 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
15684
16095
|
this.negotiationNeeded$.next();
|
|
15685
16096
|
};
|
|
15686
16097
|
this.updateSelectedInputDevice = async (kind, deviceInfo) => {
|
|
16098
|
+
const { localStream } = this;
|
|
16099
|
+
if (!localStream) {
|
|
16100
|
+
logger$17.warn("[RTCPeerConnectionController] No local stream available to update input device.");
|
|
16101
|
+
return;
|
|
16102
|
+
}
|
|
16103
|
+
const currentTrack = localStream.getTracks().find((track) => track.kind === kind);
|
|
16104
|
+
if (!currentTrack) {
|
|
16105
|
+
logger$17.debug(`[RTCPeerConnectionController] No ${kind} track to switch.`);
|
|
16106
|
+
return;
|
|
16107
|
+
}
|
|
16108
|
+
if (!deviceInfo) {
|
|
16109
|
+
logger$17.debug(`[RTCPeerConnectionController] ${kind} input device selected: none`);
|
|
16110
|
+
this.stopTrackSender(kind);
|
|
16111
|
+
return;
|
|
16112
|
+
}
|
|
16113
|
+
const constraints = {
|
|
16114
|
+
...currentTrack.getConstraints(),
|
|
16115
|
+
...this.deviceController.deviceInfoToConstraints(deviceInfo)
|
|
16116
|
+
};
|
|
15687
16117
|
try {
|
|
15688
|
-
const
|
|
15689
|
-
|
|
15690
|
-
|
|
15691
|
-
return;
|
|
15692
|
-
}
|
|
15693
|
-
logger$17.debug(`[RTCPeerConnectionController] Updating selected ${kind} input device:`, localStream.getTracks());
|
|
15694
|
-
const track = localStream.getTracks().find((track$1) => track$1.kind === kind);
|
|
15695
|
-
if (track) {
|
|
15696
|
-
this.transceiverController?.stopTrackSender(kind);
|
|
15697
|
-
this.localStreamController.removeTrack(track.id);
|
|
15698
|
-
logger$17.debug(`[RTCPeerConnectionController] Stopped existing ${kind} track: ${track.id}`, localStream.getTracks());
|
|
15699
|
-
if (!deviceInfo) {
|
|
15700
|
-
logger$17.debug(`[RTCPeerConnectionController] ${kind} input device selected: none`);
|
|
15701
|
-
return;
|
|
15702
|
-
}
|
|
15703
|
-
const streamTrack = (await this.getUserMedia({ [kind]: {
|
|
15704
|
-
...track.getConstraints(),
|
|
15705
|
-
...this.deviceController.deviceInfoToConstraints(deviceInfo)
|
|
15706
|
-
} })).getTracks().find((t) => t.kind === kind);
|
|
15707
|
-
if (streamTrack) {
|
|
15708
|
-
logger$17.debug(`[RTCPeerConnectionController] Adding new ${kind} track: ${streamTrack.id}`);
|
|
15709
|
-
this.localStreamController.addTrack(streamTrack);
|
|
15710
|
-
await this.transceiverController?.replaceSenderTrack(kind, streamTrack);
|
|
15711
|
-
logger$17.debug(`[RTCPeerConnectionController] Added new ${kind} track: ${streamTrack.id}`, this.localStream?.getTracks());
|
|
15712
|
-
}
|
|
15713
|
-
}
|
|
15714
|
-
logger$17.debug(`[RTCPeerConnectionController] ${kind} input device selected:`, deviceInfo?.label);
|
|
16118
|
+
const newTrack = await this.acquireInputTrack(kind, constraints, deviceInfo, currentTrack);
|
|
16119
|
+
await this.attachInputTrack(kind, newTrack, currentTrack);
|
|
16120
|
+
logger$17.debug(`[RTCPeerConnectionController] ${kind} input device selected:`, deviceInfo.label, newTrack.id);
|
|
15715
16121
|
} catch (error) {
|
|
15716
16122
|
logger$17.error(`[RTCPeerConnectionController] Failed to select ${kind} input device:`, error);
|
|
15717
|
-
this._errors$.next(
|
|
15718
|
-
throw error;
|
|
16123
|
+
this._errors$.next(new MediaTrackError("updateSelectedInputDevice", kind, error));
|
|
15719
16124
|
}
|
|
15720
16125
|
};
|
|
15721
16126
|
this._isNegotiating$ = this.createBehaviorSubject(false);
|
|
15722
16127
|
this._memberId = null;
|
|
16128
|
+
this._nodeId = null;
|
|
15723
16129
|
this._iceConnectionState$ = this.createReplaySubject(1);
|
|
15724
16130
|
this._connectionState$ = this.createReplaySubject(1);
|
|
15725
16131
|
this._signalingState$ = this.createReplaySubject(1);
|
|
15726
16132
|
this._iceGatheringState$ = this.createReplaySubject(1);
|
|
15727
16133
|
this._errors$ = this.createReplaySubject(1);
|
|
15728
16134
|
this._iceCandidates$ = this.createReplaySubject(1);
|
|
16135
|
+
this._localMediaSettled$ = this.createReplaySubject(1);
|
|
15729
16136
|
this._initialized$ = this.createReplaySubject(1);
|
|
15730
16137
|
this._remoteDescription$ = this.createReplaySubject(1);
|
|
15731
16138
|
this._remoteStream$ = this.createBehaviorSubject(null);
|
|
@@ -15758,6 +16165,7 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
15758
16165
|
inputVideoStream: this.options.inputVideoStream,
|
|
15759
16166
|
inputAudioDeviceConstraints: this.inputAudioDeviceConstraints,
|
|
15760
16167
|
inputVideoDeviceConstraints: this.inputVideoDeviceConstraints,
|
|
16168
|
+
screenShareAudio: this.options.screenShareAudio,
|
|
15761
16169
|
getUserMedia: async (constraints) => this.getUserMedia(constraints),
|
|
15762
16170
|
getDisplayMedia: async (options$1) => this.getDisplayMedia(options$1)
|
|
15763
16171
|
});
|
|
@@ -15784,6 +16192,13 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
15784
16192
|
get memberId() {
|
|
15785
16193
|
return this._memberId;
|
|
15786
16194
|
}
|
|
16195
|
+
/** The node this leg's invite landed on — auxiliary legs are placed independently. */
|
|
16196
|
+
setNodeId(nodeId) {
|
|
16197
|
+
this._nodeId = nodeId;
|
|
16198
|
+
}
|
|
16199
|
+
get nodeId() {
|
|
16200
|
+
return this._nodeId;
|
|
16201
|
+
}
|
|
15787
16202
|
stopTrackSender(kind, options = { updateTransceiverDirection: false }) {
|
|
15788
16203
|
const audioCovered = kind === "audio" || kind === "both";
|
|
15789
16204
|
if (audioCovered && this._localAudioPipeline) this.stopRawAudioInputForPipeline();
|
|
@@ -15793,10 +16208,7 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
15793
16208
|
}
|
|
15794
16209
|
stopRawAudioInputForPipeline() {
|
|
15795
16210
|
const rawTracks = this.localStreamController.localAudioTracks;
|
|
15796
|
-
for (const track of rawTracks) if (track.readyState === "live")
|
|
15797
|
-
track.stop();
|
|
15798
|
-
this.localStreamController.removeTrack(track.id);
|
|
15799
|
-
}
|
|
16211
|
+
for (const track of rawTracks) if (track.readyState === "live") this.localStreamController.removeTrack(track.id);
|
|
15800
16212
|
this._localAudioPipeline?.setInputTrack(null);
|
|
15801
16213
|
}
|
|
15802
16214
|
get isNegotiating$() {
|
|
@@ -15829,6 +16241,10 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
15829
16241
|
get remoteDescription$() {
|
|
15830
16242
|
return this.cachedObservable("remoteDescription$", () => this._remoteDescription$.asObservable().pipe((0, import_cjs$16.takeUntil)(this.destroyed$)));
|
|
15831
16243
|
}
|
|
16244
|
+
/** Emits once local media is settled — acquired, or knowingly receive-only. */
|
|
16245
|
+
get localMediaSettled$() {
|
|
16246
|
+
return this.cachedObservable("localMediaSettled$", () => this._localMediaSettled$.asObservable().pipe((0, import_cjs$16.takeUntil)(this.destroyed$)));
|
|
16247
|
+
}
|
|
15832
16248
|
get localStream$() {
|
|
15833
16249
|
return this.cachedObservable("localStream$", () => this.localStreamController.localStream$.pipe((0, import_cjs$16.takeUntil)(this.destroyed$)));
|
|
15834
16250
|
}
|
|
@@ -15856,6 +16272,9 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
15856
16272
|
get propose() {
|
|
15857
16273
|
return this.options.propose ?? "main";
|
|
15858
16274
|
}
|
|
16275
|
+
get connectionState() {
|
|
16276
|
+
return this.peerConnection?.connectionState;
|
|
16277
|
+
}
|
|
15859
16278
|
get isAdditionalDevice() {
|
|
15860
16279
|
return this.propose === "additional-device";
|
|
15861
16280
|
}
|
|
@@ -15936,7 +16355,7 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
15936
16355
|
case "main":
|
|
15937
16356
|
default: return {
|
|
15938
16357
|
...options,
|
|
15939
|
-
offerToReceiveAudio: true,
|
|
16358
|
+
offerToReceiveAudio: this.options.receiveAudio ?? true,
|
|
15940
16359
|
offerToReceiveVideo: this.options.receiveVideo ?? Boolean(this.inputVideoDeviceConstraints)
|
|
15941
16360
|
};
|
|
15942
16361
|
}
|
|
@@ -15984,7 +16403,7 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
15984
16403
|
this._isNegotiating$.next(true);
|
|
15985
16404
|
await this._setRemoteDescription(this.sdpInit);
|
|
15986
16405
|
} else {
|
|
15987
|
-
await this.setupTrackHandling();
|
|
16406
|
+
if (!await this.setupTrackHandling()) return;
|
|
15988
16407
|
this._initialized$.next(true);
|
|
15989
16408
|
}
|
|
15990
16409
|
} catch (error) {
|
|
@@ -16090,13 +16509,14 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
16090
16509
|
*/
|
|
16091
16510
|
async acceptInbound(mediaOverrides) {
|
|
16092
16511
|
if (mediaOverrides) {
|
|
16093
|
-
const { audio, video, receiveAudio, receiveVideo } = mediaOverrides;
|
|
16512
|
+
const { audio, video, receiveAudio, receiveVideo, fallbackToReceiveOnly } = mediaOverrides;
|
|
16094
16513
|
this.options = {
|
|
16095
16514
|
...this.options,
|
|
16096
16515
|
...audio !== void 0 ? { audio } : {},
|
|
16097
16516
|
...video !== void 0 ? { video } : {},
|
|
16098
16517
|
...receiveAudio !== void 0 ? { receiveAudio } : {},
|
|
16099
|
-
...receiveVideo !== void 0 ? { receiveVideo } : {}
|
|
16518
|
+
...receiveVideo !== void 0 ? { receiveVideo } : {},
|
|
16519
|
+
...fallbackToReceiveOnly !== void 0 ? { fallbackToReceiveOnly } : {}
|
|
16100
16520
|
};
|
|
16101
16521
|
this.transceiverController?.updateOptions({
|
|
16102
16522
|
receiveAudio: this.receiveAudio,
|
|
@@ -16107,7 +16527,10 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
16107
16527
|
inputVideoDeviceConstraints: this.inputVideoDeviceConstraints
|
|
16108
16528
|
});
|
|
16109
16529
|
}
|
|
16110
|
-
await this.setupLocalTracks()
|
|
16530
|
+
if (!await this.setupLocalTracks()) {
|
|
16531
|
+
logger$17.debug("[RTCPeerConnectionController] Inbound answer abandoned; the connection went away.");
|
|
16532
|
+
return;
|
|
16533
|
+
}
|
|
16111
16534
|
const { answerOptions } = this;
|
|
16112
16535
|
logger$17.debug("[RTCPeerConnectionController] Creating inbound answer with options:", answerOptions);
|
|
16113
16536
|
await this.createAnswer(answerOptions);
|
|
@@ -16245,23 +16668,52 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
16245
16668
|
}
|
|
16246
16669
|
/**
|
|
16247
16670
|
* Setup track handling for remote tracks.
|
|
16671
|
+
*
|
|
16672
|
+
* @returns `false` when the connection went away while local media was being
|
|
16673
|
+
* acquired — see {@link setupLocalTracks}.
|
|
16248
16674
|
*/
|
|
16249
16675
|
async setupTrackHandling() {
|
|
16250
16676
|
if (!this.peerConnection) throw new DependencyError("RTCPeerConnection is not initialized");
|
|
16251
|
-
await this.setupLocalTracks();
|
|
16677
|
+
if (!await this.setupLocalTracks()) return false;
|
|
16252
16678
|
await this.setupRemoteTracks();
|
|
16679
|
+
return true;
|
|
16253
16680
|
}
|
|
16681
|
+
/**
|
|
16682
|
+
* @returns `false` when the connection was torn down while getUserMedia was
|
|
16683
|
+
* in flight. The acquisition is not cancellable, so the caller must stop
|
|
16684
|
+
* rather than go on to touch a peer connection that is closed or gone.
|
|
16685
|
+
*/
|
|
16254
16686
|
async setupLocalTracks() {
|
|
16255
16687
|
logger$17.debug("[RTCPeerConnectionController] Setting up local tracks/transceivers.");
|
|
16256
|
-
|
|
16688
|
+
if (this.hasNoLocalMediaToSend()) {
|
|
16689
|
+
if (!this.receiveAudio && !this.receiveVideo) throw new InvalidParams("Call requests no media: enable audio/video or receiveAudio/receiveVideo");
|
|
16690
|
+
logger$17.debug("[RTCPeerConnectionController] No local media requested; negotiating receive-only.");
|
|
16691
|
+
this.setupReceiveOnlyTransceivers();
|
|
16692
|
+
this._localMediaSettled$.next();
|
|
16693
|
+
return true;
|
|
16694
|
+
}
|
|
16695
|
+
let localStream;
|
|
16696
|
+
try {
|
|
16697
|
+
localStream = this.localStream ?? await this.localStreamController.buildLocalStream();
|
|
16698
|
+
} catch (error) {
|
|
16699
|
+
this.handleLocalMediaFailure(error);
|
|
16700
|
+
this._localMediaSettled$.next();
|
|
16701
|
+
return true;
|
|
16702
|
+
}
|
|
16703
|
+
if (!this.peerConnection || this.peerConnection.signalingState === "closed") {
|
|
16704
|
+
logger$17.debug("[RTCPeerConnectionController] Local media arrived after teardown; releasing it.");
|
|
16705
|
+
localStream.getTracks().forEach((track) => track.stop());
|
|
16706
|
+
return false;
|
|
16707
|
+
}
|
|
16708
|
+
this._localMediaSettled$.next();
|
|
16257
16709
|
if (this.transceiverController?.useAddStream ?? false) {
|
|
16258
16710
|
logger$17.warn("[RTCPeerConnectionController] Using deprecated addStream API to add local stream.");
|
|
16259
|
-
this.peerConnection
|
|
16711
|
+
this.peerConnection.addStream(localStream);
|
|
16260
16712
|
if (!this.isNegotiating) {
|
|
16261
16713
|
logger$17.debug("[RTCPeerConnectionController] Forcing negotiationneeded after local tracks setup.");
|
|
16262
16714
|
this.negotiationNeeded$.next();
|
|
16263
16715
|
}
|
|
16264
|
-
return;
|
|
16716
|
+
return true;
|
|
16265
16717
|
}
|
|
16266
16718
|
for (const kind of ["audio", "video"]) {
|
|
16267
16719
|
const tracks = (kind === "audio" ? localStream.getAudioTracks() : localStream.getVideoTracks()).map((track, index) => ({
|
|
@@ -16275,10 +16727,53 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
16275
16727
|
await this.transceiverController?.setupTransceiverSender(track, localStream, transceivers[index]);
|
|
16276
16728
|
} else {
|
|
16277
16729
|
logger$17.debug(`[RTCPeerConnectionController] Using addTrack for local ${kind} track:`, track.id);
|
|
16278
|
-
this.peerConnection
|
|
16730
|
+
this.peerConnection.addTrack(track, localStream);
|
|
16279
16731
|
}
|
|
16280
16732
|
}
|
|
16281
16733
|
}
|
|
16734
|
+
return true;
|
|
16735
|
+
}
|
|
16736
|
+
/** True for a main connection with no local media to send. */
|
|
16737
|
+
hasNoLocalMediaToSend() {
|
|
16738
|
+
const hasInputStreams = Boolean(this.options.inputAudioStream ?? this.options.inputVideoStream);
|
|
16739
|
+
return this.propose === "main" && !this.localStream && !hasInputStreams && !this.inputAudioDeviceConstraints && !this.inputVideoDeviceConstraints;
|
|
16740
|
+
}
|
|
16741
|
+
/** The media kinds this connection wants to send: 'audiovideo' | 'video' | 'audio'. */
|
|
16742
|
+
get requestedMediaKinds() {
|
|
16743
|
+
const wantsAudio = Boolean(this.inputAudioDeviceConstraints);
|
|
16744
|
+
const wantsVideo = Boolean(this.inputVideoDeviceConstraints);
|
|
16745
|
+
if (wantsAudio && wantsVideo) return "audiovideo";
|
|
16746
|
+
return wantsVideo ? "video" : "audio";
|
|
16747
|
+
}
|
|
16748
|
+
/**
|
|
16749
|
+
* Handle a local media acquisition failure with a typed, semantically
|
|
16750
|
+
* accurate MediaAccessError created at the acquisition site:
|
|
16751
|
+
* - Auxiliary connections (screenshare / additional-device) throw a
|
|
16752
|
+
* non-fatal error — VertoManager surfaces it and the call is unaffected.
|
|
16753
|
+
* - The main connection degrades to receive-only when allowed (default),
|
|
16754
|
+
* otherwise fails with a fatal error.
|
|
16755
|
+
*/
|
|
16756
|
+
handleLocalMediaFailure(error) {
|
|
16757
|
+
if (this.propose === "screenshare") throw new MediaAccessError("startScreenShare", "screen", error, false);
|
|
16758
|
+
if (this.propose === "additional-device") throw new MediaAccessError("addInputDevice", this.requestedMediaKinds, error, false);
|
|
16759
|
+
const canReceive = this.receiveAudio || this.receiveVideo;
|
|
16760
|
+
if (!((this.options.fallbackToReceiveOnly ?? true) && canReceive)) throw new MediaAccessError("acquireLocalMedia", this.requestedMediaKinds, error, true);
|
|
16761
|
+
logger$17.warn("[RTCPeerConnectionController] Local media unavailable; continuing receive-only:", error);
|
|
16762
|
+
this._errors$.next(new MediaAccessError("acquireLocalMedia", this.requestedMediaKinds, error, false));
|
|
16763
|
+
this.setupReceiveOnlyTransceivers();
|
|
16764
|
+
}
|
|
16765
|
+
/**
|
|
16766
|
+
* Negotiate receive-only m-lines when there are no local tracks to send.
|
|
16767
|
+
* Only offer-type connections add transceivers — answer-type connections
|
|
16768
|
+
* reuse the transceivers created from the remote offer.
|
|
16769
|
+
*/
|
|
16770
|
+
setupReceiveOnlyTransceivers() {
|
|
16771
|
+
if (this.type !== "offer") return;
|
|
16772
|
+
if (this.transceiverController?.useAddTransceivers ?? false) {
|
|
16773
|
+
this.peerConnection?.addTransceiver("audio", { direction: this.receiveAudio ? "recvonly" : "inactive" });
|
|
16774
|
+
this.peerConnection?.addTransceiver("video", { direction: this.receiveVideo ? "recvonly" : "inactive" });
|
|
16775
|
+
}
|
|
16776
|
+
if (!this.isNegotiating) this.negotiationNeeded$.next();
|
|
16282
16777
|
}
|
|
16283
16778
|
async getUserMedia(constraints) {
|
|
16284
16779
|
return (this.options.webRTCApiProvider?.mediaDevices ?? navigator.mediaDevices).getUserMedia(constraints);
|
|
@@ -16316,7 +16811,7 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
16316
16811
|
stream = await this.getUserMedia({ audio: constraints });
|
|
16317
16812
|
} catch (error) {
|
|
16318
16813
|
logger$17.error("[RTCPeerConnectionController] Failed to re-acquire mic for pipeline restore:", error);
|
|
16319
|
-
this._errors$.next(
|
|
16814
|
+
this._errors$.next(new MediaTrackError("restoreAudioPipelineInput", "audio", error));
|
|
16320
16815
|
return;
|
|
16321
16816
|
}
|
|
16322
16817
|
const newTrack = stream.getAudioTracks().at(0);
|
|
@@ -16325,7 +16820,62 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
16325
16820
|
this._localAudioPipeline.setInputTrack(newTrack);
|
|
16326
16821
|
}
|
|
16327
16822
|
/**
|
|
16328
|
-
*
|
|
16823
|
+
* Capture the newly selected device, leaving the current capture running.
|
|
16824
|
+
*
|
|
16825
|
+
* A rejection must leave the current track sending, so nothing is released
|
|
16826
|
+
* until the replacement is in hand. The one exception is hardware that admits
|
|
16827
|
+
* a single opener — a phone's front and back cameras, typically — which
|
|
16828
|
+
* rejects the second capture until the first is closed.
|
|
16829
|
+
*/
|
|
16830
|
+
async acquireInputTrack(kind, constraints, deviceInfo, currentTrack) {
|
|
16831
|
+
try {
|
|
16832
|
+
return await this.captureTrack(kind, constraints, deviceInfo.deviceId);
|
|
16833
|
+
} catch (error) {
|
|
16834
|
+
if (!isMediaDeviceInUse(error)) throw error;
|
|
16835
|
+
logger$17.warn(`[RTCPeerConnectionController] ${kind} device is held exclusively; releasing the current capture to retry:`, error);
|
|
16836
|
+
const previousDeviceId = currentTrack.getSettings().deviceId;
|
|
16837
|
+
this.stopTrackSender(kind);
|
|
16838
|
+
try {
|
|
16839
|
+
return await this.captureTrack(kind, constraints, deviceInfo.deviceId);
|
|
16840
|
+
} catch (retryError) {
|
|
16841
|
+
await this.restorePreviousInputTrack(kind, constraints, previousDeviceId, currentTrack);
|
|
16842
|
+
throw retryError;
|
|
16843
|
+
}
|
|
16844
|
+
}
|
|
16845
|
+
}
|
|
16846
|
+
async captureTrack(kind, constraints, deviceId) {
|
|
16847
|
+
const { stream, fallbackLevel } = await getUserMediaWithFallback({ getUserMedia: async (c) => this.getUserMedia(c) }, { [kind]: constraints }, kind, deviceId);
|
|
16848
|
+
const track = stream.getTracks().find((t) => t.kind === kind);
|
|
16849
|
+
if (!track) {
|
|
16850
|
+
stream.getTracks().forEach((t) => t.stop());
|
|
16851
|
+
throw new MediaTrackError("updateSelectedInputDevice", kind, /* @__PURE__ */ new Error("getUserMedia returned no track of the requested kind"));
|
|
16852
|
+
}
|
|
16853
|
+
if (fallbackLevel !== "exact") logger$17.warn(`[RTCPeerConnectionController] ${kind} device acquired at fallback level '${fallbackLevel}'; the capture may not be the requested device.`);
|
|
16854
|
+
return track;
|
|
16855
|
+
}
|
|
16856
|
+
/** Best-effort return to the device that was released for an exclusive retry. */
|
|
16857
|
+
async restorePreviousInputTrack(kind, constraints, previousDeviceId, releasedTrack) {
|
|
16858
|
+
try {
|
|
16859
|
+
const restored = await this.captureTrack(kind, constraints, previousDeviceId);
|
|
16860
|
+
await this.attachInputTrack(kind, restored, releasedTrack);
|
|
16861
|
+
} catch (error) {
|
|
16862
|
+
logger$17.error(`[RTCPeerConnectionController] Failed to restore the previous ${kind} device:`, error);
|
|
16863
|
+
}
|
|
16864
|
+
}
|
|
16865
|
+
async attachInputTrack(kind, newTrack, oldTrack) {
|
|
16866
|
+
const pipelineOwnsAudio = kind === "audio" && this._localAudioPipeline;
|
|
16867
|
+
if (!pipelineOwnsAudio) try {
|
|
16868
|
+
await this.transceiverController?.replaceSenderTrack(kind, newTrack);
|
|
16869
|
+
} catch (error) {
|
|
16870
|
+
newTrack.stop();
|
|
16871
|
+
throw error;
|
|
16872
|
+
}
|
|
16873
|
+
this.localStreamController.removeTrack(oldTrack.id);
|
|
16874
|
+
this.localStreamController.addTrack(newTrack);
|
|
16875
|
+
if (pipelineOwnsAudio) this._localAudioPipeline?.setInputTrack(newTrack);
|
|
16876
|
+
}
|
|
16877
|
+
/**
|
|
16878
|
+
* Return the lazily-created {@link LocalAudioPipeline}, constructing it on
|
|
16329
16879
|
* first access. On creation the current audio sender's track is routed
|
|
16330
16880
|
* through the pipeline (input → gain → analyser → destination) and the
|
|
16331
16881
|
* sender is switched to emit the processed track. Returns `null` when no
|
|
@@ -16357,6 +16907,7 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
16357
16907
|
const sender = (this.transceiverController?.audioTransceivers.at(0))?.sender ?? this.peerConnection.getSenders().find((s) => s.track?.kind === "audio");
|
|
16358
16908
|
if (!sender || !raw) return;
|
|
16359
16909
|
try {
|
|
16910
|
+
this.localStreamController.setTrackOrigin(this._localAudioPipeline.outputTrack, "processed");
|
|
16360
16911
|
await sender.replaceTrack(this._localAudioPipeline.outputTrack);
|
|
16361
16912
|
} catch (error) {
|
|
16362
16913
|
logger$17.warn("[RTCPeerConnectionController] Failed to route audio sender through pipeline:", error);
|
|
@@ -16378,7 +16929,7 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
16378
16929
|
logger$17.debug(`[RTCPeerConnectionController] ${track.kind} track added:`, track.id);
|
|
16379
16930
|
} catch (error) {
|
|
16380
16931
|
logger$17.error(`[RTCPeerConnectionController] Failed to add ${track.kind} track:`, error);
|
|
16381
|
-
this._errors$.next(
|
|
16932
|
+
this._errors$.next(new MediaTrackError("addLocalTrack", track.kind, error));
|
|
16382
16933
|
throw error;
|
|
16383
16934
|
}
|
|
16384
16935
|
}
|
|
@@ -16403,7 +16954,7 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
16403
16954
|
logger$17.debug(`[RTCPeerConnectionController] ${sender.track?.kind} track removed:`, trackId);
|
|
16404
16955
|
} catch (error) {
|
|
16405
16956
|
logger$17.error(`[RTCPeerConnectionController] Failed to remove ${sender.track?.kind} track:`, error);
|
|
16406
|
-
this._errors$.next(
|
|
16957
|
+
this._errors$.next(new MediaTrackError("removeLocalTrack", sender.track?.kind ?? "unknown", error));
|
|
16407
16958
|
throw error;
|
|
16408
16959
|
}
|
|
16409
16960
|
}
|
|
@@ -16417,37 +16968,68 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
16417
16968
|
for (const existingTrack of existingTracks) this.removeLocalTrack(existingTrack.id);
|
|
16418
16969
|
this.addLocalTrack(track);
|
|
16419
16970
|
}
|
|
16971
|
+
/**
|
|
16972
|
+
* @returns whether the constraints reached the media the leg is sending.
|
|
16973
|
+
*
|
|
16974
|
+
* With the pipeline engaged the audio sender carries the processed
|
|
16975
|
+
* destination track, so the sender scan would find nothing it may touch and
|
|
16976
|
+
* every audio constraint API would silently no-op. The constraints belong to
|
|
16977
|
+
* the pipeline's device source, which is the capture that sender ultimately
|
|
16978
|
+
* carries.
|
|
16979
|
+
*/
|
|
16420
16980
|
async updateSendersConstraints(kind, constraints) {
|
|
16421
|
-
|
|
16981
|
+
if (kind === "audio" && this._localAudioPipeline) {
|
|
16982
|
+
if (!constraints) {
|
|
16983
|
+
this.stopTrackSender("audio");
|
|
16984
|
+
return false;
|
|
16985
|
+
}
|
|
16986
|
+
return this.applyPipelineSourceConstraints(constraints);
|
|
16987
|
+
}
|
|
16988
|
+
return await this.transceiverController?.updateSendersConstraints(kind, constraints) ?? false;
|
|
16422
16989
|
}
|
|
16423
16990
|
/**
|
|
16424
|
-
*
|
|
16425
|
-
*
|
|
16426
|
-
*
|
|
16427
|
-
*
|
|
16991
|
+
* Mirror of the sender path for a piped audio leg: same merge, same fallback
|
|
16992
|
+
* ladder, same device-capture invariant — but the swap target is the pipeline
|
|
16993
|
+
* input, so the sender keeps emitting the pipeline's output track and its
|
|
16994
|
+
* identity survives the change.
|
|
16428
16995
|
*/
|
|
16429
|
-
async
|
|
16430
|
-
const
|
|
16431
|
-
|
|
16432
|
-
|
|
16433
|
-
|
|
16996
|
+
async applyPipelineSourceConstraints(constraints) {
|
|
16997
|
+
const pipeline = this._localAudioPipeline;
|
|
16998
|
+
const source = this.localStreamController.localAudioTracks.at(0);
|
|
16999
|
+
if (!pipeline || !source) {
|
|
17000
|
+
logger$17.debug("[RTCPeerConnectionController] No pipeline input to constrain.");
|
|
17001
|
+
return false;
|
|
16434
17002
|
}
|
|
16435
|
-
|
|
16436
|
-
|
|
16437
|
-
|
|
16438
|
-
|
|
16439
|
-
|
|
16440
|
-
|
|
16441
|
-
|
|
16442
|
-
|
|
16443
|
-
|
|
16444
|
-
|
|
16445
|
-
|
|
16446
|
-
|
|
16447
|
-
|
|
16448
|
-
|
|
17003
|
+
if (!this.localStreamController.isDeviceCapture(source)) {
|
|
17004
|
+
logger$17.debug(`[RTCPeerConnectionController] Skipping audio constraints for a non-device pipeline input (origin: ${this.localStreamController.getTrackOrigin(source) ?? "unrecorded"}).`);
|
|
17005
|
+
return false;
|
|
17006
|
+
}
|
|
17007
|
+
const merged = {
|
|
17008
|
+
...source.getConstraints(),
|
|
17009
|
+
...constraints
|
|
17010
|
+
};
|
|
17011
|
+
try {
|
|
17012
|
+
await source.applyConstraints(merged);
|
|
17013
|
+
logger$17.debug("[RTCPeerConnectionController] Pipeline input constraints updated:", merged);
|
|
17014
|
+
return true;
|
|
17015
|
+
} catch (error) {
|
|
17016
|
+
logger$17.warn("[RTCPeerConnectionController] applyConstraints failed on the pipeline input, re-acquiring:", error);
|
|
17017
|
+
}
|
|
17018
|
+
try {
|
|
17019
|
+
const { stream } = await getUserMediaWithFallback({ getUserMedia: async (c) => this.getUserMedia(c) }, { audio: merged }, "audio", source.getSettings().deviceId);
|
|
17020
|
+
const newTrack = stream.getAudioTracks().at(0);
|
|
17021
|
+
if (!newTrack) {
|
|
17022
|
+
stream.getTracks().forEach((track) => track.stop());
|
|
17023
|
+
throw new Error("getUserMedia returned no audio track");
|
|
17024
|
+
}
|
|
17025
|
+
this.localStreamController.removeTrack(source.id);
|
|
16449
17026
|
this.localStreamController.addTrack(newTrack);
|
|
16450
|
-
|
|
17027
|
+
pipeline.setInputTrack(newTrack);
|
|
17028
|
+
return true;
|
|
17029
|
+
} catch (error) {
|
|
17030
|
+
logger$17.warn("[RTCPeerConnectionController] Failed to re-acquire the pipeline input for constraints:", error);
|
|
17031
|
+
this._errors$.next(new MediaTrackError("updateSendersConstraints", "audio", error));
|
|
17032
|
+
return false;
|
|
16451
17033
|
}
|
|
16452
17034
|
}
|
|
16453
17035
|
/**
|
|
@@ -16567,6 +17149,48 @@ const logger$16 = getLogger();
|
|
|
16567
17149
|
function resolveInviteNodeId(args) {
|
|
16568
17150
|
return args.isInvite && !args.reattach && !args.explicitNodeId ? "" : args.currentNodeId ?? "";
|
|
16569
17151
|
}
|
|
17152
|
+
/**
|
|
17153
|
+
* Surface the real outcome of a `webrtc.verto` reply.
|
|
17154
|
+
*
|
|
17155
|
+
* A webrtc.verto response nests several envelopes, each keyed by a verto-style
|
|
17156
|
+
* string `code` ("200" ok, "400"/etc. fail) rather than a JSON-RPC `error`. An outer
|
|
17157
|
+
* layer reports only whether the frame was delivered; an inner layer carries the op's
|
|
17158
|
+
* own outcome:
|
|
17159
|
+
*
|
|
17160
|
+
* response.result = { code:"200", result:{…} } ← delivery acknowledgement
|
|
17161
|
+
* .result = { jsonrpc, id, result:{…} } ← the reply payload
|
|
17162
|
+
* .result = { code:"400", message:"Bad request" } ← the actual op outcome
|
|
17163
|
+
*
|
|
17164
|
+
* A failure can appear at any layer (delivery refused, or the op itself rejected
|
|
17165
|
+
* deeper down), so walk every nested `.result` object and return the FIRST non-2xx
|
|
17166
|
+
* `code` with its message. Returns null when every `code` seen is 2xx or absent —
|
|
17167
|
+
* i.e. the op succeeded. This is the only way to detect that e.g. a mute/kick was
|
|
17168
|
+
* rejected, since the outer delivery `code` is "200" (delivered) even then.
|
|
17169
|
+
*
|
|
17170
|
+
* Pure function — exported for unit testing.
|
|
17171
|
+
*/
|
|
17172
|
+
function findNestedVertoFailure(response) {
|
|
17173
|
+
let node = response;
|
|
17174
|
+
while (node !== null && typeof node === "object") {
|
|
17175
|
+
const obj = node;
|
|
17176
|
+
const err = obj.error;
|
|
17177
|
+
if (err !== null && typeof err === "object") {
|
|
17178
|
+
const e = err;
|
|
17179
|
+
const errCode = typeof e.code === "string" || typeof e.code === "number" ? String(e.code) : void 0;
|
|
17180
|
+
if (errCode !== void 0 && !/^2\d\d$/.test(errCode)) return {
|
|
17181
|
+
code: errCode,
|
|
17182
|
+
message: typeof e.message === "string" ? e.message : void 0
|
|
17183
|
+
};
|
|
17184
|
+
}
|
|
17185
|
+
const code = typeof obj.code === "string" || typeof obj.code === "number" ? String(obj.code) : void 0;
|
|
17186
|
+
if (code !== void 0 && !/^2\d\d$/.test(code)) return {
|
|
17187
|
+
code,
|
|
17188
|
+
message: typeof obj.message === "string" ? obj.message : void 0
|
|
17189
|
+
};
|
|
17190
|
+
node = obj.result !== null && typeof obj.result === "object" ? obj.result : null;
|
|
17191
|
+
}
|
|
17192
|
+
return null;
|
|
17193
|
+
}
|
|
16570
17194
|
var VertoManager = class extends Destroyable {
|
|
16571
17195
|
constructor(callSession) {
|
|
16572
17196
|
super();
|
|
@@ -16589,7 +17213,7 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
16589
17213
|
this._signalingStatus$ = this.createReplaySubject(1);
|
|
16590
17214
|
this._screenShareStatus$ = this.createBehaviorSubject("none");
|
|
16591
17215
|
this._rtcPeerConnectionsMap = /* @__PURE__ */ new Map();
|
|
16592
|
-
this.
|
|
17216
|
+
this._legErrors$ = this.createSubject();
|
|
16593
17217
|
this._nodeId$ = this.createBehaviorSubject(options.nodeId ?? null);
|
|
16594
17218
|
this.onError = options.onError;
|
|
16595
17219
|
this.onModifyFailed = options.onModifyFailed;
|
|
@@ -16637,6 +17261,10 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
16637
17261
|
get selfId$() {
|
|
16638
17262
|
return this._selfId$.asObservable();
|
|
16639
17263
|
}
|
|
17264
|
+
/** Separates the media phase of call creation from the signalling phase. */
|
|
17265
|
+
get localMediaSettled$() {
|
|
17266
|
+
return this.mainPeerConnection.localMediaSettled$;
|
|
17267
|
+
}
|
|
16640
17268
|
get localStream() {
|
|
16641
17269
|
return this._rtcPeerConnectionsMap.get(this.webRtcCallSession.id)?.localStream ?? null;
|
|
16642
17270
|
}
|
|
@@ -16693,35 +17321,95 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
16693
17321
|
const { mediaParams, callID } = event;
|
|
16694
17322
|
const rtcPeerConnController = this._rtcPeerConnectionsMap.get(callID);
|
|
16695
17323
|
const { audio, video } = mediaParams;
|
|
16696
|
-
|
|
16697
|
-
|
|
16698
|
-
|
|
16699
|
-
|
|
16700
|
-
|
|
16701
|
-
audio,
|
|
16702
|
-
video,
|
|
16703
|
-
timestamp: Date.now()
|
|
16704
|
-
});
|
|
16705
|
-
} catch (error) {
|
|
16706
|
-
logger$16.warn("[WebRTCManager] Error applying server-pushed media params:", error);
|
|
16707
|
-
this.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
16708
|
-
}
|
|
16709
|
-
})();
|
|
17324
|
+
if (!rtcPeerConnController) {
|
|
17325
|
+
logger$16.warn(`[WebRTCManager] Ignoring server-pushed media params for unknown leg ${callID}`);
|
|
17326
|
+
return;
|
|
17327
|
+
}
|
|
17328
|
+
this.applyServerMediaParams(rtcPeerConnController, audio, video);
|
|
16710
17329
|
});
|
|
16711
17330
|
this.subscribeTo(this.vertoPing$, (vertoPing) => {
|
|
16712
|
-
this.attachManager.
|
|
17331
|
+
this.attachManager.refresh(this.buildAttachableCall());
|
|
16713
17332
|
this.sendVertoPong(vertoPing);
|
|
16714
17333
|
});
|
|
16715
17334
|
}
|
|
16716
17335
|
/**
|
|
17336
|
+
* An auxiliary-leg failure must never destroy the call; main-leg and
|
|
17337
|
+
* call-level errors keep `CallFactory.isFatalError`'s classification.
|
|
17338
|
+
*
|
|
17339
|
+
* Every site holding a peer connection reports through here, so the invariant
|
|
17340
|
+
* is structural rather than per-call-site — which is how cloud-product#20523
|
|
17341
|
+
* happened, with only one of fourteen sites passing `{ fatal: false }`.
|
|
17342
|
+
*
|
|
17343
|
+
* `override` composes rather than replaces: a caller may force non-fatal for a
|
|
17344
|
+
* reason of its own (a `verto.info` frame is best-effort whichever leg carries
|
|
17345
|
+
* it), and an auxiliary leg stays non-fatal regardless.
|
|
17346
|
+
*/
|
|
17347
|
+
reportLegError(error, rtcPeerConnController, override) {
|
|
17348
|
+
const leg = rtcPeerConnController?.propose;
|
|
17349
|
+
const legId = rtcPeerConnController?.id;
|
|
17350
|
+
const auxiliary = Boolean(rtcPeerConnController) && !rtcPeerConnController?.isMainDevice;
|
|
17351
|
+
this.onError?.(error, {
|
|
17352
|
+
...override?.fatal === false || auxiliary ? { fatal: false } : {},
|
|
17353
|
+
...leg ? { leg } : {},
|
|
17354
|
+
...legId ? { legId } : {}
|
|
17355
|
+
});
|
|
17356
|
+
if (legId) this._legErrors$.next({
|
|
17357
|
+
legId,
|
|
17358
|
+
error
|
|
17359
|
+
});
|
|
17360
|
+
}
|
|
17361
|
+
/**
|
|
17362
|
+
* Errors reported for one leg, as a stream that fails with them.
|
|
17363
|
+
*
|
|
17364
|
+
* Signaling failures are reported, never thrown — so nothing that waits on a
|
|
17365
|
+
* leg's progress would otherwise learn of a rejected invite. Merging this in
|
|
17366
|
+
* lets the wait end with the reason the server gave.
|
|
17367
|
+
*/
|
|
17368
|
+
legError$(legId) {
|
|
17369
|
+
return this._legErrors$.pipe((0, import_cjs$15.filter)((report) => report.legId === legId), (0, import_cjs$15.map)((report) => {
|
|
17370
|
+
throw report.error;
|
|
17371
|
+
}));
|
|
17372
|
+
}
|
|
17373
|
+
/**
|
|
17374
|
+
* Audio and video are applied independently so a failure in one cannot
|
|
17375
|
+
* suppress the other, and `mediaParamsUpdated` is emitted whatever happens —
|
|
17376
|
+
* an application should not be starved of the params by a constraint failure.
|
|
17377
|
+
*/
|
|
17378
|
+
async applyServerMediaParams(rtcPeerConnController, audio, video) {
|
|
17379
|
+
const failures = [];
|
|
17380
|
+
let applied = true;
|
|
17381
|
+
if (audio) try {
|
|
17382
|
+
applied = await rtcPeerConnController.updateSendersConstraints("audio", audio) && applied;
|
|
17383
|
+
} catch (error) {
|
|
17384
|
+
applied = false;
|
|
17385
|
+
failures.push(toError(error));
|
|
17386
|
+
}
|
|
17387
|
+
if (video) try {
|
|
17388
|
+
applied = await rtcPeerConnController.updateSendersConstraints("video", video) && applied;
|
|
17389
|
+
} catch (error) {
|
|
17390
|
+
applied = false;
|
|
17391
|
+
failures.push(toError(error));
|
|
17392
|
+
}
|
|
17393
|
+
this.webRtcCallSession.emitMediaParamsUpdated({
|
|
17394
|
+
audio,
|
|
17395
|
+
video,
|
|
17396
|
+
timestamp: Date.now(),
|
|
17397
|
+
applied
|
|
17398
|
+
});
|
|
17399
|
+
for (const failure of failures) {
|
|
17400
|
+
logger$16.warn("[WebRTCManager] Error applying server-pushed media params:", failure);
|
|
17401
|
+
this.reportLegError(failure, rtcPeerConnController, { fatal: false });
|
|
17402
|
+
}
|
|
17403
|
+
}
|
|
17404
|
+
/**
|
|
16717
17405
|
* Set node_id/selfId only when the current value is null.
|
|
16718
17406
|
*
|
|
16719
17407
|
* During reattach, `call.joined` and `verto.answer` events can deliver
|
|
16720
17408
|
* these identifiers before the `verto.invite` RPC response (`CALL CREATED`)
|
|
16721
17409
|
* arrives. These methods let early events populate them eagerly so that
|
|
16722
17410
|
* downstream RPC calls (e.g. `call.layout.list`) don't fail with empty
|
|
16723
|
-
* identifiers. `processInviteResponse()` remains the authoritative source
|
|
16724
|
-
*
|
|
17411
|
+
* identifiers. `processInviteResponse()` remains the authoritative source and
|
|
17412
|
+
* overwrites unconditionally — for selfId, on the main leg only.
|
|
16725
17413
|
*/
|
|
16726
17414
|
setNodeIdIfNull(nodeId) {
|
|
16727
17415
|
if (!this._nodeId$.value && nodeId) {
|
|
@@ -16744,24 +17432,33 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
16744
17432
|
this.onError?.(new VertoPongError(error));
|
|
16745
17433
|
}
|
|
16746
17434
|
}
|
|
17435
|
+
/**
|
|
17436
|
+
* @returns whether the constraints reached the media the call is sending.
|
|
17437
|
+
* `false` is an outcome, not a failure: the leg may have no live sender of
|
|
17438
|
+
* the kind, or carry media the SDK did not capture and may not replace.
|
|
17439
|
+
* A failure behind it still reaches the call's `errors$`, so a caller that
|
|
17440
|
+
* ignores this value learns of it there.
|
|
17441
|
+
*/
|
|
16747
17442
|
async updateMediaConstraints(options = {}) {
|
|
16748
17443
|
const { audio, video } = options;
|
|
17444
|
+
let applied = true;
|
|
16749
17445
|
try {
|
|
16750
|
-
if (audio) await this.mainPeerConnection.updateSendersConstraints("audio", audio);
|
|
16751
|
-
if (video) await this.mainPeerConnection.updateSendersConstraints("video", video);
|
|
17446
|
+
if (audio) applied = await this.mainPeerConnection.updateSendersConstraints("audio", audio) && applied;
|
|
17447
|
+
if (video) applied = await this.mainPeerConnection.updateSendersConstraints("video", video) && applied;
|
|
16752
17448
|
} catch (error) {
|
|
16753
17449
|
logger$16.warn("[WebRTCManager] Error updating media constraints:", error);
|
|
16754
|
-
this.
|
|
17450
|
+
this.reportLegError(toError(error), this.mainPeerConnection);
|
|
16755
17451
|
throw error;
|
|
16756
17452
|
}
|
|
17453
|
+
return applied;
|
|
16757
17454
|
}
|
|
16758
17455
|
get selfId() {
|
|
16759
17456
|
return this._selfId$.value;
|
|
16760
17457
|
}
|
|
16761
17458
|
/** Build an AttachableCall from the current call state. */
|
|
16762
|
-
buildAttachableCall(idOverride) {
|
|
17459
|
+
buildAttachableCall(idOverride, nodeIdOverride) {
|
|
16763
17460
|
return {
|
|
16764
|
-
nodeId: this.nodeId ?? void 0,
|
|
17461
|
+
nodeId: nodeIdOverride ?? this.nodeId ?? void 0,
|
|
16765
17462
|
id: idOverride ?? this.webRtcCallSession.id,
|
|
16766
17463
|
to: this.webRtcCallSession.to,
|
|
16767
17464
|
mediaDirections: this.webRtcCallSession.mediaDirections
|
|
@@ -16891,24 +17588,49 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
16891
17588
|
get vertoPing$() {
|
|
16892
17589
|
return this.cachedObservable("vertoPing$", () => this.webRtcCallSession.webrtcMessages$.pipe(filterAs(isVertoPingInnerParams, "params"), (0, import_cjs$15.takeUntil)(this.destroyed$)));
|
|
16893
17590
|
}
|
|
17591
|
+
/**
|
|
17592
|
+
* Send a member-control op in-dialog via verto.info.
|
|
17593
|
+
*
|
|
17594
|
+
* The control payload rides in `params.command` — a sibling of `dialogParams`,
|
|
17595
|
+
* at the same level as `dtmf` in {@link sendDigits} — and the inner verto.info
|
|
17596
|
+
* is matched to this call's channel by `dialogParams.callID`, the in-dialog
|
|
17597
|
+
* convention for member-scoped frames. Because it is delivered on the dialog
|
|
17598
|
+
* itself, control lands on the call's own channel with no {node_id,call_id,member_id}
|
|
17599
|
+
* "self" tuple to get wrong. The outer webrtc.verto envelope (added by executeVerto)
|
|
17600
|
+
* still carries the own-leg callID + node_id for session routing.
|
|
17601
|
+
*
|
|
17602
|
+
* Keep `command` OUT of `dialogParams`: it is read at the params level, and
|
|
17603
|
+
* filterVertoParams rewrites/filters dialogParams keys but passes params-level
|
|
17604
|
+
* keys through verbatim.
|
|
17605
|
+
*/
|
|
17606
|
+
async sendCallControl(method, params) {
|
|
17607
|
+
const response = await this.executeVerto(VertoInfo({
|
|
17608
|
+
dialogParams: { callID: this.webRtcCallSession.id },
|
|
17609
|
+
command: {
|
|
17610
|
+
method,
|
|
17611
|
+
params
|
|
17612
|
+
}
|
|
17613
|
+
}));
|
|
17614
|
+
const failure = findNestedVertoFailure(response);
|
|
17615
|
+
if (failure) throw new JSONRPCError(Number.parseInt(failure.code, 10) || 0, `Call control "${method}" failed (code ${failure.code})${failure.message ? `: ${failure.message}` : ""}`, void 0);
|
|
17616
|
+
return response;
|
|
17617
|
+
}
|
|
16894
17618
|
async executeVerto(message, optionals = {}) {
|
|
16895
|
-
const
|
|
17619
|
+
const params = {
|
|
16896
17620
|
callID: optionals.callID ?? this.webRtcCallSession.id,
|
|
16897
17621
|
node_id: optionals.node_id ?? this._nodeId$.value ?? "",
|
|
16898
17622
|
message,
|
|
16899
17623
|
subscribe: optionals.subscribe
|
|
16900
|
-
}
|
|
17624
|
+
};
|
|
17625
|
+
const webrtcVertoMessage = WebrtcVerto(params);
|
|
16901
17626
|
const response = await this.webRtcCallSession.execute(webrtcVertoMessage);
|
|
16902
|
-
|
|
16903
|
-
const error = new JSONRPCError(response.error.code, response.error.message, response.error.data);
|
|
16904
|
-
this.onError?.(error);
|
|
16905
|
-
return response;
|
|
16906
|
-
}
|
|
17627
|
+
const nonFatal = message.method === "verto.info" ? { fatal: false } : void 0;
|
|
16907
17628
|
const innerResult = getValueFrom(response, "result.result");
|
|
16908
|
-
|
|
16909
|
-
|
|
16910
|
-
|
|
16911
|
-
|
|
17629
|
+
const failure = response.error ?? innerResult?.error;
|
|
17630
|
+
if (failure) {
|
|
17631
|
+
const error = new JSONRPCError(failure.code, failure.message, failure.data);
|
|
17632
|
+
if (message.method === "verto.invite" || message.method === "verto.answer") throw error;
|
|
17633
|
+
this.reportLegError(error, this._rtcPeerConnectionsMap.get(params.callID), nonFatal);
|
|
16912
17634
|
}
|
|
16913
17635
|
return response;
|
|
16914
17636
|
}
|
|
@@ -16927,8 +17649,9 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
16927
17649
|
default:
|
|
16928
17650
|
}
|
|
16929
17651
|
} catch (error) {
|
|
17652
|
+
if (vertoMethod === "verto.answer") throw error;
|
|
16930
17653
|
logger$16.error(`[WebRTCManager] Error sending Verto ${vertoMethod}:`, error);
|
|
16931
|
-
this.
|
|
17654
|
+
this.reportLegError(toError(error), rtcPeerConnController);
|
|
16932
17655
|
if (vertoMethod === "verto.modify") this.onModifyFailed?.();
|
|
16933
17656
|
}
|
|
16934
17657
|
}
|
|
@@ -16944,7 +17667,7 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
16944
17667
|
} catch (error) {
|
|
16945
17668
|
logger$16.warn("[WebRTCManager] Error processing modify response:", error);
|
|
16946
17669
|
const modifyError = error instanceof Error ? error : new Error(String(error), { cause: error });
|
|
16947
|
-
this.
|
|
17670
|
+
this.reportLegError(modifyError, rtcPeerConnController);
|
|
16948
17671
|
}
|
|
16949
17672
|
}
|
|
16950
17673
|
}
|
|
@@ -16957,37 +17680,35 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
16957
17680
|
status,
|
|
16958
17681
|
signalingError
|
|
16959
17682
|
});
|
|
16960
|
-
this.
|
|
17683
|
+
this.reportLegError(signalingError, null, { fatal: false });
|
|
16961
17684
|
return;
|
|
16962
17685
|
}
|
|
16963
17686
|
if (rtcPeerConnController.isMainDevice) this._signalingStatus$.next(status);
|
|
16964
17687
|
}
|
|
16965
17688
|
processInviteResponse(response, rtcPeerConnController) {
|
|
16966
|
-
if (
|
|
17689
|
+
if (getValueFrom(response, "result.result.result.message") === "CALL CREATED") {
|
|
16967
17690
|
this.emitMainSignalingStatus(rtcPeerConnController.id, "trying");
|
|
16968
|
-
|
|
17691
|
+
const nodeId = getValueFrom(response, "result.node_id") ?? null;
|
|
16969
17692
|
const memberId = getValueFrom(response, "result.result.result.memberID") ?? null;
|
|
16970
|
-
const callId = getValueFrom(response, "result.result.result.callID")
|
|
17693
|
+
const callId = getValueFrom(response, "result.result.result.callID");
|
|
16971
17694
|
logger$16.debug("[WebRTCManager] Verto invite response:", {
|
|
16972
17695
|
callId,
|
|
16973
17696
|
memberId,
|
|
16974
17697
|
response
|
|
16975
17698
|
});
|
|
16976
|
-
this._selfId$.next(memberId);
|
|
16977
17699
|
rtcPeerConnController.setMemberId(memberId);
|
|
16978
|
-
|
|
16979
|
-
|
|
16980
|
-
this.
|
|
16981
|
-
|
|
16982
|
-
|
|
16983
|
-
|
|
16984
|
-
|
|
17700
|
+
rtcPeerConnController.setNodeId(nodeId);
|
|
17701
|
+
if (rtcPeerConnController.isMainDevice) {
|
|
17702
|
+
this._selfId$.next(memberId);
|
|
17703
|
+
this._nodeId$.next(nodeId);
|
|
17704
|
+
this.attachManager.attach(this.buildAttachableCall(callId, nodeId ?? void 0));
|
|
17705
|
+
}
|
|
17706
|
+
if (callId) this.webRtcCallSession.addCallId(callId);
|
|
16985
17707
|
logger$16.info("[WebRTCManager] Verto invite successful");
|
|
16986
17708
|
logger$16.debug(`[WebRTCManager] nodeid: ${this._nodeId$.value}, selfId: ${this._selfId$.value}`);
|
|
16987
17709
|
} else {
|
|
16988
17710
|
logger$16.error("[WebRTCManager] Verto invite failed:", response);
|
|
16989
|
-
|
|
16990
|
-
this.onError?.(inviteError);
|
|
17711
|
+
this.reportLegError(/* @__PURE__ */ new Error("Verto invite failed: unexpected response"), rtcPeerConnController);
|
|
16991
17712
|
}
|
|
16992
17713
|
}
|
|
16993
17714
|
get RTCPeerConnectionConfig() {
|
|
@@ -17012,6 +17733,7 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
17012
17733
|
inputVideoStream: options.inputVideoStream,
|
|
17013
17734
|
receiveAudio: options.receiveAudio,
|
|
17014
17735
|
receiveVideo: options.receiveVideo,
|
|
17736
|
+
fallbackToReceiveOnly: options.fallbackToReceiveOnly,
|
|
17015
17737
|
webRTCApiProvider: this.webRTCApiProvider,
|
|
17016
17738
|
preferredVideoCodecs: options.preferredVideoCodecs,
|
|
17017
17739
|
preferredAudioCodecs: options.preferredAudioCodecs,
|
|
@@ -17025,7 +17747,7 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
17025
17747
|
this._rtcPeerConnectionsMap.set(rtcPeerConnController.id, rtcPeerConnController);
|
|
17026
17748
|
this._rtcPeerConnections$.next(Array.from(this._rtcPeerConnectionsMap.values()));
|
|
17027
17749
|
this.subscribeTo(rtcPeerConnController.errors$, (error) => {
|
|
17028
|
-
this.
|
|
17750
|
+
this.reportLegError(error, rtcPeerConnController);
|
|
17029
17751
|
});
|
|
17030
17752
|
if (options.initOffer) this.handleInboundAnswer(rtcPeerConnController);
|
|
17031
17753
|
}
|
|
@@ -17054,7 +17776,7 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
17054
17776
|
await rtcPeerConnController.acceptInbound(answerOptions);
|
|
17055
17777
|
} catch (error) {
|
|
17056
17778
|
logger$16.error("[WebRTCManager] Error creating inbound answer:", error);
|
|
17057
|
-
this.
|
|
17779
|
+
this.reportLegError(toError(error), rtcPeerConnController);
|
|
17058
17780
|
}
|
|
17059
17781
|
}
|
|
17060
17782
|
}
|
|
@@ -17126,7 +17848,7 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
17126
17848
|
isInvite: isVertoInviteMessage(vertoMessage),
|
|
17127
17849
|
reattach: this.webRtcCallSession.options.reattach === true,
|
|
17128
17850
|
explicitNodeId: this.webRtcCallSession.options.nodeId,
|
|
17129
|
-
currentNodeId: this._nodeId$.value
|
|
17851
|
+
currentNodeId: rtcPeerConnController.nodeId ?? this._nodeId$.value
|
|
17130
17852
|
}),
|
|
17131
17853
|
subscribe
|
|
17132
17854
|
};
|
|
@@ -17158,7 +17880,7 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
17158
17880
|
await this.attachManager.attach(this.buildAttachableCall());
|
|
17159
17881
|
} catch (error) {
|
|
17160
17882
|
logger$16.error("[WebRTCManager] Error sending Verto answer:", error);
|
|
17161
|
-
this.
|
|
17883
|
+
this.reportLegError(toError(error), rtcPeerConnectionController);
|
|
17162
17884
|
await rtcPeerConnectionController.updateAnswerStatus({ status: "failed" });
|
|
17163
17885
|
}
|
|
17164
17886
|
}
|
|
@@ -17230,17 +17952,23 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
17230
17952
|
await this.mainPeerConnection.restoreTrackSender(deviceKind);
|
|
17231
17953
|
} else {
|
|
17232
17954
|
const error = new InvalidParams("No valid device to be added");
|
|
17233
|
-
this.
|
|
17955
|
+
this.reportLegError(error, this.mainPeerConnection);
|
|
17234
17956
|
throw error;
|
|
17235
17957
|
}
|
|
17236
17958
|
}
|
|
17237
|
-
async addScreenMedia(options = {
|
|
17238
|
-
await this.initAdditionalPeerConnection("screenshare",
|
|
17959
|
+
async addScreenMedia(options = {}) {
|
|
17960
|
+
await this.initAdditionalPeerConnection("screenshare", {
|
|
17961
|
+
audio: false,
|
|
17962
|
+
screenShareAudio: options.audio ?? false
|
|
17963
|
+
});
|
|
17239
17964
|
}
|
|
17240
17965
|
async initAdditionalPeerConnection(propose, options) {
|
|
17966
|
+
const isScreenShare = propose === "screenshare";
|
|
17967
|
+
if (isScreenShare && this._screenShareId && this._rtcPeerConnectionsMap.has(this._screenShareId)) throw new ScreenShareAlreadyActiveError(this._screenShareId);
|
|
17968
|
+
let firstPeerConnectionError;
|
|
17241
17969
|
let rtcPeerConnController = null;
|
|
17242
17970
|
try {
|
|
17243
|
-
this._screenShareStatus$.next("starting");
|
|
17971
|
+
if (isScreenShare) this._screenShareStatus$.next("starting");
|
|
17244
17972
|
rtcPeerConnController = new RTCPeerConnectionController({
|
|
17245
17973
|
...options,
|
|
17246
17974
|
...this.RTCPeerConnectionConfig,
|
|
@@ -17248,21 +17976,42 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
17248
17976
|
webRTCApiProvider: this.webRTCApiProvider
|
|
17249
17977
|
}, void 0, this.deviceController);
|
|
17250
17978
|
this.setupLocalDescriptionHandler(rtcPeerConnController);
|
|
17251
|
-
if (
|
|
17979
|
+
if (isScreenShare) this._screenShareId = rtcPeerConnController.id;
|
|
17252
17980
|
this._rtcPeerConnectionsMap.set(rtcPeerConnController.id, rtcPeerConnController);
|
|
17253
17981
|
this._rtcPeerConnections$.next(Array.from(this._rtcPeerConnectionsMap.values()));
|
|
17254
17982
|
this.subscribeTo(rtcPeerConnController.errors$, (error) => {
|
|
17255
|
-
|
|
17983
|
+
firstPeerConnectionError ??= error;
|
|
17984
|
+
this.reportLegError(error, rtcPeerConnController);
|
|
17256
17985
|
});
|
|
17257
|
-
|
|
17258
|
-
|
|
17259
|
-
|
|
17986
|
+
const pc = rtcPeerConnController;
|
|
17987
|
+
await (0, import_cjs$15.firstValueFrom)((0, import_cjs$15.merge)(pc.localMediaSettled$.pipe((0, import_cjs$15.take)(1), (0, import_cjs$15.switchMap)(() => pc.connectionState$.pipe((0, import_cjs$15.filter)((state) => state === "connected"), (0, import_cjs$15.take)(1), (0, import_cjs$15.timeout)(DEFAULT_AUX_LEG_CONNECT_TIMEOUT_MS)))), this.legError$(pc.id)).pipe((0, import_cjs$15.takeUntil)((0, import_cjs$15.merge)(this.destroyed$, pc.destroyed$))));
|
|
17988
|
+
if (isScreenShare) this._screenShareStatus$.next("started");
|
|
17989
|
+
logger$16.info(`[WebRTCManager] Additional peer connection connected (${propose}).`);
|
|
17260
17990
|
return rtcPeerConnController.id;
|
|
17261
17991
|
} catch (error) {
|
|
17262
|
-
|
|
17263
|
-
|
|
17264
|
-
if (
|
|
17265
|
-
this.
|
|
17992
|
+
const cancelled = error instanceof AuxiliaryLegCancelledError;
|
|
17993
|
+
const aborted = error instanceof import_cjs$15.EmptyError && !firstPeerConnectionError;
|
|
17994
|
+
if (!cancelled && !aborted) logger$16.warn("[WebRTCManager] Error initializing additional peer connection:", error);
|
|
17995
|
+
if (rtcPeerConnController && this._rtcPeerConnectionsMap.has(rtcPeerConnController.id)) {
|
|
17996
|
+
rtcPeerConnController.destroy();
|
|
17997
|
+
this._rtcPeerConnectionsMap.delete(rtcPeerConnController.id);
|
|
17998
|
+
this._rtcPeerConnections$.next(Array.from(this._rtcPeerConnectionsMap.values()));
|
|
17999
|
+
}
|
|
18000
|
+
if (isScreenShare) {
|
|
18001
|
+
this._screenShareId = void 0;
|
|
18002
|
+
this._screenShareStatus$.next("none");
|
|
18003
|
+
}
|
|
18004
|
+
if (cancelled) {
|
|
18005
|
+
logger$16.debug("[WebRTCManager] Additional peer connection removed before connecting.");
|
|
18006
|
+
throw error;
|
|
18007
|
+
}
|
|
18008
|
+
if (firstPeerConnectionError) throw firstPeerConnectionError instanceof MediaAccessError && firstPeerConnectionError.originalError instanceof Error ? firstPeerConnectionError.originalError : firstPeerConnectionError;
|
|
18009
|
+
if (error instanceof import_cjs$15.EmptyError) {
|
|
18010
|
+
logger$16.debug("[WebRTCManager] Additional peer connection aborted before connecting.");
|
|
18011
|
+
return;
|
|
18012
|
+
}
|
|
18013
|
+
if (error instanceof import_cjs$15.TimeoutError) throw new AuxiliaryLegTimeoutError(propose, error);
|
|
18014
|
+
throw error instanceof Error ? error : new Error(String(error), { cause: error });
|
|
17266
18015
|
}
|
|
17267
18016
|
}
|
|
17268
18017
|
async removeInputDevices(id) {
|
|
@@ -17278,7 +18027,10 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
17278
18027
|
if (removeTrack) return this.mainPeerConnection.stopTrackSender(removeTrack, { updateTransceiverDirection: true });
|
|
17279
18028
|
}
|
|
17280
18029
|
async removeScreenMedia() {
|
|
17281
|
-
if (!["starting", "started"].includes(this._screenShareStatus$.value))
|
|
18030
|
+
if (!["starting", "started"].includes(this._screenShareStatus$.value)) {
|
|
18031
|
+
logger$16.warn("[WebRTCManager] No active screen share to stop.");
|
|
18032
|
+
return;
|
|
18033
|
+
}
|
|
17282
18034
|
if (!this._screenShareId) {
|
|
17283
18035
|
logger$16.debug("[WebRTCManager] No screen share peer connection found.");
|
|
17284
18036
|
return;
|
|
@@ -17293,6 +18045,10 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
17293
18045
|
try {
|
|
17294
18046
|
if (rtcPeerConnController) await this.executeVertoBye(rtcPeerConnController);
|
|
17295
18047
|
} finally {
|
|
18048
|
+
if (rtcPeerConnController && rtcPeerConnController.connectionState !== "connected") this._legErrors$.next({
|
|
18049
|
+
legId: id,
|
|
18050
|
+
error: new AuxiliaryLegCancelledError(rtcPeerConnController.propose)
|
|
18051
|
+
});
|
|
17296
18052
|
rtcPeerConnController?.destroy();
|
|
17297
18053
|
this._rtcPeerConnectionsMap.delete(id);
|
|
17298
18054
|
this._rtcPeerConnections$.next(Array.from(this._rtcPeerConnectionsMap.values()));
|
|
@@ -17307,7 +18063,10 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
17307
18063
|
await this.executeVerto(VertoBye({
|
|
17308
18064
|
...causeParams,
|
|
17309
18065
|
dialogParams: this.dialogParams(rtcPeerConnController)
|
|
17310
|
-
})
|
|
18066
|
+
}), {
|
|
18067
|
+
callID: rtcPeerConnController.id,
|
|
18068
|
+
node_id: rtcPeerConnController.nodeId ?? void 0
|
|
18069
|
+
});
|
|
17311
18070
|
} catch (error) {
|
|
17312
18071
|
logger$16.warn("[WebRTCManager] Call might already be disconnected, error sending Verto bye:", error);
|
|
17313
18072
|
throw error;
|
|
@@ -18186,17 +18945,38 @@ function mosToQualityLevel(mos) {
|
|
|
18186
18945
|
return "critical";
|
|
18187
18946
|
}
|
|
18188
18947
|
|
|
18948
|
+
//#endregion
|
|
18949
|
+
//#region src/utils/unwrapVertoReply.ts
|
|
18950
|
+
/**
|
|
18951
|
+
* Unwrap a method's own reply from a `webrtc.verto` envelope.
|
|
18952
|
+
*
|
|
18953
|
+
* A control verb sent in-dialog comes back nested two levels deep:
|
|
18954
|
+
*
|
|
18955
|
+
* ```
|
|
18956
|
+
* { result: { node_id, code, result: { jsonrpc, id, result: <method payload> } } }
|
|
18957
|
+
* ```
|
|
18958
|
+
*
|
|
18959
|
+
* whereas the routed transport resolves the method payload directly under
|
|
18960
|
+
* `.result`. Readers want the latter shape, and the difference is silent when it
|
|
18961
|
+
* is wrong — `response.result.layouts` simply evaluates to `undefined` against the
|
|
18962
|
+
* envelope, so the data arrives, nothing throws, and the caller sees an empty
|
|
18963
|
+
* value. That exact failure produced an empty layout dropdown with a successful
|
|
18964
|
+
* request behind it.
|
|
18965
|
+
*
|
|
18966
|
+
* Accepting both shapes here keeps every reader indifferent to which transport
|
|
18967
|
+
* produced the response. Anything that is not a nested envelope (a plain ack, or
|
|
18968
|
+
* an already-unwrapped reply) passes through untouched.
|
|
18969
|
+
*/
|
|
18970
|
+
function unwrapVertoReply(response) {
|
|
18971
|
+
const inner = getValueFrom(response, "result.result");
|
|
18972
|
+
return inner && typeof inner === "object" && "result" in inner ? inner : response;
|
|
18973
|
+
}
|
|
18974
|
+
|
|
18189
18975
|
//#endregion
|
|
18190
18976
|
//#region src/core/entities/Call.ts
|
|
18191
18977
|
var import_cjs$11 = require_cjs();
|
|
18192
18978
|
const logger$12 = getLogger();
|
|
18193
18979
|
/**
|
|
18194
|
-
* Verto method for setting member layout positions. Its gateway DTO requires a
|
|
18195
|
-
* `targets` array whose entries are `{ target, position }` (NOT bare targets),
|
|
18196
|
-
* so {@link WebRTCCall.buildMethodParams} special-cases it. See issue #19400.
|
|
18197
|
-
*/
|
|
18198
|
-
const POSITION_SET_METHOD = "call.member.position.set";
|
|
18199
|
-
/**
|
|
18200
18980
|
* Ratio between the critical and warning RTT spike multipliers.
|
|
18201
18981
|
* Warning threshold = baseline * warningMultiplier (default 3x)
|
|
18202
18982
|
* Critical threshold = baseline * warningMultiplier * RTT_CRITICAL_TO_WARNING_RATIO
|
|
@@ -18291,8 +19071,11 @@ var WebRTCCall = class extends Destroyable {
|
|
|
18291
19071
|
emitError(callError) {
|
|
18292
19072
|
if (this._status$.value === "destroyed" || this._status$.value === "failed") return;
|
|
18293
19073
|
this._errors$.next(callError);
|
|
18294
|
-
if (callError.fatal) {
|
|
19074
|
+
if (callError.fatal && this._status$.value !== "disconnecting") {
|
|
18295
19075
|
this._status$.next("failed");
|
|
19076
|
+
this.vertoManager.bye().catch((error) => {
|
|
19077
|
+
logger$12.debug("[Call] fatal-teardown bye failed (signaling likely already dead):", error);
|
|
19078
|
+
});
|
|
18296
19079
|
this.destroy();
|
|
18297
19080
|
}
|
|
18298
19081
|
}
|
|
@@ -18347,7 +19130,7 @@ var WebRTCCall = class extends Destroyable {
|
|
|
18347
19130
|
/** Toggles the call lock state, preventing or allowing new participants from joining. */
|
|
18348
19131
|
async toggleLock() {
|
|
18349
19132
|
const method = this.locked ? "call.unlock" : "call.lock";
|
|
18350
|
-
await this.executeMethod(this.
|
|
19133
|
+
await this.executeMethod(this.callSelf, method, {});
|
|
18351
19134
|
}
|
|
18352
19135
|
/**
|
|
18353
19136
|
* Toggles the hold state of the call (pauses/resumes local media transmission).
|
|
@@ -18396,14 +19179,25 @@ var WebRTCCall = class extends Destroyable {
|
|
|
18396
19179
|
*
|
|
18397
19180
|
* Constructs call context (node_id, call_id, member_id) and sends the RPC request.
|
|
18398
19181
|
*
|
|
18399
|
-
* @param target - Target
|
|
19182
|
+
* @param target - Target {@link MemberTarget} triple, or the local member's
|
|
19183
|
+
* ID string for self-operations (any other string is rejected — a bare
|
|
19184
|
+
* member id cannot carry the remote member's own call context).
|
|
18400
19185
|
* @param method - Verto method name (e.g. `'call.mute'`, `'call.member.remove'`).
|
|
18401
19186
|
* @param args - Parameters for the RPC method.
|
|
18402
19187
|
* @returns The RPC response.
|
|
19188
|
+
* @throws {CallNotReadyError} If the call has no self member context yet.
|
|
19189
|
+
* @throws {InvalidParams} If a string target is not the local member's ID.
|
|
18403
19190
|
* @throws {JSONRPCError} If the RPC call returns an error.
|
|
18404
19191
|
*/
|
|
18405
19192
|
async executeMethod(target, method, args) {
|
|
18406
|
-
const
|
|
19193
|
+
const self = this.callSelf;
|
|
19194
|
+
if (typeof target === "string" && target !== self.member_id) throw new InvalidParams(`Target member ID ${target} does not match call's self member ID ${self.member_id}`);
|
|
19195
|
+
if (this.clientSession.callControl === "in-dialog") return this.executeMethodInDialog(target, method, args);
|
|
19196
|
+
const params = {
|
|
19197
|
+
...args,
|
|
19198
|
+
self,
|
|
19199
|
+
target: typeof target === "string" ? self : target
|
|
19200
|
+
};
|
|
18407
19201
|
const request = buildRPCRequest({
|
|
18408
19202
|
method,
|
|
18409
19203
|
params
|
|
@@ -18417,29 +19211,98 @@ var WebRTCCall = class extends Destroyable {
|
|
|
18417
19211
|
throw error;
|
|
18418
19212
|
}
|
|
18419
19213
|
}
|
|
18420
|
-
|
|
18421
|
-
|
|
18422
|
-
|
|
18423
|
-
|
|
18424
|
-
|
|
18425
|
-
|
|
18426
|
-
|
|
18427
|
-
|
|
18428
|
-
|
|
18429
|
-
|
|
18430
|
-
|
|
18431
|
-
|
|
18432
|
-
|
|
18433
|
-
|
|
18434
|
-
};
|
|
18435
|
-
|
|
18436
|
-
|
|
18437
|
-
|
|
18438
|
-
|
|
18439
|
-
|
|
19214
|
+
/**
|
|
19215
|
+
* `executeMethod` for a call opened with `callControl: 'in-dialog'`.
|
|
19216
|
+
*
|
|
19217
|
+
* Translates the routed transport's calling convention into the in-dialog one. No
|
|
19218
|
+
* `self` tuple is sent, but a `target` is — the same {call_id, member_id} the routed
|
|
19219
|
+
* transport puts in `target` (minus node_id), for self-ops and cross-member ops alike.
|
|
19220
|
+
*
|
|
19221
|
+
* Target shapes are per-verb and irregular, so they are centralised here rather
|
|
19222
|
+
* than left to callers: most verbs take a singular `target`, `call.member.remove`
|
|
19223
|
+
* takes a plural `targets` array, and `call.member.position.set` takes a flat
|
|
19224
|
+
* `targets` of `{call_id, position}` — the one verb keyed on call_id rather than
|
|
19225
|
+
* member_id, so the member triple `Participant.setPosition` built is unwrapped.
|
|
19226
|
+
*/
|
|
19227
|
+
async executeMethodInDialog(target, method, args) {
|
|
19228
|
+
const control = { ...args };
|
|
19229
|
+
if (method === "call.member.position.set") control.targets = (args.targets ?? []).map((entry) => ({
|
|
19230
|
+
call_id: entry.target?.call_id ?? entry.call_id,
|
|
19231
|
+
position: entry.position
|
|
19232
|
+
}));
|
|
19233
|
+
else {
|
|
19234
|
+
const member = typeof target === "object" ? {
|
|
19235
|
+
call_id: target.call_id,
|
|
19236
|
+
member_id: target.member_id
|
|
19237
|
+
} : {
|
|
18440
19238
|
call_id: this.id,
|
|
18441
19239
|
member_id: target
|
|
18442
|
-
}
|
|
19240
|
+
};
|
|
19241
|
+
if (method === "call.member.remove") control.targets = [member];
|
|
19242
|
+
else control.target = member;
|
|
19243
|
+
}
|
|
19244
|
+
return this.sendCommand(method, control);
|
|
19245
|
+
}
|
|
19246
|
+
/**
|
|
19247
|
+
* Sends a `call.*` control verb **in-dialog** via `verto.info`, as an alternative
|
|
19248
|
+
* to the routed {@link executeMethod} transport.
|
|
19249
|
+
*
|
|
19250
|
+
* Why both exist: `executeMethod` addresses the member with an explicit
|
|
19251
|
+
* `{node_id, call_id, member_id}` tuple, which does not resolve for every conference,
|
|
19252
|
+
* so the op can fail. An in-dialog frame carries the verb on the member's own
|
|
19253
|
+
* signaling channel instead, so control works without the client needing to know how
|
|
19254
|
+
* the conference is hosted.
|
|
19255
|
+
*
|
|
19256
|
+
* The trade-off is reach: the in-dialog transport is only accepted for calls that
|
|
19257
|
+
* join a conference over SWML (e.g. an SWML `join_conference`); use the routed
|
|
19258
|
+
* default otherwise.
|
|
19259
|
+
*
|
|
19260
|
+
* `params` are sent verbatim — nothing is built for you, which includes the target.
|
|
19261
|
+
* **A self-directed op still needs one**, or it is refused; name yourself explicitly:
|
|
19262
|
+
*
|
|
19263
|
+
* ```ts
|
|
19264
|
+
* const { call_id, member_id } = call.self.target;
|
|
19265
|
+
* await call.sendCommand('call.mute', { channels: ['audio'], target: { call_id, member_id } });
|
|
19266
|
+
* ```
|
|
19267
|
+
*
|
|
19268
|
+
* Never include `node_id` — only the two ids. The shapes are per-verb: most take a
|
|
19269
|
+
* singular `target`, `call.member.remove` takes a plural `targets` array, and
|
|
19270
|
+
* `call.member.position.set` takes a flat `targets: [{call_id, position}]` (the one
|
|
19271
|
+
* verb keyed on `call_id` rather than `member_id`). Verbs that act on the call as a
|
|
19272
|
+
* whole, or that the SDK does not wrap at all, take no target.
|
|
19273
|
+
*
|
|
19274
|
+
* For the typed alternative that handles all of this, create the client with
|
|
19275
|
+
* `callControl: 'in-dialog'` and use the ordinary `Call`/`Participant` methods.
|
|
19276
|
+
*
|
|
19277
|
+
* @internal Not part of the supported surface while the in-dialog transport is still
|
|
19278
|
+
* rolling out. `WebRTCCall` is exported from the package entry, so without this tag
|
|
19279
|
+
* TypeDoc publishes the method — and the example above — as public API.
|
|
19280
|
+
*
|
|
19281
|
+
* @param method - A `call.*` method name (e.g. `'call.mute'`).
|
|
19282
|
+
* @param params - Method parameters, sent verbatim.
|
|
19283
|
+
* @returns The method's own reply, unwrapped from the `verto.info` envelope.
|
|
19284
|
+
* @throws {JSONRPCError} If the control op fails.
|
|
19285
|
+
*/
|
|
19286
|
+
async sendCommand(method, params = {}) {
|
|
19287
|
+
return unwrapVertoReply(await this.vertoManager.sendCallControl(method, params));
|
|
19288
|
+
}
|
|
19289
|
+
/**
|
|
19290
|
+
* The local leg's member triple — sent as `self` in every member RPC
|
|
19291
|
+
* envelope, and as the `target` of call-scoped self-operations (e.g. lock,
|
|
19292
|
+
* layout).
|
|
19293
|
+
*
|
|
19294
|
+
* @throws {CallNotReadyError} Before `call.joined` delivers the self member
|
|
19295
|
+
* context (`selfId`/`nodeId`) — an RPC without it cannot be routed, so fail
|
|
19296
|
+
* fast instead of sending a doomed request.
|
|
19297
|
+
*/
|
|
19298
|
+
get callSelf() {
|
|
19299
|
+
const node_id = this.nodeId;
|
|
19300
|
+
const member_id = this.vertoManager.selfId;
|
|
19301
|
+
if (!node_id || !member_id) throw new CallNotReadyError(this.id);
|
|
19302
|
+
return {
|
|
19303
|
+
node_id,
|
|
19304
|
+
call_id: this.id,
|
|
19305
|
+
member_id
|
|
18443
19306
|
};
|
|
18444
19307
|
}
|
|
18445
19308
|
/** Observable of the current call status (e.g. `'ringing'`, `'connected'`). */
|
|
@@ -18595,13 +19458,13 @@ var WebRTCCall = class extends Destroyable {
|
|
|
18595
19458
|
get bandwidthConstrained$() {
|
|
18596
19459
|
return this.deferEmission(this._bandwidthConstrained$.asObservable());
|
|
18597
19460
|
}
|
|
18598
|
-
/** Observable that emits when server
|
|
19461
|
+
/** Observable that emits when the server pushes media params. */
|
|
18599
19462
|
get mediaParamsUpdated$() {
|
|
18600
19463
|
return this.deferEmission(this._mediaParamsUpdated$.asObservable());
|
|
18601
19464
|
}
|
|
18602
19465
|
/**
|
|
18603
19466
|
* @internal Emit a media params update event.
|
|
18604
|
-
* Called by the VertoManager when server
|
|
19467
|
+
* Called by the VertoManager when the server pushes media params.
|
|
18605
19468
|
*/
|
|
18606
19469
|
emitMediaParamsUpdated(event) {
|
|
18607
19470
|
this._mediaParamsUpdated$.next(event);
|
|
@@ -18809,6 +19672,10 @@ var WebRTCCall = class extends Destroyable {
|
|
|
18809
19672
|
get selfId$() {
|
|
18810
19673
|
return this.vertoManager.selfId$;
|
|
18811
19674
|
}
|
|
19675
|
+
/** @internal Lets call creation bound the media and signalling phases apart. */
|
|
19676
|
+
get localMediaSettled$() {
|
|
19677
|
+
return this.vertoManager.localMediaSettled$;
|
|
19678
|
+
}
|
|
18812
19679
|
/** Local participant's member ID, or `null` if not joined. */
|
|
18813
19680
|
get selfId() {
|
|
18814
19681
|
return this.vertoManager.selfId;
|
|
@@ -19002,12 +19869,17 @@ var WebRTCCall = class extends Destroyable {
|
|
|
19002
19869
|
*
|
|
19003
19870
|
* **These operations are NOT atomic.** The layout is applied first, then each
|
|
19004
19871
|
* member position sequentially, so members may briefly flash into their
|
|
19005
|
-
* default slots before being moved to the requested positions.
|
|
19872
|
+
* default slots before being moved to the requested positions. Targeted
|
|
19873
|
+
* members are validated upfront, though: when any of them has no
|
|
19874
|
+
* {@link Participant.target | member call context} yet, the whole call
|
|
19875
|
+
* rejects before any request is sent and the layout is left unchanged.
|
|
19006
19876
|
*
|
|
19007
19877
|
* @param layout - Layout name (must be one of {@link layouts}).
|
|
19008
19878
|
* @param positions - Optional map of member IDs to {@link VideoPosition} values.
|
|
19009
19879
|
* When omitted or empty, only the layout is changed.
|
|
19010
19880
|
* @throws {InvalidParams} If the layout is not in the available {@link layouts}.
|
|
19881
|
+
* @throws {ParticipantNotReadyError} If a targeted member's call context has
|
|
19882
|
+
* not been received yet — thrown before any request is sent.
|
|
19011
19883
|
*
|
|
19012
19884
|
* @example
|
|
19013
19885
|
* ```ts
|
|
@@ -19018,18 +19890,19 @@ var WebRTCCall = class extends Destroyable {
|
|
|
19018
19890
|
*/
|
|
19019
19891
|
async setLayout(layout, positions) {
|
|
19020
19892
|
if (!this.layouts.includes(layout)) throw new InvalidParams(`Layout ${layout} is not available in the current call layouts: ${this.layouts.join(", ")}`);
|
|
19021
|
-
const
|
|
19022
|
-
|
|
19023
|
-
const positionEntries = Object.entries(positions ?? {});
|
|
19024
|
-
if (positionEntries.length === 0) return;
|
|
19025
|
-
for (const [memberId, position] of positionEntries) {
|
|
19893
|
+
const targets = [];
|
|
19894
|
+
for (const [memberId, position] of Object.entries(positions ?? {})) {
|
|
19026
19895
|
const participant = this.participants.find((p) => p.id === memberId);
|
|
19027
19896
|
if (!participant) {
|
|
19028
19897
|
logger$12.warn(`[Call] setLayout: member ${memberId} not found in participants; skipping position ${position}`);
|
|
19029
19898
|
continue;
|
|
19030
19899
|
}
|
|
19031
|
-
|
|
19900
|
+
participant.target;
|
|
19901
|
+
targets.push([participant, position]);
|
|
19032
19902
|
}
|
|
19903
|
+
const selfId = await (0, import_cjs$11.firstValueFrom)(this.selfId$.pipe((0, import_cjs$11.filter)((id) => id !== null)));
|
|
19904
|
+
await this.executeMethod(selfId, "call.layout.set", { layout });
|
|
19905
|
+
for (const [participant, position] of targets) await participant.setPosition(position);
|
|
19033
19906
|
}
|
|
19034
19907
|
/**
|
|
19035
19908
|
* Transfers the call to another destination.
|
|
@@ -19131,17 +20004,28 @@ var WebRTCCall = class extends Destroyable {
|
|
|
19131
20004
|
* (notably iOS Safari) fall back to re-acquiring the track with the new
|
|
19132
20005
|
* constraint set and plumbing the replacement through the local audio
|
|
19133
20006
|
* pipeline if one is active.
|
|
20007
|
+
*
|
|
20008
|
+
* @returns whether the constraint reached the microphone. `false` is an
|
|
20009
|
+
* outcome rather than an error — a leg sending media the SDK did not capture
|
|
20010
|
+
* is left alone — so a UI that reflects the toggle must read it. Any failure
|
|
20011
|
+
* behind a `false` is also reported on {@link errors$}.
|
|
19134
20012
|
*/
|
|
19135
20013
|
async setEchoCancellation(enabled) {
|
|
19136
|
-
|
|
20014
|
+
return this.vertoManager.updateMediaConstraints({ audio: { echoCancellation: enabled } });
|
|
19137
20015
|
}
|
|
19138
|
-
/**
|
|
20016
|
+
/**
|
|
20017
|
+
* Toggle browser noise suppression on the local mic at runtime.
|
|
20018
|
+
* @returns whether the constraint reached the microphone.
|
|
20019
|
+
*/
|
|
19139
20020
|
async setNoiseSuppression(enabled) {
|
|
19140
|
-
|
|
20021
|
+
return this.vertoManager.updateMediaConstraints({ audio: { noiseSuppression: enabled } });
|
|
19141
20022
|
}
|
|
19142
|
-
/**
|
|
20023
|
+
/**
|
|
20024
|
+
* Toggle browser automatic gain control on the local mic at runtime.
|
|
20025
|
+
* @returns whether the constraint reached the microphone.
|
|
20026
|
+
*/
|
|
19143
20027
|
async setAutoGainControl(enabled) {
|
|
19144
|
-
|
|
20028
|
+
return this.vertoManager.updateMediaConstraints({ audio: { autoGainControl: enabled } });
|
|
19145
20029
|
}
|
|
19146
20030
|
/**
|
|
19147
20031
|
* Observable of the aggregate remote audio level, 0..1 RMS. The server
|
|
@@ -19198,11 +20082,15 @@ var WebRTCCall = class extends Destroyable {
|
|
|
19198
20082
|
/**
|
|
19199
20083
|
* Infers the semantic error category from a raw Error thrown by VertoManager
|
|
19200
20084
|
* or an RTCPeerConnection layer.
|
|
20085
|
+
*
|
|
20086
|
+
* Pure function — exported for unit testing.
|
|
20087
|
+
* @internal
|
|
19201
20088
|
*/
|
|
19202
20089
|
function inferCallErrorKind(error) {
|
|
19203
20090
|
if (error instanceof RPCTimeoutError) return "timeout";
|
|
19204
20091
|
if (error instanceof JSONRPCError) return "signaling";
|
|
19205
20092
|
if (error instanceof MediaTrackError) return "media";
|
|
20093
|
+
if (error instanceof MediaAccessError) return "media";
|
|
19206
20094
|
if (error instanceof WebSocketConnectionError || error instanceof TransportConnectionError) return "network";
|
|
19207
20095
|
return "internal";
|
|
19208
20096
|
}
|
|
@@ -19215,10 +20103,18 @@ const RECOVERABLE_RPC_CODES = new Set([
|
|
|
19215
20103
|
RPC_ERROR_AUTHENTICATION_FAILED,
|
|
19216
20104
|
RPC_ERROR_INVALID_PARAMS
|
|
19217
20105
|
]);
|
|
19218
|
-
/**
|
|
20106
|
+
/**
|
|
20107
|
+
* A *fallback*: callers knowing which leg failed pass an explicit `fatal` and
|
|
20108
|
+
* never reach here, so the default-fatal branch only sees call- and main-leg
|
|
20109
|
+
* errors. Auxiliary legs go through `WebRTCVertoManager.reportLegError`.
|
|
20110
|
+
*
|
|
20111
|
+
* Pure function — exported for unit testing.
|
|
20112
|
+
* @internal
|
|
20113
|
+
*/
|
|
19219
20114
|
function isFatalError(error) {
|
|
19220
20115
|
if (error instanceof VertoPongError) return false;
|
|
19221
20116
|
if (error instanceof MediaTrackError) return false;
|
|
20117
|
+
if (error instanceof MediaAccessError) return error.fatal;
|
|
19222
20118
|
if (error instanceof RPCTimeoutError) return false;
|
|
19223
20119
|
if (error instanceof JSONRPCError && RECOVERABLE_RPC_CODES.has(error.code)) return false;
|
|
19224
20120
|
return true;
|
|
@@ -19244,12 +20140,14 @@ var CallFactory = class {
|
|
|
19244
20140
|
return {
|
|
19245
20141
|
vertoManager: new WebRTCVertoManager(callInstance, this.attachManager, this.deviceController, this.webRTCApiProvider, {
|
|
19246
20142
|
nodeId: options.nodeId,
|
|
19247
|
-
onError: (error) => {
|
|
20143
|
+
onError: (error, options$1) => {
|
|
19248
20144
|
const callError = {
|
|
19249
20145
|
kind: inferCallErrorKind(error),
|
|
19250
|
-
fatal: isFatalError(error),
|
|
20146
|
+
fatal: options$1?.fatal ?? isFatalError(error),
|
|
19251
20147
|
error,
|
|
19252
|
-
callId: callInstance.id
|
|
20148
|
+
callId: callInstance.id,
|
|
20149
|
+
...options$1?.leg ? { leg: options$1.leg } : {},
|
|
20150
|
+
...options$1?.legId ? { legId: options$1.legId } : {}
|
|
19253
20151
|
};
|
|
19254
20152
|
callInstance.emitError(callError);
|
|
19255
20153
|
},
|
|
@@ -19710,6 +20608,41 @@ var PendingRPC = class PendingRPC {
|
|
|
19710
20608
|
//#region src/managers/ClientSessionManager.ts
|
|
19711
20609
|
var import_cjs$7 = require_cjs();
|
|
19712
20610
|
const logger$9 = getLogger();
|
|
20611
|
+
/**
|
|
20612
|
+
* Decide whether an error emitted on `call.errors$` during dial should
|
|
20613
|
+
* abort the dial. A non-fatal MediaAccessError means the call degraded to
|
|
20614
|
+
* receive-only and still connects — everything else rejects `dial()` with
|
|
20615
|
+
* the real cause.
|
|
20616
|
+
*
|
|
20617
|
+
* Pure function — exported for unit testing.
|
|
20618
|
+
*/
|
|
20619
|
+
function shouldAbortDial(callError) {
|
|
20620
|
+
return callError.fatal || !(callError.error instanceof MediaAccessError);
|
|
20621
|
+
}
|
|
20622
|
+
/**
|
|
20623
|
+
* Wait for a dialed call to be ready, or for the failure that stops it.
|
|
20624
|
+
*
|
|
20625
|
+
* Local media acquisition is deliberately unbounded: a permission prompt or a
|
|
20626
|
+
* device picker is human time, and `getUserMedia` cannot be cancelled anyway.
|
|
20627
|
+
* The clock starts only once acquisition settles, so a slow human never spends
|
|
20628
|
+
* the server's budget.
|
|
20629
|
+
*
|
|
20630
|
+
* `merge` rather than `race`, because the two legs settle asymmetrically. A
|
|
20631
|
+
* fatal acquisition failure reports the error and then destroys the call in the
|
|
20632
|
+
* same synchronous step; `errors$` defers delivery by a microtask while
|
|
20633
|
+
* `localMediaSettled$` completes immediately. Under `race` that bare completion
|
|
20634
|
+
* ended the wait first and `dial()` rejected with an RxJS `EmptyError`, burying
|
|
20635
|
+
* the `NotAllowedError` applications are told to inspect. Under `merge` the
|
|
20636
|
+
* completed leg is simply spent, and the queued error — enqueued before the
|
|
20637
|
+
* completion, so delivered before it — arrives to reject the wait. A dial
|
|
20638
|
+
* abandoned with no error at all still ends both legs, and the resulting
|
|
20639
|
+
* `EmptyError` remains the benign-cancel signal.
|
|
20640
|
+
*
|
|
20641
|
+
* Exported for unit testing.
|
|
20642
|
+
*/
|
|
20643
|
+
async function awaitDialReady(session, signalingTimeoutMs) {
|
|
20644
|
+
return (0, import_cjs$7.firstValueFrom)((0, import_cjs$7.merge)(session.localMediaSettled$.pipe((0, import_cjs$7.take)(1), (0, import_cjs$7.switchMap)(() => session.selfId$.pipe((0, import_cjs$7.filter)((id) => Boolean(id)), (0, import_cjs$7.take)(1), (0, import_cjs$7.timeout)(signalingTimeoutMs)))), session.errors$.pipe((0, import_cjs$7.filter)(shouldAbortDial), (0, import_cjs$7.take)(1), (0, import_cjs$7.switchMap)((callError) => (0, import_cjs$7.throwError)(() => callError.error)))));
|
|
20645
|
+
}
|
|
19713
20646
|
const getAddressSearchURI = (options) => {
|
|
19714
20647
|
const to = options.to?.split("?")[0];
|
|
19715
20648
|
const from$9 = options.from?.startsWith("subscriber://") ? options.from.replace("subscriber://", "") : options.from;
|
|
@@ -19726,7 +20659,6 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
19726
20659
|
this.authorizationStateKey = authorizationStateKey;
|
|
19727
20660
|
this.attachManager = attachManager;
|
|
19728
20661
|
this.dpopManager = dpopManager;
|
|
19729
|
-
this.callCreateTimeout = 6e3;
|
|
19730
20662
|
this.agent = `signalwire-js/4.0.0`;
|
|
19731
20663
|
this.eventAcks = true;
|
|
19732
20664
|
this.authorizationState$ = this.createReplaySubject(1);
|
|
@@ -19735,6 +20667,7 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
19735
20667
|
minor: 0,
|
|
19736
20668
|
revision: 0
|
|
19737
20669
|
};
|
|
20670
|
+
this.callControl = "routed";
|
|
19738
20671
|
this._authorization$ = this.createBehaviorSubject(void 0);
|
|
19739
20672
|
this._errors$ = this.createReplaySubject(1);
|
|
19740
20673
|
this._authState$ = this.createBehaviorSubject({ kind: "unauthenticated" });
|
|
@@ -19935,21 +20868,18 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
19935
20868
|
}
|
|
19936
20869
|
async handleAuthenticationError(error) {
|
|
19937
20870
|
logger$9.error("Authentication error:", error);
|
|
19938
|
-
const isRecoverableAuthError = error instanceof JSONRPCError && (error.code === RPC_ERROR_REQUESTER_VALIDATION_FAILED || error.code === RPC_ERROR_INVALID_PARAMS || error.code === RPC_ERROR_AUTHENTICATION_FAILED);
|
|
20871
|
+
const isRecoverableAuthError$1 = error instanceof JSONRPCError && (error.code === RPC_ERROR_REQUESTER_VALIDATION_FAILED || error.code === RPC_ERROR_INVALID_PARAMS || error.code === RPC_ERROR_AUTHENTICATION_FAILED);
|
|
19939
20872
|
const hasStoredState = await (0, import_cjs$7.firstValueFrom)(this.authorizationState$.pipe((0, import_cjs$7.take)(1))) !== void 0;
|
|
19940
|
-
if (isRecoverableAuthError && hasStoredState) {
|
|
20873
|
+
if (isRecoverableAuthError$1 && hasStoredState) {
|
|
19941
20874
|
logger$9.debug("[Session] Recoverable auth error — cleaning up stored state and reconnecting fresh");
|
|
19942
|
-
|
|
19943
|
-
await this.cleanupStoredConnectionParams();
|
|
19944
|
-
} catch (cleanupError) {
|
|
19945
|
-
logger$9.error("Failed to cleanup stored connection params:", cleanupError);
|
|
19946
|
-
} finally {
|
|
19947
|
-
this.transport.reconnect();
|
|
19948
|
-
}
|
|
20875
|
+
await this.discardResumeStateAndReconnect();
|
|
19949
20876
|
} else this._errors$.next(error);
|
|
19950
20877
|
}
|
|
19951
20878
|
/**
|
|
19952
|
-
* Clear the resume state (authorization_state + protocol)
|
|
20879
|
+
* Clear the resume state (authorization_state + protocol) and ask the
|
|
20880
|
+
* transport to reconnect. The `connected` event re-triggers
|
|
20881
|
+
* `authenticate()`, which now has no stored state and so performs a fresh
|
|
20882
|
+
* connect.
|
|
19953
20883
|
*
|
|
19954
20884
|
* This is the stale-auth-state recovery helper used by handleAuthError:
|
|
19955
20885
|
* the server rejected a reconnect, so the resume state is discarded and a
|
|
@@ -19957,9 +20887,24 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
19957
20887
|
* session lives on through the reconnect and reattachCalls() needs the
|
|
19958
20888
|
* stored call references afterwards. Do NOT add detachAll() here.
|
|
19959
20889
|
*
|
|
20890
|
+
* Connect-time recovery only. A *request* refused on an already
|
|
20891
|
+
* authenticated session is never healed here: dropping the resume state
|
|
20892
|
+
* destroys the association between the socket and the previous session,
|
|
20893
|
+
* which is what reattach depends on. That path mints a fresh credential and
|
|
20894
|
+
* reauthenticates instead (see `SignalWire.recoverAndRetry`).
|
|
20895
|
+
*
|
|
19960
20896
|
* For public teardown (disconnect/destroy), use {@link teardownSessionState}
|
|
19961
20897
|
* instead, which clears the attach records as well.
|
|
19962
20898
|
*/
|
|
20899
|
+
async discardResumeStateAndReconnect() {
|
|
20900
|
+
try {
|
|
20901
|
+
await this.cleanupStoredConnectionParams();
|
|
20902
|
+
} catch (cleanupError) {
|
|
20903
|
+
logger$9.error("Failed to cleanup stored connection params:", cleanupError);
|
|
20904
|
+
} finally {
|
|
20905
|
+
this.transport.reconnect();
|
|
20906
|
+
}
|
|
20907
|
+
}
|
|
19963
20908
|
async cleanupStoredConnectionParams() {
|
|
19964
20909
|
await this.transport.setProtocol(void 0);
|
|
19965
20910
|
await this.updateAuthorizationStateInStorage(void 0);
|
|
@@ -20033,11 +20978,15 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
20033
20978
|
const isReconnect = hasReconnectState && storedToken;
|
|
20034
20979
|
let dpopToken;
|
|
20035
20980
|
if (isReconnect) logger$9.debug("[Session] Reconnecting with stored jwt_token + authorization_state");
|
|
20036
|
-
else
|
|
20037
|
-
|
|
20038
|
-
|
|
20981
|
+
else {
|
|
20982
|
+
const credential = this.getCredential();
|
|
20983
|
+
const credentialExpired = credential.expiry_at !== void 0 && credential.expiry_at <= Date.now() + CREDENTIAL_EXPIRY_SKEW_MS;
|
|
20984
|
+
if (this.onBeforeReconnect && (this.clientBound || credentialExpired)) {
|
|
20985
|
+
logger$9.debug("[Session] Refreshing credentials before fresh connect");
|
|
20986
|
+
await this.onBeforeReconnect();
|
|
20987
|
+
}
|
|
20039
20988
|
}
|
|
20040
|
-
if (
|
|
20989
|
+
if (this.dpopManager?.initialized) try {
|
|
20041
20990
|
dpopToken = await this.dpopManager.createRpcProof({ method: "signalwire.connect" });
|
|
20042
20991
|
} catch (error) {
|
|
20043
20992
|
if (this.clientBound) throw error;
|
|
@@ -20070,6 +21019,7 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
20070
21019
|
});
|
|
20071
21020
|
if (response.protocol) await this.transport.setProtocol(response.protocol);
|
|
20072
21021
|
this._authorization$.next(response.authorization);
|
|
21022
|
+
if (response.authorization.cnf?.jkt) this._wasClientBound = true;
|
|
20073
21023
|
this._iceServers$.next(response.ice_servers ?? []);
|
|
20074
21024
|
this._authState$.next({ kind: "authenticated" });
|
|
20075
21025
|
logger$9.debug("[Session] Authentication completed successfully");
|
|
@@ -20136,7 +21086,7 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
20136
21086
|
to: destinationURI,
|
|
20137
21087
|
...options
|
|
20138
21088
|
});
|
|
20139
|
-
await (
|
|
21089
|
+
await awaitDialReady(callSession, DEFAULT_CALL_SIGNALING_TIMEOUT_MS);
|
|
20140
21090
|
this._calls$.next({
|
|
20141
21091
|
[`${callSession.id}`]: callSession,
|
|
20142
21092
|
...this._calls$.value
|
|
@@ -20189,12 +21139,23 @@ var ClientSessionWrapper = class {
|
|
|
20189
21139
|
get authenticated() {
|
|
20190
21140
|
return this.clientSessionManager.authenticated;
|
|
20191
21141
|
}
|
|
21142
|
+
/**
|
|
21143
|
+
* Whether the session is using a Client Bound SAT (DPoP). Sticky — set
|
|
21144
|
+
* when the binding is established or restored from a resumed session's
|
|
21145
|
+
* server authorization.
|
|
21146
|
+
*/
|
|
21147
|
+
get clientBound() {
|
|
21148
|
+
return this.clientSessionManager.clientBound;
|
|
21149
|
+
}
|
|
20192
21150
|
get signalingEvent$() {
|
|
20193
21151
|
return this.clientSessionManager.signalingEvent$;
|
|
20194
21152
|
}
|
|
20195
21153
|
get iceServers() {
|
|
20196
21154
|
return this.clientSessionManager.iceServers;
|
|
20197
21155
|
}
|
|
21156
|
+
get callControl() {
|
|
21157
|
+
return this.clientSessionManager.callControl;
|
|
21158
|
+
}
|
|
20198
21159
|
async execute(request, options) {
|
|
20199
21160
|
return this.clientSessionManager.execute(request, options);
|
|
20200
21161
|
}
|
|
@@ -20391,7 +21352,7 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
20391
21352
|
await session.reauthenticate(tokenData.token, rpcProof, { clientBound: true });
|
|
20392
21353
|
updateCredential({ token: tokenData.token });
|
|
20393
21354
|
logger$7.info("[DeviceToken] Client Bound SAT activated successfully");
|
|
20394
|
-
this.
|
|
21355
|
+
this.emitCurrentToken(tokenData);
|
|
20395
21356
|
return { activated: true };
|
|
20396
21357
|
} catch (error) {
|
|
20397
21358
|
logger$7.error("[DeviceToken] Failed to activate Client Bound SAT:", error);
|
|
@@ -20403,6 +21364,21 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
20403
21364
|
}
|
|
20404
21365
|
}
|
|
20405
21366
|
/**
|
|
21367
|
+
* Emit a freshly received token to the reactive pipeline, stamping an
|
|
21368
|
+
* absolute `expires_at` when the response carried only `expires_in`.
|
|
21369
|
+
* Resolving the expiry at RECEIVE time (not at read time) is what lets
|
|
21370
|
+
* {@link refreshNowIfDue} detect due-ness on resume: a bare `expires_in`
|
|
21371
|
+
* re-resolved later would always compute a full TTL from "now" and never
|
|
21372
|
+
* cross the refresh buffer.
|
|
21373
|
+
*/
|
|
21374
|
+
emitCurrentToken(token) {
|
|
21375
|
+
const stamped = token.expires_at ? token : {
|
|
21376
|
+
...token,
|
|
21377
|
+
expires_at: resolveExpiresAt(token)
|
|
21378
|
+
};
|
|
21379
|
+
this._currentToken$.next(stamped);
|
|
21380
|
+
}
|
|
21381
|
+
/**
|
|
20406
21382
|
* Returns true when the cached token has enough headroom before expiry to
|
|
20407
21383
|
* be safely reused on reactivation. The headroom matches the refresh
|
|
20408
21384
|
* buffer, so a token within the refresh window is treated as stale (the
|
|
@@ -20420,7 +21396,7 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
20420
21396
|
method: "POST",
|
|
20421
21397
|
uri: DEVICE_TOKEN_ENDPOINT
|
|
20422
21398
|
});
|
|
20423
|
-
const response = await this.http.request({
|
|
21399
|
+
const response = await this.http().request({
|
|
20424
21400
|
url: DEVICE_TOKEN_ENDPOINT,
|
|
20425
21401
|
...POST_PARAMS,
|
|
20426
21402
|
body: JSON.stringify({
|
|
@@ -20447,7 +21423,7 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
20447
21423
|
uri: DEVICE_REFRESH_ENDPOINT,
|
|
20448
21424
|
accessToken: currentToken
|
|
20449
21425
|
});
|
|
20450
|
-
const response = await this.http.request({
|
|
21426
|
+
const response = await this.http().request({
|
|
20451
21427
|
url: DEVICE_REFRESH_ENDPOINT,
|
|
20452
21428
|
...POST_PARAMS,
|
|
20453
21429
|
body: JSON.stringify({
|
|
@@ -20495,7 +21471,7 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
20495
21471
|
const currentToken = this.getCredential().token;
|
|
20496
21472
|
if (!currentToken) throw new TokenRefreshError("No current token available for refresh");
|
|
20497
21473
|
const newTokenData = await this.retryRefresh(session, currentToken, updateCredential);
|
|
20498
|
-
this.
|
|
21474
|
+
this.emitCurrentToken(newTokenData);
|
|
20499
21475
|
} catch (error) {
|
|
20500
21476
|
logger$7.error("[DeviceToken] Automatic Client Bound SAT refresh failed:", error);
|
|
20501
21477
|
this.errorHandler(error instanceof TokenRefreshError ? error : new TokenRefreshError("Automatic token refresh failed", error));
|
|
@@ -20522,6 +21498,22 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
20522
21498
|
throw lastError instanceof Error ? lastError : new TokenRefreshError("All refresh retries exhausted", lastError);
|
|
20523
21499
|
}
|
|
20524
21500
|
/**
|
|
21501
|
+
* Force an immediate refresh when the cached Client Bound SAT is already
|
|
21502
|
+
* past its refresh window. Called on resume from suspension where
|
|
21503
|
+
* background-tab throttling can delay the reactive timer past the buffer.
|
|
21504
|
+
* A no-op when no token is cached or it still has headroom; the normal
|
|
21505
|
+
* {@link executeRefresh} guards (paused / in-progress / unauthenticated)
|
|
21506
|
+
* still apply.
|
|
21507
|
+
*/
|
|
21508
|
+
refreshNowIfDue() {
|
|
21509
|
+
const token = this._currentToken$.value;
|
|
21510
|
+
if (!token) return;
|
|
21511
|
+
if (resolveExpiresAt(token) * 1e3 - Date.now() <= DEVICE_TOKEN_REFRESH_BUFFER_MS) {
|
|
21512
|
+
logger$7.debug("[DeviceToken] Resume: cached SAT past refresh window; refreshing now");
|
|
21513
|
+
this.executeRefresh();
|
|
21514
|
+
}
|
|
21515
|
+
}
|
|
21516
|
+
/**
|
|
20525
21517
|
* Stops the reactive refresh pipeline from firing. Use when the underlying
|
|
20526
21518
|
* session is being torn down (e.g., during {@link SignalWire.disconnect})
|
|
20527
21519
|
* so a scheduled refresh cannot fire against a destroyed session.
|
|
@@ -20572,6 +21564,7 @@ var CredentialRefreshCoordinator = class extends Destroyable {
|
|
|
20572
21564
|
this.deps = deps;
|
|
20573
21565
|
this._activating = false;
|
|
20574
21566
|
this._activationGeneration = 0;
|
|
21567
|
+
this._developerRefreshInProgress = false;
|
|
20575
21568
|
if (dpopManager?.initialized) this._deviceTokenManager = (deps.deviceTokenManagerFactory ?? defaultDeviceTokenManagerFactory)(dpopManager, deps.http, (error) => deps.notifier.onError(error), () => deps.store.read());
|
|
20576
21569
|
}
|
|
20577
21570
|
/** True when the Client Bound SAT path is available (DPoP initialized). */
|
|
@@ -20590,28 +21583,120 @@ var CredentialRefreshCoordinator = class extends Destroyable {
|
|
|
20590
21583
|
* invokes `deps.onRefreshExhausted` so the orchestrator can disconnect.
|
|
20591
21584
|
*/
|
|
20592
21585
|
scheduleDeveloperRefresh(provider, expiresAt, attempt = 0) {
|
|
21586
|
+
this._activeProvider = provider;
|
|
20593
21587
|
if (this._developerTimerId !== void 0) clearTimeout(this._developerTimerId);
|
|
20594
21588
|
const refreshInterval = attempt === 0 ? Math.max(expiresAt - Date.now() - CREDENTIAL_REFRESH_BUFFER_MS, 1e3) : Math.min(CREDENTIAL_REFRESH_RETRY_BASE_MS * Math.pow(2, attempt) * (.5 + Math.random() * .5), CREDENTIAL_REFRESH_MAX_DELAY_MS);
|
|
20595
|
-
this._developerTimerId = setTimeout(
|
|
21589
|
+
this._developerTimerId = setTimeout(() => {
|
|
21590
|
+
this._developerTimerId = void 0;
|
|
21591
|
+
this.executeDeveloperRefresh(provider, expiresAt, attempt);
|
|
21592
|
+
}, refreshInterval);
|
|
21593
|
+
}
|
|
21594
|
+
/**
|
|
21595
|
+
* Runs the developer-provided refresh once: mints a new credential, stores
|
|
21596
|
+
* and persists it, reauthenticates the live session (via the notifier), and
|
|
21597
|
+
* reschedules against the new expiry. On failure retries with backoff up to
|
|
21598
|
+
* {@link CREDENTIAL_REFRESH_MAX_RETRIES}, then signals exhaustion.
|
|
21599
|
+
*
|
|
21600
|
+
* Shared by the scheduled timer tick and {@link forceRefreshIfDue}. The
|
|
21601
|
+
* `_developerRefreshInProgress` guard prevents the two from overlapping.
|
|
21602
|
+
*/
|
|
21603
|
+
async executeDeveloperRefresh(provider, expiresAt, attempt) {
|
|
21604
|
+
if (this._developerRefreshInProgress) {
|
|
21605
|
+
logger$6.debug("[Coordinator] Developer refresh already in progress; skipping");
|
|
21606
|
+
return;
|
|
21607
|
+
}
|
|
21608
|
+
this._developerRefreshInProgress = true;
|
|
21609
|
+
try {
|
|
21610
|
+
const newCredentials = await this.refreshCredential(provider);
|
|
21611
|
+
this.deps.store.write(newCredentials);
|
|
21612
|
+
this.deps.store.persist(newCredentials);
|
|
20596
21613
|
try {
|
|
20597
|
-
|
|
20598
|
-
|
|
20599
|
-
|
|
20600
|
-
this.deps.store.persist(newCredentials);
|
|
20601
|
-
logger$6.info("[Coordinator] Credentials refreshed successfully.");
|
|
20602
|
-
if (newCredentials.expiry_at) this.scheduleDeveloperRefresh(provider, newCredentials.expiry_at, 0);
|
|
20603
|
-
} catch (error) {
|
|
20604
|
-
const nextAttempt = attempt + 1;
|
|
20605
|
-
logger$6.error(`[Coordinator] Credential refresh failed (attempt ${nextAttempt}/${CREDENTIAL_REFRESH_MAX_RETRIES}):`, error);
|
|
20606
|
-
this.deps.notifier.onError(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
20607
|
-
if (nextAttempt < CREDENTIAL_REFRESH_MAX_RETRIES) this.scheduleDeveloperRefresh(provider, expiresAt, nextAttempt);
|
|
20608
|
-
else {
|
|
20609
|
-
logger$6.error("[Coordinator] Credential refresh exhausted all retries. Disconnecting.");
|
|
20610
|
-
this.deps.notifier.onError(new TokenRefreshError("Credential refresh failed after max retries"));
|
|
20611
|
-
this.deps.notifier.onRefreshExhausted();
|
|
20612
|
-
}
|
|
21614
|
+
await this.deps.notifier.onCredentialRefreshed(newCredentials);
|
|
21615
|
+
} catch (reauthError) {
|
|
21616
|
+
logger$6.warn("[Coordinator] onCredentialRefreshed rejected (non-fatal):", reauthError);
|
|
20613
21617
|
}
|
|
20614
|
-
|
|
21618
|
+
logger$6.info("[Coordinator] Credentials refreshed successfully.");
|
|
21619
|
+
if (newCredentials.expiry_at) this.scheduleDeveloperRefresh(provider, newCredentials.expiry_at, 0);
|
|
21620
|
+
} catch (error) {
|
|
21621
|
+
const nextAttempt = attempt + 1;
|
|
21622
|
+
logger$6.error(`[Coordinator] Credential refresh failed (attempt ${nextAttempt}/${CREDENTIAL_REFRESH_MAX_RETRIES}):`, error);
|
|
21623
|
+
this.deps.notifier.onError(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
21624
|
+
if (nextAttempt < CREDENTIAL_REFRESH_MAX_RETRIES) this.scheduleDeveloperRefresh(provider, expiresAt, nextAttempt);
|
|
21625
|
+
else {
|
|
21626
|
+
logger$6.error("[Coordinator] Credential refresh exhausted all retries. Disconnecting.");
|
|
21627
|
+
this.deps.notifier.onError(new TokenRefreshError("Credential refresh failed after max retries"));
|
|
21628
|
+
this.deps.notifier.onRefreshExhausted();
|
|
21629
|
+
}
|
|
21630
|
+
} finally {
|
|
21631
|
+
this._developerRefreshInProgress = false;
|
|
21632
|
+
}
|
|
21633
|
+
}
|
|
21634
|
+
/**
|
|
21635
|
+
* Force an immediate refresh when the current credential is already past its
|
|
21636
|
+
* scheduled refresh window. Called on resume from suspension, where
|
|
21637
|
+
* background-tab timer throttling can delay the armed refresh well past
|
|
21638
|
+
* expiry, leaving the live session stale.
|
|
21639
|
+
*
|
|
21640
|
+
* Routes to whichever mechanism is armed: the developer timer if armed,
|
|
21641
|
+
* otherwise the Client Bound SAT pipeline. A no-op when nothing is due.
|
|
21642
|
+
*/
|
|
21643
|
+
forceRefreshIfDue() {
|
|
21644
|
+
if (this._developerTimerId !== void 0 && this._activeProvider) {
|
|
21645
|
+
const expiry = this.deps.store.read().expiry_at;
|
|
21646
|
+
if (expiry !== void 0 && Date.now() >= expiry - CREDENTIAL_REFRESH_BUFFER_MS) {
|
|
21647
|
+
logger$6.debug("[Coordinator] Resume: credential past refresh window; forcing refresh");
|
|
21648
|
+
clearTimeout(this._developerTimerId);
|
|
21649
|
+
this._developerTimerId = void 0;
|
|
21650
|
+
this.executeDeveloperRefresh(this._activeProvider, expiry, 0);
|
|
21651
|
+
}
|
|
21652
|
+
return;
|
|
21653
|
+
}
|
|
21654
|
+
this._deviceTokenManager?.refreshNowIfDue();
|
|
21655
|
+
}
|
|
21656
|
+
/**
|
|
21657
|
+
* Sync the credential's expiry from the server-provided authorization (the
|
|
21658
|
+
* `signalwire.connect` result). SATs are opaque JWE, so
|
|
21659
|
+
* `fabric_subscriber.expires_at` is the authoritative expiry of the token
|
|
21660
|
+
* the session actually connected with — the provider-reported `expiry_at`
|
|
21661
|
+
* is only a hint (and may be wrong or absent). Corrects the stored
|
|
21662
|
+
* credential and re-arms the developer refresh timer against the real
|
|
21663
|
+
* deadline when the provider supports `refresh()`.
|
|
21664
|
+
*/
|
|
21665
|
+
syncExpiryFromAuthorization(authorization, provider) {
|
|
21666
|
+
const expiresAtSec = authorization?.fabric_subscriber?.expires_at;
|
|
21667
|
+
if (!expiresAtSec) return;
|
|
21668
|
+
const expiryAt = expiresAtSec * 1e3;
|
|
21669
|
+
const credential = this.deps.store.read();
|
|
21670
|
+
if (credential.expiry_at === expiryAt) return;
|
|
21671
|
+
logger$6.debug(`[Coordinator] Correcting credential expiry from server authorization: ${new Date(expiryAt).toISOString()}`);
|
|
21672
|
+
const updated = {
|
|
21673
|
+
...credential,
|
|
21674
|
+
expiry_at: expiryAt
|
|
21675
|
+
};
|
|
21676
|
+
this.deps.store.write(updated);
|
|
21677
|
+
this.deps.store.persist(updated);
|
|
21678
|
+
if (provider?.refresh) this.scheduleDeveloperRefresh(provider, expiryAt);
|
|
21679
|
+
}
|
|
21680
|
+
/**
|
|
21681
|
+
* Invoke `provider.refresh()` deduped against any concurrent developer
|
|
21682
|
+
* refresh. Concurrent callers — the scheduled tick, a resume-forced refresh,
|
|
21683
|
+
* and the orchestrator's -32003 recovery / reconnect re-mint — share one
|
|
21684
|
+
* in-flight promise, so a provider backed by one-time-use rotating refresh
|
|
21685
|
+
* tokens is never invoked twice in parallel.
|
|
21686
|
+
*
|
|
21687
|
+
* The caller owns applying the returned credential (store write, session
|
|
21688
|
+
* reauth, rescheduling); this method only serializes the network call.
|
|
21689
|
+
*/
|
|
21690
|
+
async refreshCredential(provider) {
|
|
21691
|
+
if (this._refreshInFlight) return this._refreshInFlight;
|
|
21692
|
+
if (!provider.refresh) throw new InvalidCredentialsError("Credential provider does not support refresh");
|
|
21693
|
+
const run = provider.refresh();
|
|
21694
|
+
this._refreshInFlight = run;
|
|
21695
|
+
const clear = () => {
|
|
21696
|
+
if (this._refreshInFlight === run) this._refreshInFlight = void 0;
|
|
21697
|
+
};
|
|
21698
|
+
run.then(clear, clear);
|
|
21699
|
+
return run;
|
|
20615
21700
|
}
|
|
20616
21701
|
/**
|
|
20617
21702
|
* Cancels any scheduled developer-provided refresh. Idempotent.
|
|
@@ -21395,6 +22480,13 @@ var TransportManager = class extends Destroyable {
|
|
|
21395
22480
|
//#region src/clients/SignalWire.ts
|
|
21396
22481
|
var import_cjs$1 = require_cjs();
|
|
21397
22482
|
const logger$1 = getLogger();
|
|
22483
|
+
/**
|
|
22484
|
+
* Storage key for the client-bound marker. The SAT and authorization_state are
|
|
22485
|
+
* both opaque to the SDK, so on a page reload the preflight recovery — which
|
|
22486
|
+
* runs before any session exists — has no other way to know the session was
|
|
22487
|
+
* client-bound. See {@link SignalWire.persistClientBoundMarker}.
|
|
22488
|
+
*/
|
|
22489
|
+
const CLIENT_BOUND_STORAGE_KEY = "sw:client_bound";
|
|
21398
22490
|
const buildOptionsFromDestination = (destination) => {
|
|
21399
22491
|
if (typeof destination === "string") {
|
|
21400
22492
|
const queryStartIndex = destination.indexOf("?");
|
|
@@ -21438,6 +22530,7 @@ var SignalWire = class extends Destroyable {
|
|
|
21438
22530
|
this.preferences = new ClientPreferences();
|
|
21439
22531
|
this._user$ = this.createBehaviorSubject(void 0);
|
|
21440
22532
|
this._directory$ = this.createBehaviorSubject(void 0);
|
|
22533
|
+
this._credentialRecovered = false;
|
|
21441
22534
|
this._isConnected$ = this.createBehaviorSubject(false);
|
|
21442
22535
|
this._isRegistered$ = this.createBehaviorSubject(false);
|
|
21443
22536
|
this._errors$ = this.createReplaySubject(1);
|
|
@@ -21476,6 +22569,16 @@ var SignalWire = class extends Destroyable {
|
|
|
21476
22569
|
});
|
|
21477
22570
|
}
|
|
21478
22571
|
/**
|
|
22572
|
+
* Build the refresh path's own HTTP controller, against whatever host is current.
|
|
22573
|
+
*
|
|
22574
|
+
* Called on first use rather than up front, so `apiHost` already reflects the
|
|
22575
|
+
* token's `ch` claim. Same credential source as the container's controller — only
|
|
22576
|
+
* the instance, and therefore its observable streams, is separate.
|
|
22577
|
+
*/
|
|
22578
|
+
createRefreshHttpController() {
|
|
22579
|
+
return new HTTPRequestController(this._deps.apiHost, () => this._deps.credential);
|
|
22580
|
+
}
|
|
22581
|
+
/**
|
|
21479
22582
|
* Initializes DPoP if not already set up. Returns the fingerprint on success.
|
|
21480
22583
|
*/
|
|
21481
22584
|
async initDPoP() {
|
|
@@ -21502,11 +22605,19 @@ var SignalWire = class extends Destroyable {
|
|
|
21502
22605
|
async resolveCredentials() {
|
|
21503
22606
|
const fingerprint = await this.initDPoP();
|
|
21504
22607
|
this._refreshCoordinator = new CredentialRefreshCoordinator(this._dpopManager, {
|
|
21505
|
-
http:
|
|
22608
|
+
http: () => {
|
|
22609
|
+
if (!this._refreshHttp || this._refreshHttpHost !== this._deps.apiHost) {
|
|
22610
|
+
this._refreshHttp?.destroy();
|
|
22611
|
+
this._refreshHttp = this.createRefreshHttpController();
|
|
22612
|
+
this._refreshHttpHost = this._deps.apiHost;
|
|
22613
|
+
}
|
|
22614
|
+
return this._refreshHttp;
|
|
22615
|
+
},
|
|
21506
22616
|
notifier: {
|
|
21507
22617
|
onError: (error) => this._errors$.next(error),
|
|
21508
22618
|
onWarning: (warning) => this._warnings$.next(warning),
|
|
21509
|
-
onRefreshExhausted: () => void this.disconnect()
|
|
22619
|
+
onRefreshExhausted: () => void this.disconnect(),
|
|
22620
|
+
onCredentialRefreshed: async (credential) => this.reauthenticateLiveSession(credential)
|
|
21510
22621
|
},
|
|
21511
22622
|
store: {
|
|
21512
22623
|
read: () => this._deps.credential,
|
|
@@ -21518,6 +22629,7 @@ var SignalWire = class extends Destroyable {
|
|
|
21518
22629
|
...this._deps.credential,
|
|
21519
22630
|
...partial
|
|
21520
22631
|
};
|
|
22632
|
+
this.persistCredential(this._deps.credential);
|
|
21521
22633
|
},
|
|
21522
22634
|
persist: (credential) => this.persistCredential(credential)
|
|
21523
22635
|
}
|
|
@@ -21563,20 +22675,181 @@ var SignalWire = class extends Destroyable {
|
|
|
21563
22675
|
}
|
|
21564
22676
|
this._deps.credential = _credentials;
|
|
21565
22677
|
this.persistCredential(_credentials);
|
|
21566
|
-
|
|
21567
|
-
|
|
22678
|
+
await this.reauthenticateLiveSession(_credentials);
|
|
22679
|
+
}
|
|
22680
|
+
/**
|
|
22681
|
+
* Reauthenticate the currently-open session with a freshly obtained
|
|
22682
|
+
* credential so the new token takes effect on the live socket immediately —
|
|
22683
|
+
* not just on the next reconnect. No-op when the session is not
|
|
22684
|
+
* connected/authenticated or the credential carries no token (e.g. an
|
|
22685
|
+
* authorization-state-only refresh). Non-fatal: reauth failures surface on
|
|
22686
|
+
* `errors$` without aborting the refresh that triggered this.
|
|
22687
|
+
*/
|
|
22688
|
+
async reauthenticateLiveSession(credential) {
|
|
22689
|
+
if (!this.isConnected || !this._clientSession.authenticated || !credential.token) return;
|
|
22690
|
+
try {
|
|
22691
|
+
await this._clientSession.reauthenticate(credential.token);
|
|
21568
22692
|
logger$1.info("[SignalWire] Session refreshed with new credentials.");
|
|
21569
22693
|
} catch (error) {
|
|
21570
22694
|
logger$1.error("[SignalWire] Failed to refresh session with new credentials:", error);
|
|
21571
22695
|
this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
21572
22696
|
}
|
|
21573
22697
|
}
|
|
22698
|
+
/**
|
|
22699
|
+
* Recover a session the server is refusing: mint a fresh credential,
|
|
22700
|
+
* reauthenticate the live session with it, and retry the operation.
|
|
22701
|
+
*
|
|
22702
|
+
* The connection is deliberately kept. A reload authenticates the new socket
|
|
22703
|
+
* against the persisted `authorization_state`, and that handshake is what
|
|
22704
|
+
* associates the socket with the previous session — the association reattach
|
|
22705
|
+
* depends on. `signalwire.reauthenticate` swaps the credential *on that same
|
|
22706
|
+
* session*, so recovery never touches the resume state. Discarding it would
|
|
22707
|
+
* heal the credential by destroying the very thing the caller is trying to
|
|
22708
|
+
* get back to.
|
|
22709
|
+
*
|
|
22710
|
+
* The operation is still the verdict, never the RPC. Reauthenticating with
|
|
22711
|
+
* the in-memory token is accepted by a resume even while requests stay
|
|
22712
|
+
* refused, because the persisted `authorization_state` short-circuits token
|
|
22713
|
+
* validation — and `signalwire.reauthenticate` with a *freshly minted* token
|
|
22714
|
+
* has also been observed accepted while `subscriber.online` keeps being
|
|
22715
|
+
* refused (staging run 33826974634). Both look like success and are not.
|
|
22716
|
+
*
|
|
22717
|
+
* @returns the operation's value, or the reason recovery could not deliver
|
|
22718
|
+
* one. `error` is undefined when there was no way to mint at all.
|
|
22719
|
+
*/
|
|
22720
|
+
async recoverAndRetry(operation) {
|
|
22721
|
+
if (!await this.remintAndReauthenticate()) return { ok: false };
|
|
22722
|
+
try {
|
|
22723
|
+
const value = await operation();
|
|
22724
|
+
this._credentialRecovered = true;
|
|
22725
|
+
return {
|
|
22726
|
+
ok: true,
|
|
22727
|
+
value
|
|
22728
|
+
};
|
|
22729
|
+
} catch (error) {
|
|
22730
|
+
logger$1.warn("[SignalWire] Reauthentication was accepted but the operation is still refused:", error);
|
|
22731
|
+
return {
|
|
22732
|
+
ok: false,
|
|
22733
|
+
error
|
|
22734
|
+
};
|
|
22735
|
+
}
|
|
22736
|
+
}
|
|
22737
|
+
/**
|
|
22738
|
+
* Re-mint a credential and adopt it only if the live session accepts it.
|
|
22739
|
+
*
|
|
22740
|
+
* The mechanism follows the binding: a client-bound session re-mints a bound
|
|
22741
|
+
* base SAT through `authenticate()` with the DPoP fingerprint, because the
|
|
22742
|
+
* developer refresh handler would hand back an unbound token and silently
|
|
22743
|
+
* degrade the session. An unbound session uses the refresh handler. Rotation
|
|
22744
|
+
* cost is not a reason to skip this — the only reason is having no mechanism.
|
|
22745
|
+
*
|
|
22746
|
+
* @returns whether the session is now running on a freshly accepted credential.
|
|
22747
|
+
*/
|
|
22748
|
+
async remintAndReauthenticate() {
|
|
22749
|
+
const provider = this._credentialProvider;
|
|
22750
|
+
if (!provider) return false;
|
|
22751
|
+
const { clientBound } = this._clientSession;
|
|
22752
|
+
if (!clientBound && !provider.refresh) {
|
|
22753
|
+
logger$1.debug("[SignalWire] [SW-NO-REFRESH-HANDLER] Unbound session with no refresh handler; cannot re-mint.");
|
|
22754
|
+
return false;
|
|
22755
|
+
}
|
|
22756
|
+
try {
|
|
22757
|
+
const newCredentials = clientBound ? await provider.authenticate(this._dpopManager?.initialized ? { fingerprint: this._dpopManager.fingerprint } : void 0) : await this.remintCredential(provider);
|
|
22758
|
+
if (!newCredentials.token) {
|
|
22759
|
+
logger$1.warn("[SignalWire] Re-minted credential has no token; keeping the current one.");
|
|
22760
|
+
return false;
|
|
22761
|
+
}
|
|
22762
|
+
await this._clientSession.reauthenticate(newCredentials.token);
|
|
22763
|
+
this._deps.credential = newCredentials;
|
|
22764
|
+
this.persistCredential(newCredentials);
|
|
22765
|
+
if (newCredentials.expiry_at && provider.refresh) this._refreshCoordinator?.scheduleDeveloperRefresh(provider, newCredentials.expiry_at);
|
|
22766
|
+
return true;
|
|
22767
|
+
} catch (error) {
|
|
22768
|
+
logger$1.warn("[SignalWire] Re-mint recovery failed:", error);
|
|
22769
|
+
return false;
|
|
22770
|
+
}
|
|
22771
|
+
}
|
|
22772
|
+
/**
|
|
22773
|
+
* Re-mint a credential via `provider.refresh()`, routed through the
|
|
22774
|
+
* coordinator's shared in-flight guard so concurrent re-mint paths (a
|
|
22775
|
+
* scheduled/resume refresh, -32003 recovery, and reconnect) never fire a
|
|
22776
|
+
* second `provider.refresh()` in parallel — which rotating one-time-use
|
|
22777
|
+
* refresh tokens reject. Falls back to a direct call only if the coordinator
|
|
22778
|
+
* has not been constructed yet.
|
|
22779
|
+
*/
|
|
22780
|
+
async remintCredential(provider) {
|
|
22781
|
+
if (this._refreshCoordinator) return this._refreshCoordinator.refreshCredential(provider);
|
|
22782
|
+
if (!provider.refresh) throw new InvalidCredentialsError("Credential provider does not support refresh");
|
|
22783
|
+
return provider.refresh();
|
|
22784
|
+
}
|
|
22785
|
+
/**
|
|
22786
|
+
* Re-mint credentials before a fresh (re)connect (`onBeforeReconnect` hook).
|
|
22787
|
+
* The session invokes this only when it is client-bound OR the in-memory
|
|
22788
|
+
* token is expired. The re-mint mechanism depends on the binding:
|
|
22789
|
+
* - Client-bound: `authenticate()` with the DPoP fingerprint to obtain a
|
|
22790
|
+
* fresh base SAT the upcoming reconnect can re-bind (the
|
|
22791
|
+
* DeviceTokenManager re-activates afterwards).
|
|
22792
|
+
* - Unbound: the developer's non-interactive `refresh()` handler.
|
|
22793
|
+
* `authenticate()` is deliberately NOT used here — it may be interactive
|
|
22794
|
+
* (a login prompt) and must not fire on a background reconnect.
|
|
22795
|
+
*
|
|
22796
|
+
* Rejects on failure so the session aborts the reconnect rather than
|
|
22797
|
+
* replaying a stale token.
|
|
22798
|
+
*/
|
|
22799
|
+
async refreshCredentialForReconnect() {
|
|
22800
|
+
if (!this._credentialProvider) return;
|
|
22801
|
+
try {
|
|
22802
|
+
let newCredentials;
|
|
22803
|
+
if (this._clientSession?.clientBound ?? await this.wasClientBound()) {
|
|
22804
|
+
logger$1.debug("[SignalWire] Re-minting client-bound base SAT before reconnect");
|
|
22805
|
+
newCredentials = await this._credentialProvider.authenticate(this._dpopManager?.initialized ? { fingerprint: this._dpopManager.fingerprint } : void 0);
|
|
22806
|
+
} else if (this._credentialProvider.refresh) {
|
|
22807
|
+
logger$1.debug("[SignalWire] Refreshing unbound credential before reconnect");
|
|
22808
|
+
newCredentials = await this.remintCredential(this._credentialProvider);
|
|
22809
|
+
} else {
|
|
22810
|
+
logger$1.warn("[SignalWire] [SW-NO-REFRESH-HANDLER] Token expired on reconnect but no refresh handler; reconnecting with the existing token.");
|
|
22811
|
+
return;
|
|
22812
|
+
}
|
|
22813
|
+
if (!newCredentials.token) {
|
|
22814
|
+
logger$1.warn("[SignalWire] Re-minted credential has no token; keeping the existing credential for reconnect.");
|
|
22815
|
+
return;
|
|
22816
|
+
}
|
|
22817
|
+
this._deps.credential = newCredentials;
|
|
22818
|
+
this.persistCredential(newCredentials);
|
|
22819
|
+
if (newCredentials.expiry_at && this._credentialProvider.refresh) this._refreshCoordinator?.scheduleDeveloperRefresh(this._credentialProvider, newCredentials.expiry_at);
|
|
22820
|
+
logger$1.debug("[SignalWire] Credential refreshed successfully for reconnect");
|
|
22821
|
+
} catch (error) {
|
|
22822
|
+
logger$1.error("[SignalWire] Failed to refresh credentials for reconnect:", error);
|
|
22823
|
+
this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
22824
|
+
throw error;
|
|
22825
|
+
}
|
|
22826
|
+
}
|
|
21574
22827
|
/** Persist credential to localStorage when persistSession is enabled. */
|
|
21575
22828
|
persistCredential(credential) {
|
|
21576
22829
|
if (!credential.token) return;
|
|
21577
22830
|
this._deps.storage.setItem("sw:cached_credential", credential);
|
|
21578
22831
|
if (this._deps.persistSession) this._deps.storage.setItem("sw:cached_credential", credential, "local");
|
|
21579
22832
|
}
|
|
22833
|
+
/**
|
|
22834
|
+
* Persist whether the session is client-bound, mirroring the credential's
|
|
22835
|
+
* storage scopes so it survives a reload. The preflight recovery reads it
|
|
22836
|
+
* before any session exists to decide whether to re-bind via `authenticate()`
|
|
22837
|
+
* or refresh an unbound token; the marker tracks the latest binding, so an
|
|
22838
|
+
* unbound reconnect clears a stale marker from an earlier client-bound login.
|
|
22839
|
+
*/
|
|
22840
|
+
persistClientBoundMarker(bound) {
|
|
22841
|
+
const scopes = this._deps.persistSession ? ["session", "local"] : ["session"];
|
|
22842
|
+
for (const scope of scopes) if (bound) this._deps.storage.setItem(CLIENT_BOUND_STORAGE_KEY, true, scope);
|
|
22843
|
+
else this._deps.storage.removeItem(CLIENT_BOUND_STORAGE_KEY, scope);
|
|
22844
|
+
}
|
|
22845
|
+
/** Read the persisted client-bound marker (see {@link persistClientBoundMarker}). */
|
|
22846
|
+
async wasClientBound() {
|
|
22847
|
+
const scopes = this._deps.persistSession ? ["local", "session"] : ["session"];
|
|
22848
|
+
for (const scope of scopes) try {
|
|
22849
|
+
if (await this._deps.storage.getItem(CLIENT_BOUND_STORAGE_KEY, scope)) return true;
|
|
22850
|
+
} catch {}
|
|
22851
|
+
return false;
|
|
22852
|
+
}
|
|
21580
22853
|
async init() {
|
|
21581
22854
|
this._user$.next(new User(this._deps.http));
|
|
21582
22855
|
if (!this._options.skipConnection) await this.connect();
|
|
@@ -21600,6 +22873,42 @@ var SignalWire = class extends Destroyable {
|
|
|
21600
22873
|
} catch (error) {
|
|
21601
22874
|
logger$1.error("[SignalWire] Failed to reattach calls:", error);
|
|
21602
22875
|
this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
22876
|
+
} finally {
|
|
22877
|
+
this._credentialRecovered = false;
|
|
22878
|
+
}
|
|
22879
|
+
}
|
|
22880
|
+
/**
|
|
22881
|
+
* Fetch the authenticated user profile, recovering a stale credential.
|
|
22882
|
+
*
|
|
22883
|
+
* On a reload the persisted credential can be expired. Unlike the WS resume —
|
|
22884
|
+
* which the server accepts against the persisted `authorization_state` even
|
|
22885
|
+
* with an expired token — this REST preflight has no such short-circuit and is
|
|
22886
|
+
* refused (401). There is no session yet to reauthenticate, so recovery
|
|
22887
|
+
* re-mints the credential through the provider ({@link refreshCredentialForReconnect})
|
|
22888
|
+
* and retries with a FRESH {@link User}: Fetchable memoizes its result
|
|
22889
|
+
* (shareReplay), so reusing the instance would replay the 401 instead of
|
|
22890
|
+
* re-fetching with the new token. Without the user id the transport/session —
|
|
22891
|
+
* and the reattach a reload is trying to preserve — cannot even be addressed.
|
|
22892
|
+
*/
|
|
22893
|
+
async fetchUserOrRecover() {
|
|
22894
|
+
const fetchUser = async (user$1) => {
|
|
22895
|
+
if (!await (0, import_cjs$1.firstValueFrom)(user$1.fetched$)) throw new UnexpectedError("Failed to fetch user information - fetched$ emitted false");
|
|
22896
|
+
this._deps.user = user$1;
|
|
22897
|
+
};
|
|
22898
|
+
const user = this._user$.value;
|
|
22899
|
+
if (!user) throw new UnexpectedError("User not initialized before connect");
|
|
22900
|
+
try {
|
|
22901
|
+
await fetchUser(user);
|
|
22902
|
+
} catch (firstError) {
|
|
22903
|
+
logger$1.error(`[SignalWire] Failed to fetch user information: ${firstError instanceof Error ? firstError.message : "Unknown error"}. This usually means the user token is invalid or expired. Re-minting the credential and retrying.`);
|
|
22904
|
+
try {
|
|
22905
|
+
await this.refreshCredentialForReconnect();
|
|
22906
|
+
const refetched = new User(this._deps.http);
|
|
22907
|
+
await fetchUser(refetched);
|
|
22908
|
+
this._user$.next(refetched);
|
|
22909
|
+
} catch (retryError) {
|
|
22910
|
+
throw new UnexpectedError("Error fetching user information", { cause: retryError });
|
|
22911
|
+
}
|
|
21603
22912
|
}
|
|
21604
22913
|
}
|
|
21605
22914
|
/**
|
|
@@ -21641,40 +22950,23 @@ var SignalWire = class extends Destroyable {
|
|
|
21641
22950
|
*/
|
|
21642
22951
|
async connect() {
|
|
21643
22952
|
await this.teardownTransportAndSession();
|
|
21644
|
-
|
|
21645
|
-
const user = this._user$.value;
|
|
21646
|
-
if (!user) throw new UnexpectedError("User not initialized before connect");
|
|
21647
|
-
if (!await (0, import_cjs$1.firstValueFrom)(user.fetched$)) throw new UnexpectedError("Failed to fetch user information - fetched$ emitted false");
|
|
21648
|
-
this._deps.user = user;
|
|
21649
|
-
} catch (error) {
|
|
21650
|
-
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.`);
|
|
21651
|
-
throw new UnexpectedError("Error fetching user information", { cause: error });
|
|
21652
|
-
}
|
|
22953
|
+
await this.fetchUserOrRecover();
|
|
21653
22954
|
const errorHandler = (error) => {
|
|
21654
22955
|
this._errors$.next(error);
|
|
21655
22956
|
};
|
|
21656
22957
|
this._transport = new TransportManager(this._deps.storage, this._deps.protocolKey, this._deps.WebSocket, PreferencesContainer.instance.relayHost ?? this._deps.relayHost, errorHandler);
|
|
21657
|
-
this._attachManager = new AttachManager(this._deps.storage, this._deps.deviceController, PreferencesContainer.instance.reconnectCallsTimeout, this._deps.attachedCallsKey);
|
|
22958
|
+
this._attachManager = new AttachManager(this._deps.storage, this._deps.deviceController, PreferencesContainer.instance.reconnectCallsTimeout, this._deps.attachedCallsKey, () => this._credentialRecovered);
|
|
21658
22959
|
this._clientSession = new ClientSessionManager(() => this._deps.credential, this._transport, this._deps.storage, this._deps.authorizationStateKey, this._deps.deviceController, this._attachManager, this._deps.webRTCApiProvider, this._dpopManager, this._networkMonitor?.networkChange$);
|
|
21659
22960
|
this._publicSession = new ClientSessionWrapper(this._clientSession);
|
|
21660
|
-
this._clientSession.
|
|
21661
|
-
|
|
21662
|
-
try {
|
|
21663
|
-
const fingerprint = this._dpopManager?.initialized ? this._dpopManager.fingerprint : void 0;
|
|
21664
|
-
logger$1.debug("[SignalWire] Credential expired, refreshing before reconnect");
|
|
21665
|
-
const newCredentials = await this._credentialProvider.authenticate(fingerprint ? { fingerprint } : void 0);
|
|
21666
|
-
this._deps.credential = newCredentials;
|
|
21667
|
-
if (newCredentials.expiry_at && this._credentialProvider.refresh) this._refreshCoordinator?.scheduleDeveloperRefresh(this._credentialProvider, newCredentials.expiry_at);
|
|
21668
|
-
logger$1.debug("[SignalWire] Credential refreshed successfully for reconnect");
|
|
21669
|
-
} catch (error) {
|
|
21670
|
-
logger$1.error("[SignalWire] Failed to refresh credentials for reconnect:", error);
|
|
21671
|
-
this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
21672
|
-
throw error;
|
|
21673
|
-
}
|
|
21674
|
-
};
|
|
22961
|
+
this._clientSession.callControl = this._options.callControl ?? "routed";
|
|
22962
|
+
this._clientSession.onBeforeReconnect = async () => this.refreshCredentialForReconnect();
|
|
21675
22963
|
this.subscribeTo(this._clientSession.errors$, (error) => {
|
|
21676
22964
|
this._errors$.next(error);
|
|
21677
22965
|
});
|
|
22966
|
+
this.subscribeTo(this._clientSession.authorization$, (authorization) => {
|
|
22967
|
+
this._refreshCoordinator?.syncExpiryFromAuthorization(authorization, this._credentialProvider);
|
|
22968
|
+
this.persistClientBoundMarker(Boolean(authorization?.cnf?.jkt));
|
|
22969
|
+
});
|
|
21678
22970
|
await this._clientSession.connect();
|
|
21679
22971
|
await this._refreshCoordinator?.activate(this._deps.user, this._clientSession);
|
|
21680
22972
|
this.subscribeTo(this._clientSession.authenticated$.pipe((0, import_cjs$1.skip)(1), (0, import_cjs$1.filter)(Boolean)), async () => {
|
|
@@ -21837,6 +23129,13 @@ var SignalWire = class extends Destroyable {
|
|
|
21837
23129
|
}
|
|
21838
23130
|
try {
|
|
21839
23131
|
this._visibilityController = new VisibilityController();
|
|
23132
|
+
this.subscribeTo(this._visibilityController.visibilityChange$.pipe((0, import_cjs$1.filter)((event) => event.to === "visible")), () => {
|
|
23133
|
+
try {
|
|
23134
|
+
this._refreshCoordinator?.forceRefreshIfDue();
|
|
23135
|
+
} catch (error) {
|
|
23136
|
+
logger$1.warn("[SignalWire] Resume credential revalidation failed (non-fatal):", error);
|
|
23137
|
+
}
|
|
23138
|
+
});
|
|
21840
23139
|
this.subscribeTo(this._visibilityController.visibilityChange$.pipe((0, import_cjs$1.filter)((event) => event.to === "visible" && PreferencesContainer.instance.refreshDevicesOnVisible)), () => {
|
|
21841
23140
|
logger$1.debug("[SignalWire] Page visible, re-enumerating devices");
|
|
21842
23141
|
try {
|
|
@@ -21848,7 +23147,7 @@ var SignalWire = class extends Destroyable {
|
|
|
21848
23147
|
logger$1.warn("[SignalWire] Failed to initialize VisibilityController:", error);
|
|
21849
23148
|
}
|
|
21850
23149
|
try {
|
|
21851
|
-
this._diagnosticsCollector = new DiagnosticsCollector({ sdkVersion: "4.0.0-rc.
|
|
23150
|
+
this._diagnosticsCollector = new DiagnosticsCollector({ sdkVersion: "4.0.0-rc.3" });
|
|
21852
23151
|
} catch (error) {
|
|
21853
23152
|
logger$1.warn("[SignalWire] Failed to initialize DiagnosticsCollector:", error);
|
|
21854
23153
|
}
|
|
@@ -21919,21 +23218,22 @@ var SignalWire = class extends Destroyable {
|
|
|
21919
23218
|
this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
21920
23219
|
throw error;
|
|
21921
23220
|
}
|
|
21922
|
-
logger$1.debug("[SignalWire] Failed to register user,
|
|
21923
|
-
|
|
21924
|
-
await this._clientSession.reauthenticate(this._deps.credential.token);
|
|
21925
|
-
logger$1.debug("[SignalWire] Reauthentication successful, retrying register()");
|
|
23221
|
+
logger$1.debug("[SignalWire] Failed to register user, attempting credential recovery...");
|
|
23222
|
+
const outcome = await this.recoverAndRetry(async () => {
|
|
21926
23223
|
await this._transport.execute(RPCExecute({
|
|
21927
23224
|
method: "subscriber.online",
|
|
21928
23225
|
params: {}
|
|
21929
23226
|
}));
|
|
23227
|
+
});
|
|
23228
|
+
if (outcome.ok) {
|
|
23229
|
+
logger$1.debug("[SignalWire] Recovery restored registration");
|
|
21930
23230
|
this._isRegistered$.next(true);
|
|
21931
|
-
|
|
21932
|
-
logger$1.error("[SignalWire] Reauthentication failed during register():", reauthError);
|
|
21933
|
-
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 }) });
|
|
21934
|
-
this._errors$.next(registerError);
|
|
21935
|
-
throw registerError;
|
|
23231
|
+
return;
|
|
21936
23232
|
}
|
|
23233
|
+
const failureCause = outcome.error ?? error;
|
|
23234
|
+
const registerError = new InvalidCredentialsError("Failed to register user, and credential recovery also failed. Please check your credentials.", { cause: failureCause instanceof Error ? failureCause : new Error(String(failureCause), { cause: failureCause }) });
|
|
23235
|
+
this._errors$.next(registerError);
|
|
23236
|
+
throw registerError;
|
|
21937
23237
|
}
|
|
21938
23238
|
}
|
|
21939
23239
|
/**
|
|
@@ -21964,6 +23264,11 @@ var SignalWire = class extends Destroyable {
|
|
|
21964
23264
|
* Returns a {@link Call} in `'ringing'` state. Subscribe to {@link Call.status$}
|
|
21965
23265
|
* to track progression through `'connected'` → `'disconnected'`.
|
|
21966
23266
|
*
|
|
23267
|
+
* Local media acquisition is deliberately unbounded: an unanswered permission
|
|
23268
|
+
* prompt leaves this promise pending indefinitely, so apply your own bound if
|
|
23269
|
+
* your UI needs one. The 12 s signaling budget starts only once acquisition
|
|
23270
|
+
* settles.
|
|
23271
|
+
*
|
|
21967
23272
|
* @param destination - Address URI string (e.g. `'/public/my-room'`) or {@link Address} instance.
|
|
21968
23273
|
* @param options - Media and dial options (audio/video, device constraints). Overrides defaults.
|
|
21969
23274
|
* @returns The created {@link Call} instance.
|
|
@@ -21986,7 +23291,18 @@ var SignalWire = class extends Destroyable {
|
|
|
21986
23291
|
};
|
|
21987
23292
|
await this.waitAuthentication();
|
|
21988
23293
|
logger$1.debug("[SignalWire] Dialing with options:", computed_options);
|
|
21989
|
-
|
|
23294
|
+
try {
|
|
23295
|
+
return await this._clientSession.createOutboundCall(destination, computed_options);
|
|
23296
|
+
} catch (error) {
|
|
23297
|
+
if (!isRecoverableAuthError(error)) throw error;
|
|
23298
|
+
logger$1.debug("[SignalWire] Dial hit a recoverable auth error; recovering the session and retrying");
|
|
23299
|
+
const outcome = await this.recoverAndRetry(async () => {
|
|
23300
|
+
await this.waitAuthentication();
|
|
23301
|
+
return this._clientSession.createOutboundCall(destination, computed_options);
|
|
23302
|
+
});
|
|
23303
|
+
if (outcome.ok) return outcome.value;
|
|
23304
|
+
throw outcome.error ?? error;
|
|
23305
|
+
}
|
|
21990
23306
|
}
|
|
21991
23307
|
/**
|
|
21992
23308
|
* Runs a multi-phase connectivity test against the given destination.
|
|
@@ -22267,10 +23583,13 @@ var SignalWire = class extends Destroyable {
|
|
|
22267
23583
|
destroy() {
|
|
22268
23584
|
this._refreshCoordinator?.destroy();
|
|
22269
23585
|
this._refreshCoordinator = void 0;
|
|
23586
|
+
this._refreshHttp?.destroy();
|
|
23587
|
+
this._refreshHttp = void 0;
|
|
22270
23588
|
this._dpopManager?.destroy();
|
|
22271
|
-
this._clientSession
|
|
22272
|
-
|
|
22273
|
-
this.
|
|
23589
|
+
const session = this._clientSession;
|
|
23590
|
+
session?.teardownSessionState();
|
|
23591
|
+
this._transport?.destroy();
|
|
23592
|
+
session?.destroy();
|
|
22274
23593
|
try {
|
|
22275
23594
|
this._networkMonitor?.destroy();
|
|
22276
23595
|
} catch {}
|
|
@@ -22383,7 +23702,7 @@ var StaticCredentialProvider = class {
|
|
|
22383
23702
|
/**
|
|
22384
23703
|
* Library version from package.json, injected at build time.
|
|
22385
23704
|
*/
|
|
22386
|
-
const version = "4.0.0-rc.
|
|
23705
|
+
const version = "4.0.0-rc.3";
|
|
22387
23706
|
/**
|
|
22388
23707
|
* Flag indicating the library has been loaded and is ready to use.
|
|
22389
23708
|
* For UMD builds: `window.SignalWire.ready`
|
|
@@ -22405,7 +23724,7 @@ const ready = true;
|
|
|
22405
23724
|
*/
|
|
22406
23725
|
const emitReadyEvent = () => {
|
|
22407
23726
|
if (typeof window !== "undefined") {
|
|
22408
|
-
const event = new CustomEvent("signalwire:js:ready", { detail: { version: "4.0.0-rc.
|
|
23727
|
+
const event = new CustomEvent("signalwire:js:ready", { detail: { version: "4.0.0-rc.3" } });
|
|
22409
23728
|
window.dispatchEvent(event);
|
|
22410
23729
|
}
|
|
22411
23730
|
};
|
|
@@ -22430,19 +23749,25 @@ if (typeof process === "undefined") globalThis.process = { env: { NODE_ENV: "pro
|
|
|
22430
23749
|
|
|
22431
23750
|
//#endregion
|
|
22432
23751
|
exports.Address = Address;
|
|
23752
|
+
exports.AuxiliaryLegCancelledError = AuxiliaryLegCancelledError;
|
|
23753
|
+
exports.AuxiliaryLegTimeoutError = AuxiliaryLegTimeoutError;
|
|
22433
23754
|
exports.CallCreateError = CallCreateError;
|
|
23755
|
+
exports.CallNotReadyError = CallNotReadyError;
|
|
22434
23756
|
exports.ClientPreferences = ClientPreferences;
|
|
22435
23757
|
exports.CollectionFetchError = CollectionFetchError;
|
|
22436
23758
|
exports.DPoPInitError = DPoPInitError;
|
|
22437
23759
|
exports.DeviceTokenError = DeviceTokenError;
|
|
22438
23760
|
exports.EmbedTokenCredentialProvider = EmbedTokenCredentialProvider;
|
|
22439
23761
|
exports.InvalidCredentialsError = InvalidCredentialsError;
|
|
23762
|
+
exports.MediaAccessError = MediaAccessError;
|
|
22440
23763
|
exports.MediaTrackError = MediaTrackError;
|
|
22441
23764
|
exports.MessageParseError = MessageParseError;
|
|
22442
23765
|
exports.OverconstrainedFallbackError = OverconstrainedFallbackError;
|
|
22443
23766
|
exports.Participant = Participant;
|
|
23767
|
+
exports.ParticipantNotReadyError = ParticipantNotReadyError;
|
|
22444
23768
|
exports.PreflightError = PreflightError;
|
|
22445
23769
|
exports.RecoveryError = RecoveryError;
|
|
23770
|
+
exports.ScreenShareAlreadyActiveError = ScreenShareAlreadyActiveError;
|
|
22446
23771
|
exports.SelfCapabilities = SelfCapabilities;
|
|
22447
23772
|
exports.SelfParticipant = SelfParticipant;
|
|
22448
23773
|
exports.SignalWire = SignalWire;
|