@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 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$6() {
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$6;
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$5() {
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$5;
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$5() {
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$5;
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$7(project, resultSelector) {
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$7;
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$33 = _loggersByName[name];
9468
- if (!logger$33) logger$33 = _loggersByName[name] = new Logger(name, defaultLogger$1.methodFactory);
9469
- return logger$33;
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$33) => {
9506
- userLogger = logger$33;
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$33 = getLoggerInstance();
9551
- return new Proxy(logger$33, { get(_target, prop, _receiver) {
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$32 = getLogger();
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$32.error("[HTTPRequestController] Request error:", error);
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$32.debug("[HTTPRequestController] Executing request:", {
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$32.debug("[HTTPRequestController] Response received:", {
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$32.error("[HTTPRequestController] Request failed:", error);
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$32.debug("[HTTPRequestController] Using Bearer token auth, token length:", credential.token.length);
9739
- } else logger$32.warn("[HTTPRequestController] No credentials available for authentication");
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$31 = getLogger();
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$31.error(`[ClientPreferences] Failed to save preferences: ${String(error)}`);
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$31.error(`[ClientPreferences] Failed to load preferences: ${String(error)}`);
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$30 = getLogger();
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$30.debug("[DeviceController] Device change detected");
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$30.debug("[DeviceController] Selected audio input device changed:", info))));
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$30.debug("[DeviceController] Selected audio output device changed:", info))));
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$30.debug("[DeviceController] Selected video input device changed:", info))));
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$30.debug("[DeviceController] Setting selected video input device:", device);
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$30.debug(`[DeviceController] Device disappeared, falling back to history: ${fromHistory.label}`);
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$30.error(`[DeviceController] Failed to persist device selection for ${kind}:`, error);
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$30.error(`[DeviceController] Failed to load persisted device for ${kind}:`, error);
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$30.debug("[DeviceController] Polling devices due to interval");
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$30.debug("[DeviceController] Devices enumerated:", {
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$30.error("[DeviceController] Failed to enumerate devices:", error);
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$30.error("[DeviceController] Failed to get device capabilities:", error);
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$29 = getLogger();
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$29.warn("[DPoP] Failed to load key pair from IndexedDB:", error);
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$29.warn("[DPoP] Failed to save key pair to IndexedDB:", error);
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$29.warn("[DPoP] Failed to delete key pair from IndexedDB:", error);
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$29.debug("[DPoP] Key pair restored from IndexedDB, fingerprint:", this._fingerprint);
11602
+ logger$30.debug("[DPoP] Key pair restored from IndexedDB, fingerprint:", this._fingerprint);
11481
11603
  return this._fingerprint;
11482
11604
  } catch (error) {
11483
- logger$29.warn("[DPoP] Stored key pair unusable, generating new one:", error);
11605
+ logger$30.warn("[DPoP] Stored key pair unusable, generating new one:", error);
11484
11606
  await deleteKeyPairFromDB();
11485
11607
  }
11486
- logger$29.debug("[DPoP] Generating RSA key pair");
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$29.debug("[DPoP] Key pair generated and persisted, fingerprint:", this._fingerprint);
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$29.debug("[DPoP] Controller destroyed");
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$28 = getLogger();
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$28.debug("NetworkMonitor: no browser environment detected, skipping event listeners");
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$28.debug("NetworkMonitor: event listeners attached");
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$28.debug("NetworkMonitor: event listeners removed");
11789
+ logger$29.debug("NetworkMonitor: event listeners removed");
11668
11790
  }
11669
11791
  handleOnline() {
11670
- logger$28.info("NetworkMonitor: browser went online");
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$28.info("NetworkMonitor: browser went offline");
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$28.info(`NetworkMonitor: connection changed — effectiveType=${networkType ?? "unknown"}`);
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$27 = getLogger();
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$27.warn("[PreflightRunner] Media bandwidth test failed:", error);
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$27.error("[PreflightRunner] Preflight test failed:", error);
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$27.warn("[PreflightRunner] Device test failed:", error);
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$27.warn("[PreflightRunner] ICE connectivity test failed:", error);
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$26 = getLogger();
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$26.debug("VisibilityController: listening for visibilitychange events");
12040
- } else logger$26.debug("VisibilityController: document visibility API not available, defaulting to visible");
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$26.debug("VisibilityController: removed visibilitychange listener");
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$26.debug("VisibilityController: visibility changed", {
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$25 = getLogger();
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$25.warn("[AttachManager] Failed to retrieve attached calls from storage", error);
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$25.warn("[AttachManager] Failed to write attached calls to storage", error);
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$25.warn("[AttachManager] Skip attach for calls with no destination");
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
- const attachment = {
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
- * Failed reattach attempts are handled gracefully the stale call
12411
- * reference is cleaned up from storage.
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$25.info(`[AttachManager] Reattached call ${callId} (attempt ${attempt})`);
12620
+ logger$26.info(`[AttachManager] Reattached call ${callId} (attempt ${attempt})`);
12426
12621
  succeeded = true;
12427
12622
  break;
12428
12623
  } catch (error) {
12429
- logger$25.warn(`[AttachManager] Reattach attempt ${attempt}/3 failed for call ${callId}:`, error);
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$25.warn(`[AttachManager] Reattach failed after 3 attempts for call ${callId}, removing reference`);
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$4() {
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$4;
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$24 = getLogger();
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, executeMethod, deviceController) {
13814
+ constructor(id, callExecuteMethod, deviceController) {
13615
13815
  super();
13616
- this.executeMethod = executeMethod;
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
- const method = toggleDeafMethod(this.deaf);
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(this.id, toggleHandraiseMethod(this.handraised), {});
14078
+ await this.executeMethod(toggleHandraiseMethod(this.handraised), {});
13846
14079
  }
13847
14080
  /** Mutes the participant's audio. */
13848
14081
  async mute() {
13849
- await this.executeMethod(this.id, "call.mute", { channels: ["audio"] });
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(this.id, "call.unmute", { channels: ["audio"] });
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(this.id, "call.mute", { channels: ["video"] });
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(this.id, "call.unmute", { channels: ["video"] });
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(this.id, "call.audioflags.set", {
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(this.id, "call.audioflags.set", {
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(this.id, "call.audioflags.set", {
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(this.id, "call.lowbitrate.set", { lowbitrate: !this.lowbitrate });
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(this.id, "call.microphone.sensitivity.set", { sensitivity: value });
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(this.id, "call.microphone.volume.set", { volume: value });
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(this.id, "call.speaker.volume.set", { volume: value });
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 keys positions by the
13946
- * **target member's own** `call_id`/`node_id` (see issue #19400 and the legacy
13947
- * `setPositions` implementation), so this sends the participant's own call
13948
- * context matching {@link Participant.remove}. A resolved promise does not
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
- const state = this._state$.value;
13956
- const target = {
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
- const state = this._state$.value;
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(this.id, "call.end", {});
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.executeMethod = void 0;
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, executeMethod, vertoManager, deviceController) {
14011
- super(id, executeMethod, deviceController);
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(this.id, "call.audioflags.set", {
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(this.id, "call.audioflags.set", {
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
- /** Starts sharing the local screen. */
14055
- async startScreenShare() {
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$24.error("[Participant.startScreenShare] Screen share error:", error);
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
- /** Adds an additional media input device to the call. */
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$24.error("[Participant.startScreenShare] Screen share error:", error);
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
- /** Updates the audio input track constraints for the active call. */
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
- await this.vertoManager.updateMediaConstraints({ audio: constraints });
14368
+ return this.vertoManager.updateMediaConstraints({ audio: constraints });
14116
14369
  }
14117
- /** Updates both audio and video input track constraints for the active call. */
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
- await this.vertoManager.updateMediaConstraints(constraints);
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
- /** Updates the video input track constraints for the active call. */
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
- await this.vertoManager.updateMediaConstraints({ video: constraints });
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$24.debug("[SelfParticipant] Exiting studio audio mode due to individual flag toggle");
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$24.warn("[Participant.toggleAudioInput] Server Error while muting audio input, proceeding with local toggle anyway", error);
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$24.warn("[Participant.toggleAudioInput] Server Error while unmuting audio input, proceeding with local toggle anyway", error);
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$24.warn("[Participant.toggleVideoInput] Server Error while muting video input, proceeding with local toggle anyway", error);
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$24.warn("[Participant.toggleVideoInput] Server Error while unmuting video input, proceeding with local toggle anyway", error);
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$23 = getLogger();
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$23.error("[throwOnRPCError] RPC error response:", {
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$23.debug("[throwOnRPCError] RPC successful response:", response);
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$22 = getLogger();
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$22.debug("[CallEventsManager] Handling call.joined event for call/session IDs:", {
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$22.debug("[CallEventsManager] Handling member update event for member ID:", member);
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$22.debug("[CallEventsManager] Handling member.left event for member ID:", memberLeftEvent.member.member_id);
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$22.warn(`[CallEventsManager] Received member.left event for unknown member ID: ${memberLeftEvent.member.member_id}`);
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$22.debug("[CallEventsManager] Handling call.updated event:", callUpdatedEvent);
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$22.debug("[CallEventsManager] Handling layout.changed event:", layoutChangedEvent);
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$22.warn("[CallEventsManager] No layers with member_id found in layout.changed event. Nothing to update.");
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$22.warn(`[CallEventsManager] Skipping layout layer for unknown member_id: ${layer.member_id}`);
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: response.result.layouts
14878
+ layouts
14615
14879
  });
14616
14880
  }).catch((error) => {
14617
- logger$22.error("[CallEventsManager] Error fetching layouts:", error);
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$22.debug("[CallEventsManager] Updating participant:", member.member_id, {
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$22.debug("[CallEventsManager] Call joined event:", event);
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$22.debug("[CallEventsManager] Layout changed event:", event);
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$22.debug("[CallEventsManager] Member update event:", event);
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
- logger$19.debug("[LocalStreamController] Requesting display media for screen sharing with audio:", Boolean(this.options.inputAudioDeviceConstraints));
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: Boolean(this.options.inputAudioDeviceConstraints)
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 Promise.resolve();
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: stop the current track, acquire a new
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
- * This is critical for iOS Safari where applyConstraints on audio tracks
15596
- * silently fails or throws.
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 constraintsWithDevice = {
15601
- ...mergedConstraints,
15602
- ...deviceId ? { deviceId: { exact: deviceId } } : {}
15603
- };
15604
- const trackId = oldTrack.id;
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 { localStream } = this;
15683
- if (!localStream) {
15684
- logger$17.warn("[RTCPeerConnectionController] No local stream available to update input device.");
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(toError(error));
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
- const localStream = this.localStream ?? await this.localStreamController.buildLocalStream();
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?.addStream(localStream);
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?.addTrack(track, localStream);
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(toError(error));
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
- * Return the lazily-created {@link LocalAudioPipeline}, constructing it on
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(toError(error));
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(toError(error));
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
- await this.transceiverController?.updateSendersConstraints(kind, constraints);
16975
+ if (kind === "audio" && this._localAudioPipeline) {
16976
+ if (!constraints) {
16977
+ this.stopTrackSender("audio");
16978
+ return false;
16979
+ }
16980
+ return this.applyPipelineSourceConstraints(constraints);
16981
+ }
16982
+ return await this.transceiverController?.updateSendersConstraints(kind, constraints) ?? false;
16416
16983
  }
16417
16984
  /**
16418
- * Replace the current audio track with a new one using the given constraints.
16419
- * Used for server-pushed audio constraint changes where applyConstraints
16420
- * fails on iOS Safari. Stops the current track, acquires a new one via
16421
- * getUserMedia, and replaces the sender track.
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 replaceAudioTrackWithConstraints(constraints) {
16424
- const senders = this.peerConnection?.getSenders().filter((s) => s.track?.kind === "audio" && s.track.readyState === "live");
16425
- if (!senders || senders.length === 0) {
16426
- logger$17.warn("[RTCPeerConnectionController] No live audio sender to replace");
16427
- return;
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
- for (const sender of senders) {
16430
- const oldTrack = sender.track;
16431
- if (!oldTrack) continue;
16432
- const { deviceId } = oldTrack.getSettings();
16433
- const mergedConstraints = {
16434
- ...oldTrack.getConstraints(),
16435
- ...constraints,
16436
- ...deviceId ? { deviceId: { exact: deviceId } } : {}
16437
- };
16438
- const trackId = oldTrack.id;
16439
- oldTrack.stop();
16440
- this.localStreamController.removeTrack(trackId);
16441
- const newTrack = (await this.getUserMedia({ audio: mergedConstraints })).getAudioTracks()[0];
16442
- await sender.replaceTrack(newTrack);
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
- logger$17.debug(`[RTCPeerConnectionController] Audio track replaced for server-pushed params. New track: ${newTrack.id}`);
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._screenShareTimeoutMs = 5e4;
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
- (async () => {
16691
- try {
16692
- if (audio && rtcPeerConnController) await rtcPeerConnController.replaceAudioTrackWithConstraints(audio);
16693
- if (video) await rtcPeerConnController?.updateSendersConstraints("video", video);
16694
- this.webRtcCallSession.emitMediaParamsUpdated({
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.attach(this.buildAttachableCall());
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
- * and always overwrites unconditionally.
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.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
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 webrtcVertoMessage = WebrtcVerto({
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
- if (response.error) {
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
- if (innerResult?.error) {
16903
- const error = new JSONRPCError(innerResult.error.code, innerResult.error.message, innerResult.error.data);
16904
- this.onError?.(error);
16905
- return response;
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.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
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.onError?.(modifyError);
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.onError?.(signalingError);
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 (!response.error && getValueFrom(response, "result.result.result.message") === "CALL CREATED") {
17683
+ if (getValueFrom(response, "result.result.result.message") === "CALL CREATED") {
16961
17684
  this.emitMainSignalingStatus(rtcPeerConnController.id, "trying");
16962
- this._nodeId$.next(getValueFrom(response, "result.node_id") ?? null);
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") ?? null;
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
- if (callId) {
16973
- this.webRtcCallSession.addCallId(callId);
16974
- this.attachManager.attach(this.buildAttachableCall(callId));
16975
- } else logger$16.warn("[WebRTCManager] Cannot attach call, missing callId:", {
16976
- nodeId: this.nodeId,
16977
- callId
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
- const inviteError = response.error ? new JSONRPCError(response.error.code, response.error.message, response.error.data) : /* @__PURE__ */ new Error("Verto invite failed: unexpected response");
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.onError?.(error);
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.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
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.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
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.onError?.(error);
17949
+ this.reportLegError(error, this.mainPeerConnection);
17228
17950
  throw error;
17229
17951
  }
17230
17952
  }
17231
- async addScreenMedia(options = { audio: false }) {
17232
- await this.initAdditionalPeerConnection("screenshare", options);
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 (propose === "screenshare") this._screenShareId = rtcPeerConnController.id;
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
- this.onError?.(error);
17977
+ firstPeerConnectionError ??= error;
17978
+ this.reportLegError(error, rtcPeerConnController);
17250
17979
  });
17251
- await (0, import_cjs$15.firstValueFrom)(rtcPeerConnController.connectionState$.pipe((0, import_cjs$15.filter)((state) => state === "connected"), (0, import_cjs$15.take)(1), (0, import_cjs$15.timeout)(this._screenShareTimeoutMs), (0, import_cjs$15.takeUntil)(this.destroyed$)));
17252
- this._screenShareStatus$.next("started");
17253
- logger$16.info("[WebRTCManager] Screen share started successfully.");
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
- logger$16.warn("[WebRTCManager] Error initializing additional peer connection:", error);
17257
- this.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
17258
- if (rtcPeerConnController) rtcPeerConnController.destroy();
17259
- this._screenShareStatus$.next("none");
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)) logger$16.warn("[WebRTCManager] No active screen share to stop.");
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.selfId ?? "", method, {});
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 member ID string, or a {@link MemberTarget} object.
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 params = this.buildMethodParams(target, args, method);
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
- buildMethodParams(target, args, method) {
18415
- const self = {
18416
- node_id: this.nodeId ?? "",
18417
- call_id: this.id,
18418
- member_id: this.vertoManager.selfId ?? ""
18419
- };
18420
- if (method === POSITION_SET_METHOD) return {
18421
- ...args,
18422
- self
18423
- };
18424
- if (typeof target === "object") return {
18425
- ...args,
18426
- self,
18427
- targets: [target]
18428
- };
18429
- return {
18430
- ...args,
18431
- self,
18432
- target: {
18433
- node_id: this.nodeId ?? "",
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-pushed media params are applied. */
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-pushed media params are applied.
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 selfId = await (0, import_cjs$11.firstValueFrom)(this.selfId$.pipe((0, import_cjs$11.filter)((id) => id !== null)));
19016
- await this.executeMethod(selfId, "call.layout.set", { layout });
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
- await participant.setPosition(position);
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
- await this.vertoManager.updateMediaConstraints({ audio: { echoCancellation: enabled } });
20008
+ return this.vertoManager.updateMediaConstraints({ audio: { echoCancellation: enabled } });
19131
20009
  }
19132
- /** Toggle browser noise suppression on the local mic at runtime. */
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
- await this.vertoManager.updateMediaConstraints({ audio: { noiseSuppression: enabled } });
20015
+ return this.vertoManager.updateMediaConstraints({ audio: { noiseSuppression: enabled } });
19135
20016
  }
19136
- /** Toggle browser automatic gain control on the local mic at runtime. */
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
- await this.vertoManager.updateMediaConstraints({ audio: { autoGainControl: enabled } });
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
- /** Determines whether an error should be fatal (destroy the call). */
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
- try {
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) only.
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 if (this.onBeforeReconnect && this.clientBound) {
20031
- logger$9.debug("[Session] Refreshing credentials before fresh connect");
20032
- await this.onBeforeReconnect();
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 ((!isReconnect || this.clientBound) && this.dpopManager?.initialized) try {
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 (0, import_cjs$7.firstValueFrom)((0, import_cjs$7.race)(callSession.selfId$.pipe((0, import_cjs$7.filter)((id) => Boolean(id)), (0, import_cjs$7.take)(1), (0, import_cjs$7.timeout)(this.callCreateTimeout)), callSession.errors$.pipe((0, import_cjs$7.take)(1), (0, import_cjs$7.switchMap)((callError) => (0, import_cjs$7.throwError)(() => callError.error)))));
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._currentToken$.next(tokenData);
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._currentToken$.next(newTokenData);
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(async () => {
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
- if (!provider.refresh) throw new InvalidCredentialsError("Credential provider does not support refresh");
20592
- const newCredentials = await provider.refresh();
20593
- this.deps.store.write(newCredentials);
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
- }, refreshInterval);
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: this._deps.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
- if (this.isConnected && this._clientSession.authenticated && _credentials.token) try {
21561
- await this._clientSession.reauthenticate(_credentials.token);
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
- try {
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.onBeforeReconnect = async () => {
21655
- if (!this._credentialProvider) return;
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.1" });
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, trying reauthentication...");
21917
- try {
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
- } catch (reauthError) {
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
- return this._clientSession.createOutboundCall(destination, computed_options);
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.teardownSessionState();
22266
- this._transport.destroy();
22267
- this._clientSession.destroy();
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.1";
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.1" } });
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