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