@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.mjs
CHANGED
|
@@ -4186,7 +4186,7 @@ var require_merge$1 = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
4186
4186
|
var empty_1$7 = require_empty();
|
|
4187
4187
|
var args_1$8 = require_args();
|
|
4188
4188
|
var from_1$4 = require_from();
|
|
4189
|
-
function merge$
|
|
4189
|
+
function merge$7() {
|
|
4190
4190
|
var args = [];
|
|
4191
4191
|
for (var _i = 0; _i < arguments.length; _i++) args[_i] = arguments[_i];
|
|
4192
4192
|
var scheduler = args_1$8.popScheduler(args);
|
|
@@ -4194,7 +4194,7 @@ var require_merge$1 = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
4194
4194
|
var sources = args;
|
|
4195
4195
|
return !sources.length ? empty_1$7.EMPTY : sources.length === 1 ? innerFrom_1$31.innerFrom(sources[0]) : mergeAll_1$3.mergeAll(concurrent)(from_1$4.from(sources, scheduler));
|
|
4196
4196
|
}
|
|
4197
|
-
exports.merge = merge$
|
|
4197
|
+
exports.merge = merge$7;
|
|
4198
4198
|
}));
|
|
4199
4199
|
|
|
4200
4200
|
//#endregion
|
|
@@ -4325,13 +4325,13 @@ var require_race$1 = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
4325
4325
|
var innerFrom_1$28 = require_innerFrom();
|
|
4326
4326
|
var argsOrArgArray_1$4 = require_argsOrArgArray();
|
|
4327
4327
|
var OperatorSubscriber_1$48 = require_OperatorSubscriber();
|
|
4328
|
-
function race$
|
|
4328
|
+
function race$4() {
|
|
4329
4329
|
var sources = [];
|
|
4330
4330
|
for (var _i = 0; _i < arguments.length; _i++) sources[_i] = arguments[_i];
|
|
4331
4331
|
sources = argsOrArgArray_1$4.argsOrArgArray(sources);
|
|
4332
4332
|
return sources.length === 1 ? innerFrom_1$28.innerFrom(sources[0]) : new Observable_1$6.Observable(raceInit(sources));
|
|
4333
4333
|
}
|
|
4334
|
-
exports.race = race$
|
|
4334
|
+
exports.race = race$4;
|
|
4335
4335
|
function raceInit(sources) {
|
|
4336
4336
|
return function(subscriber) {
|
|
4337
4337
|
var subscriptions = [];
|
|
@@ -6076,7 +6076,7 @@ var require_merge = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
6076
6076
|
var mergeAll_1$2 = require_mergeAll();
|
|
6077
6077
|
var args_1$3 = require_args();
|
|
6078
6078
|
var from_1$1 = require_from();
|
|
6079
|
-
function merge$
|
|
6079
|
+
function merge$6() {
|
|
6080
6080
|
var args = [];
|
|
6081
6081
|
for (var _i = 0; _i < arguments.length; _i++) args[_i] = arguments[_i];
|
|
6082
6082
|
var scheduler = args_1$3.popScheduler(args);
|
|
@@ -6085,7 +6085,7 @@ var require_merge = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
6085
6085
|
mergeAll_1$2.mergeAll(concurrent)(from_1$1.from(__spreadArray$8([source], __read$8(args)), scheduler)).subscribe(subscriber);
|
|
6086
6086
|
});
|
|
6087
6087
|
}
|
|
6088
|
-
exports.merge = merge$
|
|
6088
|
+
exports.merge = merge$6;
|
|
6089
6089
|
}));
|
|
6090
6090
|
|
|
6091
6091
|
//#endregion
|
|
@@ -6932,7 +6932,7 @@ var require_switchMap = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
6932
6932
|
var innerFrom_1$6 = require_innerFrom();
|
|
6933
6933
|
var lift_1$13 = require_lift();
|
|
6934
6934
|
var OperatorSubscriber_1$11 = require_OperatorSubscriber();
|
|
6935
|
-
function switchMap$
|
|
6935
|
+
function switchMap$8(project, resultSelector) {
|
|
6936
6936
|
return lift_1$13.operate(function(source, subscriber) {
|
|
6937
6937
|
var innerSubscriber = null;
|
|
6938
6938
|
var index = 0;
|
|
@@ -6956,7 +6956,7 @@ var require_switchMap = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
6956
6956
|
}));
|
|
6957
6957
|
});
|
|
6958
6958
|
}
|
|
6959
|
-
exports.switchMap = switchMap$
|
|
6959
|
+
exports.switchMap = switchMap$8;
|
|
6960
6960
|
}));
|
|
6961
6961
|
|
|
6962
6962
|
//#endregion
|
|
@@ -9015,6 +9015,172 @@ var Destroyable = class {
|
|
|
9015
9015
|
}
|
|
9016
9016
|
};
|
|
9017
9017
|
|
|
9018
|
+
//#endregion
|
|
9019
|
+
//#region src/core/constants.ts
|
|
9020
|
+
const INVITE_VERSION = 1e3;
|
|
9021
|
+
const DEFAULT_ICE_CANDIDATE_TIMEOUT_MS = 600;
|
|
9022
|
+
const DEFAULT_ICE_GATHERING_TIMEOUT_MS = 6e3;
|
|
9023
|
+
const DEFAULT_RECONNECT_CALLS_TIMEOUT_MS = 300 * 1e3;
|
|
9024
|
+
const DEFAULT_CONNECTION_TIMEOUT_MS = 1e4;
|
|
9025
|
+
/**
|
|
9026
|
+
* How long call setup may wait for the network, measured from local media
|
|
9027
|
+
* settling until the member id arrives.
|
|
9028
|
+
*
|
|
9029
|
+
* Local media acquisition is deliberately NOT inside this: a permission prompt
|
|
9030
|
+
* or a device picker is human time, and a human may take as long as they like
|
|
9031
|
+
* without spending the server's budget. The clock starts when acquisition ends.
|
|
9032
|
+
*
|
|
9033
|
+
* Must exceed `iceGatheringTimeout + the RPC timeout`, which both run inside it.
|
|
9034
|
+
*/
|
|
9035
|
+
const DEFAULT_CALL_SIGNALING_TIMEOUT_MS = 12e3;
|
|
9036
|
+
/**
|
|
9037
|
+
* How long an auxiliary leg (screen share, additional device) may take to
|
|
9038
|
+
* connect once its media is in hand.
|
|
9039
|
+
*
|
|
9040
|
+
* One bound for both: the picker is human time and sits outside it. What remains
|
|
9041
|
+
* — offer, ICE, invite, answer, DTLS — is the same work for either leg kind.
|
|
9042
|
+
*/
|
|
9043
|
+
const DEFAULT_AUX_LEG_CONNECT_TIMEOUT_MS = 15e3;
|
|
9044
|
+
const DEFAULT_RECONNECT_DELAY_MIN_MS = 100;
|
|
9045
|
+
const DEFAULT_RECONNECT_DELAY_MAX_MS = 3e3;
|
|
9046
|
+
const DEFAULT_DEVICE_DEBOUNCE_TIME_MS = 1500;
|
|
9047
|
+
const DEFAULT_DEVICE_POLLING_INTERVAL_MS = 0;
|
|
9048
|
+
const PREFERENCES_STORAGE_KEY = "sw:preferences";
|
|
9049
|
+
/** Scope value that enables automatic token refresh. */
|
|
9050
|
+
const SAT_REFRESH_SCOPE = "sat:refresh";
|
|
9051
|
+
/** API endpoints for device token operations. */
|
|
9052
|
+
const DEVICE_TOKEN_ENDPOINT = "/api/fabric/subscriber/devices/token";
|
|
9053
|
+
const DEVICE_REFRESH_ENDPOINT = "/api/fabric/subscriber/devices/refresh";
|
|
9054
|
+
/** Default device token TTL in seconds (15 minutes). */
|
|
9055
|
+
const DEVICE_TOKEN_DEFAULT_EXPIRE_IN = 900;
|
|
9056
|
+
/** Buffer time in milliseconds before expiry to trigger refresh. */
|
|
9057
|
+
const DEVICE_TOKEN_REFRESH_BUFFER_MS = 3e4;
|
|
9058
|
+
/** Maximum retry attempts for device token refresh on transient failure. */
|
|
9059
|
+
const DEVICE_TOKEN_REFRESH_MAX_RETRIES = 3;
|
|
9060
|
+
/** Base delay in milliseconds for exponential backoff on refresh retry. */
|
|
9061
|
+
const DEVICE_TOKEN_REFRESH_RETRY_BASE_MS = 1e3;
|
|
9062
|
+
/** Maximum retry attempts for developer credential refresh on transient failure. */
|
|
9063
|
+
const CREDENTIAL_REFRESH_MAX_RETRIES = 5;
|
|
9064
|
+
/** Base delay in milliseconds for exponential backoff on credential refresh retry. */
|
|
9065
|
+
const CREDENTIAL_REFRESH_RETRY_BASE_MS = 1e3;
|
|
9066
|
+
/** Maximum delay in milliseconds for credential refresh backoff. */
|
|
9067
|
+
const CREDENTIAL_REFRESH_MAX_DELAY_MS = 3e4;
|
|
9068
|
+
/** Buffer in milliseconds before token expiry to trigger refresh. */
|
|
9069
|
+
const CREDENTIAL_REFRESH_BUFFER_MS = 5e3;
|
|
9070
|
+
/**
|
|
9071
|
+
* Clock-skew allowance (ms) for treating an in-memory credential as expired
|
|
9072
|
+
* when deciding whether a fresh (re)connect must re-mint the token before
|
|
9073
|
+
* authenticating. A token within this window of its `expiry_at` is treated as
|
|
9074
|
+
* stale so the reconnect re-mints via the credential provider instead of
|
|
9075
|
+
* replaying a dead token (which the server rejects with -32003).
|
|
9076
|
+
*/
|
|
9077
|
+
const CREDENTIAL_EXPIRY_SKEW_MS = 3e4;
|
|
9078
|
+
/**
|
|
9079
|
+
* Maximum time the coordinator will wait for `DeviceTokenManager.activate()`
|
|
9080
|
+
* to resolve before treating the activation as failed and falling back to
|
|
9081
|
+
* the developer-provided refresh path. Prevents a wedged HTTP layer from
|
|
9082
|
+
* leaving the session with no active refresh mechanism.
|
|
9083
|
+
*/
|
|
9084
|
+
const CREDENTIAL_ACTIVATE_TIMEOUT_MS = 3e4;
|
|
9085
|
+
/** JSON-RPC error code for requester validation failure (corrupted auth state). */
|
|
9086
|
+
const RPC_ERROR_REQUESTER_VALIDATION_FAILED = -32003;
|
|
9087
|
+
/** JSON-RPC error code for invalid params (e.g., missing authentication block). */
|
|
9088
|
+
const RPC_ERROR_INVALID_PARAMS = -32602;
|
|
9089
|
+
/** JSON-RPC error code for authentication failure (invalid token, missing DPoP, etc.). */
|
|
9090
|
+
const RPC_ERROR_AUTHENTICATION_FAILED = -32002;
|
|
9091
|
+
/** Error names browsers use for a media permission denial (user or policy). */
|
|
9092
|
+
const MEDIA_ACCESS_DENIAL_NAMES = [
|
|
9093
|
+
"NotAllowedError",
|
|
9094
|
+
"SecurityError",
|
|
9095
|
+
"PermissionDeniedError"
|
|
9096
|
+
];
|
|
9097
|
+
/** Error names browsers use when the capture hardware is already held exclusively. */
|
|
9098
|
+
const MEDIA_DEVICE_IN_USE_NAMES = ["NotReadableError", "TrackStartError"];
|
|
9099
|
+
/** Default polling interval for RTCPeerConnection.getStats() in milliseconds. */
|
|
9100
|
+
const DEFAULT_STATS_POLLING_INTERVAL_MS = 1e3;
|
|
9101
|
+
/** Number of initial samples used to build a baseline for spike detection. */
|
|
9102
|
+
const DEFAULT_STATS_BASELINE_SAMPLES = 10;
|
|
9103
|
+
/** Duration in ms with no inbound audio packets before emitting a critical issue. */
|
|
9104
|
+
const DEFAULT_STATS_NO_PACKET_THRESHOLD_MS = 2e3;
|
|
9105
|
+
/** Multiplier applied to baseline RTT to detect a warning-level RTT spike. */
|
|
9106
|
+
const DEFAULT_STATS_RTT_SPIKE_MULTIPLIER = 3;
|
|
9107
|
+
/** Packet loss fraction (0-1) above which a warning is emitted. */
|
|
9108
|
+
const DEFAULT_STATS_PACKET_LOSS_THRESHOLD = .05;
|
|
9109
|
+
/** Multiplier applied to baseline jitter to detect a jitter spike. */
|
|
9110
|
+
const DEFAULT_STATS_JITTER_SPIKE_MULTIPLIER = 4;
|
|
9111
|
+
/** Number of seconds of metrics history to retain. */
|
|
9112
|
+
const DEFAULT_STATS_HISTORY_SIZE = 30;
|
|
9113
|
+
/** Maximum keyframe requests allowed within a single burst window. */
|
|
9114
|
+
const DEFAULT_KEYFRAME_MAX_BURST$1 = 3;
|
|
9115
|
+
/** Duration of the keyframe burst window in milliseconds. */
|
|
9116
|
+
const DEFAULT_KEYFRAME_BURST_WINDOW_MS$1 = 3e3;
|
|
9117
|
+
/** Cooldown period in ms after burst limit is reached before allowing more keyframes. */
|
|
9118
|
+
const DEFAULT_KEYFRAME_COOLDOWN_MS$1 = 1e4;
|
|
9119
|
+
/** Minimum time between re-INVITE attempts in milliseconds. */
|
|
9120
|
+
const DEFAULT_REINVITE_DEBOUNCE_TIME_MS = 1e4;
|
|
9121
|
+
/** Maximum number of re-INVITE attempts per call. */
|
|
9122
|
+
const DEFAULT_REINVITE_MAX_ATTEMPTS = 3;
|
|
9123
|
+
/** Timeout for a single re-INVITE attempt in milliseconds. */
|
|
9124
|
+
const DEFAULT_REINVITE_TIMEOUT_MS = 5e3;
|
|
9125
|
+
/** Debounce window in ms to collapse multiple detection signals into one trigger. */
|
|
9126
|
+
const DEFAULT_RECOVERY_DEBOUNCE_TIME_MS = 2e3;
|
|
9127
|
+
/** Cooldown period in ms between recovery attempts. */
|
|
9128
|
+
const DEFAULT_RECOVERY_COOLDOWN_MS = 1e4;
|
|
9129
|
+
/** Grace period in ms before treating ICE 'disconnected' as a failure. */
|
|
9130
|
+
const DEFAULT_ICE_DISCONNECTED_GRACE_PERIOD_MS = 3e3;
|
|
9131
|
+
/** Timeout for a single ICE restart attempt in milliseconds. */
|
|
9132
|
+
const DEFAULT_ICE_RESTART_TIMEOUT_MS$1 = 5e3;
|
|
9133
|
+
/** Maximum recovery attempts before emitting 'max_attempts_reached'. */
|
|
9134
|
+
const DEFAULT_MAX_RECOVERY_ATTEMPTS = 3;
|
|
9135
|
+
/** Upper bound in ms for waiting on iceGatheringState === 'complete' after an ICE restart. */
|
|
9136
|
+
const ICE_GATHERING_COMPLETE_TIMEOUT_MS = 1e4;
|
|
9137
|
+
/** Upper bound in ms for waiting on RTCPeerConnection.connectionState === 'connected' after a recovery ICE restart. */
|
|
9138
|
+
const PEER_CONNECTION_RECOVERY_WAIT_MS = 5e3;
|
|
9139
|
+
/** Polling interval in ms while waiting for RTCPeerConnection.connectionState to transition. */
|
|
9140
|
+
const PEER_CONNECTION_RECOVERY_POLL_MS = 100;
|
|
9141
|
+
/** Polling interval for LocalAudioPipeline.level$ (ms). ~30fps is smooth for meters. */
|
|
9142
|
+
const AUDIO_LEVEL_POLL_INTERVAL_MS = 33;
|
|
9143
|
+
/** RMS level threshold (0..1) above which the local participant is considered speaking. */
|
|
9144
|
+
const VAD_THRESHOLD = .03;
|
|
9145
|
+
/** Hold window in ms below the threshold before speaking$ flips back to false. */
|
|
9146
|
+
const VAD_HOLD_MS = 250;
|
|
9147
|
+
/** Whether to persist device selections to storage by default. */
|
|
9148
|
+
const DEFAULT_PERSIST_DEVICE_SELECTION = true;
|
|
9149
|
+
/** Whether to auto-apply device changes to active calls by default. */
|
|
9150
|
+
const DEFAULT_SYNC_DEVICES_TO_ACTIVE_CALLS = true;
|
|
9151
|
+
/** Storage keys for persisted device selections. */
|
|
9152
|
+
const DEVICE_STORAGE_KEY_AUDIO_INPUT = "sw:device:audioinput";
|
|
9153
|
+
const DEVICE_STORAGE_KEY_AUDIO_OUTPUT = "sw:device:audiooutput";
|
|
9154
|
+
const DEVICE_STORAGE_KEY_VIDEO_INPUT = "sw:device:videoinput";
|
|
9155
|
+
/** Whether to auto-mute video when the tab becomes hidden. */
|
|
9156
|
+
const DEFAULT_AUTO_MUTE_VIDEO_ON_HIDDEN = false;
|
|
9157
|
+
/** Whether to re-enumerate devices when the page becomes visible. */
|
|
9158
|
+
const DEFAULT_REFRESH_DEVICES_ON_VISIBLE = true;
|
|
9159
|
+
/** Whether to check peer connection health when the page becomes visible. */
|
|
9160
|
+
const DEFAULT_CHECK_CONNECTION_ON_VISIBLE = true;
|
|
9161
|
+
/** Whether automatic video degradation on low bandwidth is enabled. */
|
|
9162
|
+
const DEFAULT_ENABLE_AUTO_DEGRADATION = true;
|
|
9163
|
+
/** Bitrate in kbps below which video is automatically disabled. */
|
|
9164
|
+
const DEFAULT_DEGRADATION_BITRATE_THRESHOLD_KBPS = 150;
|
|
9165
|
+
/** Bitrate in kbps above which video is automatically re-enabled (hysteresis). */
|
|
9166
|
+
const DEFAULT_DEGRADATION_RECOVERY_THRESHOLD_KBPS = 300;
|
|
9167
|
+
/** Whether relay-only escalation is enabled as a last-resort recovery tier. */
|
|
9168
|
+
const DEFAULT_ENABLE_RELAY_FALLBACK = true;
|
|
9169
|
+
/** Whether to listen for browser online/offline/connection events. */
|
|
9170
|
+
const DEFAULT_ENABLE_NETWORK_CHANGE_DETECTION = true;
|
|
9171
|
+
/** Whether to intercept server-sent media-timeout hangups and attempt recovery. */
|
|
9172
|
+
const DEFAULT_ENABLE_SERVER_HANGUP_INTERCEPTION = true;
|
|
9173
|
+
/** Default video track constraints applied when video is enabled without explicit constraints. */
|
|
9174
|
+
const DEFAULT_VIDEO_CONSTRAINTS = {
|
|
9175
|
+
width: { ideal: 1280 },
|
|
9176
|
+
height: { ideal: 720 },
|
|
9177
|
+
aspectRatio: 16 / 9
|
|
9178
|
+
};
|
|
9179
|
+
/** Whether stereo Opus is enabled by default. */
|
|
9180
|
+
const DEFAULT_STEREO_AUDIO = false;
|
|
9181
|
+
/** Max average bitrate for stereo Opus in bits per second. */
|
|
9182
|
+
const DEFAULT_STEREO_MAX_AVERAGE_BITRATE = 51e4;
|
|
9183
|
+
|
|
9018
9184
|
//#endregion
|
|
9019
9185
|
//#region src/core/errors.ts
|
|
9020
9186
|
var UnexpectedError = class extends Error {
|
|
@@ -9144,6 +9310,20 @@ var CallCreateError = class extends Error {
|
|
|
9144
9310
|
this.name = "CallCreateError";
|
|
9145
9311
|
}
|
|
9146
9312
|
};
|
|
9313
|
+
var CallNotReadyError = class extends Error {
|
|
9314
|
+
constructor(callId, options) {
|
|
9315
|
+
super(`Call "${callId}" has no self member context yet: selfId/nodeId have not been received from the server`, options);
|
|
9316
|
+
this.callId = callId;
|
|
9317
|
+
this.name = "CallNotReadyError";
|
|
9318
|
+
}
|
|
9319
|
+
};
|
|
9320
|
+
var ParticipantNotReadyError = class extends Error {
|
|
9321
|
+
constructor(memberId, options) {
|
|
9322
|
+
super(`Participant "${memberId}" has no call context yet: its member state (call_id/node_id) has not been received from the server`, options);
|
|
9323
|
+
this.memberId = memberId;
|
|
9324
|
+
this.name = "ParticipantNotReadyError";
|
|
9325
|
+
}
|
|
9326
|
+
};
|
|
9147
9327
|
var JSONRPCError = class extends Error {
|
|
9148
9328
|
constructor(code, message, data, options, requestId) {
|
|
9149
9329
|
super(message, options);
|
|
@@ -9215,6 +9395,32 @@ var CollectionFetchError = class extends Error {
|
|
|
9215
9395
|
this.name = "CollectionFetchError";
|
|
9216
9396
|
}
|
|
9217
9397
|
};
|
|
9398
|
+
/**
|
|
9399
|
+
* An auxiliary leg did not connect within its budget. Typed rather than a bare
|
|
9400
|
+
* RxJS `TimeoutError` so the leg and cause survive.
|
|
9401
|
+
*/
|
|
9402
|
+
var AuxiliaryLegTimeoutError = class extends Error {
|
|
9403
|
+
constructor(leg, originalError) {
|
|
9404
|
+
super(`Timed out waiting for the ${leg} connection to be established`, { cause: originalError });
|
|
9405
|
+
this.leg = leg;
|
|
9406
|
+
this.originalError = originalError;
|
|
9407
|
+
this.name = "AuxiliaryLegTimeoutError";
|
|
9408
|
+
}
|
|
9409
|
+
};
|
|
9410
|
+
/**
|
|
9411
|
+
* An auxiliary leg was removed before it finished connecting.
|
|
9412
|
+
*
|
|
9413
|
+
* Typed rather than a bare resolve so a caller awaiting the start can tell a
|
|
9414
|
+
* cancel apart from a share that actually came up — the public methods return
|
|
9415
|
+
* `void`, so the promise is the only signal they have.
|
|
9416
|
+
*/
|
|
9417
|
+
var AuxiliaryLegCancelledError = class extends Error {
|
|
9418
|
+
constructor(leg) {
|
|
9419
|
+
super(`The ${leg} leg was removed before it finished connecting`);
|
|
9420
|
+
this.leg = leg;
|
|
9421
|
+
this.name = "AuxiliaryLegCancelledError";
|
|
9422
|
+
}
|
|
9423
|
+
};
|
|
9218
9424
|
var MediaTrackError = class extends Error {
|
|
9219
9425
|
constructor(operation, kind, originalError) {
|
|
9220
9426
|
super(`Media track ${operation} failed for ${kind}`, { cause: originalError instanceof Error ? originalError : void 0 });
|
|
@@ -9224,6 +9430,53 @@ var MediaTrackError = class extends Error {
|
|
|
9224
9430
|
this.name = "MediaTrackError";
|
|
9225
9431
|
}
|
|
9226
9432
|
};
|
|
9433
|
+
/** True when a `getUserMedia`/`getDisplayMedia` rejection is a permission denial. */
|
|
9434
|
+
function isMediaAccessDenial(originalError) {
|
|
9435
|
+
return originalError instanceof Error && MEDIA_ACCESS_DENIAL_NAMES.includes(originalError.name);
|
|
9436
|
+
}
|
|
9437
|
+
/** True when a `getUserMedia` rejection means the hardware is already held exclusively. */
|
|
9438
|
+
function isMediaDeviceInUse(originalError) {
|
|
9439
|
+
return originalError instanceof Error && MEDIA_DEVICE_IN_USE_NAMES.includes(originalError.name);
|
|
9440
|
+
}
|
|
9441
|
+
/**
|
|
9442
|
+
* Failure to acquire local media (camera, microphone, or screen capture)
|
|
9443
|
+
* via `getUserMedia`/`getDisplayMedia`.
|
|
9444
|
+
*
|
|
9445
|
+
* Non-fatal by default: screenshare and additional-device failures never
|
|
9446
|
+
* end the call, and main-connection failures degrade to receive-only when
|
|
9447
|
+
* possible. The wrapping site sets `fatal` to `true` only when the call
|
|
9448
|
+
* cannot continue (receive-only fallback disabled or no receive intent).
|
|
9449
|
+
*/
|
|
9450
|
+
var MediaAccessError = class extends Error {
|
|
9451
|
+
constructor(operation, media, originalError, fatal = false) {
|
|
9452
|
+
super(`Media access ${isMediaAccessDenial(originalError) ? "denied" : "failed"} for ${operation} (${media})`, { cause: originalError instanceof Error ? originalError : void 0 });
|
|
9453
|
+
this.operation = operation;
|
|
9454
|
+
this.media = media;
|
|
9455
|
+
this.originalError = originalError;
|
|
9456
|
+
this.fatal = fatal;
|
|
9457
|
+
this.name = "MediaAccessError";
|
|
9458
|
+
}
|
|
9459
|
+
/** True when the underlying failure is a permission denial (user or policy). */
|
|
9460
|
+
get denied() {
|
|
9461
|
+
return isMediaAccessDenial(this.originalError);
|
|
9462
|
+
}
|
|
9463
|
+
};
|
|
9464
|
+
/**
|
|
9465
|
+
* Thrown by `startScreenShare()` when the call is already sharing a screen.
|
|
9466
|
+
*
|
|
9467
|
+
* A call carries at most one screen share. Accepting a second one would
|
|
9468
|
+
* overwrite the only reference the SDK holds to the first, leaving it
|
|
9469
|
+
* capturing and sending with no way to stop it — so the second request is
|
|
9470
|
+
* rejected and the live share is left untouched. Call `stopScreenShare()`
|
|
9471
|
+
* first to replace it.
|
|
9472
|
+
*/
|
|
9473
|
+
var ScreenShareAlreadyActiveError = class extends Error {
|
|
9474
|
+
constructor(screenShareId, options) {
|
|
9475
|
+
super(`A screen share is already active on this call (${screenShareId}). Call stopScreenShare() before starting another one.`, options);
|
|
9476
|
+
this.screenShareId = screenShareId;
|
|
9477
|
+
this.name = "ScreenShareAlreadyActiveError";
|
|
9478
|
+
}
|
|
9479
|
+
};
|
|
9227
9480
|
var DPoPInitError = class extends Error {
|
|
9228
9481
|
constructor(originalError, message = "Failed to initialize DPoP key pair") {
|
|
9229
9482
|
super(message, { cause: originalError instanceof Error ? originalError : void 0 });
|
|
@@ -9464,9 +9717,9 @@ var require_loglevel = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
9464
9717
|
defaultLogger$1 = new Logger();
|
|
9465
9718
|
defaultLogger$1.getLogger = function getLogger$1(name) {
|
|
9466
9719
|
if (typeof name !== "symbol" && typeof name !== "string" || name === "") throw new TypeError("You must supply a name when creating a logger.");
|
|
9467
|
-
var logger$
|
|
9468
|
-
if (!logger$
|
|
9469
|
-
return logger$
|
|
9720
|
+
var logger$34 = _loggersByName[name];
|
|
9721
|
+
if (!logger$34) logger$34 = _loggersByName[name] = new Logger(name, defaultLogger$1.methodFactory);
|
|
9722
|
+
return logger$34;
|
|
9470
9723
|
};
|
|
9471
9724
|
var _log = typeof window !== undefinedType ? window.log : void 0;
|
|
9472
9725
|
defaultLogger$1.noConflict = function() {
|
|
@@ -9502,8 +9755,8 @@ const defaultLoggerLevel = defaultLogger.levels.WARN;
|
|
|
9502
9755
|
defaultLogger.setLevel(defaultLoggerLevel);
|
|
9503
9756
|
let userLogger = null;
|
|
9504
9757
|
/** Replace the built-in logger with a custom implementation. Pass `null` to restore defaults. */
|
|
9505
|
-
const setLogger = (logger$
|
|
9506
|
-
userLogger = logger$
|
|
9758
|
+
const setLogger = (logger$34) => {
|
|
9759
|
+
userLogger = logger$34;
|
|
9507
9760
|
};
|
|
9508
9761
|
let debugOptions = {};
|
|
9509
9762
|
/** Configure debug options (e.g., `{ logWsTraffic: true }`). */
|
|
@@ -9547,8 +9800,8 @@ const wsTraffic = (options) => {
|
|
|
9547
9800
|
loggerInstance.debug(`${options.type.toUpperCase()}: \n`, msg, "\n");
|
|
9548
9801
|
};
|
|
9549
9802
|
const getLogger = () => {
|
|
9550
|
-
const logger$
|
|
9551
|
-
return new Proxy(logger$
|
|
9803
|
+
const logger$34 = getLoggerInstance();
|
|
9804
|
+
return new Proxy(logger$34, { get(_target, prop, _receiver) {
|
|
9552
9805
|
if (prop === "wsTraffic") return wsTraffic;
|
|
9553
9806
|
const instance = getLoggerInstance();
|
|
9554
9807
|
const value = Reflect.get(instance, prop);
|
|
@@ -9600,7 +9853,7 @@ const asyncRetry = async ({ asyncCallable, maxRetries: retries = DEFAULT_MAX_RET
|
|
|
9600
9853
|
|
|
9601
9854
|
//#endregion
|
|
9602
9855
|
//#region src/controllers/HTTPRequestController.ts
|
|
9603
|
-
const logger$
|
|
9856
|
+
const logger$33 = getLogger();
|
|
9604
9857
|
const GET_PARAMS = {
|
|
9605
9858
|
method: "GET",
|
|
9606
9859
|
headers: { Accept: "application/json" }
|
|
@@ -9664,7 +9917,7 @@ var HTTPRequestController = class HTTPRequestController extends Destroyable {
|
|
|
9664
9917
|
this._responses$.next(response);
|
|
9665
9918
|
return response;
|
|
9666
9919
|
} catch (error) {
|
|
9667
|
-
logger$
|
|
9920
|
+
logger$33.error("[HTTPRequestController] Request error:", error);
|
|
9668
9921
|
this._status$.next("error");
|
|
9669
9922
|
const err = error instanceof Error ? error : new Error("HTTP request failed", { cause: error });
|
|
9670
9923
|
this._errors$.next(err);
|
|
@@ -9691,7 +9944,7 @@ var HTTPRequestController = class HTTPRequestController extends Destroyable {
|
|
|
9691
9944
|
const url = this.buildURL(request.url);
|
|
9692
9945
|
const headers = this.buildHeaders(request.headers);
|
|
9693
9946
|
const timeout$5 = request.timeout ?? this.requestTimeout;
|
|
9694
|
-
logger$
|
|
9947
|
+
logger$33.debug("[HTTPRequestController] Executing request:", {
|
|
9695
9948
|
method: request.method,
|
|
9696
9949
|
url,
|
|
9697
9950
|
headers: Object.keys(headers).reduce((acc, key) => {
|
|
@@ -9711,7 +9964,7 @@ var HTTPRequestController = class HTTPRequestController extends Destroyable {
|
|
|
9711
9964
|
});
|
|
9712
9965
|
clearTimeout(timeoutId);
|
|
9713
9966
|
const httpResponse = await this.convertResponse(response);
|
|
9714
|
-
logger$
|
|
9967
|
+
logger$33.debug("[HTTPRequestController] Response received:", {
|
|
9715
9968
|
status: response.status,
|
|
9716
9969
|
statusText: response.statusText,
|
|
9717
9970
|
headers: [...response.headers.entries()],
|
|
@@ -9721,7 +9974,7 @@ var HTTPRequestController = class HTTPRequestController extends Destroyable {
|
|
|
9721
9974
|
} catch (error) {
|
|
9722
9975
|
clearTimeout(timeoutId);
|
|
9723
9976
|
if (error instanceof Error && error.name === "AbortError") throw new RequestTimeoutError(`Request timeout after ${timeout$5}ms`, { cause: error });
|
|
9724
|
-
logger$
|
|
9977
|
+
logger$33.error("[HTTPRequestController] Request failed:", error);
|
|
9725
9978
|
throw error;
|
|
9726
9979
|
}
|
|
9727
9980
|
}
|
|
@@ -9735,8 +9988,8 @@ var HTTPRequestController = class HTTPRequestController extends Destroyable {
|
|
|
9735
9988
|
const credential = this.getCredential();
|
|
9736
9989
|
if (credential.token) {
|
|
9737
9990
|
headers.Authorization = `Bearer ${credential.token}`;
|
|
9738
|
-
logger$
|
|
9739
|
-
} else logger$
|
|
9991
|
+
logger$33.debug("[HTTPRequestController] Using Bearer token auth, token length:", credential.token.length);
|
|
9992
|
+
} else logger$33.warn("[HTTPRequestController] No credentials available for authentication");
|
|
9740
9993
|
return headers;
|
|
9741
9994
|
}
|
|
9742
9995
|
/**
|
|
@@ -9873,137 +10126,6 @@ var DeviceHistoryManager = class {
|
|
|
9873
10126
|
}
|
|
9874
10127
|
};
|
|
9875
10128
|
|
|
9876
|
-
//#endregion
|
|
9877
|
-
//#region src/core/constants.ts
|
|
9878
|
-
const INVITE_VERSION = 1e3;
|
|
9879
|
-
const DEFAULT_ICE_CANDIDATE_TIMEOUT_MS = 600;
|
|
9880
|
-
const DEFAULT_ICE_GATHERING_TIMEOUT_MS = 6e3;
|
|
9881
|
-
const DEFAULT_RECONNECT_CALLS_TIMEOUT_MS = 300 * 1e3;
|
|
9882
|
-
const DEFAULT_CONNECTION_TIMEOUT_MS = 1e4;
|
|
9883
|
-
const DEFAULT_RECONNECT_DELAY_MIN_MS = 100;
|
|
9884
|
-
const DEFAULT_RECONNECT_DELAY_MAX_MS = 3e3;
|
|
9885
|
-
const DEFAULT_DEVICE_DEBOUNCE_TIME_MS = 1500;
|
|
9886
|
-
const DEFAULT_DEVICE_POLLING_INTERVAL_MS = 0;
|
|
9887
|
-
const PREFERENCES_STORAGE_KEY = "sw:preferences";
|
|
9888
|
-
/** Scope value that enables automatic token refresh. */
|
|
9889
|
-
const SAT_REFRESH_SCOPE = "sat:refresh";
|
|
9890
|
-
/** API endpoints for device token operations. */
|
|
9891
|
-
const DEVICE_TOKEN_ENDPOINT = "/api/fabric/subscriber/devices/token";
|
|
9892
|
-
const DEVICE_REFRESH_ENDPOINT = "/api/fabric/subscriber/devices/refresh";
|
|
9893
|
-
/** Default device token TTL in seconds (15 minutes). */
|
|
9894
|
-
const DEVICE_TOKEN_DEFAULT_EXPIRE_IN = 900;
|
|
9895
|
-
/** Buffer time in milliseconds before expiry to trigger refresh. */
|
|
9896
|
-
const DEVICE_TOKEN_REFRESH_BUFFER_MS = 3e4;
|
|
9897
|
-
/** Maximum retry attempts for device token refresh on transient failure. */
|
|
9898
|
-
const DEVICE_TOKEN_REFRESH_MAX_RETRIES = 3;
|
|
9899
|
-
/** Base delay in milliseconds for exponential backoff on refresh retry. */
|
|
9900
|
-
const DEVICE_TOKEN_REFRESH_RETRY_BASE_MS = 1e3;
|
|
9901
|
-
/** Maximum retry attempts for developer credential refresh on transient failure. */
|
|
9902
|
-
const CREDENTIAL_REFRESH_MAX_RETRIES = 5;
|
|
9903
|
-
/** Base delay in milliseconds for exponential backoff on credential refresh retry. */
|
|
9904
|
-
const CREDENTIAL_REFRESH_RETRY_BASE_MS = 1e3;
|
|
9905
|
-
/** Maximum delay in milliseconds for credential refresh backoff. */
|
|
9906
|
-
const CREDENTIAL_REFRESH_MAX_DELAY_MS = 3e4;
|
|
9907
|
-
/** Buffer in milliseconds before token expiry to trigger refresh. */
|
|
9908
|
-
const CREDENTIAL_REFRESH_BUFFER_MS = 5e3;
|
|
9909
|
-
/**
|
|
9910
|
-
* Maximum time the coordinator will wait for `DeviceTokenManager.activate()`
|
|
9911
|
-
* to resolve before treating the activation as failed and falling back to
|
|
9912
|
-
* the developer-provided refresh path. Prevents a wedged HTTP layer from
|
|
9913
|
-
* leaving the session with no active refresh mechanism.
|
|
9914
|
-
*/
|
|
9915
|
-
const CREDENTIAL_ACTIVATE_TIMEOUT_MS = 3e4;
|
|
9916
|
-
/** JSON-RPC error code for requester validation failure (corrupted auth state). */
|
|
9917
|
-
const RPC_ERROR_REQUESTER_VALIDATION_FAILED = -32003;
|
|
9918
|
-
/** JSON-RPC error code for invalid params (e.g., missing authentication block). */
|
|
9919
|
-
const RPC_ERROR_INVALID_PARAMS = -32602;
|
|
9920
|
-
/** JSON-RPC error code for authentication failure (invalid token, missing DPoP, etc.). */
|
|
9921
|
-
const RPC_ERROR_AUTHENTICATION_FAILED = -32002;
|
|
9922
|
-
/** Default polling interval for RTCPeerConnection.getStats() in milliseconds. */
|
|
9923
|
-
const DEFAULT_STATS_POLLING_INTERVAL_MS = 1e3;
|
|
9924
|
-
/** Number of initial samples used to build a baseline for spike detection. */
|
|
9925
|
-
const DEFAULT_STATS_BASELINE_SAMPLES = 10;
|
|
9926
|
-
/** Duration in ms with no inbound audio packets before emitting a critical issue. */
|
|
9927
|
-
const DEFAULT_STATS_NO_PACKET_THRESHOLD_MS = 2e3;
|
|
9928
|
-
/** Multiplier applied to baseline RTT to detect a warning-level RTT spike. */
|
|
9929
|
-
const DEFAULT_STATS_RTT_SPIKE_MULTIPLIER = 3;
|
|
9930
|
-
/** Packet loss fraction (0-1) above which a warning is emitted. */
|
|
9931
|
-
const DEFAULT_STATS_PACKET_LOSS_THRESHOLD = .05;
|
|
9932
|
-
/** Multiplier applied to baseline jitter to detect a jitter spike. */
|
|
9933
|
-
const DEFAULT_STATS_JITTER_SPIKE_MULTIPLIER = 4;
|
|
9934
|
-
/** Number of seconds of metrics history to retain. */
|
|
9935
|
-
const DEFAULT_STATS_HISTORY_SIZE = 30;
|
|
9936
|
-
/** Maximum keyframe requests allowed within a single burst window. */
|
|
9937
|
-
const DEFAULT_KEYFRAME_MAX_BURST$1 = 3;
|
|
9938
|
-
/** Duration of the keyframe burst window in milliseconds. */
|
|
9939
|
-
const DEFAULT_KEYFRAME_BURST_WINDOW_MS$1 = 3e3;
|
|
9940
|
-
/** Cooldown period in ms after burst limit is reached before allowing more keyframes. */
|
|
9941
|
-
const DEFAULT_KEYFRAME_COOLDOWN_MS$1 = 1e4;
|
|
9942
|
-
/** Minimum time between re-INVITE attempts in milliseconds. */
|
|
9943
|
-
const DEFAULT_REINVITE_DEBOUNCE_TIME_MS = 1e4;
|
|
9944
|
-
/** Maximum number of re-INVITE attempts per call. */
|
|
9945
|
-
const DEFAULT_REINVITE_MAX_ATTEMPTS = 3;
|
|
9946
|
-
/** Timeout for a single re-INVITE attempt in milliseconds. */
|
|
9947
|
-
const DEFAULT_REINVITE_TIMEOUT_MS = 5e3;
|
|
9948
|
-
/** Debounce window in ms to collapse multiple detection signals into one trigger. */
|
|
9949
|
-
const DEFAULT_RECOVERY_DEBOUNCE_TIME_MS = 2e3;
|
|
9950
|
-
/** Cooldown period in ms between recovery attempts. */
|
|
9951
|
-
const DEFAULT_RECOVERY_COOLDOWN_MS = 1e4;
|
|
9952
|
-
/** Grace period in ms before treating ICE 'disconnected' as a failure. */
|
|
9953
|
-
const DEFAULT_ICE_DISCONNECTED_GRACE_PERIOD_MS = 3e3;
|
|
9954
|
-
/** Timeout for a single ICE restart attempt in milliseconds. */
|
|
9955
|
-
const DEFAULT_ICE_RESTART_TIMEOUT_MS$1 = 5e3;
|
|
9956
|
-
/** Maximum recovery attempts before emitting 'max_attempts_reached'. */
|
|
9957
|
-
const DEFAULT_MAX_RECOVERY_ATTEMPTS = 3;
|
|
9958
|
-
/** Upper bound in ms for waiting on iceGatheringState === 'complete' after an ICE restart. */
|
|
9959
|
-
const ICE_GATHERING_COMPLETE_TIMEOUT_MS = 1e4;
|
|
9960
|
-
/** Upper bound in ms for waiting on RTCPeerConnection.connectionState === 'connected' after a recovery ICE restart. */
|
|
9961
|
-
const PEER_CONNECTION_RECOVERY_WAIT_MS = 5e3;
|
|
9962
|
-
/** Polling interval in ms while waiting for RTCPeerConnection.connectionState to transition. */
|
|
9963
|
-
const PEER_CONNECTION_RECOVERY_POLL_MS = 100;
|
|
9964
|
-
/** Polling interval for LocalAudioPipeline.level$ (ms). ~30fps is smooth for meters. */
|
|
9965
|
-
const AUDIO_LEVEL_POLL_INTERVAL_MS = 33;
|
|
9966
|
-
/** RMS level threshold (0..1) above which the local participant is considered speaking. */
|
|
9967
|
-
const VAD_THRESHOLD = .03;
|
|
9968
|
-
/** Hold window in ms below the threshold before speaking$ flips back to false. */
|
|
9969
|
-
const VAD_HOLD_MS = 250;
|
|
9970
|
-
/** Whether to persist device selections to storage by default. */
|
|
9971
|
-
const DEFAULT_PERSIST_DEVICE_SELECTION = true;
|
|
9972
|
-
/** Whether to auto-apply device changes to active calls by default. */
|
|
9973
|
-
const DEFAULT_SYNC_DEVICES_TO_ACTIVE_CALLS = true;
|
|
9974
|
-
/** Storage keys for persisted device selections. */
|
|
9975
|
-
const DEVICE_STORAGE_KEY_AUDIO_INPUT = "sw:device:audioinput";
|
|
9976
|
-
const DEVICE_STORAGE_KEY_AUDIO_OUTPUT = "sw:device:audiooutput";
|
|
9977
|
-
const DEVICE_STORAGE_KEY_VIDEO_INPUT = "sw:device:videoinput";
|
|
9978
|
-
/** Whether to auto-mute video when the tab becomes hidden. */
|
|
9979
|
-
const DEFAULT_AUTO_MUTE_VIDEO_ON_HIDDEN = false;
|
|
9980
|
-
/** Whether to re-enumerate devices when the page becomes visible. */
|
|
9981
|
-
const DEFAULT_REFRESH_DEVICES_ON_VISIBLE = true;
|
|
9982
|
-
/** Whether to check peer connection health when the page becomes visible. */
|
|
9983
|
-
const DEFAULT_CHECK_CONNECTION_ON_VISIBLE = true;
|
|
9984
|
-
/** Whether automatic video degradation on low bandwidth is enabled. */
|
|
9985
|
-
const DEFAULT_ENABLE_AUTO_DEGRADATION = true;
|
|
9986
|
-
/** Bitrate in kbps below which video is automatically disabled. */
|
|
9987
|
-
const DEFAULT_DEGRADATION_BITRATE_THRESHOLD_KBPS = 150;
|
|
9988
|
-
/** Bitrate in kbps above which video is automatically re-enabled (hysteresis). */
|
|
9989
|
-
const DEFAULT_DEGRADATION_RECOVERY_THRESHOLD_KBPS = 300;
|
|
9990
|
-
/** Whether relay-only escalation is enabled as a last-resort recovery tier. */
|
|
9991
|
-
const DEFAULT_ENABLE_RELAY_FALLBACK = true;
|
|
9992
|
-
/** Whether to listen for browser online/offline/connection events. */
|
|
9993
|
-
const DEFAULT_ENABLE_NETWORK_CHANGE_DETECTION = true;
|
|
9994
|
-
/** Whether to intercept server-sent media-timeout hangups and attempt recovery. */
|
|
9995
|
-
const DEFAULT_ENABLE_SERVER_HANGUP_INTERCEPTION = true;
|
|
9996
|
-
/** Default video track constraints applied when video is enabled without explicit constraints. */
|
|
9997
|
-
const DEFAULT_VIDEO_CONSTRAINTS = {
|
|
9998
|
-
width: { ideal: 1280 },
|
|
9999
|
-
height: { ideal: 720 },
|
|
10000
|
-
aspectRatio: 16 / 9
|
|
10001
|
-
};
|
|
10002
|
-
/** Whether stereo Opus is enabled by default. */
|
|
10003
|
-
const DEFAULT_STEREO_AUDIO = false;
|
|
10004
|
-
/** Max average bitrate for stereo Opus in bits per second. */
|
|
10005
|
-
const DEFAULT_STEREO_MAX_AVERAGE_BITRATE = 51e4;
|
|
10006
|
-
|
|
10007
10129
|
//#endregion
|
|
10008
10130
|
//#region src/utils/time.ts
|
|
10009
10131
|
function fromSecToMs(seconds) {
|
|
@@ -10015,7 +10137,7 @@ function fromMsToSec(milliseconds) {
|
|
|
10015
10137
|
|
|
10016
10138
|
//#endregion
|
|
10017
10139
|
//#region src/containers/PreferencesContainer.ts
|
|
10018
|
-
const logger$
|
|
10140
|
+
const logger$32 = getLogger();
|
|
10019
10141
|
var PreferencesContainer = class PreferencesContainer {
|
|
10020
10142
|
static get instance() {
|
|
10021
10143
|
this._instance ??= new PreferencesContainer();
|
|
@@ -10677,7 +10799,7 @@ var ClientPreferences = class {
|
|
|
10677
10799
|
if (!this._storage) return;
|
|
10678
10800
|
const data = collectStoredPreferences();
|
|
10679
10801
|
this._storage.setItem(PREFERENCES_STORAGE_KEY, data, "local").catch((error) => {
|
|
10680
|
-
logger$
|
|
10802
|
+
logger$32.error(`[ClientPreferences] Failed to save preferences: ${String(error)}`);
|
|
10681
10803
|
});
|
|
10682
10804
|
}
|
|
10683
10805
|
/** Loads preferences from storage and applies them to the container. */
|
|
@@ -10686,7 +10808,7 @@ var ClientPreferences = class {
|
|
|
10686
10808
|
this._storage.getItem(PREFERENCES_STORAGE_KEY, "local").then((stored) => {
|
|
10687
10809
|
if (stored) applyStoredPreferences(stored);
|
|
10688
10810
|
}).catch((error) => {
|
|
10689
|
-
logger$
|
|
10811
|
+
logger$32.error(`[ClientPreferences] Failed to load preferences: ${String(error)}`);
|
|
10690
10812
|
});
|
|
10691
10813
|
}
|
|
10692
10814
|
};
|
|
@@ -10708,7 +10830,7 @@ function toError(value) {
|
|
|
10708
10830
|
//#endregion
|
|
10709
10831
|
//#region src/controllers/NavigatorDeviceController.ts
|
|
10710
10832
|
var import_cjs$29 = require_cjs();
|
|
10711
|
-
const logger$
|
|
10833
|
+
const logger$31 = getLogger();
|
|
10712
10834
|
/** Maps a device kind to its storage key. */
|
|
10713
10835
|
const DEVICE_STORAGE_KEYS = {
|
|
10714
10836
|
audioinput: DEVICE_STORAGE_KEY_AUDIO_INPUT,
|
|
@@ -10730,7 +10852,7 @@ var NavigatorDeviceController = class extends Destroyable {
|
|
|
10730
10852
|
super();
|
|
10731
10853
|
this.webRTCApiProvider = webRTCApiProvider;
|
|
10732
10854
|
this.deviceChangeHandler = () => {
|
|
10733
|
-
logger$
|
|
10855
|
+
logger$31.debug("[DeviceController] Device change detected");
|
|
10734
10856
|
this.enumerateDevices();
|
|
10735
10857
|
};
|
|
10736
10858
|
this._devicesState$ = this.createBehaviorSubject(initialDevicesState);
|
|
@@ -10795,13 +10917,13 @@ var NavigatorDeviceController = class extends Destroyable {
|
|
|
10795
10917
|
return this.cachedObservable("videoInputDevices$", () => this._devicesState$.pipe((0, import_cjs$29.map)((state) => state.videoinput), (0, import_cjs$29.distinctUntilChanged)(), (0, import_cjs$29.takeUntil)(this.destroyed$)));
|
|
10796
10918
|
}
|
|
10797
10919
|
get selectedAudioInputDevice$() {
|
|
10798
|
-
return this.cachedObservable("selectedAudioInputDevice$", () => this._selectedDevicesState$.asObservable().pipe((0, import_cjs$29.map)((state) => state.audioinput), (0, import_cjs$29.distinctUntilChanged)(), (0, import_cjs$29.takeUntil)(this.destroyed$), (0, import_cjs$29.tap)((info) => logger$
|
|
10920
|
+
return this.cachedObservable("selectedAudioInputDevice$", () => this._selectedDevicesState$.asObservable().pipe((0, import_cjs$29.map)((state) => state.audioinput), (0, import_cjs$29.distinctUntilChanged)(), (0, import_cjs$29.takeUntil)(this.destroyed$), (0, import_cjs$29.tap)((info) => logger$31.debug("[DeviceController] Selected audio input device changed:", info))));
|
|
10799
10921
|
}
|
|
10800
10922
|
get selectedAudioOutputDevice$() {
|
|
10801
|
-
return this.cachedObservable("selectedAudioOutputDevice$", () => this._selectedDevicesState$.asObservable().pipe((0, import_cjs$29.map)((state) => state.audiooutput), (0, import_cjs$29.distinctUntilChanged)(), (0, import_cjs$29.takeUntil)(this.destroyed$), (0, import_cjs$29.tap)((info) => logger$
|
|
10923
|
+
return this.cachedObservable("selectedAudioOutputDevice$", () => this._selectedDevicesState$.asObservable().pipe((0, import_cjs$29.map)((state) => state.audiooutput), (0, import_cjs$29.distinctUntilChanged)(), (0, import_cjs$29.takeUntil)(this.destroyed$), (0, import_cjs$29.tap)((info) => logger$31.debug("[DeviceController] Selected audio output device changed:", info))));
|
|
10802
10924
|
}
|
|
10803
10925
|
get selectedVideoInputDevice$() {
|
|
10804
|
-
return this.cachedObservable("selectedVideoInputDevice$", () => this._selectedDevicesState$.asObservable().pipe((0, import_cjs$29.map)((state) => state.videoinput), (0, import_cjs$29.distinctUntilChanged)(), (0, import_cjs$29.takeUntil)(this.destroyed$), (0, import_cjs$29.tap)((info) => logger$
|
|
10926
|
+
return this.cachedObservable("selectedVideoInputDevice$", () => this._selectedDevicesState$.asObservable().pipe((0, import_cjs$29.map)((state) => state.videoinput), (0, import_cjs$29.distinctUntilChanged)(), (0, import_cjs$29.takeUntil)(this.destroyed$), (0, import_cjs$29.tap)((info) => logger$31.debug("[DeviceController] Selected video input device changed:", info))));
|
|
10805
10927
|
}
|
|
10806
10928
|
get selectedAudioInputDevice() {
|
|
10807
10929
|
if (this._audioInputDisabled$.value) return null;
|
|
@@ -10876,7 +10998,7 @@ var NavigatorDeviceController = class extends Destroyable {
|
|
|
10876
10998
|
if (device) this.persistDeviceSelection("audioinput", device);
|
|
10877
10999
|
}
|
|
10878
11000
|
selectVideoInputDevice(device) {
|
|
10879
|
-
logger$
|
|
11001
|
+
logger$31.debug("[DeviceController] Setting selected video input device:", device);
|
|
10880
11002
|
if (this._videoInputDisabled$.value && device) this._videoInputDisabled$.next(false);
|
|
10881
11003
|
const previous = this._selectedDevicesState$.value.videoinput;
|
|
10882
11004
|
if (previous && previous.deviceId !== device?.deviceId) this._deviceHistory.push("videoinput", previous);
|
|
@@ -10933,7 +11055,7 @@ var NavigatorDeviceController = class extends Destroyable {
|
|
|
10933
11055
|
}
|
|
10934
11056
|
const fromHistory = this._deviceHistory.findInHistory(kind, devices);
|
|
10935
11057
|
if (fromHistory) {
|
|
10936
|
-
logger$
|
|
11058
|
+
logger$31.debug(`[DeviceController] Device disappeared, falling back to history: ${fromHistory.label}`);
|
|
10937
11059
|
this.emitDeviceRecovered(kind, selected, fromHistory, "device_disconnected");
|
|
10938
11060
|
return fromHistory;
|
|
10939
11061
|
}
|
|
@@ -10986,7 +11108,7 @@ var NavigatorDeviceController = class extends Destroyable {
|
|
|
10986
11108
|
try {
|
|
10987
11109
|
await this._storageManager.setItem(DEVICE_STORAGE_KEYS[kind], stored, "local");
|
|
10988
11110
|
} catch (error) {
|
|
10989
|
-
logger$
|
|
11111
|
+
logger$31.error(`[DeviceController] Failed to persist device selection for ${kind}:`, error);
|
|
10990
11112
|
}
|
|
10991
11113
|
}
|
|
10992
11114
|
async loadPersistedDevices() {
|
|
@@ -11002,7 +11124,7 @@ var NavigatorDeviceController = class extends Destroyable {
|
|
|
11002
11124
|
[kind]: stored
|
|
11003
11125
|
};
|
|
11004
11126
|
} catch (error) {
|
|
11005
|
-
logger$
|
|
11127
|
+
logger$31.error(`[DeviceController] Failed to load persisted device for ${kind}:`, error);
|
|
11006
11128
|
}
|
|
11007
11129
|
}
|
|
11008
11130
|
/** Clears device history, persisted selections, and re-enumerates devices. */
|
|
@@ -11020,7 +11142,7 @@ var NavigatorDeviceController = class extends Destroyable {
|
|
|
11020
11142
|
this.disableDeviceMonitoring();
|
|
11021
11143
|
this.webRTCApiProvider.mediaDevices.addEventListener("devicechange", this.deviceChangeHandler);
|
|
11022
11144
|
if (PreferencesContainer.instance.devicePollingInterval > 0) this._devicesPoolingSubscription = (0, import_cjs$29.interval)(PreferencesContainer.instance.devicePollingInterval).subscribe(() => {
|
|
11023
|
-
logger$
|
|
11145
|
+
logger$31.debug("[DeviceController] Polling devices due to interval");
|
|
11024
11146
|
this.enumerateDevices();
|
|
11025
11147
|
});
|
|
11026
11148
|
this.enumerateDevices();
|
|
@@ -11046,13 +11168,13 @@ var NavigatorDeviceController = class extends Destroyable {
|
|
|
11046
11168
|
videoinput: []
|
|
11047
11169
|
});
|
|
11048
11170
|
this._devicesState$.next(devicesByKind);
|
|
11049
|
-
logger$
|
|
11171
|
+
logger$31.debug("[DeviceController] Devices enumerated:", {
|
|
11050
11172
|
audioInputs: devicesByKind.audioinput.length,
|
|
11051
11173
|
audioOutputs: devicesByKind.audiooutput.length,
|
|
11052
11174
|
videoInputs: devicesByKind.videoinput.length
|
|
11053
11175
|
});
|
|
11054
11176
|
} catch (error) {
|
|
11055
|
-
logger$
|
|
11177
|
+
logger$31.error("[DeviceController] Failed to enumerate devices:", error);
|
|
11056
11178
|
this._errors$.next(toError(error));
|
|
11057
11179
|
}
|
|
11058
11180
|
}
|
|
@@ -11068,7 +11190,7 @@ var NavigatorDeviceController = class extends Destroyable {
|
|
|
11068
11190
|
stream.getTracks().forEach((t) => t.stop());
|
|
11069
11191
|
return capabilities;
|
|
11070
11192
|
} catch (error) {
|
|
11071
|
-
logger$
|
|
11193
|
+
logger$31.error("[DeviceController] Failed to get device capabilities:", error);
|
|
11072
11194
|
this._errors$.next(toError(error));
|
|
11073
11195
|
throw error;
|
|
11074
11196
|
}
|
|
@@ -11319,7 +11441,7 @@ var DependencyContainer = class {
|
|
|
11319
11441
|
|
|
11320
11442
|
//#endregion
|
|
11321
11443
|
//#region src/controllers/CryptoController.ts
|
|
11322
|
-
const logger$
|
|
11444
|
+
const logger$30 = getLogger();
|
|
11323
11445
|
const DPOP_DB_NAME = "sw-dpop";
|
|
11324
11446
|
const DPOP_DB_VERSION = 1;
|
|
11325
11447
|
const DPOP_STORE_NAME = "keys";
|
|
@@ -11378,7 +11500,7 @@ async function loadKeyPairFromDB() {
|
|
|
11378
11500
|
tx.oncomplete = () => db.close();
|
|
11379
11501
|
});
|
|
11380
11502
|
} catch (error) {
|
|
11381
|
-
logger$
|
|
11503
|
+
logger$30.warn("[DPoP] Failed to load key pair from IndexedDB:", error);
|
|
11382
11504
|
return null;
|
|
11383
11505
|
}
|
|
11384
11506
|
}
|
|
@@ -11398,7 +11520,7 @@ async function saveKeyPairToDB(keyPair) {
|
|
|
11398
11520
|
};
|
|
11399
11521
|
});
|
|
11400
11522
|
} catch (error) {
|
|
11401
|
-
logger$
|
|
11523
|
+
logger$30.warn("[DPoP] Failed to save key pair to IndexedDB:", error);
|
|
11402
11524
|
}
|
|
11403
11525
|
}
|
|
11404
11526
|
async function deleteKeyPairFromDB() {
|
|
@@ -11417,7 +11539,7 @@ async function deleteKeyPairFromDB() {
|
|
|
11417
11539
|
};
|
|
11418
11540
|
});
|
|
11419
11541
|
} catch (error) {
|
|
11420
|
-
logger$
|
|
11542
|
+
logger$30.warn("[DPoP] Failed to delete key pair from IndexedDB:", error);
|
|
11421
11543
|
}
|
|
11422
11544
|
}
|
|
11423
11545
|
/**
|
|
@@ -11477,13 +11599,13 @@ var CryptoController = class {
|
|
|
11477
11599
|
this._publicJwk = await crypto.subtle.exportKey("jwk", stored.publicKey);
|
|
11478
11600
|
this._fingerprint = await computeJwkThumbprint(this._publicJwk);
|
|
11479
11601
|
this._initialized = true;
|
|
11480
|
-
logger$
|
|
11602
|
+
logger$30.debug("[DPoP] Key pair restored from IndexedDB, fingerprint:", this._fingerprint);
|
|
11481
11603
|
return this._fingerprint;
|
|
11482
11604
|
} catch (error) {
|
|
11483
|
-
logger$
|
|
11605
|
+
logger$30.warn("[DPoP] Stored key pair unusable, generating new one:", error);
|
|
11484
11606
|
await deleteKeyPairFromDB();
|
|
11485
11607
|
}
|
|
11486
|
-
logger$
|
|
11608
|
+
logger$30.debug("[DPoP] Generating RSA key pair");
|
|
11487
11609
|
this._keyPair = await crypto.subtle.generateKey({
|
|
11488
11610
|
name: "RSASSA-PKCS1-v1_5",
|
|
11489
11611
|
modulusLength: 2048,
|
|
@@ -11498,7 +11620,7 @@ var CryptoController = class {
|
|
|
11498
11620
|
this._fingerprint = await computeJwkThumbprint(this._publicJwk);
|
|
11499
11621
|
this._initialized = true;
|
|
11500
11622
|
await saveKeyPairToDB(this._keyPair);
|
|
11501
|
-
logger$
|
|
11623
|
+
logger$30.debug("[DPoP] Key pair generated and persisted, fingerprint:", this._fingerprint);
|
|
11502
11624
|
return this._fingerprint;
|
|
11503
11625
|
}
|
|
11504
11626
|
/**
|
|
@@ -11564,7 +11686,7 @@ var CryptoController = class {
|
|
|
11564
11686
|
this._fingerprint = null;
|
|
11565
11687
|
this._initialized = false;
|
|
11566
11688
|
deleteKeyPairFromDB();
|
|
11567
|
-
logger$
|
|
11689
|
+
logger$30.debug("[DPoP] Controller destroyed");
|
|
11568
11690
|
}
|
|
11569
11691
|
get publicJwk() {
|
|
11570
11692
|
if (!this._publicJwk) throw new DPoPInitError("CryptoController not initialized. Call init() first.");
|
|
@@ -11588,7 +11710,7 @@ var CryptoController = class {
|
|
|
11588
11710
|
//#endregion
|
|
11589
11711
|
//#region src/controllers/NetworkMonitor.ts
|
|
11590
11712
|
var import_cjs$28 = require_cjs();
|
|
11591
|
-
const logger$
|
|
11713
|
+
const logger$29 = getLogger();
|
|
11592
11714
|
/**
|
|
11593
11715
|
* Safely check whether we are running in a browser environment
|
|
11594
11716
|
* with `window` and the relevant event targets.
|
|
@@ -11645,7 +11767,7 @@ var NetworkMonitor = class extends Destroyable {
|
|
|
11645
11767
|
}
|
|
11646
11768
|
attachListeners() {
|
|
11647
11769
|
if (!hasBrowserNetworkEvents()) {
|
|
11648
|
-
logger$
|
|
11770
|
+
logger$29.debug("NetworkMonitor: no browser environment detected, skipping event listeners");
|
|
11649
11771
|
return;
|
|
11650
11772
|
}
|
|
11651
11773
|
window.addEventListener("online", this._onOnline);
|
|
@@ -11653,7 +11775,7 @@ var NetworkMonitor = class extends Destroyable {
|
|
|
11653
11775
|
const connection = getNetworkConnection();
|
|
11654
11776
|
if (connection) connection.addEventListener("change", this._onConnectionChange);
|
|
11655
11777
|
this._listenersAttached = true;
|
|
11656
|
-
logger$
|
|
11778
|
+
logger$29.debug("NetworkMonitor: event listeners attached");
|
|
11657
11779
|
}
|
|
11658
11780
|
removeListeners() {
|
|
11659
11781
|
if (!this._listenersAttached) return;
|
|
@@ -11664,10 +11786,10 @@ var NetworkMonitor = class extends Destroyable {
|
|
|
11664
11786
|
if (connection) connection.removeEventListener("change", this._onConnectionChange);
|
|
11665
11787
|
}
|
|
11666
11788
|
this._listenersAttached = false;
|
|
11667
|
-
logger$
|
|
11789
|
+
logger$29.debug("NetworkMonitor: event listeners removed");
|
|
11668
11790
|
}
|
|
11669
11791
|
handleOnline() {
|
|
11670
|
-
logger$
|
|
11792
|
+
logger$29.info("NetworkMonitor: browser went online");
|
|
11671
11793
|
this._isOnline$.next(true);
|
|
11672
11794
|
this._networkChange$.next({
|
|
11673
11795
|
type: "online",
|
|
@@ -11676,7 +11798,7 @@ var NetworkMonitor = class extends Destroyable {
|
|
|
11676
11798
|
});
|
|
11677
11799
|
}
|
|
11678
11800
|
handleOffline() {
|
|
11679
|
-
logger$
|
|
11801
|
+
logger$29.info("NetworkMonitor: browser went offline");
|
|
11680
11802
|
this._isOnline$.next(false);
|
|
11681
11803
|
this._networkChange$.next({
|
|
11682
11804
|
type: "offline",
|
|
@@ -11685,7 +11807,7 @@ var NetworkMonitor = class extends Destroyable {
|
|
|
11685
11807
|
}
|
|
11686
11808
|
handleConnectionChange() {
|
|
11687
11809
|
const networkType = getNetworkType();
|
|
11688
|
-
logger$
|
|
11810
|
+
logger$29.info(`NetworkMonitor: connection changed — effectiveType=${networkType ?? "unknown"}`);
|
|
11689
11811
|
this._networkChange$.next({
|
|
11690
11812
|
type: "connection_change",
|
|
11691
11813
|
timestamp: Date.now(),
|
|
@@ -11801,7 +11923,7 @@ function getNavigatorMediaDevices() {
|
|
|
11801
11923
|
//#endregion
|
|
11802
11924
|
//#region src/controllers/PreflightRunner.ts
|
|
11803
11925
|
var import_cjs$27 = require_cjs();
|
|
11804
|
-
const logger$
|
|
11926
|
+
const logger$28 = getLogger();
|
|
11805
11927
|
const DEFAULT_MEDIA_TEST_DURATION_S = 10;
|
|
11806
11928
|
const ICE_GATHERING_TIMEOUT_MS = 1e4;
|
|
11807
11929
|
const SIGNALING_RTT_TIMEOUT_MS = 5e3;
|
|
@@ -11850,7 +11972,7 @@ var PreflightRunner = class extends Destroyable {
|
|
|
11850
11972
|
if (!this._options.skipMediaTest) try {
|
|
11851
11973
|
bandwidth = await this.testMediaBandwidth(destination);
|
|
11852
11974
|
} catch (error) {
|
|
11853
|
-
logger$
|
|
11975
|
+
logger$28.warn("[PreflightRunner] Media bandwidth test failed:", error);
|
|
11854
11976
|
warnings.push("Media bandwidth test failed");
|
|
11855
11977
|
}
|
|
11856
11978
|
return {
|
|
@@ -11862,7 +11984,7 @@ var PreflightRunner = class extends Destroyable {
|
|
|
11862
11984
|
warnings
|
|
11863
11985
|
};
|
|
11864
11986
|
} catch (error) {
|
|
11865
|
-
logger$
|
|
11987
|
+
logger$28.error("[PreflightRunner] Preflight test failed:", error);
|
|
11866
11988
|
throw new PreflightError("preflight", error instanceof Error ? error : new Error(String(error)));
|
|
11867
11989
|
} finally {
|
|
11868
11990
|
this.destroy();
|
|
@@ -11893,7 +12015,7 @@ var PreflightRunner = class extends Destroyable {
|
|
|
11893
12015
|
if (track.kind === "video" && track.readyState === "live") videoWorking = true;
|
|
11894
12016
|
}
|
|
11895
12017
|
} catch (error) {
|
|
11896
|
-
logger$
|
|
12018
|
+
logger$28.warn("[PreflightRunner] Device test failed:", error);
|
|
11897
12019
|
} finally {
|
|
11898
12020
|
if (audioStream) audioStream.getTracks().forEach((t) => t.stop());
|
|
11899
12021
|
}
|
|
@@ -11951,7 +12073,7 @@ var PreflightRunner = class extends Destroyable {
|
|
|
11951
12073
|
rttMs
|
|
11952
12074
|
};
|
|
11953
12075
|
} catch (error) {
|
|
11954
|
-
logger$
|
|
12076
|
+
logger$28.warn("[PreflightRunner] ICE connectivity test failed:", error);
|
|
11955
12077
|
return {
|
|
11956
12078
|
type: "failed",
|
|
11957
12079
|
turnReachable: false,
|
|
@@ -11999,7 +12121,7 @@ var PreflightRunner = class extends Destroyable {
|
|
|
11999
12121
|
//#endregion
|
|
12000
12122
|
//#region src/controllers/VisibilityController.ts
|
|
12001
12123
|
var import_cjs$26 = require_cjs();
|
|
12002
|
-
const logger$
|
|
12124
|
+
const logger$27 = getLogger();
|
|
12003
12125
|
/**
|
|
12004
12126
|
* Checks whether the document visibility API is available.
|
|
12005
12127
|
*/
|
|
@@ -12036,8 +12158,8 @@ var VisibilityController = class extends Destroyable {
|
|
|
12036
12158
|
this._boundHandler = this._handleVisibilityChange.bind(this);
|
|
12037
12159
|
if (this._hasVisibilityApi) {
|
|
12038
12160
|
document.addEventListener("visibilitychange", this._boundHandler);
|
|
12039
|
-
logger$
|
|
12040
|
-
} else logger$
|
|
12161
|
+
logger$27.debug("VisibilityController: listening for visibilitychange events");
|
|
12162
|
+
} else logger$27.debug("VisibilityController: document visibility API not available, defaulting to visible");
|
|
12041
12163
|
}
|
|
12042
12164
|
/**
|
|
12043
12165
|
* Observable of the current visibility state.
|
|
@@ -12062,7 +12184,7 @@ var VisibilityController = class extends Destroyable {
|
|
|
12062
12184
|
destroy() {
|
|
12063
12185
|
if (this._hasVisibilityApi) {
|
|
12064
12186
|
document.removeEventListener("visibilitychange", this._boundHandler);
|
|
12065
|
-
logger$
|
|
12187
|
+
logger$27.debug("VisibilityController: removed visibilitychange listener");
|
|
12066
12188
|
}
|
|
12067
12189
|
super.destroy();
|
|
12068
12190
|
}
|
|
@@ -12080,7 +12202,7 @@ var VisibilityController = class extends Destroyable {
|
|
|
12080
12202
|
timestamp: Date.now()
|
|
12081
12203
|
};
|
|
12082
12204
|
this._visibilityChange$.next(changeEvent);
|
|
12083
|
-
logger$
|
|
12205
|
+
logger$27.debug("VisibilityController: visibility changed", {
|
|
12084
12206
|
from: previousState,
|
|
12085
12207
|
to: newState
|
|
12086
12208
|
});
|
|
@@ -12319,15 +12441,57 @@ const RPCEventAckResponse = (id) => makeRPCResponse({
|
|
|
12319
12441
|
result: {}
|
|
12320
12442
|
});
|
|
12321
12443
|
|
|
12444
|
+
//#endregion
|
|
12445
|
+
//#region src/utils/authRecovery.ts
|
|
12446
|
+
/**
|
|
12447
|
+
* Walk an error's `error`/`cause` chain looking for a {@link JSONRPCError}.
|
|
12448
|
+
* Errors thrown by call creation are wrapped (e.g. `CallCreateError`), so the
|
|
12449
|
+
* underlying signaling error is nested. Bounded by a visited set to guard
|
|
12450
|
+
* against cyclic causes.
|
|
12451
|
+
*/
|
|
12452
|
+
function findJSONRPCError(error) {
|
|
12453
|
+
const seen = /* @__PURE__ */ new Set();
|
|
12454
|
+
let current = error;
|
|
12455
|
+
while (current instanceof Error && !seen.has(current)) {
|
|
12456
|
+
seen.add(current);
|
|
12457
|
+
if (current instanceof JSONRPCError) return current;
|
|
12458
|
+
current = current.error ?? current.cause;
|
|
12459
|
+
}
|
|
12460
|
+
}
|
|
12461
|
+
/**
|
|
12462
|
+
* Whether an error is a session-recoverable authentication failure
|
|
12463
|
+
* (`-32002` authentication failed or `-32003` requester validation failed)
|
|
12464
|
+
* that a credential re-mint + retry can heal.
|
|
12465
|
+
*/
|
|
12466
|
+
function isRecoverableAuthError(error) {
|
|
12467
|
+
const rpcError = findJSONRPCError(error);
|
|
12468
|
+
return rpcError !== void 0 && (rpcError.code === RPC_ERROR_REQUESTER_VALIDATION_FAILED || rpcError.code === RPC_ERROR_AUTHENTICATION_FAILED);
|
|
12469
|
+
}
|
|
12470
|
+
/**
|
|
12471
|
+
* Whether an error is specifically a requester-validation rejection
|
|
12472
|
+
* (`-32003`) — the server refusing the session's credential.
|
|
12473
|
+
*
|
|
12474
|
+
* Narrower than {@link isRecoverableAuthError} on purpose. `-32002` is
|
|
12475
|
+
* overloaded server-side: a rejected reattach arrives as `-32002` with
|
|
12476
|
+
* `cause: INVALID_MSG_UNSPECIFIED` and message `CALL ERROR`, which is a
|
|
12477
|
+
* call-level rejection and says nothing about the credential. Use this where
|
|
12478
|
+
* the decision must not be fooled by that, such as deciding whether retrying
|
|
12479
|
+
* an operation could possibly succeed.
|
|
12480
|
+
*/
|
|
12481
|
+
function isRequesterValidationError(error) {
|
|
12482
|
+
return findJSONRPCError(error)?.code === RPC_ERROR_REQUESTER_VALIDATION_FAILED;
|
|
12483
|
+
}
|
|
12484
|
+
|
|
12322
12485
|
//#endregion
|
|
12323
12486
|
//#region src/managers/AttachManager.ts
|
|
12324
|
-
const logger$
|
|
12487
|
+
const logger$26 = getLogger();
|
|
12325
12488
|
var AttachManager = class {
|
|
12326
|
-
constructor(storage, deviceController, reconnectCallsTimeout, attachKey) {
|
|
12489
|
+
constructor(storage, deviceController, reconnectCallsTimeout, attachKey, credentialRecovered) {
|
|
12327
12490
|
this.storage = storage;
|
|
12328
12491
|
this.deviceController = deviceController;
|
|
12329
12492
|
this.reconnectCallsTimeout = reconnectCallsTimeout;
|
|
12330
12493
|
this.attachKey = attachKey;
|
|
12494
|
+
this.credentialRecovered = credentialRecovered;
|
|
12331
12495
|
this.writeQueue = Promise.resolve();
|
|
12332
12496
|
}
|
|
12333
12497
|
async detachAll() {
|
|
@@ -12342,7 +12506,7 @@ var AttachManager = class {
|
|
|
12342
12506
|
try {
|
|
12343
12507
|
return await this.storage.getItem(this.attachKey) ?? {};
|
|
12344
12508
|
} catch (error) {
|
|
12345
|
-
logger$
|
|
12509
|
+
logger$26.warn("[AttachManager] Failed to retrieve attached calls from storage", error);
|
|
12346
12510
|
return {};
|
|
12347
12511
|
}
|
|
12348
12512
|
}
|
|
@@ -12350,7 +12514,7 @@ var AttachManager = class {
|
|
|
12350
12514
|
try {
|
|
12351
12515
|
await this.storage.setItem(this.attachKey, attached);
|
|
12352
12516
|
} catch (error) {
|
|
12353
|
-
logger$
|
|
12517
|
+
logger$26.warn("[AttachManager] Failed to write attached calls to storage", error);
|
|
12354
12518
|
}
|
|
12355
12519
|
}
|
|
12356
12520
|
/**
|
|
@@ -12369,11 +12533,39 @@ var AttachManager = class {
|
|
|
12369
12533
|
}
|
|
12370
12534
|
async attach(call) {
|
|
12371
12535
|
if (!call.to) {
|
|
12372
|
-
logger$
|
|
12536
|
+
logger$26.warn("[AttachManager] Skip attach for calls with no destination");
|
|
12373
12537
|
return;
|
|
12374
12538
|
}
|
|
12539
|
+
const attachment = this.buildAttachment(call, call.to);
|
|
12540
|
+
await this.mutate((attached) => ({
|
|
12541
|
+
...attached,
|
|
12542
|
+
[call.id]: attachment
|
|
12543
|
+
}));
|
|
12544
|
+
}
|
|
12545
|
+
/**
|
|
12546
|
+
* Keep an already-stored call's reference alive and current — the periodic
|
|
12547
|
+
* refresh the `verto.ping` keepalive drives.
|
|
12548
|
+
*
|
|
12549
|
+
* Only ever updates: a call with no record is one nothing wants reattached,
|
|
12550
|
+
* and re-creating it here would undo a `detach`. That matters because a ping
|
|
12551
|
+
* can land in the window between `bye()` detaching and the call being torn
|
|
12552
|
+
* down, and a record revived there survives the hangup — so the next page
|
|
12553
|
+
* load dials a call nobody is on. The existence check and the write share
|
|
12554
|
+
* one {@link mutate} turn, so a concurrent detach cannot slip between them.
|
|
12555
|
+
*/
|
|
12556
|
+
async refresh(call) {
|
|
12557
|
+
if (!call.to) return;
|
|
12375
12558
|
const destination = call.to;
|
|
12376
|
-
|
|
12559
|
+
await this.mutate((attached) => {
|
|
12560
|
+
if (!Object.hasOwn(attached, call.id)) return attached;
|
|
12561
|
+
return {
|
|
12562
|
+
...attached,
|
|
12563
|
+
[call.id]: this.buildAttachment(call, destination)
|
|
12564
|
+
};
|
|
12565
|
+
});
|
|
12566
|
+
}
|
|
12567
|
+
buildAttachment(call, destination) {
|
|
12568
|
+
return {
|
|
12377
12569
|
nodeId: call.nodeId,
|
|
12378
12570
|
destination,
|
|
12379
12571
|
mediaDirections: call.mediaDirections,
|
|
@@ -12381,10 +12573,6 @@ var AttachManager = class {
|
|
|
12381
12573
|
videoInputDevice: call.mediaDirections.video !== "inactive" ? this.deviceController.selectedVideoInputDevice : null,
|
|
12382
12574
|
attachedAt: Date.now()
|
|
12383
12575
|
};
|
|
12384
|
-
await this.mutate((attached) => ({
|
|
12385
|
-
...attached,
|
|
12386
|
-
[call.id]: attachment
|
|
12387
|
-
}));
|
|
12388
12576
|
}
|
|
12389
12577
|
async detach(call) {
|
|
12390
12578
|
await this.mutate((attached) => {
|
|
@@ -12407,8 +12595,14 @@ var AttachManager = class {
|
|
|
12407
12595
|
* rejecting. Once that fix is deployed, this will work for both
|
|
12408
12596
|
* page reloads and WebSocket reconnects.
|
|
12409
12597
|
*
|
|
12410
|
-
*
|
|
12411
|
-
*
|
|
12598
|
+
* A failed reattach does NOT generally cost the stored reference. It is
|
|
12599
|
+
* discarded only when the server denied the reattach on a session whose
|
|
12600
|
+
* credential it had already accepted — a verified reauthentication followed
|
|
12601
|
+
* by a refusal is the server saying the call is gone, and that is the one
|
|
12602
|
+
* refusal worth acting on. Until then the credential may be what is being
|
|
12603
|
+
* refused, and the record is the only way a later reload can try again;
|
|
12604
|
+
* keeping it costs nothing, since `detachExpired` reaps it once it is older
|
|
12605
|
+
* than `reconnectCallsTimeout`.
|
|
12412
12606
|
*/
|
|
12413
12607
|
async reattachCalls() {
|
|
12414
12608
|
const attached = await this.readAttached();
|
|
@@ -12417,25 +12611,31 @@ var AttachManager = class {
|
|
|
12417
12611
|
const { destination } = attachment;
|
|
12418
12612
|
const options = this.buildCallOptions(attachment);
|
|
12419
12613
|
let succeeded = false;
|
|
12614
|
+
let refusedOnCredentials = false;
|
|
12420
12615
|
for (let attempt = 1; attempt <= 3; attempt++) try {
|
|
12421
12616
|
await this.session.createOutboundCall(destination, {
|
|
12422
12617
|
callId,
|
|
12423
12618
|
...options
|
|
12424
12619
|
});
|
|
12425
|
-
logger$
|
|
12620
|
+
logger$26.info(`[AttachManager] Reattached call ${callId} (attempt ${attempt})`);
|
|
12426
12621
|
succeeded = true;
|
|
12427
12622
|
break;
|
|
12428
12623
|
} catch (error) {
|
|
12429
|
-
logger$
|
|
12624
|
+
logger$26.warn(`[AttachManager] Reattach attempt ${attempt}/3 failed for call ${callId}:`, error);
|
|
12625
|
+
if (isRequesterValidationError(error)) {
|
|
12626
|
+
refusedOnCredentials = true;
|
|
12627
|
+
logger$26.warn(`[AttachManager] Reattach of ${callId} was refused on credentials; not retrying.`);
|
|
12628
|
+
break;
|
|
12629
|
+
}
|
|
12430
12630
|
if (attempt < 3) await new Promise((r) => setTimeout(r, (attempt + 1) * 1e3));
|
|
12431
12631
|
}
|
|
12432
|
-
if (!succeeded) {
|
|
12433
|
-
logger$
|
|
12632
|
+
if (!succeeded) if (this.credentialRecovered() && !refusedOnCredentials) {
|
|
12633
|
+
logger$26.warn(`[AttachManager] Reattach of ${callId} was denied after a verified reauthentication, removing reference`);
|
|
12434
12634
|
await this.detach({
|
|
12435
12635
|
id: callId,
|
|
12436
12636
|
mediaDirections: attachment.mediaDirections
|
|
12437
12637
|
});
|
|
12438
|
-
}
|
|
12638
|
+
} else logger$26.warn(`[AttachManager] Reattach failed for call ${callId}; keeping the reference (credential refused or never proven good)`);
|
|
12439
12639
|
}
|
|
12440
12640
|
}
|
|
12441
12641
|
/**
|
|
@@ -12536,12 +12736,12 @@ var require_race = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
12536
12736
|
exports.race = void 0;
|
|
12537
12737
|
var argsOrArgArray_1 = require_argsOrArgArray();
|
|
12538
12738
|
var raceWith_1$1 = require_raceWith();
|
|
12539
|
-
function race$
|
|
12739
|
+
function race$3() {
|
|
12540
12740
|
var args = [];
|
|
12541
12741
|
for (var _i = 0; _i < arguments.length; _i++) args[_i] = arguments[_i];
|
|
12542
12742
|
return raceWith_1$1.raceWith.apply(void 0, __spreadArray([], __read(argsOrArgArray_1.argsOrArgArray(args))));
|
|
12543
12743
|
}
|
|
12544
|
-
exports.race = race$
|
|
12744
|
+
exports.race = race$3;
|
|
12545
12745
|
}));
|
|
12546
12746
|
|
|
12547
12747
|
//#endregion
|
|
@@ -13601,7 +13801,7 @@ function toggleHandraiseMethod(is) {
|
|
|
13601
13801
|
|
|
13602
13802
|
//#endregion
|
|
13603
13803
|
//#region src/core/entities/Participant.ts
|
|
13604
|
-
const logger$
|
|
13804
|
+
const logger$25 = getLogger();
|
|
13605
13805
|
const initialState = {};
|
|
13606
13806
|
/**
|
|
13607
13807
|
* Represents a participant in a call.
|
|
@@ -13611,9 +13811,9 @@ const initialState = {};
|
|
|
13611
13811
|
* the local participant with additional device control.
|
|
13612
13812
|
*/
|
|
13613
13813
|
var Participant = class extends Destroyable {
|
|
13614
|
-
constructor(id,
|
|
13814
|
+
constructor(id, callExecuteMethod, deviceController) {
|
|
13615
13815
|
super();
|
|
13616
|
-
this.
|
|
13816
|
+
this.callExecuteMethod = callExecuteMethod;
|
|
13617
13817
|
this.deviceController = deviceController;
|
|
13618
13818
|
this._state$ = this.createBehaviorSubject(initialState);
|
|
13619
13819
|
this.id = id;
|
|
@@ -13835,22 +14035,55 @@ var Participant = class extends Destroyable {
|
|
|
13835
14035
|
get value() {
|
|
13836
14036
|
return this._state$.value;
|
|
13837
14037
|
}
|
|
14038
|
+
/**
|
|
14039
|
+
* Target triple for member RPCs, built from the participant's own state.
|
|
14040
|
+
* The backend locates the member's session by the target `call_id`/`node_id`,
|
|
14041
|
+
* so this must always be the participant's own call context — never the
|
|
14042
|
+
* local call's id (issue #19400).
|
|
14043
|
+
*
|
|
14044
|
+
* Reading it doubles as a readiness probe: it throws until the first full
|
|
14045
|
+
* member event (`member.joined`/`member.updated` or the `call.joined`
|
|
14046
|
+
* roster) arrives, and never regresses afterwards.
|
|
14047
|
+
*
|
|
14048
|
+
* @throws {ParticipantNotReadyError} If the member state has not been
|
|
14049
|
+
* received yet (e.g. a participant first seen via `member.talking`) — an
|
|
14050
|
+
* empty call context can never address the member, so fail fast instead of
|
|
14051
|
+
* sending a doomed RPC.
|
|
14052
|
+
*/
|
|
14053
|
+
get target() {
|
|
14054
|
+
const { call_id, node_id } = this._state$.value;
|
|
14055
|
+
if (!call_id || !node_id) throw new ParticipantNotReadyError(this.id);
|
|
14056
|
+
return {
|
|
14057
|
+
member_id: this.id,
|
|
14058
|
+
call_id,
|
|
14059
|
+
node_id
|
|
14060
|
+
};
|
|
14061
|
+
}
|
|
14062
|
+
/**
|
|
14063
|
+
* Executes a member RPC against this participant, injecting its own
|
|
14064
|
+
* {@link target} as the target.
|
|
14065
|
+
*
|
|
14066
|
+
* @throws {ParticipantNotReadyError} Via {@link target}, when the
|
|
14067
|
+
* member state has not been received yet.
|
|
14068
|
+
*/
|
|
14069
|
+
async executeMethod(method, args) {
|
|
14070
|
+
return this.callExecuteMethod(this.target, method, args);
|
|
14071
|
+
}
|
|
13838
14072
|
/** Toggles the deafened state (mutes/unmutes incoming audio). */
|
|
13839
14073
|
async toggleDeaf() {
|
|
13840
|
-
|
|
13841
|
-
await this.executeMethod(this.id, method, {});
|
|
14074
|
+
await this.executeMethod(toggleDeafMethod(this.deaf), {});
|
|
13842
14075
|
}
|
|
13843
14076
|
/** Toggles the hand-raised state. */
|
|
13844
14077
|
async toggleHandraise() {
|
|
13845
|
-
await this.executeMethod(
|
|
14078
|
+
await this.executeMethod(toggleHandraiseMethod(this.handraised), {});
|
|
13846
14079
|
}
|
|
13847
14080
|
/** Mutes the participant's audio. */
|
|
13848
14081
|
async mute() {
|
|
13849
|
-
await this.executeMethod(
|
|
14082
|
+
await this.executeMethod("call.mute", { channels: ["audio"] });
|
|
13850
14083
|
}
|
|
13851
14084
|
/** Unmutes the participant's audio. */
|
|
13852
14085
|
async unmute() {
|
|
13853
|
-
await this.executeMethod(
|
|
14086
|
+
await this.executeMethod("call.unmute", { channels: ["audio"] });
|
|
13854
14087
|
}
|
|
13855
14088
|
/** Toggles the participant's audio mute state. */
|
|
13856
14089
|
async toggleMute() {
|
|
@@ -13858,11 +14091,11 @@ var Participant = class extends Destroyable {
|
|
|
13858
14091
|
}
|
|
13859
14092
|
/** Mutes the participant's video. */
|
|
13860
14093
|
async muteVideo() {
|
|
13861
|
-
await this.executeMethod(
|
|
14094
|
+
await this.executeMethod("call.mute", { channels: ["video"] });
|
|
13862
14095
|
}
|
|
13863
14096
|
/** Unmutes the participant's video. */
|
|
13864
14097
|
async unmuteVideo() {
|
|
13865
|
-
await this.executeMethod(
|
|
14098
|
+
await this.executeMethod("call.unmute", { channels: ["video"] });
|
|
13866
14099
|
}
|
|
13867
14100
|
/** Toggles the participant's video mute state. */
|
|
13868
14101
|
async toggleMuteVideo() {
|
|
@@ -13870,7 +14103,7 @@ var Participant = class extends Destroyable {
|
|
|
13870
14103
|
}
|
|
13871
14104
|
/** Toggles echo cancellation on the audio input. */
|
|
13872
14105
|
async toggleEchoCancellation() {
|
|
13873
|
-
await this.executeMethod(
|
|
14106
|
+
await this.executeMethod("call.audioflags.set", {
|
|
13874
14107
|
echo_cancellation: !this.echoCancellation,
|
|
13875
14108
|
auto_gain: this.autoGain,
|
|
13876
14109
|
noise_suppression: this.noiseSuppression
|
|
@@ -13878,7 +14111,7 @@ var Participant = class extends Destroyable {
|
|
|
13878
14111
|
}
|
|
13879
14112
|
/** Toggles automatic gain control on the audio input. */
|
|
13880
14113
|
async toggleAudioInputAutoGain() {
|
|
13881
|
-
await this.executeMethod(
|
|
14114
|
+
await this.executeMethod("call.audioflags.set", {
|
|
13882
14115
|
echo_cancellation: this.echoCancellation,
|
|
13883
14116
|
auto_gain: !this.autoGain,
|
|
13884
14117
|
noise_suppression: this.noiseSuppression
|
|
@@ -13886,7 +14119,7 @@ var Participant = class extends Destroyable {
|
|
|
13886
14119
|
}
|
|
13887
14120
|
/** Toggles noise suppression on the audio input. */
|
|
13888
14121
|
async toggleNoiseSuppression() {
|
|
13889
|
-
await this.executeMethod(
|
|
14122
|
+
await this.executeMethod("call.audioflags.set", {
|
|
13890
14123
|
echo_cancellation: this.echoCancellation,
|
|
13891
14124
|
auto_gain: this.autoGain,
|
|
13892
14125
|
noise_suppression: !this.noiseSuppression
|
|
@@ -13894,7 +14127,7 @@ var Participant = class extends Destroyable {
|
|
|
13894
14127
|
}
|
|
13895
14128
|
/** Toggles low-bitrate mode for this participant's media. */
|
|
13896
14129
|
async toggleLowbitrate() {
|
|
13897
|
-
await this.executeMethod(
|
|
14130
|
+
await this.executeMethod("call.lowbitrate.set", { lowbitrate: !this.lowbitrate });
|
|
13898
14131
|
}
|
|
13899
14132
|
/**
|
|
13900
14133
|
* Adjusts the **conference-only** microphone energy gate / sensitivity level
|
|
@@ -13910,7 +14143,7 @@ var Participant = class extends Destroyable {
|
|
|
13910
14143
|
* (integer, larger values are more sensitive).
|
|
13911
14144
|
*/
|
|
13912
14145
|
async setAudioInputSensitivity(value) {
|
|
13913
|
-
await this.executeMethod(
|
|
14146
|
+
await this.executeMethod("call.microphone.sensitivity.set", { sensitivity: value });
|
|
13914
14147
|
}
|
|
13915
14148
|
/**
|
|
13916
14149
|
* Sets the **server-side** microphone volume on this participant's bridged
|
|
@@ -13923,7 +14156,7 @@ var Participant = class extends Destroyable {
|
|
|
13923
14156
|
* @param value - Volume level (0-100).
|
|
13924
14157
|
*/
|
|
13925
14158
|
async setAudioInputVolume(value) {
|
|
13926
|
-
await this.executeMethod(
|
|
14159
|
+
await this.executeMethod("call.microphone.volume.set", { volume: value });
|
|
13927
14160
|
}
|
|
13928
14161
|
/**
|
|
13929
14162
|
* Sets the **server-side** speaker volume on this participant's bridged call
|
|
@@ -13937,45 +14170,31 @@ var Participant = class extends Destroyable {
|
|
|
13937
14170
|
* @param value - Volume level (0-100).
|
|
13938
14171
|
*/
|
|
13939
14172
|
async setAudioOutputVolume(value) {
|
|
13940
|
-
await this.executeMethod(
|
|
14173
|
+
await this.executeMethod("call.speaker.volume.set", { volume: value });
|
|
13941
14174
|
}
|
|
13942
14175
|
/**
|
|
13943
14176
|
* Sets the participant's position in the video layout.
|
|
13944
14177
|
*
|
|
13945
|
-
* Requires the `member.position` capability. The gateway
|
|
13946
|
-
*
|
|
13947
|
-
*
|
|
13948
|
-
*
|
|
13949
|
-
* guarantee a visible change: the backend silently returns `200` (no-op) for
|
|
13950
|
-
* non-conference targets.
|
|
14178
|
+
* Requires the `member.position` capability. The gateway requires a
|
|
14179
|
+
* `targets` array of `{ target, position }` entries (issue #19400). A
|
|
14180
|
+
* resolved promise does not guarantee a visible change: the backend silently
|
|
14181
|
+
* returns `200` (no-op) for non-conference targets.
|
|
13951
14182
|
*
|
|
13952
14183
|
* @param value - The {@link VideoPosition} to assign (e.g. `'auto'`, `'reserved-0'`).
|
|
13953
14184
|
*/
|
|
13954
14185
|
async setPosition(value) {
|
|
13955
|
-
|
|
13956
|
-
|
|
13957
|
-
member_id: this.id,
|
|
13958
|
-
call_id: state.call_id ?? "",
|
|
13959
|
-
node_id: state.node_id ?? ""
|
|
13960
|
-
};
|
|
13961
|
-
await this.executeMethod(target, "call.member.position.set", { targets: [{
|
|
13962
|
-
target,
|
|
14186
|
+
await this.executeMethod("call.member.position.set", { targets: [{
|
|
14187
|
+
target: this.target,
|
|
13963
14188
|
position: value
|
|
13964
14189
|
}] });
|
|
13965
14190
|
}
|
|
13966
14191
|
/** Removes this participant from the call. */
|
|
13967
14192
|
async remove() {
|
|
13968
|
-
|
|
13969
|
-
const target = {
|
|
13970
|
-
member_id: this.id,
|
|
13971
|
-
call_id: state.call_id ?? "",
|
|
13972
|
-
node_id: state.node_id ?? ""
|
|
13973
|
-
};
|
|
13974
|
-
await this.executeMethod(target, "call.member.remove", {});
|
|
14193
|
+
await this.executeMethod("call.member.remove", { targets: [this.target] });
|
|
13975
14194
|
}
|
|
13976
14195
|
/** Ends the call for this participant. */
|
|
13977
14196
|
async end() {
|
|
13978
|
-
await this.executeMethod(
|
|
14197
|
+
await this.executeMethod("call.end", {});
|
|
13979
14198
|
}
|
|
13980
14199
|
/**
|
|
13981
14200
|
* Replaces custom metadata for this participant.
|
|
@@ -13995,7 +14214,7 @@ var Participant = class extends Destroyable {
|
|
|
13995
14214
|
}
|
|
13996
14215
|
/** Destroys the participant, releasing all subscriptions and references. */
|
|
13997
14216
|
destroy() {
|
|
13998
|
-
this.
|
|
14217
|
+
this.callExecuteMethod = void 0;
|
|
13999
14218
|
super.destroy();
|
|
14000
14219
|
}
|
|
14001
14220
|
};
|
|
@@ -14007,8 +14226,8 @@ var Participant = class extends Destroyable {
|
|
|
14007
14226
|
*/
|
|
14008
14227
|
var SelfParticipant = class extends Participant {
|
|
14009
14228
|
/** @internal */
|
|
14010
|
-
constructor(id,
|
|
14011
|
-
super(id,
|
|
14229
|
+
constructor(id, callExecuteMethod, vertoManager, deviceController) {
|
|
14230
|
+
super(id, callExecuteMethod, deviceController);
|
|
14012
14231
|
this.vertoManager = vertoManager;
|
|
14013
14232
|
this._studioAudio$ = this.createBehaviorSubject(false);
|
|
14014
14233
|
this.capabilities = new SelfCapabilities();
|
|
@@ -14032,7 +14251,7 @@ var SelfParticipant = class extends Participant {
|
|
|
14032
14251
|
async enableStudioAudio() {
|
|
14033
14252
|
if (this._studioAudio$.value) return;
|
|
14034
14253
|
this._studioAudio$.next(true);
|
|
14035
|
-
await this.executeMethod(
|
|
14254
|
+
await this.executeMethod("call.audioflags.set", {
|
|
14036
14255
|
echo_cancellation: false,
|
|
14037
14256
|
auto_gain: false,
|
|
14038
14257
|
noise_suppression: false
|
|
@@ -14045,18 +14264,37 @@ var SelfParticipant = class extends Participant {
|
|
|
14045
14264
|
async disableStudioAudio() {
|
|
14046
14265
|
if (!this._studioAudio$.value) return;
|
|
14047
14266
|
this._studioAudio$.next(false);
|
|
14048
|
-
await this.executeMethod(
|
|
14267
|
+
await this.executeMethod("call.audioflags.set", {
|
|
14049
14268
|
echo_cancellation: true,
|
|
14050
14269
|
auto_gain: true,
|
|
14051
14270
|
noise_suppression: true
|
|
14052
14271
|
});
|
|
14053
14272
|
}
|
|
14054
|
-
/**
|
|
14055
|
-
|
|
14273
|
+
/**
|
|
14274
|
+
* Starts sharing the local screen.
|
|
14275
|
+
*
|
|
14276
|
+
* A call carries at most one screen share. Read `screenShareStatus` before
|
|
14277
|
+
* calling and treat `'starting'`/`'stopping'` as busy.
|
|
14278
|
+
*
|
|
14279
|
+
* The call is unaffected when acquisition fails.
|
|
14280
|
+
*
|
|
14281
|
+
* @param options - Pass `{ audio: true }` to also request the shared
|
|
14282
|
+
* surface's audio. Defaults to video only.
|
|
14283
|
+
* @throws {ScreenShareAlreadyActiveError} When this call is already
|
|
14284
|
+
* sharing a screen. Call {@link stopScreenShare} before starting another.
|
|
14285
|
+
* @throws {AuxiliaryLegCancelledError} When {@link stopScreenShare} removes
|
|
14286
|
+
* the share before its leg finishes connecting.
|
|
14287
|
+
* @throws The raw `getDisplayMedia` error. A dismissed picker or a
|
|
14288
|
+
* permission denial rejects with a `NotAllowedError` `DOMException` —
|
|
14289
|
+
* inspect `error.name` to tell benign cancels apart from real failures.
|
|
14290
|
+
*/
|
|
14291
|
+
async startScreenShare(options) {
|
|
14056
14292
|
try {
|
|
14057
|
-
await this.vertoManager.addScreenMedia();
|
|
14293
|
+
await this.vertoManager.addScreenMedia(options);
|
|
14058
14294
|
} catch (error) {
|
|
14059
|
-
logger$
|
|
14295
|
+
if (error instanceof AuxiliaryLegCancelledError) logger$25.debug("[Participant.startScreenShare] Screen share cancelled before connecting.");
|
|
14296
|
+
else logger$25.error("[Participant.startScreenShare] Screen share error:", error);
|
|
14297
|
+
throw error;
|
|
14060
14298
|
}
|
|
14061
14299
|
}
|
|
14062
14300
|
/** Observable of the current screen share status. */
|
|
@@ -14071,12 +14309,24 @@ var SelfParticipant = class extends Participant {
|
|
|
14071
14309
|
async stopScreenShare() {
|
|
14072
14310
|
return this.vertoManager.removeScreenMedia();
|
|
14073
14311
|
}
|
|
14074
|
-
/**
|
|
14312
|
+
/**
|
|
14313
|
+
* Adds an additional media input device to the call.
|
|
14314
|
+
*
|
|
14315
|
+
* The call is unaffected when acquisition fails.
|
|
14316
|
+
*
|
|
14317
|
+
* @throws {AuxiliaryLegCancelledError} When {@link removeAdditionalDevice}
|
|
14318
|
+
* removes the device before its leg finishes connecting.
|
|
14319
|
+
* @throws The raw `getUserMedia` error (e.g. `NotAllowedError` on
|
|
14320
|
+
* permission denial) — inspect `error.name` to decide how to react — or
|
|
14321
|
+
* `AuxiliaryLegTimeoutError` if the leg does not connect in time.
|
|
14322
|
+
*/
|
|
14075
14323
|
async addAdditionalDevice(options) {
|
|
14076
14324
|
try {
|
|
14077
14325
|
await this.vertoManager.addInputDevice(options);
|
|
14078
14326
|
} catch (error) {
|
|
14079
|
-
logger$
|
|
14327
|
+
if (error instanceof AuxiliaryLegCancelledError) logger$25.debug("[Participant.addAdditionalDevice] Device removed before connecting.");
|
|
14328
|
+
else logger$25.error("[Participant.addAdditionalDevice] Additional device error:", error);
|
|
14329
|
+
throw error;
|
|
14080
14330
|
}
|
|
14081
14331
|
}
|
|
14082
14332
|
/** Removes an additional media input device by ID. */
|
|
@@ -14110,22 +14360,31 @@ var SelfParticipant = class extends Participant {
|
|
|
14110
14360
|
this.deviceController.selectAudioInputDevice(device);
|
|
14111
14361
|
if (options.savePreference) PreferencesContainer.instance.preferredAudioInput = device;
|
|
14112
14362
|
}
|
|
14113
|
-
/**
|
|
14363
|
+
/**
|
|
14364
|
+
* Updates the audio input track constraints for the active call.
|
|
14365
|
+
* @returns whether the constraints reached the media the call is sending.
|
|
14366
|
+
*/
|
|
14114
14367
|
async setAudioInputDeviceConstraints(constraints) {
|
|
14115
|
-
|
|
14368
|
+
return this.vertoManager.updateMediaConstraints({ audio: constraints });
|
|
14116
14369
|
}
|
|
14117
|
-
/**
|
|
14370
|
+
/**
|
|
14371
|
+
* Updates both audio and video input track constraints for the active call.
|
|
14372
|
+
* @returns whether both kinds took the constraints.
|
|
14373
|
+
*/
|
|
14118
14374
|
async setInputDevicesConstraints(constraints) {
|
|
14119
|
-
|
|
14375
|
+
return this.vertoManager.updateMediaConstraints(constraints);
|
|
14120
14376
|
}
|
|
14121
14377
|
/** Selects the video input device for future calls. Optionally saves as a preference. */
|
|
14122
14378
|
selectVideoInputDevice(device, options = {}) {
|
|
14123
14379
|
this.deviceController.selectVideoInputDevice(device);
|
|
14124
14380
|
if (options.savePreference) PreferencesContainer.instance.preferredVideoInput = device;
|
|
14125
14381
|
}
|
|
14126
|
-
/**
|
|
14382
|
+
/**
|
|
14383
|
+
* Updates the video input track constraints for the active call.
|
|
14384
|
+
* @returns whether the constraints reached the media the call is sending.
|
|
14385
|
+
*/
|
|
14127
14386
|
async setVideoInputDeviceConstraints(constraints) {
|
|
14128
|
-
|
|
14387
|
+
return this.vertoManager.updateMediaConstraints({ video: constraints });
|
|
14129
14388
|
}
|
|
14130
14389
|
/** Selects the audio output device. Optionally saves as a preference. */
|
|
14131
14390
|
selectAudioOutputDevice(device, options = {}) {
|
|
@@ -14138,7 +14397,7 @@ var SelfParticipant = class extends Participant {
|
|
|
14138
14397
|
*/
|
|
14139
14398
|
exitStudioModeIfActive() {
|
|
14140
14399
|
if (this._studioAudio$.value) {
|
|
14141
|
-
logger$
|
|
14400
|
+
logger$25.debug("[SelfParticipant] Exiting studio audio mode due to individual flag toggle");
|
|
14142
14401
|
this._studioAudio$.next(false);
|
|
14143
14402
|
}
|
|
14144
14403
|
}
|
|
@@ -14162,7 +14421,7 @@ var SelfParticipant = class extends Participant {
|
|
|
14162
14421
|
try {
|
|
14163
14422
|
await super.mute();
|
|
14164
14423
|
} catch (error) {
|
|
14165
|
-
logger$
|
|
14424
|
+
logger$25.warn("[Participant.toggleAudioInput] Server Error while muting audio input, proceeding with local toggle anyway", error);
|
|
14166
14425
|
} finally {
|
|
14167
14426
|
this.vertoManager.muteMainAudioInputDevice();
|
|
14168
14427
|
}
|
|
@@ -14172,7 +14431,7 @@ var SelfParticipant = class extends Participant {
|
|
|
14172
14431
|
try {
|
|
14173
14432
|
await super.unmute();
|
|
14174
14433
|
} catch (error) {
|
|
14175
|
-
logger$
|
|
14434
|
+
logger$25.warn("[Participant.toggleAudioInput] Server Error while unmuting audio input, proceeding with local toggle anyway", error);
|
|
14176
14435
|
} finally {
|
|
14177
14436
|
await this.vertoManager.unmuteMainAudioInputDevice();
|
|
14178
14437
|
}
|
|
@@ -14182,7 +14441,7 @@ var SelfParticipant = class extends Participant {
|
|
|
14182
14441
|
try {
|
|
14183
14442
|
await super.muteVideo();
|
|
14184
14443
|
} catch (error) {
|
|
14185
|
-
logger$
|
|
14444
|
+
logger$25.warn("[Participant.toggleVideoInput] Server Error while muting video input, proceeding with local toggle anyway", error);
|
|
14186
14445
|
} finally {
|
|
14187
14446
|
this.vertoManager.muteMainVideoInputDevice();
|
|
14188
14447
|
}
|
|
@@ -14192,7 +14451,7 @@ var SelfParticipant = class extends Participant {
|
|
|
14192
14451
|
try {
|
|
14193
14452
|
await super.unmuteVideo();
|
|
14194
14453
|
} catch (error) {
|
|
14195
|
-
logger$
|
|
14454
|
+
logger$25.warn("[Participant.toggleVideoInput] Server Error while unmuting video input, proceeding with local toggle anyway", error);
|
|
14196
14455
|
} finally {
|
|
14197
14456
|
await this.vertoManager.unmuteMainVideoInputDevice();
|
|
14198
14457
|
}
|
|
@@ -14397,7 +14656,7 @@ function filterAs(predicate, resultPath) {
|
|
|
14397
14656
|
//#endregion
|
|
14398
14657
|
//#region src/operators/throwOnRPCError.ts
|
|
14399
14658
|
var import_cjs$21 = require_cjs();
|
|
14400
|
-
const logger$
|
|
14659
|
+
const logger$24 = getLogger();
|
|
14401
14660
|
/**
|
|
14402
14661
|
* RxJS operator that throws a {@link JSONRPCError} when the RPC response contains an error.
|
|
14403
14662
|
* Passes successful responses through unchanged.
|
|
@@ -14405,14 +14664,14 @@ const logger$23 = getLogger();
|
|
|
14405
14664
|
function throwOnRPCError() {
|
|
14406
14665
|
return (0, import_cjs$21.map)((response) => {
|
|
14407
14666
|
if (response.error) {
|
|
14408
|
-
logger$
|
|
14667
|
+
logger$24.error("[throwOnRPCError] RPC error response:", {
|
|
14409
14668
|
code: response.error.code,
|
|
14410
14669
|
message: response.error.message,
|
|
14411
14670
|
data: response.error.data
|
|
14412
14671
|
});
|
|
14413
14672
|
throw new JSONRPCError(response.error.code, response.error.message, response.error.data);
|
|
14414
14673
|
}
|
|
14415
|
-
logger$
|
|
14674
|
+
logger$24.debug("[throwOnRPCError] RPC successful response:", response);
|
|
14416
14675
|
return response;
|
|
14417
14676
|
});
|
|
14418
14677
|
}
|
|
@@ -14420,7 +14679,7 @@ function throwOnRPCError() {
|
|
|
14420
14679
|
//#endregion
|
|
14421
14680
|
//#region src/managers/CallEventsManager.ts
|
|
14422
14681
|
var import_cjs$20 = require_cjs();
|
|
14423
|
-
const logger$
|
|
14682
|
+
const logger$23 = getLogger();
|
|
14424
14683
|
const initialSessionState = {};
|
|
14425
14684
|
/** @internal */
|
|
14426
14685
|
var CallEventsManager = class extends Destroyable {
|
|
@@ -14524,7 +14783,7 @@ var CallEventsManager = class extends Destroyable {
|
|
|
14524
14783
|
}
|
|
14525
14784
|
initSubscriptions() {
|
|
14526
14785
|
this.subscribeTo(this.callJoinedEvent$, (callJoinedEvent) => {
|
|
14527
|
-
logger$
|
|
14786
|
+
logger$23.debug("[CallEventsManager] Handling call.joined event for call/session IDs:", {
|
|
14528
14787
|
callId: callJoinedEvent.call_id,
|
|
14529
14788
|
roomSessionId: callJoinedEvent.room_session_id
|
|
14530
14789
|
});
|
|
@@ -14551,19 +14810,19 @@ var CallEventsManager = class extends Destroyable {
|
|
|
14551
14810
|
if (this._self$.value?.capabilities.setLayout) this.updateLayouts();
|
|
14552
14811
|
});
|
|
14553
14812
|
this.subscribeTo(this.memberUpdates$, (member) => {
|
|
14554
|
-
logger$
|
|
14813
|
+
logger$23.debug("[CallEventsManager] Handling member update event for member ID:", member);
|
|
14555
14814
|
this.upsertParticipant(member);
|
|
14556
14815
|
});
|
|
14557
14816
|
this.subscribeTo(this.webRtcCallSession.memberLeft$, (memberLeftEvent) => {
|
|
14558
|
-
logger$
|
|
14817
|
+
logger$23.debug("[CallEventsManager] Handling member.left event for member ID:", memberLeftEvent.member.member_id);
|
|
14559
14818
|
const participants = { ...this._participants$.value };
|
|
14560
14819
|
if (memberLeftEvent.member.member_id in participants) {
|
|
14561
14820
|
delete participants[memberLeftEvent.member.member_id];
|
|
14562
14821
|
this._participants$.next(participants);
|
|
14563
|
-
} else logger$
|
|
14822
|
+
} else logger$23.warn(`[CallEventsManager] Received member.left event for unknown member ID: ${memberLeftEvent.member.member_id}`);
|
|
14564
14823
|
});
|
|
14565
14824
|
this.subscribeTo(this.webRtcCallSession.callUpdated$, (callUpdatedEvent) => {
|
|
14566
|
-
logger$
|
|
14825
|
+
logger$23.debug("[CallEventsManager] Handling call.updated event:", callUpdatedEvent);
|
|
14567
14826
|
const roomSession = callUpdatedEvent.room_session;
|
|
14568
14827
|
this._sessionState$.next({
|
|
14569
14828
|
...this._sessionState$.value,
|
|
@@ -14578,7 +14837,7 @@ var CallEventsManager = class extends Destroyable {
|
|
|
14578
14837
|
});
|
|
14579
14838
|
});
|
|
14580
14839
|
this.subscribeTo(this.layoutChangedEvent$, (layoutChangedEvent) => {
|
|
14581
|
-
logger$
|
|
14840
|
+
logger$23.debug("[CallEventsManager] Handling layout.changed event:", layoutChangedEvent);
|
|
14582
14841
|
this._sessionState$.next({
|
|
14583
14842
|
...this._sessionState$.value,
|
|
14584
14843
|
layout_name: layoutChangedEvent.id,
|
|
@@ -14588,10 +14847,10 @@ var CallEventsManager = class extends Destroyable {
|
|
|
14588
14847
|
});
|
|
14589
14848
|
}
|
|
14590
14849
|
updateParticipantPositions(layoutChangedEvent) {
|
|
14591
|
-
if (Object.keys(this._participants$.value).length > 0 && !layoutChangedEvent.layers.some((layer) => !!layer.member_id)) logger$
|
|
14850
|
+
if (Object.keys(this._participants$.value).length > 0 && !layoutChangedEvent.layers.some((layer) => !!layer.member_id)) logger$23.warn("[CallEventsManager] No layers with member_id found in layout.changed event. Nothing to update.");
|
|
14592
14851
|
layoutChangedEvent.layers.filter((layer) => !!layer.member_id).filter((layer) => {
|
|
14593
14852
|
if (!(layer.member_id in this._participants$.value)) {
|
|
14594
|
-
logger$
|
|
14853
|
+
logger$23.warn(`[CallEventsManager] Skipping layout layer for unknown member_id: ${layer.member_id}`);
|
|
14595
14854
|
return false;
|
|
14596
14855
|
}
|
|
14597
14856
|
return true;
|
|
@@ -14609,12 +14868,17 @@ var CallEventsManager = class extends Destroyable {
|
|
|
14609
14868
|
updateLayouts() {
|
|
14610
14869
|
if (!this.selfId) return;
|
|
14611
14870
|
this.webRtcCallSession.executeMethod(this.selfId, "call.layout.list", {}).then((response) => {
|
|
14871
|
+
const layouts = response.result?.layouts;
|
|
14872
|
+
if (!layouts) {
|
|
14873
|
+
logger$23.warn("[CallEventsManager] Layout list response carried no layouts; keeping current layouts");
|
|
14874
|
+
return;
|
|
14875
|
+
}
|
|
14612
14876
|
this._sessionState$.next({
|
|
14613
14877
|
...this._sessionState$.value,
|
|
14614
|
-
layouts
|
|
14878
|
+
layouts
|
|
14615
14879
|
});
|
|
14616
14880
|
}).catch((error) => {
|
|
14617
|
-
logger$
|
|
14881
|
+
logger$23.error("[CallEventsManager] Error fetching layouts:", error);
|
|
14618
14882
|
});
|
|
14619
14883
|
}
|
|
14620
14884
|
updateParticipants(members) {
|
|
@@ -14630,7 +14894,7 @@ var CallEventsManager = class extends Destroyable {
|
|
|
14630
14894
|
}
|
|
14631
14895
|
const participant = this._participants$.value[member.member_id];
|
|
14632
14896
|
const oldValue = participant.value;
|
|
14633
|
-
logger$
|
|
14897
|
+
logger$23.debug("[CallEventsManager] Updating participant:", member.member_id, {
|
|
14634
14898
|
oldValue,
|
|
14635
14899
|
newValue: member
|
|
14636
14900
|
});
|
|
@@ -14643,17 +14907,17 @@ var CallEventsManager = class extends Destroyable {
|
|
|
14643
14907
|
}
|
|
14644
14908
|
get callJoinedEvent$() {
|
|
14645
14909
|
return this.cachedObservable("callJoinedEvent$", () => this.webRtcCallSession.callEvent$.pipe((0, import_cjs$20.filter)(isCallJoinedPayload), (0, import_cjs$20.tap)((event) => {
|
|
14646
|
-
logger$
|
|
14910
|
+
logger$23.debug("[CallEventsManager] Call joined event:", event);
|
|
14647
14911
|
})));
|
|
14648
14912
|
}
|
|
14649
14913
|
get layoutChangedEvent$() {
|
|
14650
14914
|
return this.cachedObservable("layoutChangedEvent$", () => this.webRtcCallSession.callEvent$.pipe(filterAs(isLayoutChangedPayload, "layout"), (0, import_cjs$20.tap)((event) => {
|
|
14651
|
-
logger$
|
|
14915
|
+
logger$23.debug("[CallEventsManager] Layout changed event:", event);
|
|
14652
14916
|
})));
|
|
14653
14917
|
}
|
|
14654
14918
|
get memberUpdates$() {
|
|
14655
14919
|
return this.cachedObservable("memberUpdates$", () => (0, import_cjs$20.merge)(this.webRtcCallSession.memberJoined$, this.webRtcCallSession.memberUpdated$, this.webRtcCallSession.memberTalking$).pipe((0, import_cjs$20.map)((event) => event.member), (0, import_cjs$20.tap)((event) => {
|
|
14656
|
-
logger$
|
|
14920
|
+
logger$23.debug("[CallEventsManager] Member update event:", event);
|
|
14657
14921
|
})));
|
|
14658
14922
|
}
|
|
14659
14923
|
destroy() {
|
|
@@ -14672,6 +14936,111 @@ var CallEventsManager = class extends Destroyable {
|
|
|
14672
14936
|
}
|
|
14673
14937
|
};
|
|
14674
14938
|
|
|
14939
|
+
//#endregion
|
|
14940
|
+
//#region src/controllers/ConstraintFallbackHelper.ts
|
|
14941
|
+
/**
|
|
14942
|
+
* ConstraintFallbackHelper - Provides getUserMedia with automatic constraint
|
|
14943
|
+
* fallback on OverconstrainedError.
|
|
14944
|
+
*
|
|
14945
|
+
* When a specific device ID is requested, the helper tries progressively
|
|
14946
|
+
* looser constraints:
|
|
14947
|
+
* 1. `{ deviceId: { exact: deviceId } }` -- exact match
|
|
14948
|
+
* 2. `{ deviceId: deviceId }` -- preferred (browser may pick another)
|
|
14949
|
+
* 3. `{}` -- no constraint, browser default
|
|
14950
|
+
*
|
|
14951
|
+
* This prevents stale device IDs from blocking call setup.
|
|
14952
|
+
*
|
|
14953
|
+
* @see Section 5.8 and Section 11 of the Implementation Guide
|
|
14954
|
+
*/
|
|
14955
|
+
const logger$22 = getLogger();
|
|
14956
|
+
/**
|
|
14957
|
+
* Attempts getUserMedia with progressively looser constraints.
|
|
14958
|
+
*
|
|
14959
|
+
* The function tries three levels of constraint specificity for the given
|
|
14960
|
+
* device kind. Each level is only attempted if the previous one fails with
|
|
14961
|
+
* an OverconstrainedError. Non-OverconstrainedError failures (e.g.,
|
|
14962
|
+
* NotAllowedError) are thrown immediately without fallback.
|
|
14963
|
+
*
|
|
14964
|
+
* @param mediaDevices - Anything exposing `getUserMedia` (a full
|
|
14965
|
+
* `WebRTCMediaDevices`, or a shim wrapping one)
|
|
14966
|
+
* @param constraints - The full MediaStreamConstraints to use as a base
|
|
14967
|
+
* @param kind - Which track kind to apply fallback to ('audio' | 'video')
|
|
14968
|
+
* @param deviceId - The device ID to try (if undefined, calls getUserMedia as-is)
|
|
14969
|
+
* @returns The stream and the fallback level that succeeded
|
|
14970
|
+
* @throws When all fallback levels fail, or when a non-OverconstrainedError occurs
|
|
14971
|
+
*/
|
|
14972
|
+
async function getUserMediaWithFallback(mediaDevices, constraints, kind, deviceId) {
|
|
14973
|
+
if (!deviceId) return {
|
|
14974
|
+
stream: await mediaDevices.getUserMedia(constraints),
|
|
14975
|
+
fallbackLevel: "default"
|
|
14976
|
+
};
|
|
14977
|
+
const baseConstraints = typeof constraints[kind] === "object" ? constraints[kind] : {};
|
|
14978
|
+
try {
|
|
14979
|
+
const exactConstraints = {
|
|
14980
|
+
...constraints,
|
|
14981
|
+
[kind]: {
|
|
14982
|
+
...baseConstraints,
|
|
14983
|
+
deviceId: { exact: deviceId }
|
|
14984
|
+
}
|
|
14985
|
+
};
|
|
14986
|
+
return {
|
|
14987
|
+
stream: await mediaDevices.getUserMedia(exactConstraints),
|
|
14988
|
+
fallbackLevel: "exact"
|
|
14989
|
+
};
|
|
14990
|
+
} catch (error) {
|
|
14991
|
+
if (!isOverconstrainedError(error)) throw error;
|
|
14992
|
+
logger$22.debug(`[ConstraintFallbackHelper] Exact constraint failed for ${kind}, trying preferred`, { deviceId });
|
|
14993
|
+
}
|
|
14994
|
+
try {
|
|
14995
|
+
const preferredConstraints = {
|
|
14996
|
+
...constraints,
|
|
14997
|
+
[kind]: {
|
|
14998
|
+
...baseConstraints,
|
|
14999
|
+
deviceId
|
|
15000
|
+
}
|
|
15001
|
+
};
|
|
15002
|
+
return {
|
|
15003
|
+
stream: await mediaDevices.getUserMedia(preferredConstraints),
|
|
15004
|
+
fallbackLevel: "preferred"
|
|
15005
|
+
};
|
|
15006
|
+
} catch (error) {
|
|
15007
|
+
if (!isOverconstrainedError(error)) throw error;
|
|
15008
|
+
logger$22.debug(`[ConstraintFallbackHelper] Preferred constraint failed for ${kind}, trying default`, { deviceId });
|
|
15009
|
+
}
|
|
15010
|
+
try {
|
|
15011
|
+
const defaultConstraints = {
|
|
15012
|
+
...constraints,
|
|
15013
|
+
[kind]: { ...baseConstraints }
|
|
15014
|
+
};
|
|
15015
|
+
if (typeof defaultConstraints[kind] === "object") {
|
|
15016
|
+
const { deviceId: _removed, ...rest } = defaultConstraints[kind];
|
|
15017
|
+
defaultConstraints[kind] = rest;
|
|
15018
|
+
}
|
|
15019
|
+
const stream = await mediaDevices.getUserMedia(defaultConstraints);
|
|
15020
|
+
logger$22.warn(`[ConstraintFallbackHelper] Fell back to browser default for ${kind}`, { requestedDeviceId: deviceId });
|
|
15021
|
+
return {
|
|
15022
|
+
stream,
|
|
15023
|
+
fallbackLevel: "default"
|
|
15024
|
+
};
|
|
15025
|
+
} catch (error) {
|
|
15026
|
+
logger$22.error(`[ConstraintFallbackHelper] All fallback levels exhausted for ${kind}`, {
|
|
15027
|
+
deviceId,
|
|
15028
|
+
error
|
|
15029
|
+
});
|
|
15030
|
+
throw error;
|
|
15031
|
+
}
|
|
15032
|
+
}
|
|
15033
|
+
/**
|
|
15034
|
+
* Checks whether an error is an OverconstrainedError.
|
|
15035
|
+
*
|
|
15036
|
+
* Browsers may throw either a native OverconstrainedError or a DOMException
|
|
15037
|
+
* with a specific name.
|
|
15038
|
+
*/
|
|
15039
|
+
function isOverconstrainedError(error) {
|
|
15040
|
+
if (error instanceof Error) return error.name === "OverconstrainedError" || error.name === "ConstraintNotSatisfiedError";
|
|
15041
|
+
return false;
|
|
15042
|
+
}
|
|
15043
|
+
|
|
14675
15044
|
//#endregion
|
|
14676
15045
|
//#region src/helpers/SDPHelper.ts
|
|
14677
15046
|
/**
|
|
@@ -15230,6 +15599,7 @@ var LocalStreamController = class extends Destroyable {
|
|
|
15230
15599
|
this._localAudioTracks$ = this.createBehaviorSubject([]);
|
|
15231
15600
|
this._localVideoTracks$ = this.createBehaviorSubject([]);
|
|
15232
15601
|
this._mediaTrackEnded$ = this.createSubject();
|
|
15602
|
+
this._trackOrigins = /* @__PURE__ */ new WeakMap();
|
|
15233
15603
|
}
|
|
15234
15604
|
get localStream$() {
|
|
15235
15605
|
return this._localStream$.asObservable().pipe((0, import_cjs$17.takeUntil)(this.destroyed$));
|
|
@@ -15252,6 +15622,22 @@ var LocalStreamController = class extends Destroyable {
|
|
|
15252
15622
|
get localVideoTracks() {
|
|
15253
15623
|
return this._localVideoTracks$.value;
|
|
15254
15624
|
}
|
|
15625
|
+
tagTracks(tracks, origin) {
|
|
15626
|
+
for (const track of tracks) this._trackOrigins.set(track, origin);
|
|
15627
|
+
}
|
|
15628
|
+
setTrackOrigin(track, origin) {
|
|
15629
|
+
this._trackOrigins.set(track, origin);
|
|
15630
|
+
}
|
|
15631
|
+
getTrackOrigin(track) {
|
|
15632
|
+
return this._trackOrigins.get(track);
|
|
15633
|
+
}
|
|
15634
|
+
/**
|
|
15635
|
+
* Fail-safe: an unrecorded track reads as not-a-device-capture, so a missed
|
|
15636
|
+
* tagging site leaves media alone rather than destroying it.
|
|
15637
|
+
*/
|
|
15638
|
+
isDeviceCapture(track) {
|
|
15639
|
+
return this._trackOrigins.get(track) === "device";
|
|
15640
|
+
}
|
|
15255
15641
|
/**
|
|
15256
15642
|
* Build the local media stream based on the provided options.
|
|
15257
15643
|
*/
|
|
@@ -15261,13 +15647,16 @@ var LocalStreamController = class extends Destroyable {
|
|
|
15261
15647
|
if (this.options.inputAudioStream ?? this.options.inputVideoStream) {
|
|
15262
15648
|
const tracks = [...this.options.inputAudioStream?.getTracks() ?? [], ...this.options.inputVideoStream?.getTracks() ?? []];
|
|
15263
15649
|
stream = new MediaStream(tracks);
|
|
15650
|
+
this.tagTracks(tracks, "application");
|
|
15264
15651
|
} else if (this.options.propose === "screenshare") {
|
|
15265
|
-
|
|
15652
|
+
const audio = this.options.screenShareAudio ?? false;
|
|
15653
|
+
logger$19.debug("[LocalStreamController] Requesting display media for screen sharing with audio:", audio);
|
|
15266
15654
|
stream = await this.options.getDisplayMedia({
|
|
15267
15655
|
video: true,
|
|
15268
|
-
audio
|
|
15656
|
+
audio
|
|
15269
15657
|
});
|
|
15270
15658
|
logger$19.debug("[LocalStreamController] Screen share media obtained:", stream);
|
|
15659
|
+
this.tagTracks(stream.getTracks(), "display");
|
|
15271
15660
|
} else {
|
|
15272
15661
|
const constraints = {
|
|
15273
15662
|
audio: this.options.inputAudioDeviceConstraints,
|
|
@@ -15276,6 +15665,7 @@ var LocalStreamController = class extends Destroyable {
|
|
|
15276
15665
|
logger$19.debug("[LocalStreamController] Requesting user media with constraints:", constraints);
|
|
15277
15666
|
stream = await this.options.getUserMedia(constraints);
|
|
15278
15667
|
logger$19.debug("[LocalStreamController] User media obtained:", stream);
|
|
15668
|
+
this.tagTracks(stream.getTracks(), "device");
|
|
15279
15669
|
}
|
|
15280
15670
|
this._localStream$.next(stream);
|
|
15281
15671
|
this._localAudioTracks$.next(stream.getAudioTracks());
|
|
@@ -15285,10 +15675,13 @@ var LocalStreamController = class extends Destroyable {
|
|
|
15285
15675
|
/**
|
|
15286
15676
|
* Add a local media track to the local stream.
|
|
15287
15677
|
* @param track - The MediaStreamTrack to add
|
|
15678
|
+
* @param origin - Defaults to `'device'`; every internal caller passes a
|
|
15679
|
+
* fresh `getUserMedia` capture.
|
|
15288
15680
|
* @returns The MediaStream (either existing or newly created)
|
|
15289
15681
|
*/
|
|
15290
|
-
addTrack(track) {
|
|
15682
|
+
addTrack(track, origin = "device") {
|
|
15291
15683
|
const localStream = this._localStream$.value ?? new MediaStream();
|
|
15684
|
+
this._trackOrigins.set(track, origin);
|
|
15292
15685
|
track.addEventListener("ended", this.mediaTrackEndedHandler);
|
|
15293
15686
|
localStream.addTrack(track);
|
|
15294
15687
|
this._localStream$.next(localStream);
|
|
@@ -15558,15 +15951,27 @@ var TransceiverController = class extends Destroyable {
|
|
|
15558
15951
|
for (let i = 0; i < Number(msStreamsNumber); i++) this.peerConnection.addTransceiver("video", { direction: "recvonly" });
|
|
15559
15952
|
}
|
|
15560
15953
|
}
|
|
15954
|
+
/**
|
|
15955
|
+
* @returns whether every live sender of the kind took the constraints. A
|
|
15956
|
+
* skipped non-device sender, an exhausted fallback, and having no live sender
|
|
15957
|
+
* at all all report `false` — `mediaParamsUpdated.applied` is built from this,
|
|
15958
|
+
* and an application told `true` cannot tell a working push from a no-op.
|
|
15959
|
+
*/
|
|
15561
15960
|
async updateSendersConstraints(kind, constraints) {
|
|
15562
15961
|
if (!constraints) {
|
|
15563
15962
|
this.stopTrackSender(kind);
|
|
15564
|
-
return
|
|
15963
|
+
return false;
|
|
15565
15964
|
}
|
|
15566
15965
|
const senders = this.peerConnection.getSenders().filter((sender) => sender.track?.kind === kind && sender.track.readyState === "live");
|
|
15966
|
+
let applied = senders.length > 0;
|
|
15567
15967
|
for (const sender of senders) {
|
|
15568
15968
|
const { track } = sender;
|
|
15569
15969
|
if (track) {
|
|
15970
|
+
if (!this.options.localStreamController.isDeviceCapture(track)) {
|
|
15971
|
+
logger$18.debug(`[TransceiverController] Skipping ${kind} constraints for a non-device track (origin: ${this.options.localStreamController.getTrackOrigin(track) ?? "unrecorded"}), track ${track.id}`);
|
|
15972
|
+
applied = false;
|
|
15973
|
+
continue;
|
|
15974
|
+
}
|
|
15570
15975
|
const constraintsToApply = {
|
|
15571
15976
|
...track.getConstraints(),
|
|
15572
15977
|
...constraints
|
|
@@ -15582,33 +15987,39 @@ var TransceiverController = class extends Destroyable {
|
|
|
15582
15987
|
} catch (fallbackError) {
|
|
15583
15988
|
logger$18.warn(`[TransceiverController] Track replacement fallback also failed for ${kind} track:`, fallbackError);
|
|
15584
15989
|
this.options.onError?.(new MediaTrackError("updateSendersConstraints", kind, fallbackError));
|
|
15990
|
+
applied = false;
|
|
15585
15991
|
}
|
|
15586
15992
|
}
|
|
15587
15993
|
}
|
|
15588
15994
|
}
|
|
15995
|
+
return applied;
|
|
15589
15996
|
}
|
|
15590
15997
|
/**
|
|
15591
|
-
* Fallback when applyConstraints fails
|
|
15592
|
-
* one via getUserMedia with the merged constraints (preserving the current
|
|
15593
|
-
* deviceId), replace the sender track, and update the localStream.
|
|
15998
|
+
* Fallback when applyConstraints fails, which on iOS Safari it silently does.
|
|
15594
15999
|
*
|
|
15595
|
-
*
|
|
15596
|
-
*
|
|
16000
|
+
* Order matters: acquiring before stopping means a failed acquisition leaves
|
|
16001
|
+
* the existing media playing. The deviceId goes through the fallback ladder
|
|
16002
|
+
* rather than pinned `{ exact }`, so a stale id degrades instead of failing.
|
|
15597
16003
|
*/
|
|
15598
16004
|
async replaceTrackFallback(sender, oldTrack, kind, mergedConstraints) {
|
|
15599
16005
|
const { deviceId } = oldTrack.getSettings();
|
|
15600
|
-
const
|
|
15601
|
-
|
|
15602
|
-
|
|
15603
|
-
|
|
15604
|
-
|
|
16006
|
+
const { stream, fallbackLevel } = await getUserMediaWithFallback({ getUserMedia: this.options.getUserMedia }, { [kind]: mergedConstraints }, kind, deviceId);
|
|
16007
|
+
const newTrack = stream.getTracks().find((t) => t.kind === kind);
|
|
16008
|
+
if (!newTrack) {
|
|
16009
|
+
stream.getTracks().forEach((t) => t.stop());
|
|
16010
|
+
throw new MediaTrackError("replaceTrackFallback", kind, /* @__PURE__ */ new Error("getUserMedia returned no track of the requested kind"));
|
|
16011
|
+
}
|
|
16012
|
+
try {
|
|
16013
|
+
await sender.replaceTrack(newTrack);
|
|
16014
|
+
} catch (error) {
|
|
16015
|
+
stream.getTracks().forEach((t) => t.stop());
|
|
16016
|
+
throw error;
|
|
16017
|
+
}
|
|
16018
|
+
const oldTrackId = oldTrack.id;
|
|
16019
|
+
this.options.localStreamController.removeTrack(oldTrackId);
|
|
15605
16020
|
oldTrack.stop();
|
|
15606
|
-
this.options.localStreamController.removeTrack(trackId);
|
|
15607
|
-
const newTrack = (await this.options.getUserMedia({ [kind]: constraintsWithDevice })).getTracks().find((t) => t.kind === kind);
|
|
15608
|
-
if (!newTrack) throw new MediaTrackError("replaceTrackFallback", kind, /* @__PURE__ */ new Error("getUserMedia returned no track of the requested kind"));
|
|
15609
|
-
await sender.replaceTrack(newTrack);
|
|
15610
16021
|
this.options.localStreamController.addTrack(newTrack);
|
|
15611
|
-
logger$18.debug(`[TransceiverController] Track replacement fallback succeeded for ${kind}. New track: ${newTrack.id}`);
|
|
16022
|
+
logger$18.debug(`[TransceiverController] Track replacement fallback succeeded for ${kind} (deviceId fallback level: ${fallbackLevel}). New track: ${newTrack.id}`);
|
|
15612
16023
|
}
|
|
15613
16024
|
getMediaDirections() {
|
|
15614
16025
|
if (this.peerConnection.connectionState === "connected") return this.peerConnection.getTransceivers().reduce((acc, transceiver) => {
|
|
@@ -15678,48 +16089,44 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
15678
16089
|
this.negotiationNeeded$.next();
|
|
15679
16090
|
};
|
|
15680
16091
|
this.updateSelectedInputDevice = async (kind, deviceInfo) => {
|
|
16092
|
+
const { localStream } = this;
|
|
16093
|
+
if (!localStream) {
|
|
16094
|
+
logger$17.warn("[RTCPeerConnectionController] No local stream available to update input device.");
|
|
16095
|
+
return;
|
|
16096
|
+
}
|
|
16097
|
+
const currentTrack = localStream.getTracks().find((track) => track.kind === kind);
|
|
16098
|
+
if (!currentTrack) {
|
|
16099
|
+
logger$17.debug(`[RTCPeerConnectionController] No ${kind} track to switch.`);
|
|
16100
|
+
return;
|
|
16101
|
+
}
|
|
16102
|
+
if (!deviceInfo) {
|
|
16103
|
+
logger$17.debug(`[RTCPeerConnectionController] ${kind} input device selected: none`);
|
|
16104
|
+
this.stopTrackSender(kind);
|
|
16105
|
+
return;
|
|
16106
|
+
}
|
|
16107
|
+
const constraints = {
|
|
16108
|
+
...currentTrack.getConstraints(),
|
|
16109
|
+
...this.deviceController.deviceInfoToConstraints(deviceInfo)
|
|
16110
|
+
};
|
|
15681
16111
|
try {
|
|
15682
|
-
const
|
|
15683
|
-
|
|
15684
|
-
|
|
15685
|
-
return;
|
|
15686
|
-
}
|
|
15687
|
-
logger$17.debug(`[RTCPeerConnectionController] Updating selected ${kind} input device:`, localStream.getTracks());
|
|
15688
|
-
const track = localStream.getTracks().find((track$1) => track$1.kind === kind);
|
|
15689
|
-
if (track) {
|
|
15690
|
-
this.transceiverController?.stopTrackSender(kind);
|
|
15691
|
-
this.localStreamController.removeTrack(track.id);
|
|
15692
|
-
logger$17.debug(`[RTCPeerConnectionController] Stopped existing ${kind} track: ${track.id}`, localStream.getTracks());
|
|
15693
|
-
if (!deviceInfo) {
|
|
15694
|
-
logger$17.debug(`[RTCPeerConnectionController] ${kind} input device selected: none`);
|
|
15695
|
-
return;
|
|
15696
|
-
}
|
|
15697
|
-
const streamTrack = (await this.getUserMedia({ [kind]: {
|
|
15698
|
-
...track.getConstraints(),
|
|
15699
|
-
...this.deviceController.deviceInfoToConstraints(deviceInfo)
|
|
15700
|
-
} })).getTracks().find((t) => t.kind === kind);
|
|
15701
|
-
if (streamTrack) {
|
|
15702
|
-
logger$17.debug(`[RTCPeerConnectionController] Adding new ${kind} track: ${streamTrack.id}`);
|
|
15703
|
-
this.localStreamController.addTrack(streamTrack);
|
|
15704
|
-
await this.transceiverController?.replaceSenderTrack(kind, streamTrack);
|
|
15705
|
-
logger$17.debug(`[RTCPeerConnectionController] Added new ${kind} track: ${streamTrack.id}`, this.localStream?.getTracks());
|
|
15706
|
-
}
|
|
15707
|
-
}
|
|
15708
|
-
logger$17.debug(`[RTCPeerConnectionController] ${kind} input device selected:`, deviceInfo?.label);
|
|
16112
|
+
const newTrack = await this.acquireInputTrack(kind, constraints, deviceInfo, currentTrack);
|
|
16113
|
+
await this.attachInputTrack(kind, newTrack, currentTrack);
|
|
16114
|
+
logger$17.debug(`[RTCPeerConnectionController] ${kind} input device selected:`, deviceInfo.label, newTrack.id);
|
|
15709
16115
|
} catch (error) {
|
|
15710
16116
|
logger$17.error(`[RTCPeerConnectionController] Failed to select ${kind} input device:`, error);
|
|
15711
|
-
this._errors$.next(
|
|
15712
|
-
throw error;
|
|
16117
|
+
this._errors$.next(new MediaTrackError("updateSelectedInputDevice", kind, error));
|
|
15713
16118
|
}
|
|
15714
16119
|
};
|
|
15715
16120
|
this._isNegotiating$ = this.createBehaviorSubject(false);
|
|
15716
16121
|
this._memberId = null;
|
|
16122
|
+
this._nodeId = null;
|
|
15717
16123
|
this._iceConnectionState$ = this.createReplaySubject(1);
|
|
15718
16124
|
this._connectionState$ = this.createReplaySubject(1);
|
|
15719
16125
|
this._signalingState$ = this.createReplaySubject(1);
|
|
15720
16126
|
this._iceGatheringState$ = this.createReplaySubject(1);
|
|
15721
16127
|
this._errors$ = this.createReplaySubject(1);
|
|
15722
16128
|
this._iceCandidates$ = this.createReplaySubject(1);
|
|
16129
|
+
this._localMediaSettled$ = this.createReplaySubject(1);
|
|
15723
16130
|
this._initialized$ = this.createReplaySubject(1);
|
|
15724
16131
|
this._remoteDescription$ = this.createReplaySubject(1);
|
|
15725
16132
|
this._remoteStream$ = this.createBehaviorSubject(null);
|
|
@@ -15752,6 +16159,7 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
15752
16159
|
inputVideoStream: this.options.inputVideoStream,
|
|
15753
16160
|
inputAudioDeviceConstraints: this.inputAudioDeviceConstraints,
|
|
15754
16161
|
inputVideoDeviceConstraints: this.inputVideoDeviceConstraints,
|
|
16162
|
+
screenShareAudio: this.options.screenShareAudio,
|
|
15755
16163
|
getUserMedia: async (constraints) => this.getUserMedia(constraints),
|
|
15756
16164
|
getDisplayMedia: async (options$1) => this.getDisplayMedia(options$1)
|
|
15757
16165
|
});
|
|
@@ -15778,6 +16186,13 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
15778
16186
|
get memberId() {
|
|
15779
16187
|
return this._memberId;
|
|
15780
16188
|
}
|
|
16189
|
+
/** The node this leg's invite landed on — auxiliary legs are placed independently. */
|
|
16190
|
+
setNodeId(nodeId) {
|
|
16191
|
+
this._nodeId = nodeId;
|
|
16192
|
+
}
|
|
16193
|
+
get nodeId() {
|
|
16194
|
+
return this._nodeId;
|
|
16195
|
+
}
|
|
15781
16196
|
stopTrackSender(kind, options = { updateTransceiverDirection: false }) {
|
|
15782
16197
|
const audioCovered = kind === "audio" || kind === "both";
|
|
15783
16198
|
if (audioCovered && this._localAudioPipeline) this.stopRawAudioInputForPipeline();
|
|
@@ -15787,10 +16202,7 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
15787
16202
|
}
|
|
15788
16203
|
stopRawAudioInputForPipeline() {
|
|
15789
16204
|
const rawTracks = this.localStreamController.localAudioTracks;
|
|
15790
|
-
for (const track of rawTracks) if (track.readyState === "live")
|
|
15791
|
-
track.stop();
|
|
15792
|
-
this.localStreamController.removeTrack(track.id);
|
|
15793
|
-
}
|
|
16205
|
+
for (const track of rawTracks) if (track.readyState === "live") this.localStreamController.removeTrack(track.id);
|
|
15794
16206
|
this._localAudioPipeline?.setInputTrack(null);
|
|
15795
16207
|
}
|
|
15796
16208
|
get isNegotiating$() {
|
|
@@ -15823,6 +16235,10 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
15823
16235
|
get remoteDescription$() {
|
|
15824
16236
|
return this.cachedObservable("remoteDescription$", () => this._remoteDescription$.asObservable().pipe((0, import_cjs$16.takeUntil)(this.destroyed$)));
|
|
15825
16237
|
}
|
|
16238
|
+
/** Emits once local media is settled — acquired, or knowingly receive-only. */
|
|
16239
|
+
get localMediaSettled$() {
|
|
16240
|
+
return this.cachedObservable("localMediaSettled$", () => this._localMediaSettled$.asObservable().pipe((0, import_cjs$16.takeUntil)(this.destroyed$)));
|
|
16241
|
+
}
|
|
15826
16242
|
get localStream$() {
|
|
15827
16243
|
return this.cachedObservable("localStream$", () => this.localStreamController.localStream$.pipe((0, import_cjs$16.takeUntil)(this.destroyed$)));
|
|
15828
16244
|
}
|
|
@@ -15850,6 +16266,9 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
15850
16266
|
get propose() {
|
|
15851
16267
|
return this.options.propose ?? "main";
|
|
15852
16268
|
}
|
|
16269
|
+
get connectionState() {
|
|
16270
|
+
return this.peerConnection?.connectionState;
|
|
16271
|
+
}
|
|
15853
16272
|
get isAdditionalDevice() {
|
|
15854
16273
|
return this.propose === "additional-device";
|
|
15855
16274
|
}
|
|
@@ -15930,7 +16349,7 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
15930
16349
|
case "main":
|
|
15931
16350
|
default: return {
|
|
15932
16351
|
...options,
|
|
15933
|
-
offerToReceiveAudio: true,
|
|
16352
|
+
offerToReceiveAudio: this.options.receiveAudio ?? true,
|
|
15934
16353
|
offerToReceiveVideo: this.options.receiveVideo ?? Boolean(this.inputVideoDeviceConstraints)
|
|
15935
16354
|
};
|
|
15936
16355
|
}
|
|
@@ -15978,7 +16397,7 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
15978
16397
|
this._isNegotiating$.next(true);
|
|
15979
16398
|
await this._setRemoteDescription(this.sdpInit);
|
|
15980
16399
|
} else {
|
|
15981
|
-
await this.setupTrackHandling();
|
|
16400
|
+
if (!await this.setupTrackHandling()) return;
|
|
15982
16401
|
this._initialized$.next(true);
|
|
15983
16402
|
}
|
|
15984
16403
|
} catch (error) {
|
|
@@ -16084,13 +16503,14 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
16084
16503
|
*/
|
|
16085
16504
|
async acceptInbound(mediaOverrides) {
|
|
16086
16505
|
if (mediaOverrides) {
|
|
16087
|
-
const { audio, video, receiveAudio, receiveVideo } = mediaOverrides;
|
|
16506
|
+
const { audio, video, receiveAudio, receiveVideo, fallbackToReceiveOnly } = mediaOverrides;
|
|
16088
16507
|
this.options = {
|
|
16089
16508
|
...this.options,
|
|
16090
16509
|
...audio !== void 0 ? { audio } : {},
|
|
16091
16510
|
...video !== void 0 ? { video } : {},
|
|
16092
16511
|
...receiveAudio !== void 0 ? { receiveAudio } : {},
|
|
16093
|
-
...receiveVideo !== void 0 ? { receiveVideo } : {}
|
|
16512
|
+
...receiveVideo !== void 0 ? { receiveVideo } : {},
|
|
16513
|
+
...fallbackToReceiveOnly !== void 0 ? { fallbackToReceiveOnly } : {}
|
|
16094
16514
|
};
|
|
16095
16515
|
this.transceiverController?.updateOptions({
|
|
16096
16516
|
receiveAudio: this.receiveAudio,
|
|
@@ -16101,7 +16521,10 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
16101
16521
|
inputVideoDeviceConstraints: this.inputVideoDeviceConstraints
|
|
16102
16522
|
});
|
|
16103
16523
|
}
|
|
16104
|
-
await this.setupLocalTracks()
|
|
16524
|
+
if (!await this.setupLocalTracks()) {
|
|
16525
|
+
logger$17.debug("[RTCPeerConnectionController] Inbound answer abandoned; the connection went away.");
|
|
16526
|
+
return;
|
|
16527
|
+
}
|
|
16105
16528
|
const { answerOptions } = this;
|
|
16106
16529
|
logger$17.debug("[RTCPeerConnectionController] Creating inbound answer with options:", answerOptions);
|
|
16107
16530
|
await this.createAnswer(answerOptions);
|
|
@@ -16239,23 +16662,52 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
16239
16662
|
}
|
|
16240
16663
|
/**
|
|
16241
16664
|
* Setup track handling for remote tracks.
|
|
16665
|
+
*
|
|
16666
|
+
* @returns `false` when the connection went away while local media was being
|
|
16667
|
+
* acquired — see {@link setupLocalTracks}.
|
|
16242
16668
|
*/
|
|
16243
16669
|
async setupTrackHandling() {
|
|
16244
16670
|
if (!this.peerConnection) throw new DependencyError("RTCPeerConnection is not initialized");
|
|
16245
|
-
await this.setupLocalTracks();
|
|
16671
|
+
if (!await this.setupLocalTracks()) return false;
|
|
16246
16672
|
await this.setupRemoteTracks();
|
|
16673
|
+
return true;
|
|
16247
16674
|
}
|
|
16675
|
+
/**
|
|
16676
|
+
* @returns `false` when the connection was torn down while getUserMedia was
|
|
16677
|
+
* in flight. The acquisition is not cancellable, so the caller must stop
|
|
16678
|
+
* rather than go on to touch a peer connection that is closed or gone.
|
|
16679
|
+
*/
|
|
16248
16680
|
async setupLocalTracks() {
|
|
16249
16681
|
logger$17.debug("[RTCPeerConnectionController] Setting up local tracks/transceivers.");
|
|
16250
|
-
|
|
16682
|
+
if (this.hasNoLocalMediaToSend()) {
|
|
16683
|
+
if (!this.receiveAudio && !this.receiveVideo) throw new InvalidParams("Call requests no media: enable audio/video or receiveAudio/receiveVideo");
|
|
16684
|
+
logger$17.debug("[RTCPeerConnectionController] No local media requested; negotiating receive-only.");
|
|
16685
|
+
this.setupReceiveOnlyTransceivers();
|
|
16686
|
+
this._localMediaSettled$.next();
|
|
16687
|
+
return true;
|
|
16688
|
+
}
|
|
16689
|
+
let localStream;
|
|
16690
|
+
try {
|
|
16691
|
+
localStream = this.localStream ?? await this.localStreamController.buildLocalStream();
|
|
16692
|
+
} catch (error) {
|
|
16693
|
+
this.handleLocalMediaFailure(error);
|
|
16694
|
+
this._localMediaSettled$.next();
|
|
16695
|
+
return true;
|
|
16696
|
+
}
|
|
16697
|
+
if (!this.peerConnection || this.peerConnection.signalingState === "closed") {
|
|
16698
|
+
logger$17.debug("[RTCPeerConnectionController] Local media arrived after teardown; releasing it.");
|
|
16699
|
+
localStream.getTracks().forEach((track) => track.stop());
|
|
16700
|
+
return false;
|
|
16701
|
+
}
|
|
16702
|
+
this._localMediaSettled$.next();
|
|
16251
16703
|
if (this.transceiverController?.useAddStream ?? false) {
|
|
16252
16704
|
logger$17.warn("[RTCPeerConnectionController] Using deprecated addStream API to add local stream.");
|
|
16253
|
-
this.peerConnection
|
|
16705
|
+
this.peerConnection.addStream(localStream);
|
|
16254
16706
|
if (!this.isNegotiating) {
|
|
16255
16707
|
logger$17.debug("[RTCPeerConnectionController] Forcing negotiationneeded after local tracks setup.");
|
|
16256
16708
|
this.negotiationNeeded$.next();
|
|
16257
16709
|
}
|
|
16258
|
-
return;
|
|
16710
|
+
return true;
|
|
16259
16711
|
}
|
|
16260
16712
|
for (const kind of ["audio", "video"]) {
|
|
16261
16713
|
const tracks = (kind === "audio" ? localStream.getAudioTracks() : localStream.getVideoTracks()).map((track, index) => ({
|
|
@@ -16269,10 +16721,53 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
16269
16721
|
await this.transceiverController?.setupTransceiverSender(track, localStream, transceivers[index]);
|
|
16270
16722
|
} else {
|
|
16271
16723
|
logger$17.debug(`[RTCPeerConnectionController] Using addTrack for local ${kind} track:`, track.id);
|
|
16272
|
-
this.peerConnection
|
|
16724
|
+
this.peerConnection.addTrack(track, localStream);
|
|
16273
16725
|
}
|
|
16274
16726
|
}
|
|
16275
16727
|
}
|
|
16728
|
+
return true;
|
|
16729
|
+
}
|
|
16730
|
+
/** True for a main connection with no local media to send. */
|
|
16731
|
+
hasNoLocalMediaToSend() {
|
|
16732
|
+
const hasInputStreams = Boolean(this.options.inputAudioStream ?? this.options.inputVideoStream);
|
|
16733
|
+
return this.propose === "main" && !this.localStream && !hasInputStreams && !this.inputAudioDeviceConstraints && !this.inputVideoDeviceConstraints;
|
|
16734
|
+
}
|
|
16735
|
+
/** The media kinds this connection wants to send: 'audiovideo' | 'video' | 'audio'. */
|
|
16736
|
+
get requestedMediaKinds() {
|
|
16737
|
+
const wantsAudio = Boolean(this.inputAudioDeviceConstraints);
|
|
16738
|
+
const wantsVideo = Boolean(this.inputVideoDeviceConstraints);
|
|
16739
|
+
if (wantsAudio && wantsVideo) return "audiovideo";
|
|
16740
|
+
return wantsVideo ? "video" : "audio";
|
|
16741
|
+
}
|
|
16742
|
+
/**
|
|
16743
|
+
* Handle a local media acquisition failure with a typed, semantically
|
|
16744
|
+
* accurate MediaAccessError created at the acquisition site:
|
|
16745
|
+
* - Auxiliary connections (screenshare / additional-device) throw a
|
|
16746
|
+
* non-fatal error — VertoManager surfaces it and the call is unaffected.
|
|
16747
|
+
* - The main connection degrades to receive-only when allowed (default),
|
|
16748
|
+
* otherwise fails with a fatal error.
|
|
16749
|
+
*/
|
|
16750
|
+
handleLocalMediaFailure(error) {
|
|
16751
|
+
if (this.propose === "screenshare") throw new MediaAccessError("startScreenShare", "screen", error, false);
|
|
16752
|
+
if (this.propose === "additional-device") throw new MediaAccessError("addInputDevice", this.requestedMediaKinds, error, false);
|
|
16753
|
+
const canReceive = this.receiveAudio || this.receiveVideo;
|
|
16754
|
+
if (!((this.options.fallbackToReceiveOnly ?? true) && canReceive)) throw new MediaAccessError("acquireLocalMedia", this.requestedMediaKinds, error, true);
|
|
16755
|
+
logger$17.warn("[RTCPeerConnectionController] Local media unavailable; continuing receive-only:", error);
|
|
16756
|
+
this._errors$.next(new MediaAccessError("acquireLocalMedia", this.requestedMediaKinds, error, false));
|
|
16757
|
+
this.setupReceiveOnlyTransceivers();
|
|
16758
|
+
}
|
|
16759
|
+
/**
|
|
16760
|
+
* Negotiate receive-only m-lines when there are no local tracks to send.
|
|
16761
|
+
* Only offer-type connections add transceivers — answer-type connections
|
|
16762
|
+
* reuse the transceivers created from the remote offer.
|
|
16763
|
+
*/
|
|
16764
|
+
setupReceiveOnlyTransceivers() {
|
|
16765
|
+
if (this.type !== "offer") return;
|
|
16766
|
+
if (this.transceiverController?.useAddTransceivers ?? false) {
|
|
16767
|
+
this.peerConnection?.addTransceiver("audio", { direction: this.receiveAudio ? "recvonly" : "inactive" });
|
|
16768
|
+
this.peerConnection?.addTransceiver("video", { direction: this.receiveVideo ? "recvonly" : "inactive" });
|
|
16769
|
+
}
|
|
16770
|
+
if (!this.isNegotiating) this.negotiationNeeded$.next();
|
|
16276
16771
|
}
|
|
16277
16772
|
async getUserMedia(constraints) {
|
|
16278
16773
|
return (this.options.webRTCApiProvider?.mediaDevices ?? navigator.mediaDevices).getUserMedia(constraints);
|
|
@@ -16310,7 +16805,7 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
16310
16805
|
stream = await this.getUserMedia({ audio: constraints });
|
|
16311
16806
|
} catch (error) {
|
|
16312
16807
|
logger$17.error("[RTCPeerConnectionController] Failed to re-acquire mic for pipeline restore:", error);
|
|
16313
|
-
this._errors$.next(
|
|
16808
|
+
this._errors$.next(new MediaTrackError("restoreAudioPipelineInput", "audio", error));
|
|
16314
16809
|
return;
|
|
16315
16810
|
}
|
|
16316
16811
|
const newTrack = stream.getAudioTracks().at(0);
|
|
@@ -16319,7 +16814,62 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
16319
16814
|
this._localAudioPipeline.setInputTrack(newTrack);
|
|
16320
16815
|
}
|
|
16321
16816
|
/**
|
|
16322
|
-
*
|
|
16817
|
+
* Capture the newly selected device, leaving the current capture running.
|
|
16818
|
+
*
|
|
16819
|
+
* A rejection must leave the current track sending, so nothing is released
|
|
16820
|
+
* until the replacement is in hand. The one exception is hardware that admits
|
|
16821
|
+
* a single opener — a phone's front and back cameras, typically — which
|
|
16822
|
+
* rejects the second capture until the first is closed.
|
|
16823
|
+
*/
|
|
16824
|
+
async acquireInputTrack(kind, constraints, deviceInfo, currentTrack) {
|
|
16825
|
+
try {
|
|
16826
|
+
return await this.captureTrack(kind, constraints, deviceInfo.deviceId);
|
|
16827
|
+
} catch (error) {
|
|
16828
|
+
if (!isMediaDeviceInUse(error)) throw error;
|
|
16829
|
+
logger$17.warn(`[RTCPeerConnectionController] ${kind} device is held exclusively; releasing the current capture to retry:`, error);
|
|
16830
|
+
const previousDeviceId = currentTrack.getSettings().deviceId;
|
|
16831
|
+
this.stopTrackSender(kind);
|
|
16832
|
+
try {
|
|
16833
|
+
return await this.captureTrack(kind, constraints, deviceInfo.deviceId);
|
|
16834
|
+
} catch (retryError) {
|
|
16835
|
+
await this.restorePreviousInputTrack(kind, constraints, previousDeviceId, currentTrack);
|
|
16836
|
+
throw retryError;
|
|
16837
|
+
}
|
|
16838
|
+
}
|
|
16839
|
+
}
|
|
16840
|
+
async captureTrack(kind, constraints, deviceId) {
|
|
16841
|
+
const { stream, fallbackLevel } = await getUserMediaWithFallback({ getUserMedia: async (c) => this.getUserMedia(c) }, { [kind]: constraints }, kind, deviceId);
|
|
16842
|
+
const track = stream.getTracks().find((t) => t.kind === kind);
|
|
16843
|
+
if (!track) {
|
|
16844
|
+
stream.getTracks().forEach((t) => t.stop());
|
|
16845
|
+
throw new MediaTrackError("updateSelectedInputDevice", kind, /* @__PURE__ */ new Error("getUserMedia returned no track of the requested kind"));
|
|
16846
|
+
}
|
|
16847
|
+
if (fallbackLevel !== "exact") logger$17.warn(`[RTCPeerConnectionController] ${kind} device acquired at fallback level '${fallbackLevel}'; the capture may not be the requested device.`);
|
|
16848
|
+
return track;
|
|
16849
|
+
}
|
|
16850
|
+
/** Best-effort return to the device that was released for an exclusive retry. */
|
|
16851
|
+
async restorePreviousInputTrack(kind, constraints, previousDeviceId, releasedTrack) {
|
|
16852
|
+
try {
|
|
16853
|
+
const restored = await this.captureTrack(kind, constraints, previousDeviceId);
|
|
16854
|
+
await this.attachInputTrack(kind, restored, releasedTrack);
|
|
16855
|
+
} catch (error) {
|
|
16856
|
+
logger$17.error(`[RTCPeerConnectionController] Failed to restore the previous ${kind} device:`, error);
|
|
16857
|
+
}
|
|
16858
|
+
}
|
|
16859
|
+
async attachInputTrack(kind, newTrack, oldTrack) {
|
|
16860
|
+
const pipelineOwnsAudio = kind === "audio" && this._localAudioPipeline;
|
|
16861
|
+
if (!pipelineOwnsAudio) try {
|
|
16862
|
+
await this.transceiverController?.replaceSenderTrack(kind, newTrack);
|
|
16863
|
+
} catch (error) {
|
|
16864
|
+
newTrack.stop();
|
|
16865
|
+
throw error;
|
|
16866
|
+
}
|
|
16867
|
+
this.localStreamController.removeTrack(oldTrack.id);
|
|
16868
|
+
this.localStreamController.addTrack(newTrack);
|
|
16869
|
+
if (pipelineOwnsAudio) this._localAudioPipeline?.setInputTrack(newTrack);
|
|
16870
|
+
}
|
|
16871
|
+
/**
|
|
16872
|
+
* Return the lazily-created {@link LocalAudioPipeline}, constructing it on
|
|
16323
16873
|
* first access. On creation the current audio sender's track is routed
|
|
16324
16874
|
* through the pipeline (input → gain → analyser → destination) and the
|
|
16325
16875
|
* sender is switched to emit the processed track. Returns `null` when no
|
|
@@ -16351,6 +16901,7 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
16351
16901
|
const sender = (this.transceiverController?.audioTransceivers.at(0))?.sender ?? this.peerConnection.getSenders().find((s) => s.track?.kind === "audio");
|
|
16352
16902
|
if (!sender || !raw) return;
|
|
16353
16903
|
try {
|
|
16904
|
+
this.localStreamController.setTrackOrigin(this._localAudioPipeline.outputTrack, "processed");
|
|
16354
16905
|
await sender.replaceTrack(this._localAudioPipeline.outputTrack);
|
|
16355
16906
|
} catch (error) {
|
|
16356
16907
|
logger$17.warn("[RTCPeerConnectionController] Failed to route audio sender through pipeline:", error);
|
|
@@ -16372,7 +16923,7 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
16372
16923
|
logger$17.debug(`[RTCPeerConnectionController] ${track.kind} track added:`, track.id);
|
|
16373
16924
|
} catch (error) {
|
|
16374
16925
|
logger$17.error(`[RTCPeerConnectionController] Failed to add ${track.kind} track:`, error);
|
|
16375
|
-
this._errors$.next(
|
|
16926
|
+
this._errors$.next(new MediaTrackError("addLocalTrack", track.kind, error));
|
|
16376
16927
|
throw error;
|
|
16377
16928
|
}
|
|
16378
16929
|
}
|
|
@@ -16397,7 +16948,7 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
16397
16948
|
logger$17.debug(`[RTCPeerConnectionController] ${sender.track?.kind} track removed:`, trackId);
|
|
16398
16949
|
} catch (error) {
|
|
16399
16950
|
logger$17.error(`[RTCPeerConnectionController] Failed to remove ${sender.track?.kind} track:`, error);
|
|
16400
|
-
this._errors$.next(
|
|
16951
|
+
this._errors$.next(new MediaTrackError("removeLocalTrack", sender.track?.kind ?? "unknown", error));
|
|
16401
16952
|
throw error;
|
|
16402
16953
|
}
|
|
16403
16954
|
}
|
|
@@ -16411,37 +16962,68 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
16411
16962
|
for (const existingTrack of existingTracks) this.removeLocalTrack(existingTrack.id);
|
|
16412
16963
|
this.addLocalTrack(track);
|
|
16413
16964
|
}
|
|
16965
|
+
/**
|
|
16966
|
+
* @returns whether the constraints reached the media the leg is sending.
|
|
16967
|
+
*
|
|
16968
|
+
* With the pipeline engaged the audio sender carries the processed
|
|
16969
|
+
* destination track, so the sender scan would find nothing it may touch and
|
|
16970
|
+
* every audio constraint API would silently no-op. The constraints belong to
|
|
16971
|
+
* the pipeline's device source, which is the capture that sender ultimately
|
|
16972
|
+
* carries.
|
|
16973
|
+
*/
|
|
16414
16974
|
async updateSendersConstraints(kind, constraints) {
|
|
16415
|
-
|
|
16975
|
+
if (kind === "audio" && this._localAudioPipeline) {
|
|
16976
|
+
if (!constraints) {
|
|
16977
|
+
this.stopTrackSender("audio");
|
|
16978
|
+
return false;
|
|
16979
|
+
}
|
|
16980
|
+
return this.applyPipelineSourceConstraints(constraints);
|
|
16981
|
+
}
|
|
16982
|
+
return await this.transceiverController?.updateSendersConstraints(kind, constraints) ?? false;
|
|
16416
16983
|
}
|
|
16417
16984
|
/**
|
|
16418
|
-
*
|
|
16419
|
-
*
|
|
16420
|
-
*
|
|
16421
|
-
*
|
|
16985
|
+
* Mirror of the sender path for a piped audio leg: same merge, same fallback
|
|
16986
|
+
* ladder, same device-capture invariant — but the swap target is the pipeline
|
|
16987
|
+
* input, so the sender keeps emitting the pipeline's output track and its
|
|
16988
|
+
* identity survives the change.
|
|
16422
16989
|
*/
|
|
16423
|
-
async
|
|
16424
|
-
const
|
|
16425
|
-
|
|
16426
|
-
|
|
16427
|
-
|
|
16990
|
+
async applyPipelineSourceConstraints(constraints) {
|
|
16991
|
+
const pipeline = this._localAudioPipeline;
|
|
16992
|
+
const source = this.localStreamController.localAudioTracks.at(0);
|
|
16993
|
+
if (!pipeline || !source) {
|
|
16994
|
+
logger$17.debug("[RTCPeerConnectionController] No pipeline input to constrain.");
|
|
16995
|
+
return false;
|
|
16428
16996
|
}
|
|
16429
|
-
|
|
16430
|
-
|
|
16431
|
-
|
|
16432
|
-
|
|
16433
|
-
|
|
16434
|
-
|
|
16435
|
-
|
|
16436
|
-
|
|
16437
|
-
|
|
16438
|
-
|
|
16439
|
-
|
|
16440
|
-
|
|
16441
|
-
|
|
16442
|
-
|
|
16997
|
+
if (!this.localStreamController.isDeviceCapture(source)) {
|
|
16998
|
+
logger$17.debug(`[RTCPeerConnectionController] Skipping audio constraints for a non-device pipeline input (origin: ${this.localStreamController.getTrackOrigin(source) ?? "unrecorded"}).`);
|
|
16999
|
+
return false;
|
|
17000
|
+
}
|
|
17001
|
+
const merged = {
|
|
17002
|
+
...source.getConstraints(),
|
|
17003
|
+
...constraints
|
|
17004
|
+
};
|
|
17005
|
+
try {
|
|
17006
|
+
await source.applyConstraints(merged);
|
|
17007
|
+
logger$17.debug("[RTCPeerConnectionController] Pipeline input constraints updated:", merged);
|
|
17008
|
+
return true;
|
|
17009
|
+
} catch (error) {
|
|
17010
|
+
logger$17.warn("[RTCPeerConnectionController] applyConstraints failed on the pipeline input, re-acquiring:", error);
|
|
17011
|
+
}
|
|
17012
|
+
try {
|
|
17013
|
+
const { stream } = await getUserMediaWithFallback({ getUserMedia: async (c) => this.getUserMedia(c) }, { audio: merged }, "audio", source.getSettings().deviceId);
|
|
17014
|
+
const newTrack = stream.getAudioTracks().at(0);
|
|
17015
|
+
if (!newTrack) {
|
|
17016
|
+
stream.getTracks().forEach((track) => track.stop());
|
|
17017
|
+
throw new Error("getUserMedia returned no audio track");
|
|
17018
|
+
}
|
|
17019
|
+
this.localStreamController.removeTrack(source.id);
|
|
16443
17020
|
this.localStreamController.addTrack(newTrack);
|
|
16444
|
-
|
|
17021
|
+
pipeline.setInputTrack(newTrack);
|
|
17022
|
+
return true;
|
|
17023
|
+
} catch (error) {
|
|
17024
|
+
logger$17.warn("[RTCPeerConnectionController] Failed to re-acquire the pipeline input for constraints:", error);
|
|
17025
|
+
this._errors$.next(new MediaTrackError("updateSendersConstraints", "audio", error));
|
|
17026
|
+
return false;
|
|
16445
17027
|
}
|
|
16446
17028
|
}
|
|
16447
17029
|
/**
|
|
@@ -16561,6 +17143,48 @@ const logger$16 = getLogger();
|
|
|
16561
17143
|
function resolveInviteNodeId(args) {
|
|
16562
17144
|
return args.isInvite && !args.reattach && !args.explicitNodeId ? "" : args.currentNodeId ?? "";
|
|
16563
17145
|
}
|
|
17146
|
+
/**
|
|
17147
|
+
* Surface the real outcome of a `webrtc.verto` reply.
|
|
17148
|
+
*
|
|
17149
|
+
* A webrtc.verto response nests several envelopes, each keyed by a verto-style
|
|
17150
|
+
* string `code` ("200" ok, "400"/etc. fail) rather than a JSON-RPC `error`. An outer
|
|
17151
|
+
* layer reports only whether the frame was delivered; an inner layer carries the op's
|
|
17152
|
+
* own outcome:
|
|
17153
|
+
*
|
|
17154
|
+
* response.result = { code:"200", result:{…} } ← delivery acknowledgement
|
|
17155
|
+
* .result = { jsonrpc, id, result:{…} } ← the reply payload
|
|
17156
|
+
* .result = { code:"400", message:"Bad request" } ← the actual op outcome
|
|
17157
|
+
*
|
|
17158
|
+
* A failure can appear at any layer (delivery refused, or the op itself rejected
|
|
17159
|
+
* deeper down), so walk every nested `.result` object and return the FIRST non-2xx
|
|
17160
|
+
* `code` with its message. Returns null when every `code` seen is 2xx or absent —
|
|
17161
|
+
* i.e. the op succeeded. This is the only way to detect that e.g. a mute/kick was
|
|
17162
|
+
* rejected, since the outer delivery `code` is "200" (delivered) even then.
|
|
17163
|
+
*
|
|
17164
|
+
* Pure function — exported for unit testing.
|
|
17165
|
+
*/
|
|
17166
|
+
function findNestedVertoFailure(response) {
|
|
17167
|
+
let node = response;
|
|
17168
|
+
while (node !== null && typeof node === "object") {
|
|
17169
|
+
const obj = node;
|
|
17170
|
+
const err = obj.error;
|
|
17171
|
+
if (err !== null && typeof err === "object") {
|
|
17172
|
+
const e = err;
|
|
17173
|
+
const errCode = typeof e.code === "string" || typeof e.code === "number" ? String(e.code) : void 0;
|
|
17174
|
+
if (errCode !== void 0 && !/^2\d\d$/.test(errCode)) return {
|
|
17175
|
+
code: errCode,
|
|
17176
|
+
message: typeof e.message === "string" ? e.message : void 0
|
|
17177
|
+
};
|
|
17178
|
+
}
|
|
17179
|
+
const code = typeof obj.code === "string" || typeof obj.code === "number" ? String(obj.code) : void 0;
|
|
17180
|
+
if (code !== void 0 && !/^2\d\d$/.test(code)) return {
|
|
17181
|
+
code,
|
|
17182
|
+
message: typeof obj.message === "string" ? obj.message : void 0
|
|
17183
|
+
};
|
|
17184
|
+
node = obj.result !== null && typeof obj.result === "object" ? obj.result : null;
|
|
17185
|
+
}
|
|
17186
|
+
return null;
|
|
17187
|
+
}
|
|
16564
17188
|
var VertoManager = class extends Destroyable {
|
|
16565
17189
|
constructor(callSession) {
|
|
16566
17190
|
super();
|
|
@@ -16583,7 +17207,7 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
16583
17207
|
this._signalingStatus$ = this.createReplaySubject(1);
|
|
16584
17208
|
this._screenShareStatus$ = this.createBehaviorSubject("none");
|
|
16585
17209
|
this._rtcPeerConnectionsMap = /* @__PURE__ */ new Map();
|
|
16586
|
-
this.
|
|
17210
|
+
this._legErrors$ = this.createSubject();
|
|
16587
17211
|
this._nodeId$ = this.createBehaviorSubject(options.nodeId ?? null);
|
|
16588
17212
|
this.onError = options.onError;
|
|
16589
17213
|
this.onModifyFailed = options.onModifyFailed;
|
|
@@ -16631,6 +17255,10 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
16631
17255
|
get selfId$() {
|
|
16632
17256
|
return this._selfId$.asObservable();
|
|
16633
17257
|
}
|
|
17258
|
+
/** Separates the media phase of call creation from the signalling phase. */
|
|
17259
|
+
get localMediaSettled$() {
|
|
17260
|
+
return this.mainPeerConnection.localMediaSettled$;
|
|
17261
|
+
}
|
|
16634
17262
|
get localStream() {
|
|
16635
17263
|
return this._rtcPeerConnectionsMap.get(this.webRtcCallSession.id)?.localStream ?? null;
|
|
16636
17264
|
}
|
|
@@ -16687,35 +17315,95 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
16687
17315
|
const { mediaParams, callID } = event;
|
|
16688
17316
|
const rtcPeerConnController = this._rtcPeerConnectionsMap.get(callID);
|
|
16689
17317
|
const { audio, video } = mediaParams;
|
|
16690
|
-
|
|
16691
|
-
|
|
16692
|
-
|
|
16693
|
-
|
|
16694
|
-
|
|
16695
|
-
audio,
|
|
16696
|
-
video,
|
|
16697
|
-
timestamp: Date.now()
|
|
16698
|
-
});
|
|
16699
|
-
} catch (error) {
|
|
16700
|
-
logger$16.warn("[WebRTCManager] Error applying server-pushed media params:", error);
|
|
16701
|
-
this.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
16702
|
-
}
|
|
16703
|
-
})();
|
|
17318
|
+
if (!rtcPeerConnController) {
|
|
17319
|
+
logger$16.warn(`[WebRTCManager] Ignoring server-pushed media params for unknown leg ${callID}`);
|
|
17320
|
+
return;
|
|
17321
|
+
}
|
|
17322
|
+
this.applyServerMediaParams(rtcPeerConnController, audio, video);
|
|
16704
17323
|
});
|
|
16705
17324
|
this.subscribeTo(this.vertoPing$, (vertoPing) => {
|
|
16706
|
-
this.attachManager.
|
|
17325
|
+
this.attachManager.refresh(this.buildAttachableCall());
|
|
16707
17326
|
this.sendVertoPong(vertoPing);
|
|
16708
17327
|
});
|
|
16709
17328
|
}
|
|
16710
17329
|
/**
|
|
17330
|
+
* An auxiliary-leg failure must never destroy the call; main-leg and
|
|
17331
|
+
* call-level errors keep `CallFactory.isFatalError`'s classification.
|
|
17332
|
+
*
|
|
17333
|
+
* Every site holding a peer connection reports through here, so the invariant
|
|
17334
|
+
* is structural rather than per-call-site — which is how cloud-product#20523
|
|
17335
|
+
* happened, with only one of fourteen sites passing `{ fatal: false }`.
|
|
17336
|
+
*
|
|
17337
|
+
* `override` composes rather than replaces: a caller may force non-fatal for a
|
|
17338
|
+
* reason of its own (a `verto.info` frame is best-effort whichever leg carries
|
|
17339
|
+
* it), and an auxiliary leg stays non-fatal regardless.
|
|
17340
|
+
*/
|
|
17341
|
+
reportLegError(error, rtcPeerConnController, override) {
|
|
17342
|
+
const leg = rtcPeerConnController?.propose;
|
|
17343
|
+
const legId = rtcPeerConnController?.id;
|
|
17344
|
+
const auxiliary = Boolean(rtcPeerConnController) && !rtcPeerConnController?.isMainDevice;
|
|
17345
|
+
this.onError?.(error, {
|
|
17346
|
+
...override?.fatal === false || auxiliary ? { fatal: false } : {},
|
|
17347
|
+
...leg ? { leg } : {},
|
|
17348
|
+
...legId ? { legId } : {}
|
|
17349
|
+
});
|
|
17350
|
+
if (legId) this._legErrors$.next({
|
|
17351
|
+
legId,
|
|
17352
|
+
error
|
|
17353
|
+
});
|
|
17354
|
+
}
|
|
17355
|
+
/**
|
|
17356
|
+
* Errors reported for one leg, as a stream that fails with them.
|
|
17357
|
+
*
|
|
17358
|
+
* Signaling failures are reported, never thrown — so nothing that waits on a
|
|
17359
|
+
* leg's progress would otherwise learn of a rejected invite. Merging this in
|
|
17360
|
+
* lets the wait end with the reason the server gave.
|
|
17361
|
+
*/
|
|
17362
|
+
legError$(legId) {
|
|
17363
|
+
return this._legErrors$.pipe((0, import_cjs$15.filter)((report) => report.legId === legId), (0, import_cjs$15.map)((report) => {
|
|
17364
|
+
throw report.error;
|
|
17365
|
+
}));
|
|
17366
|
+
}
|
|
17367
|
+
/**
|
|
17368
|
+
* Audio and video are applied independently so a failure in one cannot
|
|
17369
|
+
* suppress the other, and `mediaParamsUpdated` is emitted whatever happens —
|
|
17370
|
+
* an application should not be starved of the params by a constraint failure.
|
|
17371
|
+
*/
|
|
17372
|
+
async applyServerMediaParams(rtcPeerConnController, audio, video) {
|
|
17373
|
+
const failures = [];
|
|
17374
|
+
let applied = true;
|
|
17375
|
+
if (audio) try {
|
|
17376
|
+
applied = await rtcPeerConnController.updateSendersConstraints("audio", audio) && applied;
|
|
17377
|
+
} catch (error) {
|
|
17378
|
+
applied = false;
|
|
17379
|
+
failures.push(toError(error));
|
|
17380
|
+
}
|
|
17381
|
+
if (video) try {
|
|
17382
|
+
applied = await rtcPeerConnController.updateSendersConstraints("video", video) && applied;
|
|
17383
|
+
} catch (error) {
|
|
17384
|
+
applied = false;
|
|
17385
|
+
failures.push(toError(error));
|
|
17386
|
+
}
|
|
17387
|
+
this.webRtcCallSession.emitMediaParamsUpdated({
|
|
17388
|
+
audio,
|
|
17389
|
+
video,
|
|
17390
|
+
timestamp: Date.now(),
|
|
17391
|
+
applied
|
|
17392
|
+
});
|
|
17393
|
+
for (const failure of failures) {
|
|
17394
|
+
logger$16.warn("[WebRTCManager] Error applying server-pushed media params:", failure);
|
|
17395
|
+
this.reportLegError(failure, rtcPeerConnController, { fatal: false });
|
|
17396
|
+
}
|
|
17397
|
+
}
|
|
17398
|
+
/**
|
|
16711
17399
|
* Set node_id/selfId only when the current value is null.
|
|
16712
17400
|
*
|
|
16713
17401
|
* During reattach, `call.joined` and `verto.answer` events can deliver
|
|
16714
17402
|
* these identifiers before the `verto.invite` RPC response (`CALL CREATED`)
|
|
16715
17403
|
* arrives. These methods let early events populate them eagerly so that
|
|
16716
17404
|
* downstream RPC calls (e.g. `call.layout.list`) don't fail with empty
|
|
16717
|
-
* identifiers. `processInviteResponse()` remains the authoritative source
|
|
16718
|
-
*
|
|
17405
|
+
* identifiers. `processInviteResponse()` remains the authoritative source and
|
|
17406
|
+
* overwrites unconditionally — for selfId, on the main leg only.
|
|
16719
17407
|
*/
|
|
16720
17408
|
setNodeIdIfNull(nodeId) {
|
|
16721
17409
|
if (!this._nodeId$.value && nodeId) {
|
|
@@ -16738,24 +17426,33 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
16738
17426
|
this.onError?.(new VertoPongError(error));
|
|
16739
17427
|
}
|
|
16740
17428
|
}
|
|
17429
|
+
/**
|
|
17430
|
+
* @returns whether the constraints reached the media the call is sending.
|
|
17431
|
+
* `false` is an outcome, not a failure: the leg may have no live sender of
|
|
17432
|
+
* the kind, or carry media the SDK did not capture and may not replace.
|
|
17433
|
+
* A failure behind it still reaches the call's `errors$`, so a caller that
|
|
17434
|
+
* ignores this value learns of it there.
|
|
17435
|
+
*/
|
|
16741
17436
|
async updateMediaConstraints(options = {}) {
|
|
16742
17437
|
const { audio, video } = options;
|
|
17438
|
+
let applied = true;
|
|
16743
17439
|
try {
|
|
16744
|
-
if (audio) await this.mainPeerConnection.updateSendersConstraints("audio", audio);
|
|
16745
|
-
if (video) await this.mainPeerConnection.updateSendersConstraints("video", video);
|
|
17440
|
+
if (audio) applied = await this.mainPeerConnection.updateSendersConstraints("audio", audio) && applied;
|
|
17441
|
+
if (video) applied = await this.mainPeerConnection.updateSendersConstraints("video", video) && applied;
|
|
16746
17442
|
} catch (error) {
|
|
16747
17443
|
logger$16.warn("[WebRTCManager] Error updating media constraints:", error);
|
|
16748
|
-
this.
|
|
17444
|
+
this.reportLegError(toError(error), this.mainPeerConnection);
|
|
16749
17445
|
throw error;
|
|
16750
17446
|
}
|
|
17447
|
+
return applied;
|
|
16751
17448
|
}
|
|
16752
17449
|
get selfId() {
|
|
16753
17450
|
return this._selfId$.value;
|
|
16754
17451
|
}
|
|
16755
17452
|
/** Build an AttachableCall from the current call state. */
|
|
16756
|
-
buildAttachableCall(idOverride) {
|
|
17453
|
+
buildAttachableCall(idOverride, nodeIdOverride) {
|
|
16757
17454
|
return {
|
|
16758
|
-
nodeId: this.nodeId ?? void 0,
|
|
17455
|
+
nodeId: nodeIdOverride ?? this.nodeId ?? void 0,
|
|
16759
17456
|
id: idOverride ?? this.webRtcCallSession.id,
|
|
16760
17457
|
to: this.webRtcCallSession.to,
|
|
16761
17458
|
mediaDirections: this.webRtcCallSession.mediaDirections
|
|
@@ -16885,24 +17582,49 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
16885
17582
|
get vertoPing$() {
|
|
16886
17583
|
return this.cachedObservable("vertoPing$", () => this.webRtcCallSession.webrtcMessages$.pipe(filterAs(isVertoPingInnerParams, "params"), (0, import_cjs$15.takeUntil)(this.destroyed$)));
|
|
16887
17584
|
}
|
|
17585
|
+
/**
|
|
17586
|
+
* Send a member-control op in-dialog via verto.info.
|
|
17587
|
+
*
|
|
17588
|
+
* The control payload rides in `params.command` — a sibling of `dialogParams`,
|
|
17589
|
+
* at the same level as `dtmf` in {@link sendDigits} — and the inner verto.info
|
|
17590
|
+
* is matched to this call's channel by `dialogParams.callID`, the in-dialog
|
|
17591
|
+
* convention for member-scoped frames. Because it is delivered on the dialog
|
|
17592
|
+
* itself, control lands on the call's own channel with no {node_id,call_id,member_id}
|
|
17593
|
+
* "self" tuple to get wrong. The outer webrtc.verto envelope (added by executeVerto)
|
|
17594
|
+
* still carries the own-leg callID + node_id for session routing.
|
|
17595
|
+
*
|
|
17596
|
+
* Keep `command` OUT of `dialogParams`: it is read at the params level, and
|
|
17597
|
+
* filterVertoParams rewrites/filters dialogParams keys but passes params-level
|
|
17598
|
+
* keys through verbatim.
|
|
17599
|
+
*/
|
|
17600
|
+
async sendCallControl(method, params) {
|
|
17601
|
+
const response = await this.executeVerto(VertoInfo({
|
|
17602
|
+
dialogParams: { callID: this.webRtcCallSession.id },
|
|
17603
|
+
command: {
|
|
17604
|
+
method,
|
|
17605
|
+
params
|
|
17606
|
+
}
|
|
17607
|
+
}));
|
|
17608
|
+
const failure = findNestedVertoFailure(response);
|
|
17609
|
+
if (failure) throw new JSONRPCError(Number.parseInt(failure.code, 10) || 0, `Call control "${method}" failed (code ${failure.code})${failure.message ? `: ${failure.message}` : ""}`, void 0);
|
|
17610
|
+
return response;
|
|
17611
|
+
}
|
|
16888
17612
|
async executeVerto(message, optionals = {}) {
|
|
16889
|
-
const
|
|
17613
|
+
const params = {
|
|
16890
17614
|
callID: optionals.callID ?? this.webRtcCallSession.id,
|
|
16891
17615
|
node_id: optionals.node_id ?? this._nodeId$.value ?? "",
|
|
16892
17616
|
message,
|
|
16893
17617
|
subscribe: optionals.subscribe
|
|
16894
|
-
}
|
|
17618
|
+
};
|
|
17619
|
+
const webrtcVertoMessage = WebrtcVerto(params);
|
|
16895
17620
|
const response = await this.webRtcCallSession.execute(webrtcVertoMessage);
|
|
16896
|
-
|
|
16897
|
-
const error = new JSONRPCError(response.error.code, response.error.message, response.error.data);
|
|
16898
|
-
this.onError?.(error);
|
|
16899
|
-
return response;
|
|
16900
|
-
}
|
|
17621
|
+
const nonFatal = message.method === "verto.info" ? { fatal: false } : void 0;
|
|
16901
17622
|
const innerResult = getValueFrom(response, "result.result");
|
|
16902
|
-
|
|
16903
|
-
|
|
16904
|
-
|
|
16905
|
-
|
|
17623
|
+
const failure = response.error ?? innerResult?.error;
|
|
17624
|
+
if (failure) {
|
|
17625
|
+
const error = new JSONRPCError(failure.code, failure.message, failure.data);
|
|
17626
|
+
if (message.method === "verto.invite" || message.method === "verto.answer") throw error;
|
|
17627
|
+
this.reportLegError(error, this._rtcPeerConnectionsMap.get(params.callID), nonFatal);
|
|
16906
17628
|
}
|
|
16907
17629
|
return response;
|
|
16908
17630
|
}
|
|
@@ -16921,8 +17643,9 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
16921
17643
|
default:
|
|
16922
17644
|
}
|
|
16923
17645
|
} catch (error) {
|
|
17646
|
+
if (vertoMethod === "verto.answer") throw error;
|
|
16924
17647
|
logger$16.error(`[WebRTCManager] Error sending Verto ${vertoMethod}:`, error);
|
|
16925
|
-
this.
|
|
17648
|
+
this.reportLegError(toError(error), rtcPeerConnController);
|
|
16926
17649
|
if (vertoMethod === "verto.modify") this.onModifyFailed?.();
|
|
16927
17650
|
}
|
|
16928
17651
|
}
|
|
@@ -16938,7 +17661,7 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
16938
17661
|
} catch (error) {
|
|
16939
17662
|
logger$16.warn("[WebRTCManager] Error processing modify response:", error);
|
|
16940
17663
|
const modifyError = error instanceof Error ? error : new Error(String(error), { cause: error });
|
|
16941
|
-
this.
|
|
17664
|
+
this.reportLegError(modifyError, rtcPeerConnController);
|
|
16942
17665
|
}
|
|
16943
17666
|
}
|
|
16944
17667
|
}
|
|
@@ -16951,37 +17674,35 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
16951
17674
|
status,
|
|
16952
17675
|
signalingError
|
|
16953
17676
|
});
|
|
16954
|
-
this.
|
|
17677
|
+
this.reportLegError(signalingError, null, { fatal: false });
|
|
16955
17678
|
return;
|
|
16956
17679
|
}
|
|
16957
17680
|
if (rtcPeerConnController.isMainDevice) this._signalingStatus$.next(status);
|
|
16958
17681
|
}
|
|
16959
17682
|
processInviteResponse(response, rtcPeerConnController) {
|
|
16960
|
-
if (
|
|
17683
|
+
if (getValueFrom(response, "result.result.result.message") === "CALL CREATED") {
|
|
16961
17684
|
this.emitMainSignalingStatus(rtcPeerConnController.id, "trying");
|
|
16962
|
-
|
|
17685
|
+
const nodeId = getValueFrom(response, "result.node_id") ?? null;
|
|
16963
17686
|
const memberId = getValueFrom(response, "result.result.result.memberID") ?? null;
|
|
16964
|
-
const callId = getValueFrom(response, "result.result.result.callID")
|
|
17687
|
+
const callId = getValueFrom(response, "result.result.result.callID");
|
|
16965
17688
|
logger$16.debug("[WebRTCManager] Verto invite response:", {
|
|
16966
17689
|
callId,
|
|
16967
17690
|
memberId,
|
|
16968
17691
|
response
|
|
16969
17692
|
});
|
|
16970
|
-
this._selfId$.next(memberId);
|
|
16971
17693
|
rtcPeerConnController.setMemberId(memberId);
|
|
16972
|
-
|
|
16973
|
-
|
|
16974
|
-
this.
|
|
16975
|
-
|
|
16976
|
-
|
|
16977
|
-
|
|
16978
|
-
|
|
17694
|
+
rtcPeerConnController.setNodeId(nodeId);
|
|
17695
|
+
if (rtcPeerConnController.isMainDevice) {
|
|
17696
|
+
this._selfId$.next(memberId);
|
|
17697
|
+
this._nodeId$.next(nodeId);
|
|
17698
|
+
this.attachManager.attach(this.buildAttachableCall(callId, nodeId ?? void 0));
|
|
17699
|
+
}
|
|
17700
|
+
if (callId) this.webRtcCallSession.addCallId(callId);
|
|
16979
17701
|
logger$16.info("[WebRTCManager] Verto invite successful");
|
|
16980
17702
|
logger$16.debug(`[WebRTCManager] nodeid: ${this._nodeId$.value}, selfId: ${this._selfId$.value}`);
|
|
16981
17703
|
} else {
|
|
16982
17704
|
logger$16.error("[WebRTCManager] Verto invite failed:", response);
|
|
16983
|
-
|
|
16984
|
-
this.onError?.(inviteError);
|
|
17705
|
+
this.reportLegError(/* @__PURE__ */ new Error("Verto invite failed: unexpected response"), rtcPeerConnController);
|
|
16985
17706
|
}
|
|
16986
17707
|
}
|
|
16987
17708
|
get RTCPeerConnectionConfig() {
|
|
@@ -17006,6 +17727,7 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
17006
17727
|
inputVideoStream: options.inputVideoStream,
|
|
17007
17728
|
receiveAudio: options.receiveAudio,
|
|
17008
17729
|
receiveVideo: options.receiveVideo,
|
|
17730
|
+
fallbackToReceiveOnly: options.fallbackToReceiveOnly,
|
|
17009
17731
|
webRTCApiProvider: this.webRTCApiProvider,
|
|
17010
17732
|
preferredVideoCodecs: options.preferredVideoCodecs,
|
|
17011
17733
|
preferredAudioCodecs: options.preferredAudioCodecs,
|
|
@@ -17019,7 +17741,7 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
17019
17741
|
this._rtcPeerConnectionsMap.set(rtcPeerConnController.id, rtcPeerConnController);
|
|
17020
17742
|
this._rtcPeerConnections$.next(Array.from(this._rtcPeerConnectionsMap.values()));
|
|
17021
17743
|
this.subscribeTo(rtcPeerConnController.errors$, (error) => {
|
|
17022
|
-
this.
|
|
17744
|
+
this.reportLegError(error, rtcPeerConnController);
|
|
17023
17745
|
});
|
|
17024
17746
|
if (options.initOffer) this.handleInboundAnswer(rtcPeerConnController);
|
|
17025
17747
|
}
|
|
@@ -17048,7 +17770,7 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
17048
17770
|
await rtcPeerConnController.acceptInbound(answerOptions);
|
|
17049
17771
|
} catch (error) {
|
|
17050
17772
|
logger$16.error("[WebRTCManager] Error creating inbound answer:", error);
|
|
17051
|
-
this.
|
|
17773
|
+
this.reportLegError(toError(error), rtcPeerConnController);
|
|
17052
17774
|
}
|
|
17053
17775
|
}
|
|
17054
17776
|
}
|
|
@@ -17120,7 +17842,7 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
17120
17842
|
isInvite: isVertoInviteMessage(vertoMessage),
|
|
17121
17843
|
reattach: this.webRtcCallSession.options.reattach === true,
|
|
17122
17844
|
explicitNodeId: this.webRtcCallSession.options.nodeId,
|
|
17123
|
-
currentNodeId: this._nodeId$.value
|
|
17845
|
+
currentNodeId: rtcPeerConnController.nodeId ?? this._nodeId$.value
|
|
17124
17846
|
}),
|
|
17125
17847
|
subscribe
|
|
17126
17848
|
};
|
|
@@ -17152,7 +17874,7 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
17152
17874
|
await this.attachManager.attach(this.buildAttachableCall());
|
|
17153
17875
|
} catch (error) {
|
|
17154
17876
|
logger$16.error("[WebRTCManager] Error sending Verto answer:", error);
|
|
17155
|
-
this.
|
|
17877
|
+
this.reportLegError(toError(error), rtcPeerConnectionController);
|
|
17156
17878
|
await rtcPeerConnectionController.updateAnswerStatus({ status: "failed" });
|
|
17157
17879
|
}
|
|
17158
17880
|
}
|
|
@@ -17224,17 +17946,23 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
17224
17946
|
await this.mainPeerConnection.restoreTrackSender(deviceKind);
|
|
17225
17947
|
} else {
|
|
17226
17948
|
const error = new InvalidParams("No valid device to be added");
|
|
17227
|
-
this.
|
|
17949
|
+
this.reportLegError(error, this.mainPeerConnection);
|
|
17228
17950
|
throw error;
|
|
17229
17951
|
}
|
|
17230
17952
|
}
|
|
17231
|
-
async addScreenMedia(options = {
|
|
17232
|
-
await this.initAdditionalPeerConnection("screenshare",
|
|
17953
|
+
async addScreenMedia(options = {}) {
|
|
17954
|
+
await this.initAdditionalPeerConnection("screenshare", {
|
|
17955
|
+
audio: false,
|
|
17956
|
+
screenShareAudio: options.audio ?? false
|
|
17957
|
+
});
|
|
17233
17958
|
}
|
|
17234
17959
|
async initAdditionalPeerConnection(propose, options) {
|
|
17960
|
+
const isScreenShare = propose === "screenshare";
|
|
17961
|
+
if (isScreenShare && this._screenShareId && this._rtcPeerConnectionsMap.has(this._screenShareId)) throw new ScreenShareAlreadyActiveError(this._screenShareId);
|
|
17962
|
+
let firstPeerConnectionError;
|
|
17235
17963
|
let rtcPeerConnController = null;
|
|
17236
17964
|
try {
|
|
17237
|
-
this._screenShareStatus$.next("starting");
|
|
17965
|
+
if (isScreenShare) this._screenShareStatus$.next("starting");
|
|
17238
17966
|
rtcPeerConnController = new RTCPeerConnectionController({
|
|
17239
17967
|
...options,
|
|
17240
17968
|
...this.RTCPeerConnectionConfig,
|
|
@@ -17242,21 +17970,42 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
17242
17970
|
webRTCApiProvider: this.webRTCApiProvider
|
|
17243
17971
|
}, void 0, this.deviceController);
|
|
17244
17972
|
this.setupLocalDescriptionHandler(rtcPeerConnController);
|
|
17245
|
-
if (
|
|
17973
|
+
if (isScreenShare) this._screenShareId = rtcPeerConnController.id;
|
|
17246
17974
|
this._rtcPeerConnectionsMap.set(rtcPeerConnController.id, rtcPeerConnController);
|
|
17247
17975
|
this._rtcPeerConnections$.next(Array.from(this._rtcPeerConnectionsMap.values()));
|
|
17248
17976
|
this.subscribeTo(rtcPeerConnController.errors$, (error) => {
|
|
17249
|
-
|
|
17977
|
+
firstPeerConnectionError ??= error;
|
|
17978
|
+
this.reportLegError(error, rtcPeerConnController);
|
|
17250
17979
|
});
|
|
17251
|
-
|
|
17252
|
-
|
|
17253
|
-
|
|
17980
|
+
const pc = rtcPeerConnController;
|
|
17981
|
+
await (0, import_cjs$15.firstValueFrom)((0, import_cjs$15.merge)(pc.localMediaSettled$.pipe((0, import_cjs$15.take)(1), (0, import_cjs$15.switchMap)(() => pc.connectionState$.pipe((0, import_cjs$15.filter)((state) => state === "connected"), (0, import_cjs$15.take)(1), (0, import_cjs$15.timeout)(DEFAULT_AUX_LEG_CONNECT_TIMEOUT_MS)))), this.legError$(pc.id)).pipe((0, import_cjs$15.takeUntil)((0, import_cjs$15.merge)(this.destroyed$, pc.destroyed$))));
|
|
17982
|
+
if (isScreenShare) this._screenShareStatus$.next("started");
|
|
17983
|
+
logger$16.info(`[WebRTCManager] Additional peer connection connected (${propose}).`);
|
|
17254
17984
|
return rtcPeerConnController.id;
|
|
17255
17985
|
} catch (error) {
|
|
17256
|
-
|
|
17257
|
-
|
|
17258
|
-
if (
|
|
17259
|
-
this.
|
|
17986
|
+
const cancelled = error instanceof AuxiliaryLegCancelledError;
|
|
17987
|
+
const aborted = error instanceof import_cjs$15.EmptyError && !firstPeerConnectionError;
|
|
17988
|
+
if (!cancelled && !aborted) logger$16.warn("[WebRTCManager] Error initializing additional peer connection:", error);
|
|
17989
|
+
if (rtcPeerConnController && this._rtcPeerConnectionsMap.has(rtcPeerConnController.id)) {
|
|
17990
|
+
rtcPeerConnController.destroy();
|
|
17991
|
+
this._rtcPeerConnectionsMap.delete(rtcPeerConnController.id);
|
|
17992
|
+
this._rtcPeerConnections$.next(Array.from(this._rtcPeerConnectionsMap.values()));
|
|
17993
|
+
}
|
|
17994
|
+
if (isScreenShare) {
|
|
17995
|
+
this._screenShareId = void 0;
|
|
17996
|
+
this._screenShareStatus$.next("none");
|
|
17997
|
+
}
|
|
17998
|
+
if (cancelled) {
|
|
17999
|
+
logger$16.debug("[WebRTCManager] Additional peer connection removed before connecting.");
|
|
18000
|
+
throw error;
|
|
18001
|
+
}
|
|
18002
|
+
if (firstPeerConnectionError) throw firstPeerConnectionError instanceof MediaAccessError && firstPeerConnectionError.originalError instanceof Error ? firstPeerConnectionError.originalError : firstPeerConnectionError;
|
|
18003
|
+
if (error instanceof import_cjs$15.EmptyError) {
|
|
18004
|
+
logger$16.debug("[WebRTCManager] Additional peer connection aborted before connecting.");
|
|
18005
|
+
return;
|
|
18006
|
+
}
|
|
18007
|
+
if (error instanceof import_cjs$15.TimeoutError) throw new AuxiliaryLegTimeoutError(propose, error);
|
|
18008
|
+
throw error instanceof Error ? error : new Error(String(error), { cause: error });
|
|
17260
18009
|
}
|
|
17261
18010
|
}
|
|
17262
18011
|
async removeInputDevices(id) {
|
|
@@ -17272,7 +18021,10 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
17272
18021
|
if (removeTrack) return this.mainPeerConnection.stopTrackSender(removeTrack, { updateTransceiverDirection: true });
|
|
17273
18022
|
}
|
|
17274
18023
|
async removeScreenMedia() {
|
|
17275
|
-
if (!["starting", "started"].includes(this._screenShareStatus$.value))
|
|
18024
|
+
if (!["starting", "started"].includes(this._screenShareStatus$.value)) {
|
|
18025
|
+
logger$16.warn("[WebRTCManager] No active screen share to stop.");
|
|
18026
|
+
return;
|
|
18027
|
+
}
|
|
17276
18028
|
if (!this._screenShareId) {
|
|
17277
18029
|
logger$16.debug("[WebRTCManager] No screen share peer connection found.");
|
|
17278
18030
|
return;
|
|
@@ -17287,6 +18039,10 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
17287
18039
|
try {
|
|
17288
18040
|
if (rtcPeerConnController) await this.executeVertoBye(rtcPeerConnController);
|
|
17289
18041
|
} finally {
|
|
18042
|
+
if (rtcPeerConnController && rtcPeerConnController.connectionState !== "connected") this._legErrors$.next({
|
|
18043
|
+
legId: id,
|
|
18044
|
+
error: new AuxiliaryLegCancelledError(rtcPeerConnController.propose)
|
|
18045
|
+
});
|
|
17290
18046
|
rtcPeerConnController?.destroy();
|
|
17291
18047
|
this._rtcPeerConnectionsMap.delete(id);
|
|
17292
18048
|
this._rtcPeerConnections$.next(Array.from(this._rtcPeerConnectionsMap.values()));
|
|
@@ -17301,7 +18057,10 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
17301
18057
|
await this.executeVerto(VertoBye({
|
|
17302
18058
|
...causeParams,
|
|
17303
18059
|
dialogParams: this.dialogParams(rtcPeerConnController)
|
|
17304
|
-
})
|
|
18060
|
+
}), {
|
|
18061
|
+
callID: rtcPeerConnController.id,
|
|
18062
|
+
node_id: rtcPeerConnController.nodeId ?? void 0
|
|
18063
|
+
});
|
|
17305
18064
|
} catch (error) {
|
|
17306
18065
|
logger$16.warn("[WebRTCManager] Call might already be disconnected, error sending Verto bye:", error);
|
|
17307
18066
|
throw error;
|
|
@@ -18180,17 +18939,38 @@ function mosToQualityLevel(mos) {
|
|
|
18180
18939
|
return "critical";
|
|
18181
18940
|
}
|
|
18182
18941
|
|
|
18942
|
+
//#endregion
|
|
18943
|
+
//#region src/utils/unwrapVertoReply.ts
|
|
18944
|
+
/**
|
|
18945
|
+
* Unwrap a method's own reply from a `webrtc.verto` envelope.
|
|
18946
|
+
*
|
|
18947
|
+
* A control verb sent in-dialog comes back nested two levels deep:
|
|
18948
|
+
*
|
|
18949
|
+
* ```
|
|
18950
|
+
* { result: { node_id, code, result: { jsonrpc, id, result: <method payload> } } }
|
|
18951
|
+
* ```
|
|
18952
|
+
*
|
|
18953
|
+
* whereas the routed transport resolves the method payload directly under
|
|
18954
|
+
* `.result`. Readers want the latter shape, and the difference is silent when it
|
|
18955
|
+
* is wrong — `response.result.layouts` simply evaluates to `undefined` against the
|
|
18956
|
+
* envelope, so the data arrives, nothing throws, and the caller sees an empty
|
|
18957
|
+
* value. That exact failure produced an empty layout dropdown with a successful
|
|
18958
|
+
* request behind it.
|
|
18959
|
+
*
|
|
18960
|
+
* Accepting both shapes here keeps every reader indifferent to which transport
|
|
18961
|
+
* produced the response. Anything that is not a nested envelope (a plain ack, or
|
|
18962
|
+
* an already-unwrapped reply) passes through untouched.
|
|
18963
|
+
*/
|
|
18964
|
+
function unwrapVertoReply(response) {
|
|
18965
|
+
const inner = getValueFrom(response, "result.result");
|
|
18966
|
+
return inner && typeof inner === "object" && "result" in inner ? inner : response;
|
|
18967
|
+
}
|
|
18968
|
+
|
|
18183
18969
|
//#endregion
|
|
18184
18970
|
//#region src/core/entities/Call.ts
|
|
18185
18971
|
var import_cjs$11 = require_cjs();
|
|
18186
18972
|
const logger$12 = getLogger();
|
|
18187
18973
|
/**
|
|
18188
|
-
* Verto method for setting member layout positions. Its gateway DTO requires a
|
|
18189
|
-
* `targets` array whose entries are `{ target, position }` (NOT bare targets),
|
|
18190
|
-
* so {@link WebRTCCall.buildMethodParams} special-cases it. See issue #19400.
|
|
18191
|
-
*/
|
|
18192
|
-
const POSITION_SET_METHOD = "call.member.position.set";
|
|
18193
|
-
/**
|
|
18194
18974
|
* Ratio between the critical and warning RTT spike multipliers.
|
|
18195
18975
|
* Warning threshold = baseline * warningMultiplier (default 3x)
|
|
18196
18976
|
* Critical threshold = baseline * warningMultiplier * RTT_CRITICAL_TO_WARNING_RATIO
|
|
@@ -18285,8 +19065,11 @@ var WebRTCCall = class extends Destroyable {
|
|
|
18285
19065
|
emitError(callError) {
|
|
18286
19066
|
if (this._status$.value === "destroyed" || this._status$.value === "failed") return;
|
|
18287
19067
|
this._errors$.next(callError);
|
|
18288
|
-
if (callError.fatal) {
|
|
19068
|
+
if (callError.fatal && this._status$.value !== "disconnecting") {
|
|
18289
19069
|
this._status$.next("failed");
|
|
19070
|
+
this.vertoManager.bye().catch((error) => {
|
|
19071
|
+
logger$12.debug("[Call] fatal-teardown bye failed (signaling likely already dead):", error);
|
|
19072
|
+
});
|
|
18290
19073
|
this.destroy();
|
|
18291
19074
|
}
|
|
18292
19075
|
}
|
|
@@ -18341,7 +19124,7 @@ var WebRTCCall = class extends Destroyable {
|
|
|
18341
19124
|
/** Toggles the call lock state, preventing or allowing new participants from joining. */
|
|
18342
19125
|
async toggleLock() {
|
|
18343
19126
|
const method = this.locked ? "call.unlock" : "call.lock";
|
|
18344
|
-
await this.executeMethod(this.
|
|
19127
|
+
await this.executeMethod(this.callSelf, method, {});
|
|
18345
19128
|
}
|
|
18346
19129
|
/**
|
|
18347
19130
|
* Toggles the hold state of the call (pauses/resumes local media transmission).
|
|
@@ -18390,14 +19173,25 @@ var WebRTCCall = class extends Destroyable {
|
|
|
18390
19173
|
*
|
|
18391
19174
|
* Constructs call context (node_id, call_id, member_id) and sends the RPC request.
|
|
18392
19175
|
*
|
|
18393
|
-
* @param target - Target
|
|
19176
|
+
* @param target - Target {@link MemberTarget} triple, or the local member's
|
|
19177
|
+
* ID string for self-operations (any other string is rejected — a bare
|
|
19178
|
+
* member id cannot carry the remote member's own call context).
|
|
18394
19179
|
* @param method - Verto method name (e.g. `'call.mute'`, `'call.member.remove'`).
|
|
18395
19180
|
* @param args - Parameters for the RPC method.
|
|
18396
19181
|
* @returns The RPC response.
|
|
19182
|
+
* @throws {CallNotReadyError} If the call has no self member context yet.
|
|
19183
|
+
* @throws {InvalidParams} If a string target is not the local member's ID.
|
|
18397
19184
|
* @throws {JSONRPCError} If the RPC call returns an error.
|
|
18398
19185
|
*/
|
|
18399
19186
|
async executeMethod(target, method, args) {
|
|
18400
|
-
const
|
|
19187
|
+
const self = this.callSelf;
|
|
19188
|
+
if (typeof target === "string" && target !== self.member_id) throw new InvalidParams(`Target member ID ${target} does not match call's self member ID ${self.member_id}`);
|
|
19189
|
+
if (this.clientSession.callControl === "in-dialog") return this.executeMethodInDialog(target, method, args);
|
|
19190
|
+
const params = {
|
|
19191
|
+
...args,
|
|
19192
|
+
self,
|
|
19193
|
+
target: typeof target === "string" ? self : target
|
|
19194
|
+
};
|
|
18401
19195
|
const request = buildRPCRequest({
|
|
18402
19196
|
method,
|
|
18403
19197
|
params
|
|
@@ -18411,29 +19205,98 @@ var WebRTCCall = class extends Destroyable {
|
|
|
18411
19205
|
throw error;
|
|
18412
19206
|
}
|
|
18413
19207
|
}
|
|
18414
|
-
|
|
18415
|
-
|
|
18416
|
-
|
|
18417
|
-
|
|
18418
|
-
|
|
18419
|
-
|
|
18420
|
-
|
|
18421
|
-
|
|
18422
|
-
|
|
18423
|
-
|
|
18424
|
-
|
|
18425
|
-
|
|
18426
|
-
|
|
18427
|
-
|
|
18428
|
-
};
|
|
18429
|
-
|
|
18430
|
-
|
|
18431
|
-
|
|
18432
|
-
|
|
18433
|
-
|
|
19208
|
+
/**
|
|
19209
|
+
* `executeMethod` for a call opened with `callControl: 'in-dialog'`.
|
|
19210
|
+
*
|
|
19211
|
+
* Translates the routed transport's calling convention into the in-dialog one. No
|
|
19212
|
+
* `self` tuple is sent, but a `target` is — the same {call_id, member_id} the routed
|
|
19213
|
+
* transport puts in `target` (minus node_id), for self-ops and cross-member ops alike.
|
|
19214
|
+
*
|
|
19215
|
+
* Target shapes are per-verb and irregular, so they are centralised here rather
|
|
19216
|
+
* than left to callers: most verbs take a singular `target`, `call.member.remove`
|
|
19217
|
+
* takes a plural `targets` array, and `call.member.position.set` takes a flat
|
|
19218
|
+
* `targets` of `{call_id, position}` — the one verb keyed on call_id rather than
|
|
19219
|
+
* member_id, so the member triple `Participant.setPosition` built is unwrapped.
|
|
19220
|
+
*/
|
|
19221
|
+
async executeMethodInDialog(target, method, args) {
|
|
19222
|
+
const control = { ...args };
|
|
19223
|
+
if (method === "call.member.position.set") control.targets = (args.targets ?? []).map((entry) => ({
|
|
19224
|
+
call_id: entry.target?.call_id ?? entry.call_id,
|
|
19225
|
+
position: entry.position
|
|
19226
|
+
}));
|
|
19227
|
+
else {
|
|
19228
|
+
const member = typeof target === "object" ? {
|
|
19229
|
+
call_id: target.call_id,
|
|
19230
|
+
member_id: target.member_id
|
|
19231
|
+
} : {
|
|
18434
19232
|
call_id: this.id,
|
|
18435
19233
|
member_id: target
|
|
18436
|
-
}
|
|
19234
|
+
};
|
|
19235
|
+
if (method === "call.member.remove") control.targets = [member];
|
|
19236
|
+
else control.target = member;
|
|
19237
|
+
}
|
|
19238
|
+
return this.sendCommand(method, control);
|
|
19239
|
+
}
|
|
19240
|
+
/**
|
|
19241
|
+
* Sends a `call.*` control verb **in-dialog** via `verto.info`, as an alternative
|
|
19242
|
+
* to the routed {@link executeMethod} transport.
|
|
19243
|
+
*
|
|
19244
|
+
* Why both exist: `executeMethod` addresses the member with an explicit
|
|
19245
|
+
* `{node_id, call_id, member_id}` tuple, which does not resolve for every conference,
|
|
19246
|
+
* so the op can fail. An in-dialog frame carries the verb on the member's own
|
|
19247
|
+
* signaling channel instead, so control works without the client needing to know how
|
|
19248
|
+
* the conference is hosted.
|
|
19249
|
+
*
|
|
19250
|
+
* The trade-off is reach: the in-dialog transport is only accepted for calls that
|
|
19251
|
+
* join a conference over SWML (e.g. an SWML `join_conference`); use the routed
|
|
19252
|
+
* default otherwise.
|
|
19253
|
+
*
|
|
19254
|
+
* `params` are sent verbatim — nothing is built for you, which includes the target.
|
|
19255
|
+
* **A self-directed op still needs one**, or it is refused; name yourself explicitly:
|
|
19256
|
+
*
|
|
19257
|
+
* ```ts
|
|
19258
|
+
* const { call_id, member_id } = call.self.target;
|
|
19259
|
+
* await call.sendCommand('call.mute', { channels: ['audio'], target: { call_id, member_id } });
|
|
19260
|
+
* ```
|
|
19261
|
+
*
|
|
19262
|
+
* Never include `node_id` — only the two ids. The shapes are per-verb: most take a
|
|
19263
|
+
* singular `target`, `call.member.remove` takes a plural `targets` array, and
|
|
19264
|
+
* `call.member.position.set` takes a flat `targets: [{call_id, position}]` (the one
|
|
19265
|
+
* verb keyed on `call_id` rather than `member_id`). Verbs that act on the call as a
|
|
19266
|
+
* whole, or that the SDK does not wrap at all, take no target.
|
|
19267
|
+
*
|
|
19268
|
+
* For the typed alternative that handles all of this, create the client with
|
|
19269
|
+
* `callControl: 'in-dialog'` and use the ordinary `Call`/`Participant` methods.
|
|
19270
|
+
*
|
|
19271
|
+
* @internal Not part of the supported surface while the in-dialog transport is still
|
|
19272
|
+
* rolling out. `WebRTCCall` is exported from the package entry, so without this tag
|
|
19273
|
+
* TypeDoc publishes the method — and the example above — as public API.
|
|
19274
|
+
*
|
|
19275
|
+
* @param method - A `call.*` method name (e.g. `'call.mute'`).
|
|
19276
|
+
* @param params - Method parameters, sent verbatim.
|
|
19277
|
+
* @returns The method's own reply, unwrapped from the `verto.info` envelope.
|
|
19278
|
+
* @throws {JSONRPCError} If the control op fails.
|
|
19279
|
+
*/
|
|
19280
|
+
async sendCommand(method, params = {}) {
|
|
19281
|
+
return unwrapVertoReply(await this.vertoManager.sendCallControl(method, params));
|
|
19282
|
+
}
|
|
19283
|
+
/**
|
|
19284
|
+
* The local leg's member triple — sent as `self` in every member RPC
|
|
19285
|
+
* envelope, and as the `target` of call-scoped self-operations (e.g. lock,
|
|
19286
|
+
* layout).
|
|
19287
|
+
*
|
|
19288
|
+
* @throws {CallNotReadyError} Before `call.joined` delivers the self member
|
|
19289
|
+
* context (`selfId`/`nodeId`) — an RPC without it cannot be routed, so fail
|
|
19290
|
+
* fast instead of sending a doomed request.
|
|
19291
|
+
*/
|
|
19292
|
+
get callSelf() {
|
|
19293
|
+
const node_id = this.nodeId;
|
|
19294
|
+
const member_id = this.vertoManager.selfId;
|
|
19295
|
+
if (!node_id || !member_id) throw new CallNotReadyError(this.id);
|
|
19296
|
+
return {
|
|
19297
|
+
node_id,
|
|
19298
|
+
call_id: this.id,
|
|
19299
|
+
member_id
|
|
18437
19300
|
};
|
|
18438
19301
|
}
|
|
18439
19302
|
/** Observable of the current call status (e.g. `'ringing'`, `'connected'`). */
|
|
@@ -18589,13 +19452,13 @@ var WebRTCCall = class extends Destroyable {
|
|
|
18589
19452
|
get bandwidthConstrained$() {
|
|
18590
19453
|
return this.deferEmission(this._bandwidthConstrained$.asObservable());
|
|
18591
19454
|
}
|
|
18592
|
-
/** Observable that emits when server
|
|
19455
|
+
/** Observable that emits when the server pushes media params. */
|
|
18593
19456
|
get mediaParamsUpdated$() {
|
|
18594
19457
|
return this.deferEmission(this._mediaParamsUpdated$.asObservable());
|
|
18595
19458
|
}
|
|
18596
19459
|
/**
|
|
18597
19460
|
* @internal Emit a media params update event.
|
|
18598
|
-
* Called by the VertoManager when server
|
|
19461
|
+
* Called by the VertoManager when the server pushes media params.
|
|
18599
19462
|
*/
|
|
18600
19463
|
emitMediaParamsUpdated(event) {
|
|
18601
19464
|
this._mediaParamsUpdated$.next(event);
|
|
@@ -18803,6 +19666,10 @@ var WebRTCCall = class extends Destroyable {
|
|
|
18803
19666
|
get selfId$() {
|
|
18804
19667
|
return this.vertoManager.selfId$;
|
|
18805
19668
|
}
|
|
19669
|
+
/** @internal Lets call creation bound the media and signalling phases apart. */
|
|
19670
|
+
get localMediaSettled$() {
|
|
19671
|
+
return this.vertoManager.localMediaSettled$;
|
|
19672
|
+
}
|
|
18806
19673
|
/** Local participant's member ID, or `null` if not joined. */
|
|
18807
19674
|
get selfId() {
|
|
18808
19675
|
return this.vertoManager.selfId;
|
|
@@ -18996,12 +19863,17 @@ var WebRTCCall = class extends Destroyable {
|
|
|
18996
19863
|
*
|
|
18997
19864
|
* **These operations are NOT atomic.** The layout is applied first, then each
|
|
18998
19865
|
* member position sequentially, so members may briefly flash into their
|
|
18999
|
-
* default slots before being moved to the requested positions.
|
|
19866
|
+
* default slots before being moved to the requested positions. Targeted
|
|
19867
|
+
* members are validated upfront, though: when any of them has no
|
|
19868
|
+
* {@link Participant.target | member call context} yet, the whole call
|
|
19869
|
+
* rejects before any request is sent and the layout is left unchanged.
|
|
19000
19870
|
*
|
|
19001
19871
|
* @param layout - Layout name (must be one of {@link layouts}).
|
|
19002
19872
|
* @param positions - Optional map of member IDs to {@link VideoPosition} values.
|
|
19003
19873
|
* When omitted or empty, only the layout is changed.
|
|
19004
19874
|
* @throws {InvalidParams} If the layout is not in the available {@link layouts}.
|
|
19875
|
+
* @throws {ParticipantNotReadyError} If a targeted member's call context has
|
|
19876
|
+
* not been received yet — thrown before any request is sent.
|
|
19005
19877
|
*
|
|
19006
19878
|
* @example
|
|
19007
19879
|
* ```ts
|
|
@@ -19012,18 +19884,19 @@ var WebRTCCall = class extends Destroyable {
|
|
|
19012
19884
|
*/
|
|
19013
19885
|
async setLayout(layout, positions) {
|
|
19014
19886
|
if (!this.layouts.includes(layout)) throw new InvalidParams(`Layout ${layout} is not available in the current call layouts: ${this.layouts.join(", ")}`);
|
|
19015
|
-
const
|
|
19016
|
-
|
|
19017
|
-
const positionEntries = Object.entries(positions ?? {});
|
|
19018
|
-
if (positionEntries.length === 0) return;
|
|
19019
|
-
for (const [memberId, position] of positionEntries) {
|
|
19887
|
+
const targets = [];
|
|
19888
|
+
for (const [memberId, position] of Object.entries(positions ?? {})) {
|
|
19020
19889
|
const participant = this.participants.find((p) => p.id === memberId);
|
|
19021
19890
|
if (!participant) {
|
|
19022
19891
|
logger$12.warn(`[Call] setLayout: member ${memberId} not found in participants; skipping position ${position}`);
|
|
19023
19892
|
continue;
|
|
19024
19893
|
}
|
|
19025
|
-
|
|
19894
|
+
participant.target;
|
|
19895
|
+
targets.push([participant, position]);
|
|
19026
19896
|
}
|
|
19897
|
+
const selfId = await (0, import_cjs$11.firstValueFrom)(this.selfId$.pipe((0, import_cjs$11.filter)((id) => id !== null)));
|
|
19898
|
+
await this.executeMethod(selfId, "call.layout.set", { layout });
|
|
19899
|
+
for (const [participant, position] of targets) await participant.setPosition(position);
|
|
19027
19900
|
}
|
|
19028
19901
|
/**
|
|
19029
19902
|
* Transfers the call to another destination.
|
|
@@ -19125,17 +19998,28 @@ var WebRTCCall = class extends Destroyable {
|
|
|
19125
19998
|
* (notably iOS Safari) fall back to re-acquiring the track with the new
|
|
19126
19999
|
* constraint set and plumbing the replacement through the local audio
|
|
19127
20000
|
* pipeline if one is active.
|
|
20001
|
+
*
|
|
20002
|
+
* @returns whether the constraint reached the microphone. `false` is an
|
|
20003
|
+
* outcome rather than an error — a leg sending media the SDK did not capture
|
|
20004
|
+
* is left alone — so a UI that reflects the toggle must read it. Any failure
|
|
20005
|
+
* behind a `false` is also reported on {@link errors$}.
|
|
19128
20006
|
*/
|
|
19129
20007
|
async setEchoCancellation(enabled) {
|
|
19130
|
-
|
|
20008
|
+
return this.vertoManager.updateMediaConstraints({ audio: { echoCancellation: enabled } });
|
|
19131
20009
|
}
|
|
19132
|
-
/**
|
|
20010
|
+
/**
|
|
20011
|
+
* Toggle browser noise suppression on the local mic at runtime.
|
|
20012
|
+
* @returns whether the constraint reached the microphone.
|
|
20013
|
+
*/
|
|
19133
20014
|
async setNoiseSuppression(enabled) {
|
|
19134
|
-
|
|
20015
|
+
return this.vertoManager.updateMediaConstraints({ audio: { noiseSuppression: enabled } });
|
|
19135
20016
|
}
|
|
19136
|
-
/**
|
|
20017
|
+
/**
|
|
20018
|
+
* Toggle browser automatic gain control on the local mic at runtime.
|
|
20019
|
+
* @returns whether the constraint reached the microphone.
|
|
20020
|
+
*/
|
|
19137
20021
|
async setAutoGainControl(enabled) {
|
|
19138
|
-
|
|
20022
|
+
return this.vertoManager.updateMediaConstraints({ audio: { autoGainControl: enabled } });
|
|
19139
20023
|
}
|
|
19140
20024
|
/**
|
|
19141
20025
|
* Observable of the aggregate remote audio level, 0..1 RMS. The server
|
|
@@ -19192,11 +20076,15 @@ var WebRTCCall = class extends Destroyable {
|
|
|
19192
20076
|
/**
|
|
19193
20077
|
* Infers the semantic error category from a raw Error thrown by VertoManager
|
|
19194
20078
|
* or an RTCPeerConnection layer.
|
|
20079
|
+
*
|
|
20080
|
+
* Pure function — exported for unit testing.
|
|
20081
|
+
* @internal
|
|
19195
20082
|
*/
|
|
19196
20083
|
function inferCallErrorKind(error) {
|
|
19197
20084
|
if (error instanceof RPCTimeoutError) return "timeout";
|
|
19198
20085
|
if (error instanceof JSONRPCError) return "signaling";
|
|
19199
20086
|
if (error instanceof MediaTrackError) return "media";
|
|
20087
|
+
if (error instanceof MediaAccessError) return "media";
|
|
19200
20088
|
if (error instanceof WebSocketConnectionError || error instanceof TransportConnectionError) return "network";
|
|
19201
20089
|
return "internal";
|
|
19202
20090
|
}
|
|
@@ -19209,10 +20097,18 @@ const RECOVERABLE_RPC_CODES = new Set([
|
|
|
19209
20097
|
RPC_ERROR_AUTHENTICATION_FAILED,
|
|
19210
20098
|
RPC_ERROR_INVALID_PARAMS
|
|
19211
20099
|
]);
|
|
19212
|
-
/**
|
|
20100
|
+
/**
|
|
20101
|
+
* A *fallback*: callers knowing which leg failed pass an explicit `fatal` and
|
|
20102
|
+
* never reach here, so the default-fatal branch only sees call- and main-leg
|
|
20103
|
+
* errors. Auxiliary legs go through `WebRTCVertoManager.reportLegError`.
|
|
20104
|
+
*
|
|
20105
|
+
* Pure function — exported for unit testing.
|
|
20106
|
+
* @internal
|
|
20107
|
+
*/
|
|
19213
20108
|
function isFatalError(error) {
|
|
19214
20109
|
if (error instanceof VertoPongError) return false;
|
|
19215
20110
|
if (error instanceof MediaTrackError) return false;
|
|
20111
|
+
if (error instanceof MediaAccessError) return error.fatal;
|
|
19216
20112
|
if (error instanceof RPCTimeoutError) return false;
|
|
19217
20113
|
if (error instanceof JSONRPCError && RECOVERABLE_RPC_CODES.has(error.code)) return false;
|
|
19218
20114
|
return true;
|
|
@@ -19238,12 +20134,14 @@ var CallFactory = class {
|
|
|
19238
20134
|
return {
|
|
19239
20135
|
vertoManager: new WebRTCVertoManager(callInstance, this.attachManager, this.deviceController, this.webRTCApiProvider, {
|
|
19240
20136
|
nodeId: options.nodeId,
|
|
19241
|
-
onError: (error) => {
|
|
20137
|
+
onError: (error, options$1) => {
|
|
19242
20138
|
const callError = {
|
|
19243
20139
|
kind: inferCallErrorKind(error),
|
|
19244
|
-
fatal: isFatalError(error),
|
|
20140
|
+
fatal: options$1?.fatal ?? isFatalError(error),
|
|
19245
20141
|
error,
|
|
19246
|
-
callId: callInstance.id
|
|
20142
|
+
callId: callInstance.id,
|
|
20143
|
+
...options$1?.leg ? { leg: options$1.leg } : {},
|
|
20144
|
+
...options$1?.legId ? { legId: options$1.legId } : {}
|
|
19247
20145
|
};
|
|
19248
20146
|
callInstance.emitError(callError);
|
|
19249
20147
|
},
|
|
@@ -19704,6 +20602,41 @@ var PendingRPC = class PendingRPC {
|
|
|
19704
20602
|
//#region src/managers/ClientSessionManager.ts
|
|
19705
20603
|
var import_cjs$7 = require_cjs();
|
|
19706
20604
|
const logger$9 = getLogger();
|
|
20605
|
+
/**
|
|
20606
|
+
* Decide whether an error emitted on `call.errors$` during dial should
|
|
20607
|
+
* abort the dial. A non-fatal MediaAccessError means the call degraded to
|
|
20608
|
+
* receive-only and still connects — everything else rejects `dial()` with
|
|
20609
|
+
* the real cause.
|
|
20610
|
+
*
|
|
20611
|
+
* Pure function — exported for unit testing.
|
|
20612
|
+
*/
|
|
20613
|
+
function shouldAbortDial(callError) {
|
|
20614
|
+
return callError.fatal || !(callError.error instanceof MediaAccessError);
|
|
20615
|
+
}
|
|
20616
|
+
/**
|
|
20617
|
+
* Wait for a dialed call to be ready, or for the failure that stops it.
|
|
20618
|
+
*
|
|
20619
|
+
* Local media acquisition is deliberately unbounded: a permission prompt or a
|
|
20620
|
+
* device picker is human time, and `getUserMedia` cannot be cancelled anyway.
|
|
20621
|
+
* The clock starts only once acquisition settles, so a slow human never spends
|
|
20622
|
+
* the server's budget.
|
|
20623
|
+
*
|
|
20624
|
+
* `merge` rather than `race`, because the two legs settle asymmetrically. A
|
|
20625
|
+
* fatal acquisition failure reports the error and then destroys the call in the
|
|
20626
|
+
* same synchronous step; `errors$` defers delivery by a microtask while
|
|
20627
|
+
* `localMediaSettled$` completes immediately. Under `race` that bare completion
|
|
20628
|
+
* ended the wait first and `dial()` rejected with an RxJS `EmptyError`, burying
|
|
20629
|
+
* the `NotAllowedError` applications are told to inspect. Under `merge` the
|
|
20630
|
+
* completed leg is simply spent, and the queued error — enqueued before the
|
|
20631
|
+
* completion, so delivered before it — arrives to reject the wait. A dial
|
|
20632
|
+
* abandoned with no error at all still ends both legs, and the resulting
|
|
20633
|
+
* `EmptyError` remains the benign-cancel signal.
|
|
20634
|
+
*
|
|
20635
|
+
* Exported for unit testing.
|
|
20636
|
+
*/
|
|
20637
|
+
async function awaitDialReady(session, signalingTimeoutMs) {
|
|
20638
|
+
return (0, import_cjs$7.firstValueFrom)((0, import_cjs$7.merge)(session.localMediaSettled$.pipe((0, import_cjs$7.take)(1), (0, import_cjs$7.switchMap)(() => session.selfId$.pipe((0, import_cjs$7.filter)((id) => Boolean(id)), (0, import_cjs$7.take)(1), (0, import_cjs$7.timeout)(signalingTimeoutMs)))), session.errors$.pipe((0, import_cjs$7.filter)(shouldAbortDial), (0, import_cjs$7.take)(1), (0, import_cjs$7.switchMap)((callError) => (0, import_cjs$7.throwError)(() => callError.error)))));
|
|
20639
|
+
}
|
|
19707
20640
|
const getAddressSearchURI = (options) => {
|
|
19708
20641
|
const to = options.to?.split("?")[0];
|
|
19709
20642
|
const from$9 = options.from?.startsWith("subscriber://") ? options.from.replace("subscriber://", "") : options.from;
|
|
@@ -19720,7 +20653,6 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
19720
20653
|
this.authorizationStateKey = authorizationStateKey;
|
|
19721
20654
|
this.attachManager = attachManager;
|
|
19722
20655
|
this.dpopManager = dpopManager;
|
|
19723
|
-
this.callCreateTimeout = 6e3;
|
|
19724
20656
|
this.agent = `signalwire-js/4.0.0`;
|
|
19725
20657
|
this.eventAcks = true;
|
|
19726
20658
|
this.authorizationState$ = this.createReplaySubject(1);
|
|
@@ -19729,6 +20661,7 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
19729
20661
|
minor: 0,
|
|
19730
20662
|
revision: 0
|
|
19731
20663
|
};
|
|
20664
|
+
this.callControl = "routed";
|
|
19732
20665
|
this._authorization$ = this.createBehaviorSubject(void 0);
|
|
19733
20666
|
this._errors$ = this.createReplaySubject(1);
|
|
19734
20667
|
this._authState$ = this.createBehaviorSubject({ kind: "unauthenticated" });
|
|
@@ -19929,21 +20862,18 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
19929
20862
|
}
|
|
19930
20863
|
async handleAuthenticationError(error) {
|
|
19931
20864
|
logger$9.error("Authentication error:", error);
|
|
19932
|
-
const isRecoverableAuthError = error instanceof JSONRPCError && (error.code === RPC_ERROR_REQUESTER_VALIDATION_FAILED || error.code === RPC_ERROR_INVALID_PARAMS || error.code === RPC_ERROR_AUTHENTICATION_FAILED);
|
|
20865
|
+
const isRecoverableAuthError$1 = error instanceof JSONRPCError && (error.code === RPC_ERROR_REQUESTER_VALIDATION_FAILED || error.code === RPC_ERROR_INVALID_PARAMS || error.code === RPC_ERROR_AUTHENTICATION_FAILED);
|
|
19933
20866
|
const hasStoredState = await (0, import_cjs$7.firstValueFrom)(this.authorizationState$.pipe((0, import_cjs$7.take)(1))) !== void 0;
|
|
19934
|
-
if (isRecoverableAuthError && hasStoredState) {
|
|
20867
|
+
if (isRecoverableAuthError$1 && hasStoredState) {
|
|
19935
20868
|
logger$9.debug("[Session] Recoverable auth error — cleaning up stored state and reconnecting fresh");
|
|
19936
|
-
|
|
19937
|
-
await this.cleanupStoredConnectionParams();
|
|
19938
|
-
} catch (cleanupError) {
|
|
19939
|
-
logger$9.error("Failed to cleanup stored connection params:", cleanupError);
|
|
19940
|
-
} finally {
|
|
19941
|
-
this.transport.reconnect();
|
|
19942
|
-
}
|
|
20869
|
+
await this.discardResumeStateAndReconnect();
|
|
19943
20870
|
} else this._errors$.next(error);
|
|
19944
20871
|
}
|
|
19945
20872
|
/**
|
|
19946
|
-
* Clear the resume state (authorization_state + protocol)
|
|
20873
|
+
* Clear the resume state (authorization_state + protocol) and ask the
|
|
20874
|
+
* transport to reconnect. The `connected` event re-triggers
|
|
20875
|
+
* `authenticate()`, which now has no stored state and so performs a fresh
|
|
20876
|
+
* connect.
|
|
19947
20877
|
*
|
|
19948
20878
|
* This is the stale-auth-state recovery helper used by handleAuthError:
|
|
19949
20879
|
* the server rejected a reconnect, so the resume state is discarded and a
|
|
@@ -19951,9 +20881,24 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
19951
20881
|
* session lives on through the reconnect and reattachCalls() needs the
|
|
19952
20882
|
* stored call references afterwards. Do NOT add detachAll() here.
|
|
19953
20883
|
*
|
|
20884
|
+
* Connect-time recovery only. A *request* refused on an already
|
|
20885
|
+
* authenticated session is never healed here: dropping the resume state
|
|
20886
|
+
* destroys the association between the socket and the previous session,
|
|
20887
|
+
* which is what reattach depends on. That path mints a fresh credential and
|
|
20888
|
+
* reauthenticates instead (see `SignalWire.recoverAndRetry`).
|
|
20889
|
+
*
|
|
19954
20890
|
* For public teardown (disconnect/destroy), use {@link teardownSessionState}
|
|
19955
20891
|
* instead, which clears the attach records as well.
|
|
19956
20892
|
*/
|
|
20893
|
+
async discardResumeStateAndReconnect() {
|
|
20894
|
+
try {
|
|
20895
|
+
await this.cleanupStoredConnectionParams();
|
|
20896
|
+
} catch (cleanupError) {
|
|
20897
|
+
logger$9.error("Failed to cleanup stored connection params:", cleanupError);
|
|
20898
|
+
} finally {
|
|
20899
|
+
this.transport.reconnect();
|
|
20900
|
+
}
|
|
20901
|
+
}
|
|
19957
20902
|
async cleanupStoredConnectionParams() {
|
|
19958
20903
|
await this.transport.setProtocol(void 0);
|
|
19959
20904
|
await this.updateAuthorizationStateInStorage(void 0);
|
|
@@ -20027,11 +20972,15 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
20027
20972
|
const isReconnect = hasReconnectState && storedToken;
|
|
20028
20973
|
let dpopToken;
|
|
20029
20974
|
if (isReconnect) logger$9.debug("[Session] Reconnecting with stored jwt_token + authorization_state");
|
|
20030
|
-
else
|
|
20031
|
-
|
|
20032
|
-
|
|
20975
|
+
else {
|
|
20976
|
+
const credential = this.getCredential();
|
|
20977
|
+
const credentialExpired = credential.expiry_at !== void 0 && credential.expiry_at <= Date.now() + CREDENTIAL_EXPIRY_SKEW_MS;
|
|
20978
|
+
if (this.onBeforeReconnect && (this.clientBound || credentialExpired)) {
|
|
20979
|
+
logger$9.debug("[Session] Refreshing credentials before fresh connect");
|
|
20980
|
+
await this.onBeforeReconnect();
|
|
20981
|
+
}
|
|
20033
20982
|
}
|
|
20034
|
-
if (
|
|
20983
|
+
if (this.dpopManager?.initialized) try {
|
|
20035
20984
|
dpopToken = await this.dpopManager.createRpcProof({ method: "signalwire.connect" });
|
|
20036
20985
|
} catch (error) {
|
|
20037
20986
|
if (this.clientBound) throw error;
|
|
@@ -20064,6 +21013,7 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
20064
21013
|
});
|
|
20065
21014
|
if (response.protocol) await this.transport.setProtocol(response.protocol);
|
|
20066
21015
|
this._authorization$.next(response.authorization);
|
|
21016
|
+
if (response.authorization.cnf?.jkt) this._wasClientBound = true;
|
|
20067
21017
|
this._iceServers$.next(response.ice_servers ?? []);
|
|
20068
21018
|
this._authState$.next({ kind: "authenticated" });
|
|
20069
21019
|
logger$9.debug("[Session] Authentication completed successfully");
|
|
@@ -20130,7 +21080,7 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
20130
21080
|
to: destinationURI,
|
|
20131
21081
|
...options
|
|
20132
21082
|
});
|
|
20133
|
-
await (
|
|
21083
|
+
await awaitDialReady(callSession, DEFAULT_CALL_SIGNALING_TIMEOUT_MS);
|
|
20134
21084
|
this._calls$.next({
|
|
20135
21085
|
[`${callSession.id}`]: callSession,
|
|
20136
21086
|
...this._calls$.value
|
|
@@ -20183,12 +21133,23 @@ var ClientSessionWrapper = class {
|
|
|
20183
21133
|
get authenticated() {
|
|
20184
21134
|
return this.clientSessionManager.authenticated;
|
|
20185
21135
|
}
|
|
21136
|
+
/**
|
|
21137
|
+
* Whether the session is using a Client Bound SAT (DPoP). Sticky — set
|
|
21138
|
+
* when the binding is established or restored from a resumed session's
|
|
21139
|
+
* server authorization.
|
|
21140
|
+
*/
|
|
21141
|
+
get clientBound() {
|
|
21142
|
+
return this.clientSessionManager.clientBound;
|
|
21143
|
+
}
|
|
20186
21144
|
get signalingEvent$() {
|
|
20187
21145
|
return this.clientSessionManager.signalingEvent$;
|
|
20188
21146
|
}
|
|
20189
21147
|
get iceServers() {
|
|
20190
21148
|
return this.clientSessionManager.iceServers;
|
|
20191
21149
|
}
|
|
21150
|
+
get callControl() {
|
|
21151
|
+
return this.clientSessionManager.callControl;
|
|
21152
|
+
}
|
|
20192
21153
|
async execute(request, options) {
|
|
20193
21154
|
return this.clientSessionManager.execute(request, options);
|
|
20194
21155
|
}
|
|
@@ -20385,7 +21346,7 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
20385
21346
|
await session.reauthenticate(tokenData.token, rpcProof, { clientBound: true });
|
|
20386
21347
|
updateCredential({ token: tokenData.token });
|
|
20387
21348
|
logger$7.info("[DeviceToken] Client Bound SAT activated successfully");
|
|
20388
|
-
this.
|
|
21349
|
+
this.emitCurrentToken(tokenData);
|
|
20389
21350
|
return { activated: true };
|
|
20390
21351
|
} catch (error) {
|
|
20391
21352
|
logger$7.error("[DeviceToken] Failed to activate Client Bound SAT:", error);
|
|
@@ -20397,6 +21358,21 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
20397
21358
|
}
|
|
20398
21359
|
}
|
|
20399
21360
|
/**
|
|
21361
|
+
* Emit a freshly received token to the reactive pipeline, stamping an
|
|
21362
|
+
* absolute `expires_at` when the response carried only `expires_in`.
|
|
21363
|
+
* Resolving the expiry at RECEIVE time (not at read time) is what lets
|
|
21364
|
+
* {@link refreshNowIfDue} detect due-ness on resume: a bare `expires_in`
|
|
21365
|
+
* re-resolved later would always compute a full TTL from "now" and never
|
|
21366
|
+
* cross the refresh buffer.
|
|
21367
|
+
*/
|
|
21368
|
+
emitCurrentToken(token) {
|
|
21369
|
+
const stamped = token.expires_at ? token : {
|
|
21370
|
+
...token,
|
|
21371
|
+
expires_at: resolveExpiresAt(token)
|
|
21372
|
+
};
|
|
21373
|
+
this._currentToken$.next(stamped);
|
|
21374
|
+
}
|
|
21375
|
+
/**
|
|
20400
21376
|
* Returns true when the cached token has enough headroom before expiry to
|
|
20401
21377
|
* be safely reused on reactivation. The headroom matches the refresh
|
|
20402
21378
|
* buffer, so a token within the refresh window is treated as stale (the
|
|
@@ -20414,7 +21390,7 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
20414
21390
|
method: "POST",
|
|
20415
21391
|
uri: DEVICE_TOKEN_ENDPOINT
|
|
20416
21392
|
});
|
|
20417
|
-
const response = await this.http.request({
|
|
21393
|
+
const response = await this.http().request({
|
|
20418
21394
|
url: DEVICE_TOKEN_ENDPOINT,
|
|
20419
21395
|
...POST_PARAMS,
|
|
20420
21396
|
body: JSON.stringify({
|
|
@@ -20441,7 +21417,7 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
20441
21417
|
uri: DEVICE_REFRESH_ENDPOINT,
|
|
20442
21418
|
accessToken: currentToken
|
|
20443
21419
|
});
|
|
20444
|
-
const response = await this.http.request({
|
|
21420
|
+
const response = await this.http().request({
|
|
20445
21421
|
url: DEVICE_REFRESH_ENDPOINT,
|
|
20446
21422
|
...POST_PARAMS,
|
|
20447
21423
|
body: JSON.stringify({
|
|
@@ -20489,7 +21465,7 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
20489
21465
|
const currentToken = this.getCredential().token;
|
|
20490
21466
|
if (!currentToken) throw new TokenRefreshError("No current token available for refresh");
|
|
20491
21467
|
const newTokenData = await this.retryRefresh(session, currentToken, updateCredential);
|
|
20492
|
-
this.
|
|
21468
|
+
this.emitCurrentToken(newTokenData);
|
|
20493
21469
|
} catch (error) {
|
|
20494
21470
|
logger$7.error("[DeviceToken] Automatic Client Bound SAT refresh failed:", error);
|
|
20495
21471
|
this.errorHandler(error instanceof TokenRefreshError ? error : new TokenRefreshError("Automatic token refresh failed", error));
|
|
@@ -20516,6 +21492,22 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
20516
21492
|
throw lastError instanceof Error ? lastError : new TokenRefreshError("All refresh retries exhausted", lastError);
|
|
20517
21493
|
}
|
|
20518
21494
|
/**
|
|
21495
|
+
* Force an immediate refresh when the cached Client Bound SAT is already
|
|
21496
|
+
* past its refresh window. Called on resume from suspension where
|
|
21497
|
+
* background-tab throttling can delay the reactive timer past the buffer.
|
|
21498
|
+
* A no-op when no token is cached or it still has headroom; the normal
|
|
21499
|
+
* {@link executeRefresh} guards (paused / in-progress / unauthenticated)
|
|
21500
|
+
* still apply.
|
|
21501
|
+
*/
|
|
21502
|
+
refreshNowIfDue() {
|
|
21503
|
+
const token = this._currentToken$.value;
|
|
21504
|
+
if (!token) return;
|
|
21505
|
+
if (resolveExpiresAt(token) * 1e3 - Date.now() <= DEVICE_TOKEN_REFRESH_BUFFER_MS) {
|
|
21506
|
+
logger$7.debug("[DeviceToken] Resume: cached SAT past refresh window; refreshing now");
|
|
21507
|
+
this.executeRefresh();
|
|
21508
|
+
}
|
|
21509
|
+
}
|
|
21510
|
+
/**
|
|
20519
21511
|
* Stops the reactive refresh pipeline from firing. Use when the underlying
|
|
20520
21512
|
* session is being torn down (e.g., during {@link SignalWire.disconnect})
|
|
20521
21513
|
* so a scheduled refresh cannot fire against a destroyed session.
|
|
@@ -20566,6 +21558,7 @@ var CredentialRefreshCoordinator = class extends Destroyable {
|
|
|
20566
21558
|
this.deps = deps;
|
|
20567
21559
|
this._activating = false;
|
|
20568
21560
|
this._activationGeneration = 0;
|
|
21561
|
+
this._developerRefreshInProgress = false;
|
|
20569
21562
|
if (dpopManager?.initialized) this._deviceTokenManager = (deps.deviceTokenManagerFactory ?? defaultDeviceTokenManagerFactory)(dpopManager, deps.http, (error) => deps.notifier.onError(error), () => deps.store.read());
|
|
20570
21563
|
}
|
|
20571
21564
|
/** True when the Client Bound SAT path is available (DPoP initialized). */
|
|
@@ -20584,28 +21577,120 @@ var CredentialRefreshCoordinator = class extends Destroyable {
|
|
|
20584
21577
|
* invokes `deps.onRefreshExhausted` so the orchestrator can disconnect.
|
|
20585
21578
|
*/
|
|
20586
21579
|
scheduleDeveloperRefresh(provider, expiresAt, attempt = 0) {
|
|
21580
|
+
this._activeProvider = provider;
|
|
20587
21581
|
if (this._developerTimerId !== void 0) clearTimeout(this._developerTimerId);
|
|
20588
21582
|
const refreshInterval = attempt === 0 ? Math.max(expiresAt - Date.now() - CREDENTIAL_REFRESH_BUFFER_MS, 1e3) : Math.min(CREDENTIAL_REFRESH_RETRY_BASE_MS * Math.pow(2, attempt) * (.5 + Math.random() * .5), CREDENTIAL_REFRESH_MAX_DELAY_MS);
|
|
20589
|
-
this._developerTimerId = setTimeout(
|
|
21583
|
+
this._developerTimerId = setTimeout(() => {
|
|
21584
|
+
this._developerTimerId = void 0;
|
|
21585
|
+
this.executeDeveloperRefresh(provider, expiresAt, attempt);
|
|
21586
|
+
}, refreshInterval);
|
|
21587
|
+
}
|
|
21588
|
+
/**
|
|
21589
|
+
* Runs the developer-provided refresh once: mints a new credential, stores
|
|
21590
|
+
* and persists it, reauthenticates the live session (via the notifier), and
|
|
21591
|
+
* reschedules against the new expiry. On failure retries with backoff up to
|
|
21592
|
+
* {@link CREDENTIAL_REFRESH_MAX_RETRIES}, then signals exhaustion.
|
|
21593
|
+
*
|
|
21594
|
+
* Shared by the scheduled timer tick and {@link forceRefreshIfDue}. The
|
|
21595
|
+
* `_developerRefreshInProgress` guard prevents the two from overlapping.
|
|
21596
|
+
*/
|
|
21597
|
+
async executeDeveloperRefresh(provider, expiresAt, attempt) {
|
|
21598
|
+
if (this._developerRefreshInProgress) {
|
|
21599
|
+
logger$6.debug("[Coordinator] Developer refresh already in progress; skipping");
|
|
21600
|
+
return;
|
|
21601
|
+
}
|
|
21602
|
+
this._developerRefreshInProgress = true;
|
|
21603
|
+
try {
|
|
21604
|
+
const newCredentials = await this.refreshCredential(provider);
|
|
21605
|
+
this.deps.store.write(newCredentials);
|
|
21606
|
+
this.deps.store.persist(newCredentials);
|
|
20590
21607
|
try {
|
|
20591
|
-
|
|
20592
|
-
|
|
20593
|
-
|
|
20594
|
-
this.deps.store.persist(newCredentials);
|
|
20595
|
-
logger$6.info("[Coordinator] Credentials refreshed successfully.");
|
|
20596
|
-
if (newCredentials.expiry_at) this.scheduleDeveloperRefresh(provider, newCredentials.expiry_at, 0);
|
|
20597
|
-
} catch (error) {
|
|
20598
|
-
const nextAttempt = attempt + 1;
|
|
20599
|
-
logger$6.error(`[Coordinator] Credential refresh failed (attempt ${nextAttempt}/${CREDENTIAL_REFRESH_MAX_RETRIES}):`, error);
|
|
20600
|
-
this.deps.notifier.onError(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
20601
|
-
if (nextAttempt < CREDENTIAL_REFRESH_MAX_RETRIES) this.scheduleDeveloperRefresh(provider, expiresAt, nextAttempt);
|
|
20602
|
-
else {
|
|
20603
|
-
logger$6.error("[Coordinator] Credential refresh exhausted all retries. Disconnecting.");
|
|
20604
|
-
this.deps.notifier.onError(new TokenRefreshError("Credential refresh failed after max retries"));
|
|
20605
|
-
this.deps.notifier.onRefreshExhausted();
|
|
20606
|
-
}
|
|
21608
|
+
await this.deps.notifier.onCredentialRefreshed(newCredentials);
|
|
21609
|
+
} catch (reauthError) {
|
|
21610
|
+
logger$6.warn("[Coordinator] onCredentialRefreshed rejected (non-fatal):", reauthError);
|
|
20607
21611
|
}
|
|
20608
|
-
|
|
21612
|
+
logger$6.info("[Coordinator] Credentials refreshed successfully.");
|
|
21613
|
+
if (newCredentials.expiry_at) this.scheduleDeveloperRefresh(provider, newCredentials.expiry_at, 0);
|
|
21614
|
+
} catch (error) {
|
|
21615
|
+
const nextAttempt = attempt + 1;
|
|
21616
|
+
logger$6.error(`[Coordinator] Credential refresh failed (attempt ${nextAttempt}/${CREDENTIAL_REFRESH_MAX_RETRIES}):`, error);
|
|
21617
|
+
this.deps.notifier.onError(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
21618
|
+
if (nextAttempt < CREDENTIAL_REFRESH_MAX_RETRIES) this.scheduleDeveloperRefresh(provider, expiresAt, nextAttempt);
|
|
21619
|
+
else {
|
|
21620
|
+
logger$6.error("[Coordinator] Credential refresh exhausted all retries. Disconnecting.");
|
|
21621
|
+
this.deps.notifier.onError(new TokenRefreshError("Credential refresh failed after max retries"));
|
|
21622
|
+
this.deps.notifier.onRefreshExhausted();
|
|
21623
|
+
}
|
|
21624
|
+
} finally {
|
|
21625
|
+
this._developerRefreshInProgress = false;
|
|
21626
|
+
}
|
|
21627
|
+
}
|
|
21628
|
+
/**
|
|
21629
|
+
* Force an immediate refresh when the current credential is already past its
|
|
21630
|
+
* scheduled refresh window. Called on resume from suspension, where
|
|
21631
|
+
* background-tab timer throttling can delay the armed refresh well past
|
|
21632
|
+
* expiry, leaving the live session stale.
|
|
21633
|
+
*
|
|
21634
|
+
* Routes to whichever mechanism is armed: the developer timer if armed,
|
|
21635
|
+
* otherwise the Client Bound SAT pipeline. A no-op when nothing is due.
|
|
21636
|
+
*/
|
|
21637
|
+
forceRefreshIfDue() {
|
|
21638
|
+
if (this._developerTimerId !== void 0 && this._activeProvider) {
|
|
21639
|
+
const expiry = this.deps.store.read().expiry_at;
|
|
21640
|
+
if (expiry !== void 0 && Date.now() >= expiry - CREDENTIAL_REFRESH_BUFFER_MS) {
|
|
21641
|
+
logger$6.debug("[Coordinator] Resume: credential past refresh window; forcing refresh");
|
|
21642
|
+
clearTimeout(this._developerTimerId);
|
|
21643
|
+
this._developerTimerId = void 0;
|
|
21644
|
+
this.executeDeveloperRefresh(this._activeProvider, expiry, 0);
|
|
21645
|
+
}
|
|
21646
|
+
return;
|
|
21647
|
+
}
|
|
21648
|
+
this._deviceTokenManager?.refreshNowIfDue();
|
|
21649
|
+
}
|
|
21650
|
+
/**
|
|
21651
|
+
* Sync the credential's expiry from the server-provided authorization (the
|
|
21652
|
+
* `signalwire.connect` result). SATs are opaque JWE, so
|
|
21653
|
+
* `fabric_subscriber.expires_at` is the authoritative expiry of the token
|
|
21654
|
+
* the session actually connected with — the provider-reported `expiry_at`
|
|
21655
|
+
* is only a hint (and may be wrong or absent). Corrects the stored
|
|
21656
|
+
* credential and re-arms the developer refresh timer against the real
|
|
21657
|
+
* deadline when the provider supports `refresh()`.
|
|
21658
|
+
*/
|
|
21659
|
+
syncExpiryFromAuthorization(authorization, provider) {
|
|
21660
|
+
const expiresAtSec = authorization?.fabric_subscriber?.expires_at;
|
|
21661
|
+
if (!expiresAtSec) return;
|
|
21662
|
+
const expiryAt = expiresAtSec * 1e3;
|
|
21663
|
+
const credential = this.deps.store.read();
|
|
21664
|
+
if (credential.expiry_at === expiryAt) return;
|
|
21665
|
+
logger$6.debug(`[Coordinator] Correcting credential expiry from server authorization: ${new Date(expiryAt).toISOString()}`);
|
|
21666
|
+
const updated = {
|
|
21667
|
+
...credential,
|
|
21668
|
+
expiry_at: expiryAt
|
|
21669
|
+
};
|
|
21670
|
+
this.deps.store.write(updated);
|
|
21671
|
+
this.deps.store.persist(updated);
|
|
21672
|
+
if (provider?.refresh) this.scheduleDeveloperRefresh(provider, expiryAt);
|
|
21673
|
+
}
|
|
21674
|
+
/**
|
|
21675
|
+
* Invoke `provider.refresh()` deduped against any concurrent developer
|
|
21676
|
+
* refresh. Concurrent callers — the scheduled tick, a resume-forced refresh,
|
|
21677
|
+
* and the orchestrator's -32003 recovery / reconnect re-mint — share one
|
|
21678
|
+
* in-flight promise, so a provider backed by one-time-use rotating refresh
|
|
21679
|
+
* tokens is never invoked twice in parallel.
|
|
21680
|
+
*
|
|
21681
|
+
* The caller owns applying the returned credential (store write, session
|
|
21682
|
+
* reauth, rescheduling); this method only serializes the network call.
|
|
21683
|
+
*/
|
|
21684
|
+
async refreshCredential(provider) {
|
|
21685
|
+
if (this._refreshInFlight) return this._refreshInFlight;
|
|
21686
|
+
if (!provider.refresh) throw new InvalidCredentialsError("Credential provider does not support refresh");
|
|
21687
|
+
const run = provider.refresh();
|
|
21688
|
+
this._refreshInFlight = run;
|
|
21689
|
+
const clear = () => {
|
|
21690
|
+
if (this._refreshInFlight === run) this._refreshInFlight = void 0;
|
|
21691
|
+
};
|
|
21692
|
+
run.then(clear, clear);
|
|
21693
|
+
return run;
|
|
20609
21694
|
}
|
|
20610
21695
|
/**
|
|
20611
21696
|
* Cancels any scheduled developer-provided refresh. Idempotent.
|
|
@@ -21389,6 +22474,13 @@ var TransportManager = class extends Destroyable {
|
|
|
21389
22474
|
//#region src/clients/SignalWire.ts
|
|
21390
22475
|
var import_cjs$1 = require_cjs();
|
|
21391
22476
|
const logger$1 = getLogger();
|
|
22477
|
+
/**
|
|
22478
|
+
* Storage key for the client-bound marker. The SAT and authorization_state are
|
|
22479
|
+
* both opaque to the SDK, so on a page reload the preflight recovery — which
|
|
22480
|
+
* runs before any session exists — has no other way to know the session was
|
|
22481
|
+
* client-bound. See {@link SignalWire.persistClientBoundMarker}.
|
|
22482
|
+
*/
|
|
22483
|
+
const CLIENT_BOUND_STORAGE_KEY = "sw:client_bound";
|
|
21392
22484
|
const buildOptionsFromDestination = (destination) => {
|
|
21393
22485
|
if (typeof destination === "string") {
|
|
21394
22486
|
const queryStartIndex = destination.indexOf("?");
|
|
@@ -21432,6 +22524,7 @@ var SignalWire = class extends Destroyable {
|
|
|
21432
22524
|
this.preferences = new ClientPreferences();
|
|
21433
22525
|
this._user$ = this.createBehaviorSubject(void 0);
|
|
21434
22526
|
this._directory$ = this.createBehaviorSubject(void 0);
|
|
22527
|
+
this._credentialRecovered = false;
|
|
21435
22528
|
this._isConnected$ = this.createBehaviorSubject(false);
|
|
21436
22529
|
this._isRegistered$ = this.createBehaviorSubject(false);
|
|
21437
22530
|
this._errors$ = this.createReplaySubject(1);
|
|
@@ -21470,6 +22563,16 @@ var SignalWire = class extends Destroyable {
|
|
|
21470
22563
|
});
|
|
21471
22564
|
}
|
|
21472
22565
|
/**
|
|
22566
|
+
* Build the refresh path's own HTTP controller, against whatever host is current.
|
|
22567
|
+
*
|
|
22568
|
+
* Called on first use rather than up front, so `apiHost` already reflects the
|
|
22569
|
+
* token's `ch` claim. Same credential source as the container's controller — only
|
|
22570
|
+
* the instance, and therefore its observable streams, is separate.
|
|
22571
|
+
*/
|
|
22572
|
+
createRefreshHttpController() {
|
|
22573
|
+
return new HTTPRequestController(this._deps.apiHost, () => this._deps.credential);
|
|
22574
|
+
}
|
|
22575
|
+
/**
|
|
21473
22576
|
* Initializes DPoP if not already set up. Returns the fingerprint on success.
|
|
21474
22577
|
*/
|
|
21475
22578
|
async initDPoP() {
|
|
@@ -21496,11 +22599,19 @@ var SignalWire = class extends Destroyable {
|
|
|
21496
22599
|
async resolveCredentials() {
|
|
21497
22600
|
const fingerprint = await this.initDPoP();
|
|
21498
22601
|
this._refreshCoordinator = new CredentialRefreshCoordinator(this._dpopManager, {
|
|
21499
|
-
http:
|
|
22602
|
+
http: () => {
|
|
22603
|
+
if (!this._refreshHttp || this._refreshHttpHost !== this._deps.apiHost) {
|
|
22604
|
+
this._refreshHttp?.destroy();
|
|
22605
|
+
this._refreshHttp = this.createRefreshHttpController();
|
|
22606
|
+
this._refreshHttpHost = this._deps.apiHost;
|
|
22607
|
+
}
|
|
22608
|
+
return this._refreshHttp;
|
|
22609
|
+
},
|
|
21500
22610
|
notifier: {
|
|
21501
22611
|
onError: (error) => this._errors$.next(error),
|
|
21502
22612
|
onWarning: (warning) => this._warnings$.next(warning),
|
|
21503
|
-
onRefreshExhausted: () => void this.disconnect()
|
|
22613
|
+
onRefreshExhausted: () => void this.disconnect(),
|
|
22614
|
+
onCredentialRefreshed: async (credential) => this.reauthenticateLiveSession(credential)
|
|
21504
22615
|
},
|
|
21505
22616
|
store: {
|
|
21506
22617
|
read: () => this._deps.credential,
|
|
@@ -21512,6 +22623,7 @@ var SignalWire = class extends Destroyable {
|
|
|
21512
22623
|
...this._deps.credential,
|
|
21513
22624
|
...partial
|
|
21514
22625
|
};
|
|
22626
|
+
this.persistCredential(this._deps.credential);
|
|
21515
22627
|
},
|
|
21516
22628
|
persist: (credential) => this.persistCredential(credential)
|
|
21517
22629
|
}
|
|
@@ -21557,20 +22669,181 @@ var SignalWire = class extends Destroyable {
|
|
|
21557
22669
|
}
|
|
21558
22670
|
this._deps.credential = _credentials;
|
|
21559
22671
|
this.persistCredential(_credentials);
|
|
21560
|
-
|
|
21561
|
-
|
|
22672
|
+
await this.reauthenticateLiveSession(_credentials);
|
|
22673
|
+
}
|
|
22674
|
+
/**
|
|
22675
|
+
* Reauthenticate the currently-open session with a freshly obtained
|
|
22676
|
+
* credential so the new token takes effect on the live socket immediately —
|
|
22677
|
+
* not just on the next reconnect. No-op when the session is not
|
|
22678
|
+
* connected/authenticated or the credential carries no token (e.g. an
|
|
22679
|
+
* authorization-state-only refresh). Non-fatal: reauth failures surface on
|
|
22680
|
+
* `errors$` without aborting the refresh that triggered this.
|
|
22681
|
+
*/
|
|
22682
|
+
async reauthenticateLiveSession(credential) {
|
|
22683
|
+
if (!this.isConnected || !this._clientSession.authenticated || !credential.token) return;
|
|
22684
|
+
try {
|
|
22685
|
+
await this._clientSession.reauthenticate(credential.token);
|
|
21562
22686
|
logger$1.info("[SignalWire] Session refreshed with new credentials.");
|
|
21563
22687
|
} catch (error) {
|
|
21564
22688
|
logger$1.error("[SignalWire] Failed to refresh session with new credentials:", error);
|
|
21565
22689
|
this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
21566
22690
|
}
|
|
21567
22691
|
}
|
|
22692
|
+
/**
|
|
22693
|
+
* Recover a session the server is refusing: mint a fresh credential,
|
|
22694
|
+
* reauthenticate the live session with it, and retry the operation.
|
|
22695
|
+
*
|
|
22696
|
+
* The connection is deliberately kept. A reload authenticates the new socket
|
|
22697
|
+
* against the persisted `authorization_state`, and that handshake is what
|
|
22698
|
+
* associates the socket with the previous session — the association reattach
|
|
22699
|
+
* depends on. `signalwire.reauthenticate` swaps the credential *on that same
|
|
22700
|
+
* session*, so recovery never touches the resume state. Discarding it would
|
|
22701
|
+
* heal the credential by destroying the very thing the caller is trying to
|
|
22702
|
+
* get back to.
|
|
22703
|
+
*
|
|
22704
|
+
* The operation is still the verdict, never the RPC. Reauthenticating with
|
|
22705
|
+
* the in-memory token is accepted by a resume even while requests stay
|
|
22706
|
+
* refused, because the persisted `authorization_state` short-circuits token
|
|
22707
|
+
* validation — and `signalwire.reauthenticate` with a *freshly minted* token
|
|
22708
|
+
* has also been observed accepted while `subscriber.online` keeps being
|
|
22709
|
+
* refused (staging run 33826974634). Both look like success and are not.
|
|
22710
|
+
*
|
|
22711
|
+
* @returns the operation's value, or the reason recovery could not deliver
|
|
22712
|
+
* one. `error` is undefined when there was no way to mint at all.
|
|
22713
|
+
*/
|
|
22714
|
+
async recoverAndRetry(operation) {
|
|
22715
|
+
if (!await this.remintAndReauthenticate()) return { ok: false };
|
|
22716
|
+
try {
|
|
22717
|
+
const value = await operation();
|
|
22718
|
+
this._credentialRecovered = true;
|
|
22719
|
+
return {
|
|
22720
|
+
ok: true,
|
|
22721
|
+
value
|
|
22722
|
+
};
|
|
22723
|
+
} catch (error) {
|
|
22724
|
+
logger$1.warn("[SignalWire] Reauthentication was accepted but the operation is still refused:", error);
|
|
22725
|
+
return {
|
|
22726
|
+
ok: false,
|
|
22727
|
+
error
|
|
22728
|
+
};
|
|
22729
|
+
}
|
|
22730
|
+
}
|
|
22731
|
+
/**
|
|
22732
|
+
* Re-mint a credential and adopt it only if the live session accepts it.
|
|
22733
|
+
*
|
|
22734
|
+
* The mechanism follows the binding: a client-bound session re-mints a bound
|
|
22735
|
+
* base SAT through `authenticate()` with the DPoP fingerprint, because the
|
|
22736
|
+
* developer refresh handler would hand back an unbound token and silently
|
|
22737
|
+
* degrade the session. An unbound session uses the refresh handler. Rotation
|
|
22738
|
+
* cost is not a reason to skip this — the only reason is having no mechanism.
|
|
22739
|
+
*
|
|
22740
|
+
* @returns whether the session is now running on a freshly accepted credential.
|
|
22741
|
+
*/
|
|
22742
|
+
async remintAndReauthenticate() {
|
|
22743
|
+
const provider = this._credentialProvider;
|
|
22744
|
+
if (!provider) return false;
|
|
22745
|
+
const { clientBound } = this._clientSession;
|
|
22746
|
+
if (!clientBound && !provider.refresh) {
|
|
22747
|
+
logger$1.debug("[SignalWire] [SW-NO-REFRESH-HANDLER] Unbound session with no refresh handler; cannot re-mint.");
|
|
22748
|
+
return false;
|
|
22749
|
+
}
|
|
22750
|
+
try {
|
|
22751
|
+
const newCredentials = clientBound ? await provider.authenticate(this._dpopManager?.initialized ? { fingerprint: this._dpopManager.fingerprint } : void 0) : await this.remintCredential(provider);
|
|
22752
|
+
if (!newCredentials.token) {
|
|
22753
|
+
logger$1.warn("[SignalWire] Re-minted credential has no token; keeping the current one.");
|
|
22754
|
+
return false;
|
|
22755
|
+
}
|
|
22756
|
+
await this._clientSession.reauthenticate(newCredentials.token);
|
|
22757
|
+
this._deps.credential = newCredentials;
|
|
22758
|
+
this.persistCredential(newCredentials);
|
|
22759
|
+
if (newCredentials.expiry_at && provider.refresh) this._refreshCoordinator?.scheduleDeveloperRefresh(provider, newCredentials.expiry_at);
|
|
22760
|
+
return true;
|
|
22761
|
+
} catch (error) {
|
|
22762
|
+
logger$1.warn("[SignalWire] Re-mint recovery failed:", error);
|
|
22763
|
+
return false;
|
|
22764
|
+
}
|
|
22765
|
+
}
|
|
22766
|
+
/**
|
|
22767
|
+
* Re-mint a credential via `provider.refresh()`, routed through the
|
|
22768
|
+
* coordinator's shared in-flight guard so concurrent re-mint paths (a
|
|
22769
|
+
* scheduled/resume refresh, -32003 recovery, and reconnect) never fire a
|
|
22770
|
+
* second `provider.refresh()` in parallel — which rotating one-time-use
|
|
22771
|
+
* refresh tokens reject. Falls back to a direct call only if the coordinator
|
|
22772
|
+
* has not been constructed yet.
|
|
22773
|
+
*/
|
|
22774
|
+
async remintCredential(provider) {
|
|
22775
|
+
if (this._refreshCoordinator) return this._refreshCoordinator.refreshCredential(provider);
|
|
22776
|
+
if (!provider.refresh) throw new InvalidCredentialsError("Credential provider does not support refresh");
|
|
22777
|
+
return provider.refresh();
|
|
22778
|
+
}
|
|
22779
|
+
/**
|
|
22780
|
+
* Re-mint credentials before a fresh (re)connect (`onBeforeReconnect` hook).
|
|
22781
|
+
* The session invokes this only when it is client-bound OR the in-memory
|
|
22782
|
+
* token is expired. The re-mint mechanism depends on the binding:
|
|
22783
|
+
* - Client-bound: `authenticate()` with the DPoP fingerprint to obtain a
|
|
22784
|
+
* fresh base SAT the upcoming reconnect can re-bind (the
|
|
22785
|
+
* DeviceTokenManager re-activates afterwards).
|
|
22786
|
+
* - Unbound: the developer's non-interactive `refresh()` handler.
|
|
22787
|
+
* `authenticate()` is deliberately NOT used here — it may be interactive
|
|
22788
|
+
* (a login prompt) and must not fire on a background reconnect.
|
|
22789
|
+
*
|
|
22790
|
+
* Rejects on failure so the session aborts the reconnect rather than
|
|
22791
|
+
* replaying a stale token.
|
|
22792
|
+
*/
|
|
22793
|
+
async refreshCredentialForReconnect() {
|
|
22794
|
+
if (!this._credentialProvider) return;
|
|
22795
|
+
try {
|
|
22796
|
+
let newCredentials;
|
|
22797
|
+
if (this._clientSession?.clientBound ?? await this.wasClientBound()) {
|
|
22798
|
+
logger$1.debug("[SignalWire] Re-minting client-bound base SAT before reconnect");
|
|
22799
|
+
newCredentials = await this._credentialProvider.authenticate(this._dpopManager?.initialized ? { fingerprint: this._dpopManager.fingerprint } : void 0);
|
|
22800
|
+
} else if (this._credentialProvider.refresh) {
|
|
22801
|
+
logger$1.debug("[SignalWire] Refreshing unbound credential before reconnect");
|
|
22802
|
+
newCredentials = await this.remintCredential(this._credentialProvider);
|
|
22803
|
+
} else {
|
|
22804
|
+
logger$1.warn("[SignalWire] [SW-NO-REFRESH-HANDLER] Token expired on reconnect but no refresh handler; reconnecting with the existing token.");
|
|
22805
|
+
return;
|
|
22806
|
+
}
|
|
22807
|
+
if (!newCredentials.token) {
|
|
22808
|
+
logger$1.warn("[SignalWire] Re-minted credential has no token; keeping the existing credential for reconnect.");
|
|
22809
|
+
return;
|
|
22810
|
+
}
|
|
22811
|
+
this._deps.credential = newCredentials;
|
|
22812
|
+
this.persistCredential(newCredentials);
|
|
22813
|
+
if (newCredentials.expiry_at && this._credentialProvider.refresh) this._refreshCoordinator?.scheduleDeveloperRefresh(this._credentialProvider, newCredentials.expiry_at);
|
|
22814
|
+
logger$1.debug("[SignalWire] Credential refreshed successfully for reconnect");
|
|
22815
|
+
} catch (error) {
|
|
22816
|
+
logger$1.error("[SignalWire] Failed to refresh credentials for reconnect:", error);
|
|
22817
|
+
this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
22818
|
+
throw error;
|
|
22819
|
+
}
|
|
22820
|
+
}
|
|
21568
22821
|
/** Persist credential to localStorage when persistSession is enabled. */
|
|
21569
22822
|
persistCredential(credential) {
|
|
21570
22823
|
if (!credential.token) return;
|
|
21571
22824
|
this._deps.storage.setItem("sw:cached_credential", credential);
|
|
21572
22825
|
if (this._deps.persistSession) this._deps.storage.setItem("sw:cached_credential", credential, "local");
|
|
21573
22826
|
}
|
|
22827
|
+
/**
|
|
22828
|
+
* Persist whether the session is client-bound, mirroring the credential's
|
|
22829
|
+
* storage scopes so it survives a reload. The preflight recovery reads it
|
|
22830
|
+
* before any session exists to decide whether to re-bind via `authenticate()`
|
|
22831
|
+
* or refresh an unbound token; the marker tracks the latest binding, so an
|
|
22832
|
+
* unbound reconnect clears a stale marker from an earlier client-bound login.
|
|
22833
|
+
*/
|
|
22834
|
+
persistClientBoundMarker(bound) {
|
|
22835
|
+
const scopes = this._deps.persistSession ? ["session", "local"] : ["session"];
|
|
22836
|
+
for (const scope of scopes) if (bound) this._deps.storage.setItem(CLIENT_BOUND_STORAGE_KEY, true, scope);
|
|
22837
|
+
else this._deps.storage.removeItem(CLIENT_BOUND_STORAGE_KEY, scope);
|
|
22838
|
+
}
|
|
22839
|
+
/** Read the persisted client-bound marker (see {@link persistClientBoundMarker}). */
|
|
22840
|
+
async wasClientBound() {
|
|
22841
|
+
const scopes = this._deps.persistSession ? ["local", "session"] : ["session"];
|
|
22842
|
+
for (const scope of scopes) try {
|
|
22843
|
+
if (await this._deps.storage.getItem(CLIENT_BOUND_STORAGE_KEY, scope)) return true;
|
|
22844
|
+
} catch {}
|
|
22845
|
+
return false;
|
|
22846
|
+
}
|
|
21574
22847
|
async init() {
|
|
21575
22848
|
this._user$.next(new User(this._deps.http));
|
|
21576
22849
|
if (!this._options.skipConnection) await this.connect();
|
|
@@ -21594,6 +22867,42 @@ var SignalWire = class extends Destroyable {
|
|
|
21594
22867
|
} catch (error) {
|
|
21595
22868
|
logger$1.error("[SignalWire] Failed to reattach calls:", error);
|
|
21596
22869
|
this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
22870
|
+
} finally {
|
|
22871
|
+
this._credentialRecovered = false;
|
|
22872
|
+
}
|
|
22873
|
+
}
|
|
22874
|
+
/**
|
|
22875
|
+
* Fetch the authenticated user profile, recovering a stale credential.
|
|
22876
|
+
*
|
|
22877
|
+
* On a reload the persisted credential can be expired. Unlike the WS resume —
|
|
22878
|
+
* which the server accepts against the persisted `authorization_state` even
|
|
22879
|
+
* with an expired token — this REST preflight has no such short-circuit and is
|
|
22880
|
+
* refused (401). There is no session yet to reauthenticate, so recovery
|
|
22881
|
+
* re-mints the credential through the provider ({@link refreshCredentialForReconnect})
|
|
22882
|
+
* and retries with a FRESH {@link User}: Fetchable memoizes its result
|
|
22883
|
+
* (shareReplay), so reusing the instance would replay the 401 instead of
|
|
22884
|
+
* re-fetching with the new token. Without the user id the transport/session —
|
|
22885
|
+
* and the reattach a reload is trying to preserve — cannot even be addressed.
|
|
22886
|
+
*/
|
|
22887
|
+
async fetchUserOrRecover() {
|
|
22888
|
+
const fetchUser = async (user$1) => {
|
|
22889
|
+
if (!await (0, import_cjs$1.firstValueFrom)(user$1.fetched$)) throw new UnexpectedError("Failed to fetch user information - fetched$ emitted false");
|
|
22890
|
+
this._deps.user = user$1;
|
|
22891
|
+
};
|
|
22892
|
+
const user = this._user$.value;
|
|
22893
|
+
if (!user) throw new UnexpectedError("User not initialized before connect");
|
|
22894
|
+
try {
|
|
22895
|
+
await fetchUser(user);
|
|
22896
|
+
} catch (firstError) {
|
|
22897
|
+
logger$1.error(`[SignalWire] Failed to fetch user information: ${firstError instanceof Error ? firstError.message : "Unknown error"}. This usually means the user token is invalid or expired. Re-minting the credential and retrying.`);
|
|
22898
|
+
try {
|
|
22899
|
+
await this.refreshCredentialForReconnect();
|
|
22900
|
+
const refetched = new User(this._deps.http);
|
|
22901
|
+
await fetchUser(refetched);
|
|
22902
|
+
this._user$.next(refetched);
|
|
22903
|
+
} catch (retryError) {
|
|
22904
|
+
throw new UnexpectedError("Error fetching user information", { cause: retryError });
|
|
22905
|
+
}
|
|
21597
22906
|
}
|
|
21598
22907
|
}
|
|
21599
22908
|
/**
|
|
@@ -21635,40 +22944,23 @@ var SignalWire = class extends Destroyable {
|
|
|
21635
22944
|
*/
|
|
21636
22945
|
async connect() {
|
|
21637
22946
|
await this.teardownTransportAndSession();
|
|
21638
|
-
|
|
21639
|
-
const user = this._user$.value;
|
|
21640
|
-
if (!user) throw new UnexpectedError("User not initialized before connect");
|
|
21641
|
-
if (!await (0, import_cjs$1.firstValueFrom)(user.fetched$)) throw new UnexpectedError("Failed to fetch user information - fetched$ emitted false");
|
|
21642
|
-
this._deps.user = user;
|
|
21643
|
-
} catch (error) {
|
|
21644
|
-
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.`);
|
|
21645
|
-
throw new UnexpectedError("Error fetching user information", { cause: error });
|
|
21646
|
-
}
|
|
22947
|
+
await this.fetchUserOrRecover();
|
|
21647
22948
|
const errorHandler = (error) => {
|
|
21648
22949
|
this._errors$.next(error);
|
|
21649
22950
|
};
|
|
21650
22951
|
this._transport = new TransportManager(this._deps.storage, this._deps.protocolKey, this._deps.WebSocket, PreferencesContainer.instance.relayHost ?? this._deps.relayHost, errorHandler);
|
|
21651
|
-
this._attachManager = new AttachManager(this._deps.storage, this._deps.deviceController, PreferencesContainer.instance.reconnectCallsTimeout, this._deps.attachedCallsKey);
|
|
22952
|
+
this._attachManager = new AttachManager(this._deps.storage, this._deps.deviceController, PreferencesContainer.instance.reconnectCallsTimeout, this._deps.attachedCallsKey, () => this._credentialRecovered);
|
|
21652
22953
|
this._clientSession = new ClientSessionManager(() => this._deps.credential, this._transport, this._deps.storage, this._deps.authorizationStateKey, this._deps.deviceController, this._attachManager, this._deps.webRTCApiProvider, this._dpopManager, this._networkMonitor?.networkChange$);
|
|
21653
22954
|
this._publicSession = new ClientSessionWrapper(this._clientSession);
|
|
21654
|
-
this._clientSession.
|
|
21655
|
-
|
|
21656
|
-
try {
|
|
21657
|
-
const fingerprint = this._dpopManager?.initialized ? this._dpopManager.fingerprint : void 0;
|
|
21658
|
-
logger$1.debug("[SignalWire] Credential expired, refreshing before reconnect");
|
|
21659
|
-
const newCredentials = await this._credentialProvider.authenticate(fingerprint ? { fingerprint } : void 0);
|
|
21660
|
-
this._deps.credential = newCredentials;
|
|
21661
|
-
if (newCredentials.expiry_at && this._credentialProvider.refresh) this._refreshCoordinator?.scheduleDeveloperRefresh(this._credentialProvider, newCredentials.expiry_at);
|
|
21662
|
-
logger$1.debug("[SignalWire] Credential refreshed successfully for reconnect");
|
|
21663
|
-
} catch (error) {
|
|
21664
|
-
logger$1.error("[SignalWire] Failed to refresh credentials for reconnect:", error);
|
|
21665
|
-
this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
21666
|
-
throw error;
|
|
21667
|
-
}
|
|
21668
|
-
};
|
|
22955
|
+
this._clientSession.callControl = this._options.callControl ?? "routed";
|
|
22956
|
+
this._clientSession.onBeforeReconnect = async () => this.refreshCredentialForReconnect();
|
|
21669
22957
|
this.subscribeTo(this._clientSession.errors$, (error) => {
|
|
21670
22958
|
this._errors$.next(error);
|
|
21671
22959
|
});
|
|
22960
|
+
this.subscribeTo(this._clientSession.authorization$, (authorization) => {
|
|
22961
|
+
this._refreshCoordinator?.syncExpiryFromAuthorization(authorization, this._credentialProvider);
|
|
22962
|
+
this.persistClientBoundMarker(Boolean(authorization?.cnf?.jkt));
|
|
22963
|
+
});
|
|
21672
22964
|
await this._clientSession.connect();
|
|
21673
22965
|
await this._refreshCoordinator?.activate(this._deps.user, this._clientSession);
|
|
21674
22966
|
this.subscribeTo(this._clientSession.authenticated$.pipe((0, import_cjs$1.skip)(1), (0, import_cjs$1.filter)(Boolean)), async () => {
|
|
@@ -21831,6 +23123,13 @@ var SignalWire = class extends Destroyable {
|
|
|
21831
23123
|
}
|
|
21832
23124
|
try {
|
|
21833
23125
|
this._visibilityController = new VisibilityController();
|
|
23126
|
+
this.subscribeTo(this._visibilityController.visibilityChange$.pipe((0, import_cjs$1.filter)((event) => event.to === "visible")), () => {
|
|
23127
|
+
try {
|
|
23128
|
+
this._refreshCoordinator?.forceRefreshIfDue();
|
|
23129
|
+
} catch (error) {
|
|
23130
|
+
logger$1.warn("[SignalWire] Resume credential revalidation failed (non-fatal):", error);
|
|
23131
|
+
}
|
|
23132
|
+
});
|
|
21834
23133
|
this.subscribeTo(this._visibilityController.visibilityChange$.pipe((0, import_cjs$1.filter)((event) => event.to === "visible" && PreferencesContainer.instance.refreshDevicesOnVisible)), () => {
|
|
21835
23134
|
logger$1.debug("[SignalWire] Page visible, re-enumerating devices");
|
|
21836
23135
|
try {
|
|
@@ -21842,7 +23141,7 @@ var SignalWire = class extends Destroyable {
|
|
|
21842
23141
|
logger$1.warn("[SignalWire] Failed to initialize VisibilityController:", error);
|
|
21843
23142
|
}
|
|
21844
23143
|
try {
|
|
21845
|
-
this._diagnosticsCollector = new DiagnosticsCollector({ sdkVersion: "4.0.0-rc.
|
|
23144
|
+
this._diagnosticsCollector = new DiagnosticsCollector({ sdkVersion: "4.0.0-rc.3" });
|
|
21846
23145
|
} catch (error) {
|
|
21847
23146
|
logger$1.warn("[SignalWire] Failed to initialize DiagnosticsCollector:", error);
|
|
21848
23147
|
}
|
|
@@ -21913,21 +23212,22 @@ var SignalWire = class extends Destroyable {
|
|
|
21913
23212
|
this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
21914
23213
|
throw error;
|
|
21915
23214
|
}
|
|
21916
|
-
logger$1.debug("[SignalWire] Failed to register user,
|
|
21917
|
-
|
|
21918
|
-
await this._clientSession.reauthenticate(this._deps.credential.token);
|
|
21919
|
-
logger$1.debug("[SignalWire] Reauthentication successful, retrying register()");
|
|
23215
|
+
logger$1.debug("[SignalWire] Failed to register user, attempting credential recovery...");
|
|
23216
|
+
const outcome = await this.recoverAndRetry(async () => {
|
|
21920
23217
|
await this._transport.execute(RPCExecute({
|
|
21921
23218
|
method: "subscriber.online",
|
|
21922
23219
|
params: {}
|
|
21923
23220
|
}));
|
|
23221
|
+
});
|
|
23222
|
+
if (outcome.ok) {
|
|
23223
|
+
logger$1.debug("[SignalWire] Recovery restored registration");
|
|
21924
23224
|
this._isRegistered$.next(true);
|
|
21925
|
-
|
|
21926
|
-
logger$1.error("[SignalWire] Reauthentication failed during register():", reauthError);
|
|
21927
|
-
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 }) });
|
|
21928
|
-
this._errors$.next(registerError);
|
|
21929
|
-
throw registerError;
|
|
23225
|
+
return;
|
|
21930
23226
|
}
|
|
23227
|
+
const failureCause = outcome.error ?? error;
|
|
23228
|
+
const registerError = new InvalidCredentialsError("Failed to register user, and credential recovery also failed. Please check your credentials.", { cause: failureCause instanceof Error ? failureCause : new Error(String(failureCause), { cause: failureCause }) });
|
|
23229
|
+
this._errors$.next(registerError);
|
|
23230
|
+
throw registerError;
|
|
21931
23231
|
}
|
|
21932
23232
|
}
|
|
21933
23233
|
/**
|
|
@@ -21958,6 +23258,11 @@ var SignalWire = class extends Destroyable {
|
|
|
21958
23258
|
* Returns a {@link Call} in `'ringing'` state. Subscribe to {@link Call.status$}
|
|
21959
23259
|
* to track progression through `'connected'` → `'disconnected'`.
|
|
21960
23260
|
*
|
|
23261
|
+
* Local media acquisition is deliberately unbounded: an unanswered permission
|
|
23262
|
+
* prompt leaves this promise pending indefinitely, so apply your own bound if
|
|
23263
|
+
* your UI needs one. The 12 s signaling budget starts only once acquisition
|
|
23264
|
+
* settles.
|
|
23265
|
+
*
|
|
21961
23266
|
* @param destination - Address URI string (e.g. `'/public/my-room'`) or {@link Address} instance.
|
|
21962
23267
|
* @param options - Media and dial options (audio/video, device constraints). Overrides defaults.
|
|
21963
23268
|
* @returns The created {@link Call} instance.
|
|
@@ -21980,7 +23285,18 @@ var SignalWire = class extends Destroyable {
|
|
|
21980
23285
|
};
|
|
21981
23286
|
await this.waitAuthentication();
|
|
21982
23287
|
logger$1.debug("[SignalWire] Dialing with options:", computed_options);
|
|
21983
|
-
|
|
23288
|
+
try {
|
|
23289
|
+
return await this._clientSession.createOutboundCall(destination, computed_options);
|
|
23290
|
+
} catch (error) {
|
|
23291
|
+
if (!isRecoverableAuthError(error)) throw error;
|
|
23292
|
+
logger$1.debug("[SignalWire] Dial hit a recoverable auth error; recovering the session and retrying");
|
|
23293
|
+
const outcome = await this.recoverAndRetry(async () => {
|
|
23294
|
+
await this.waitAuthentication();
|
|
23295
|
+
return this._clientSession.createOutboundCall(destination, computed_options);
|
|
23296
|
+
});
|
|
23297
|
+
if (outcome.ok) return outcome.value;
|
|
23298
|
+
throw outcome.error ?? error;
|
|
23299
|
+
}
|
|
21984
23300
|
}
|
|
21985
23301
|
/**
|
|
21986
23302
|
* Runs a multi-phase connectivity test against the given destination.
|
|
@@ -22261,10 +23577,13 @@ var SignalWire = class extends Destroyable {
|
|
|
22261
23577
|
destroy() {
|
|
22262
23578
|
this._refreshCoordinator?.destroy();
|
|
22263
23579
|
this._refreshCoordinator = void 0;
|
|
23580
|
+
this._refreshHttp?.destroy();
|
|
23581
|
+
this._refreshHttp = void 0;
|
|
22264
23582
|
this._dpopManager?.destroy();
|
|
22265
|
-
this._clientSession
|
|
22266
|
-
|
|
22267
|
-
this.
|
|
23583
|
+
const session = this._clientSession;
|
|
23584
|
+
session?.teardownSessionState();
|
|
23585
|
+
this._transport?.destroy();
|
|
23586
|
+
session?.destroy();
|
|
22268
23587
|
try {
|
|
22269
23588
|
this._networkMonitor?.destroy();
|
|
22270
23589
|
} catch {}
|
|
@@ -22377,7 +23696,7 @@ var StaticCredentialProvider = class {
|
|
|
22377
23696
|
/**
|
|
22378
23697
|
* Library version from package.json, injected at build time.
|
|
22379
23698
|
*/
|
|
22380
|
-
const version = "4.0.0-rc.
|
|
23699
|
+
const version = "4.0.0-rc.3";
|
|
22381
23700
|
/**
|
|
22382
23701
|
* Flag indicating the library has been loaded and is ready to use.
|
|
22383
23702
|
* For UMD builds: `window.SignalWire.ready`
|
|
@@ -22399,7 +23718,7 @@ const ready = true;
|
|
|
22399
23718
|
*/
|
|
22400
23719
|
const emitReadyEvent = () => {
|
|
22401
23720
|
if (typeof window !== "undefined") {
|
|
22402
|
-
const event = new CustomEvent("signalwire:js:ready", { detail: { version: "4.0.0-rc.
|
|
23721
|
+
const event = new CustomEvent("signalwire:js:ready", { detail: { version: "4.0.0-rc.3" } });
|
|
22403
23722
|
window.dispatchEvent(event);
|
|
22404
23723
|
}
|
|
22405
23724
|
};
|
|
@@ -22423,5 +23742,5 @@ emitReadyEvent();
|
|
|
22423
23742
|
if (typeof process === "undefined") globalThis.process = { env: { NODE_ENV: "production" } };
|
|
22424
23743
|
|
|
22425
23744
|
//#endregion
|
|
22426
|
-
export { Address, CallCreateError, ClientPreferences, CollectionFetchError, DPoPInitError, DeviceTokenError, EmbedTokenCredentialProvider, InvalidCredentialsError, MediaTrackError, MessageParseError, OverconstrainedFallbackError, Participant, PreflightError, RecoveryError, SelfCapabilities, SelfParticipant, SignalWire, StaticCredentialProvider, TokenRefreshError, UnexpectedError, User, VertoPongError, WebRTCCall, embeddableCall, getLogger, isSelfParticipant, ready, setDebugOptions, setLogLevel, setLogger, version };
|
|
23745
|
+
export { Address, AuxiliaryLegCancelledError, AuxiliaryLegTimeoutError, CallCreateError, CallNotReadyError, ClientPreferences, CollectionFetchError, DPoPInitError, DeviceTokenError, EmbedTokenCredentialProvider, InvalidCredentialsError, MediaAccessError, MediaTrackError, MessageParseError, OverconstrainedFallbackError, Participant, ParticipantNotReadyError, PreflightError, RecoveryError, ScreenShareAlreadyActiveError, SelfCapabilities, SelfParticipant, SignalWire, StaticCredentialProvider, TokenRefreshError, UnexpectedError, User, VertoPongError, WebRTCCall, embeddableCall, getLogger, isSelfParticipant, ready, setDebugOptions, setLogLevel, setLogger, version };
|
|
22427
23746
|
//# sourceMappingURL=browser.mjs.map
|