@signalwire/js 4.0.0-beta.12 → 4.0.0-beta.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/browser.mjs CHANGED
@@ -1,5 +1,3 @@
1
- import { randomFillSync, randomUUID } from "node:crypto";
2
-
3
1
  //#region rolldown:runtime
4
2
  var __create = Object.create;
5
3
  var __defProp = Object.defineProperty;
@@ -9017,6 +9015,151 @@ var Destroyable = class {
9017
9015
  }
9018
9016
  };
9019
9017
 
9018
+ //#endregion
9019
+ //#region src/core/constants.ts
9020
+ const INVITE_VERSION = 1e3;
9021
+ const DEFAULT_ICE_CANDIDATE_TIMEOUT_MS = 600;
9022
+ const DEFAULT_ICE_GATHERING_TIMEOUT_MS = 6e3;
9023
+ const DEFAULT_RECONNECT_CALLS_TIMEOUT_MS = 300 * 1e3;
9024
+ const DEFAULT_CONNECTION_TIMEOUT_MS = 1e4;
9025
+ const DEFAULT_RECONNECT_DELAY_MIN_MS = 100;
9026
+ const DEFAULT_RECONNECT_DELAY_MAX_MS = 3e3;
9027
+ const DEFAULT_DEVICE_DEBOUNCE_TIME_MS = 1500;
9028
+ const DEFAULT_DEVICE_POLLING_INTERVAL_MS = 0;
9029
+ const PREFERENCES_STORAGE_KEY = "sw:preferences";
9030
+ /** Scope value that enables automatic token refresh. */
9031
+ const SAT_REFRESH_SCOPE = "sat:refresh";
9032
+ /** API endpoints for device token operations. */
9033
+ const DEVICE_TOKEN_ENDPOINT = "/api/fabric/subscriber/devices/token";
9034
+ const DEVICE_REFRESH_ENDPOINT = "/api/fabric/subscriber/devices/refresh";
9035
+ /** Default device token TTL in seconds (15 minutes). */
9036
+ const DEVICE_TOKEN_DEFAULT_EXPIRE_IN = 900;
9037
+ /** Buffer time in milliseconds before expiry to trigger refresh. */
9038
+ const DEVICE_TOKEN_REFRESH_BUFFER_MS = 3e4;
9039
+ /** Maximum retry attempts for device token refresh on transient failure. */
9040
+ const DEVICE_TOKEN_REFRESH_MAX_RETRIES = 3;
9041
+ /** Base delay in milliseconds for exponential backoff on refresh retry. */
9042
+ const DEVICE_TOKEN_REFRESH_RETRY_BASE_MS = 1e3;
9043
+ /** Maximum retry attempts for developer credential refresh on transient failure. */
9044
+ const CREDENTIAL_REFRESH_MAX_RETRIES = 5;
9045
+ /** Base delay in milliseconds for exponential backoff on credential refresh retry. */
9046
+ const CREDENTIAL_REFRESH_RETRY_BASE_MS = 1e3;
9047
+ /** Maximum delay in milliseconds for credential refresh backoff. */
9048
+ const CREDENTIAL_REFRESH_MAX_DELAY_MS = 3e4;
9049
+ /** Buffer in milliseconds before token expiry to trigger refresh. */
9050
+ const CREDENTIAL_REFRESH_BUFFER_MS = 5e3;
9051
+ /**
9052
+ * Clock-skew allowance (ms) for treating an in-memory credential as expired
9053
+ * when deciding whether a fresh (re)connect must re-mint the token before
9054
+ * authenticating. A token within this window of its `expiry_at` is treated as
9055
+ * stale so the reconnect re-mints via the credential provider instead of
9056
+ * replaying a dead token (which the server rejects with -32003).
9057
+ */
9058
+ const CREDENTIAL_EXPIRY_SKEW_MS = 3e4;
9059
+ /**
9060
+ * Maximum time the coordinator will wait for `DeviceTokenManager.activate()`
9061
+ * to resolve before treating the activation as failed and falling back to
9062
+ * the developer-provided refresh path. Prevents a wedged HTTP layer from
9063
+ * leaving the session with no active refresh mechanism.
9064
+ */
9065
+ const CREDENTIAL_ACTIVATE_TIMEOUT_MS = 3e4;
9066
+ /** JSON-RPC error code for requester validation failure (corrupted auth state). */
9067
+ const RPC_ERROR_REQUESTER_VALIDATION_FAILED = -32003;
9068
+ /** JSON-RPC error code for invalid params (e.g., missing authentication block). */
9069
+ const RPC_ERROR_INVALID_PARAMS = -32602;
9070
+ /** JSON-RPC error code for authentication failure (invalid token, missing DPoP, etc.). */
9071
+ const RPC_ERROR_AUTHENTICATION_FAILED = -32002;
9072
+ /** Error names browsers use for a media permission denial (user or policy). */
9073
+ const MEDIA_ACCESS_DENIAL_NAMES = [
9074
+ "NotAllowedError",
9075
+ "SecurityError",
9076
+ "PermissionDeniedError"
9077
+ ];
9078
+ /** Default polling interval for RTCPeerConnection.getStats() in milliseconds. */
9079
+ const DEFAULT_STATS_POLLING_INTERVAL_MS = 1e3;
9080
+ /** Number of initial samples used to build a baseline for spike detection. */
9081
+ const DEFAULT_STATS_BASELINE_SAMPLES = 10;
9082
+ /** Duration in ms with no inbound audio packets before emitting a critical issue. */
9083
+ const DEFAULT_STATS_NO_PACKET_THRESHOLD_MS = 2e3;
9084
+ /** Multiplier applied to baseline RTT to detect a warning-level RTT spike. */
9085
+ const DEFAULT_STATS_RTT_SPIKE_MULTIPLIER = 3;
9086
+ /** Packet loss fraction (0-1) above which a warning is emitted. */
9087
+ const DEFAULT_STATS_PACKET_LOSS_THRESHOLD = .05;
9088
+ /** Multiplier applied to baseline jitter to detect a jitter spike. */
9089
+ const DEFAULT_STATS_JITTER_SPIKE_MULTIPLIER = 4;
9090
+ /** Number of seconds of metrics history to retain. */
9091
+ const DEFAULT_STATS_HISTORY_SIZE = 30;
9092
+ /** Maximum keyframe requests allowed within a single burst window. */
9093
+ const DEFAULT_KEYFRAME_MAX_BURST$1 = 3;
9094
+ /** Duration of the keyframe burst window in milliseconds. */
9095
+ const DEFAULT_KEYFRAME_BURST_WINDOW_MS$1 = 3e3;
9096
+ /** Cooldown period in ms after burst limit is reached before allowing more keyframes. */
9097
+ const DEFAULT_KEYFRAME_COOLDOWN_MS$1 = 1e4;
9098
+ /** Minimum time between re-INVITE attempts in milliseconds. */
9099
+ const DEFAULT_REINVITE_DEBOUNCE_TIME_MS = 1e4;
9100
+ /** Maximum number of re-INVITE attempts per call. */
9101
+ const DEFAULT_REINVITE_MAX_ATTEMPTS = 3;
9102
+ /** Timeout for a single re-INVITE attempt in milliseconds. */
9103
+ const DEFAULT_REINVITE_TIMEOUT_MS = 5e3;
9104
+ /** Debounce window in ms to collapse multiple detection signals into one trigger. */
9105
+ const DEFAULT_RECOVERY_DEBOUNCE_TIME_MS = 2e3;
9106
+ /** Cooldown period in ms between recovery attempts. */
9107
+ const DEFAULT_RECOVERY_COOLDOWN_MS = 1e4;
9108
+ /** Grace period in ms before treating ICE 'disconnected' as a failure. */
9109
+ const DEFAULT_ICE_DISCONNECTED_GRACE_PERIOD_MS = 3e3;
9110
+ /** Timeout for a single ICE restart attempt in milliseconds. */
9111
+ const DEFAULT_ICE_RESTART_TIMEOUT_MS$1 = 5e3;
9112
+ /** Maximum recovery attempts before emitting 'max_attempts_reached'. */
9113
+ const DEFAULT_MAX_RECOVERY_ATTEMPTS = 3;
9114
+ /** Upper bound in ms for waiting on iceGatheringState === 'complete' after an ICE restart. */
9115
+ const ICE_GATHERING_COMPLETE_TIMEOUT_MS = 1e4;
9116
+ /** Upper bound in ms for waiting on RTCPeerConnection.connectionState === 'connected' after a recovery ICE restart. */
9117
+ const PEER_CONNECTION_RECOVERY_WAIT_MS = 5e3;
9118
+ /** Polling interval in ms while waiting for RTCPeerConnection.connectionState to transition. */
9119
+ const PEER_CONNECTION_RECOVERY_POLL_MS = 100;
9120
+ /** Polling interval for LocalAudioPipeline.level$ (ms). ~30fps is smooth for meters. */
9121
+ const AUDIO_LEVEL_POLL_INTERVAL_MS = 33;
9122
+ /** RMS level threshold (0..1) above which the local participant is considered speaking. */
9123
+ const VAD_THRESHOLD = .03;
9124
+ /** Hold window in ms below the threshold before speaking$ flips back to false. */
9125
+ const VAD_HOLD_MS = 250;
9126
+ /** Whether to persist device selections to storage by default. */
9127
+ const DEFAULT_PERSIST_DEVICE_SELECTION = true;
9128
+ /** Whether to auto-apply device changes to active calls by default. */
9129
+ const DEFAULT_SYNC_DEVICES_TO_ACTIVE_CALLS = true;
9130
+ /** Storage keys for persisted device selections. */
9131
+ const DEVICE_STORAGE_KEY_AUDIO_INPUT = "sw:device:audioinput";
9132
+ const DEVICE_STORAGE_KEY_AUDIO_OUTPUT = "sw:device:audiooutput";
9133
+ const DEVICE_STORAGE_KEY_VIDEO_INPUT = "sw:device:videoinput";
9134
+ /** Whether to auto-mute video when the tab becomes hidden. */
9135
+ const DEFAULT_AUTO_MUTE_VIDEO_ON_HIDDEN = false;
9136
+ /** Whether to re-enumerate devices when the page becomes visible. */
9137
+ const DEFAULT_REFRESH_DEVICES_ON_VISIBLE = true;
9138
+ /** Whether to check peer connection health when the page becomes visible. */
9139
+ const DEFAULT_CHECK_CONNECTION_ON_VISIBLE = true;
9140
+ /** Whether automatic video degradation on low bandwidth is enabled. */
9141
+ const DEFAULT_ENABLE_AUTO_DEGRADATION = true;
9142
+ /** Bitrate in kbps below which video is automatically disabled. */
9143
+ const DEFAULT_DEGRADATION_BITRATE_THRESHOLD_KBPS = 150;
9144
+ /** Bitrate in kbps above which video is automatically re-enabled (hysteresis). */
9145
+ const DEFAULT_DEGRADATION_RECOVERY_THRESHOLD_KBPS = 300;
9146
+ /** Whether relay-only escalation is enabled as a last-resort recovery tier. */
9147
+ const DEFAULT_ENABLE_RELAY_FALLBACK = true;
9148
+ /** Whether to listen for browser online/offline/connection events. */
9149
+ const DEFAULT_ENABLE_NETWORK_CHANGE_DETECTION = true;
9150
+ /** Whether to intercept server-sent media-timeout hangups and attempt recovery. */
9151
+ const DEFAULT_ENABLE_SERVER_HANGUP_INTERCEPTION = true;
9152
+ /** Default video track constraints applied when video is enabled without explicit constraints. */
9153
+ const DEFAULT_VIDEO_CONSTRAINTS = {
9154
+ width: { ideal: 1280 },
9155
+ height: { ideal: 720 },
9156
+ aspectRatio: 16 / 9
9157
+ };
9158
+ /** Whether stereo Opus is enabled by default. */
9159
+ const DEFAULT_STEREO_AUDIO = false;
9160
+ /** Max average bitrate for stereo Opus in bits per second. */
9161
+ const DEFAULT_STEREO_MAX_AVERAGE_BITRATE = 51e4;
9162
+
9020
9163
  //#endregion
9021
9164
  //#region src/core/errors.ts
9022
9165
  var UnexpectedError = class extends Error {
@@ -9146,6 +9289,20 @@ var CallCreateError = class extends Error {
9146
9289
  this.name = "CallCreateError";
9147
9290
  }
9148
9291
  };
9292
+ var CallNotReadyError = class extends Error {
9293
+ constructor(callId, options) {
9294
+ super(`Call "${callId}" has no self member context yet: selfId/nodeId have not been received from the server`, options);
9295
+ this.callId = callId;
9296
+ this.name = "CallNotReadyError";
9297
+ }
9298
+ };
9299
+ var ParticipantNotReadyError = class extends Error {
9300
+ constructor(memberId, options) {
9301
+ super(`Participant "${memberId}" has no call context yet: its member state (call_id/node_id) has not been received from the server`, options);
9302
+ this.memberId = memberId;
9303
+ this.name = "ParticipantNotReadyError";
9304
+ }
9305
+ };
9149
9306
  var JSONRPCError = class extends Error {
9150
9307
  constructor(code, message, data, options, requestId) {
9151
9308
  super(message, options);
@@ -9226,6 +9383,33 @@ var MediaTrackError = class extends Error {
9226
9383
  this.name = "MediaTrackError";
9227
9384
  }
9228
9385
  };
9386
+ /** True when a `getUserMedia`/`getDisplayMedia` rejection is a permission denial. */
9387
+ function isMediaAccessDenial(originalError) {
9388
+ return originalError instanceof Error && MEDIA_ACCESS_DENIAL_NAMES.includes(originalError.name);
9389
+ }
9390
+ /**
9391
+ * Failure to acquire local media (camera, microphone, or screen capture)
9392
+ * via `getUserMedia`/`getDisplayMedia`.
9393
+ *
9394
+ * Non-fatal by default: screenshare and additional-device failures never
9395
+ * end the call, and main-connection failures degrade to receive-only when
9396
+ * possible. The wrapping site sets `fatal` to `true` only when the call
9397
+ * cannot continue (receive-only fallback disabled or no receive intent).
9398
+ */
9399
+ var MediaAccessError = class extends Error {
9400
+ constructor(operation, media, originalError, fatal = false) {
9401
+ super(`Media access ${isMediaAccessDenial(originalError) ? "denied" : "failed"} for ${operation} (${media})`, { cause: originalError instanceof Error ? originalError : void 0 });
9402
+ this.operation = operation;
9403
+ this.media = media;
9404
+ this.originalError = originalError;
9405
+ this.fatal = fatal;
9406
+ this.name = "MediaAccessError";
9407
+ }
9408
+ /** True when the underlying failure is a permission denial (user or policy). */
9409
+ get denied() {
9410
+ return isMediaAccessDenial(this.originalError);
9411
+ }
9412
+ };
9229
9413
  var DPoPInitError = class extends Error {
9230
9414
  constructor(originalError, message = "Failed to initialize DPoP key pair") {
9231
9415
  super(message, { cause: originalError instanceof Error ? originalError : void 0 });
@@ -9466,9 +9650,9 @@ var require_loglevel = /* @__PURE__ */ __commonJSMin(((exports, module) => {
9466
9650
  defaultLogger$1 = new Logger();
9467
9651
  defaultLogger$1.getLogger = function getLogger$1(name) {
9468
9652
  if (typeof name !== "symbol" && typeof name !== "string" || name === "") throw new TypeError("You must supply a name when creating a logger.");
9469
- var logger$32 = _loggersByName[name];
9470
- if (!logger$32) logger$32 = _loggersByName[name] = new Logger(name, defaultLogger$1.methodFactory);
9471
- return logger$32;
9653
+ var logger$33 = _loggersByName[name];
9654
+ if (!logger$33) logger$33 = _loggersByName[name] = new Logger(name, defaultLogger$1.methodFactory);
9655
+ return logger$33;
9472
9656
  };
9473
9657
  var _log = typeof window !== undefinedType ? window.log : void 0;
9474
9658
  defaultLogger$1.noConflict = function() {
@@ -9504,8 +9688,8 @@ const defaultLoggerLevel = defaultLogger.levels.WARN;
9504
9688
  defaultLogger.setLevel(defaultLoggerLevel);
9505
9689
  let userLogger = null;
9506
9690
  /** Replace the built-in logger with a custom implementation. Pass `null` to restore defaults. */
9507
- const setLogger = (logger$32) => {
9508
- userLogger = logger$32;
9691
+ const setLogger = (logger$33) => {
9692
+ userLogger = logger$33;
9509
9693
  };
9510
9694
  let debugOptions = {};
9511
9695
  /** Configure debug options (e.g., `{ logWsTraffic: true }`). */
@@ -9549,8 +9733,8 @@ const wsTraffic = (options) => {
9549
9733
  loggerInstance.debug(`${options.type.toUpperCase()}: \n`, msg, "\n");
9550
9734
  };
9551
9735
  const getLogger = () => {
9552
- const logger$32 = getLoggerInstance();
9553
- return new Proxy(logger$32, { get(_target, prop, _receiver) {
9736
+ const logger$33 = getLoggerInstance();
9737
+ return new Proxy(logger$33, { get(_target, prop, _receiver) {
9554
9738
  if (prop === "wsTraffic") return wsTraffic;
9555
9739
  const instance = getLoggerInstance();
9556
9740
  const value = Reflect.get(instance, prop);
@@ -9602,7 +9786,7 @@ const asyncRetry = async ({ asyncCallable, maxRetries: retries = DEFAULT_MAX_RET
9602
9786
 
9603
9787
  //#endregion
9604
9788
  //#region src/controllers/HTTPRequestController.ts
9605
- const logger$31 = getLogger();
9789
+ const logger$32 = getLogger();
9606
9790
  const GET_PARAMS = {
9607
9791
  method: "GET",
9608
9792
  headers: { Accept: "application/json" }
@@ -9666,7 +9850,7 @@ var HTTPRequestController = class HTTPRequestController extends Destroyable {
9666
9850
  this._responses$.next(response);
9667
9851
  return response;
9668
9852
  } catch (error) {
9669
- logger$31.error("[HTTPRequestController] Request error:", error);
9853
+ logger$32.error("[HTTPRequestController] Request error:", error);
9670
9854
  this._status$.next("error");
9671
9855
  const err = error instanceof Error ? error : new Error("HTTP request failed", { cause: error });
9672
9856
  this._errors$.next(err);
@@ -9693,7 +9877,7 @@ var HTTPRequestController = class HTTPRequestController extends Destroyable {
9693
9877
  const url = this.buildURL(request.url);
9694
9878
  const headers = this.buildHeaders(request.headers);
9695
9879
  const timeout$5 = request.timeout ?? this.requestTimeout;
9696
- logger$31.debug("[HTTPRequestController] Executing request:", {
9880
+ logger$32.debug("[HTTPRequestController] Executing request:", {
9697
9881
  method: request.method,
9698
9882
  url,
9699
9883
  headers: Object.keys(headers).reduce((acc, key) => {
@@ -9713,7 +9897,7 @@ var HTTPRequestController = class HTTPRequestController extends Destroyable {
9713
9897
  });
9714
9898
  clearTimeout(timeoutId);
9715
9899
  const httpResponse = await this.convertResponse(response);
9716
- logger$31.debug("[HTTPRequestController] Response received:", {
9900
+ logger$32.debug("[HTTPRequestController] Response received:", {
9717
9901
  status: response.status,
9718
9902
  statusText: response.statusText,
9719
9903
  headers: [...response.headers.entries()],
@@ -9723,7 +9907,7 @@ var HTTPRequestController = class HTTPRequestController extends Destroyable {
9723
9907
  } catch (error) {
9724
9908
  clearTimeout(timeoutId);
9725
9909
  if (error instanceof Error && error.name === "AbortError") throw new RequestTimeoutError(`Request timeout after ${timeout$5}ms`, { cause: error });
9726
- logger$31.error("[HTTPRequestController] Request failed:", error);
9910
+ logger$32.error("[HTTPRequestController] Request failed:", error);
9727
9911
  throw error;
9728
9912
  }
9729
9913
  }
@@ -9737,8 +9921,8 @@ var HTTPRequestController = class HTTPRequestController extends Destroyable {
9737
9921
  const credential = this.getCredential();
9738
9922
  if (credential.token) {
9739
9923
  headers.Authorization = `Bearer ${credential.token}`;
9740
- logger$31.debug("[HTTPRequestController] Using Bearer token auth, token length:", credential.token.length);
9741
- } else logger$31.warn("[HTTPRequestController] No credentials available for authentication");
9924
+ logger$32.debug("[HTTPRequestController] Using Bearer token auth, token length:", credential.token.length);
9925
+ } else logger$32.warn("[HTTPRequestController] No credentials available for authentication");
9742
9926
  return headers;
9743
9927
  }
9744
9928
  /**
@@ -9875,130 +10059,6 @@ var DeviceHistoryManager = class {
9875
10059
  }
9876
10060
  };
9877
10061
 
9878
- //#endregion
9879
- //#region src/core/constants.ts
9880
- const INVITE_VERSION = 1e3;
9881
- const DEFAULT_ICE_CANDIDATE_TIMEOUT_MS = 600;
9882
- const DEFAULT_ICE_GATHERING_TIMEOUT_MS = 6e3;
9883
- const DEFAULT_RECONNECT_CALLS_TIMEOUT_MS = 300 * 1e3;
9884
- const DEFAULT_CONNECTION_TIMEOUT_MS = 1e4;
9885
- const DEFAULT_RECONNECT_DELAY_MIN_MS = 100;
9886
- const DEFAULT_RECONNECT_DELAY_MAX_MS = 3e3;
9887
- const DEFAULT_DEVICE_DEBOUNCE_TIME_MS = 1500;
9888
- const DEFAULT_DEVICE_POLLING_INTERVAL_MS = 0;
9889
- const PREFERENCES_STORAGE_KEY = "sw:preferences";
9890
- /** Scope value that enables automatic token refresh. */
9891
- const SAT_REFRESH_SCOPE = "sat:refresh";
9892
- /** API endpoints for device token operations. */
9893
- const DEVICE_TOKEN_ENDPOINT = "/api/fabric/subscriber/devices/token";
9894
- const DEVICE_REFRESH_ENDPOINT = "/api/fabric/subscriber/devices/refresh";
9895
- /** Default device token TTL in seconds (15 minutes). */
9896
- const DEVICE_TOKEN_DEFAULT_EXPIRE_IN = 900;
9897
- /** Buffer time in milliseconds before expiry to trigger refresh. */
9898
- const DEVICE_TOKEN_REFRESH_BUFFER_MS = 3e4;
9899
- /** Maximum retry attempts for device token refresh on transient failure. */
9900
- const DEVICE_TOKEN_REFRESH_MAX_RETRIES = 3;
9901
- /** Base delay in milliseconds for exponential backoff on refresh retry. */
9902
- const DEVICE_TOKEN_REFRESH_RETRY_BASE_MS = 1e3;
9903
- /** Maximum retry attempts for developer credential refresh on transient failure. */
9904
- const CREDENTIAL_REFRESH_MAX_RETRIES = 5;
9905
- /** Base delay in milliseconds for exponential backoff on credential refresh retry. */
9906
- const CREDENTIAL_REFRESH_RETRY_BASE_MS = 1e3;
9907
- /** Maximum delay in milliseconds for credential refresh backoff. */
9908
- const CREDENTIAL_REFRESH_MAX_DELAY_MS = 3e4;
9909
- /** Buffer in milliseconds before token expiry to trigger refresh. */
9910
- const CREDENTIAL_REFRESH_BUFFER_MS = 5e3;
9911
- /** JSON-RPC error code for requester validation failure (corrupted auth state). */
9912
- const RPC_ERROR_REQUESTER_VALIDATION_FAILED = -32003;
9913
- /** JSON-RPC error code for invalid params (e.g., missing authentication block). */
9914
- const RPC_ERROR_INVALID_PARAMS = -32602;
9915
- /** JSON-RPC error code for authentication failure (invalid token, missing DPoP, etc.). */
9916
- const RPC_ERROR_AUTHENTICATION_FAILED = -32002;
9917
- /** Default polling interval for RTCPeerConnection.getStats() in milliseconds. */
9918
- const DEFAULT_STATS_POLLING_INTERVAL_MS = 1e3;
9919
- /** Number of initial samples used to build a baseline for spike detection. */
9920
- const DEFAULT_STATS_BASELINE_SAMPLES = 10;
9921
- /** Duration in ms with no inbound audio packets before emitting a critical issue. */
9922
- const DEFAULT_STATS_NO_PACKET_THRESHOLD_MS = 2e3;
9923
- /** Multiplier applied to baseline RTT to detect a warning-level RTT spike. */
9924
- const DEFAULT_STATS_RTT_SPIKE_MULTIPLIER = 3;
9925
- /** Packet loss fraction (0-1) above which a warning is emitted. */
9926
- const DEFAULT_STATS_PACKET_LOSS_THRESHOLD = .05;
9927
- /** Multiplier applied to baseline jitter to detect a jitter spike. */
9928
- const DEFAULT_STATS_JITTER_SPIKE_MULTIPLIER = 4;
9929
- /** Number of seconds of metrics history to retain. */
9930
- const DEFAULT_STATS_HISTORY_SIZE = 30;
9931
- /** Maximum keyframe requests allowed within a single burst window. */
9932
- const DEFAULT_KEYFRAME_MAX_BURST$1 = 3;
9933
- /** Duration of the keyframe burst window in milliseconds. */
9934
- const DEFAULT_KEYFRAME_BURST_WINDOW_MS$1 = 3e3;
9935
- /** Cooldown period in ms after burst limit is reached before allowing more keyframes. */
9936
- const DEFAULT_KEYFRAME_COOLDOWN_MS$1 = 1e4;
9937
- /** Minimum time between re-INVITE attempts in milliseconds. */
9938
- const DEFAULT_REINVITE_DEBOUNCE_TIME_MS = 1e4;
9939
- /** Maximum number of re-INVITE attempts per call. */
9940
- const DEFAULT_REINVITE_MAX_ATTEMPTS = 3;
9941
- /** Timeout for a single re-INVITE attempt in milliseconds. */
9942
- const DEFAULT_REINVITE_TIMEOUT_MS = 5e3;
9943
- /** Debounce window in ms to collapse multiple detection signals into one trigger. */
9944
- const DEFAULT_RECOVERY_DEBOUNCE_TIME_MS = 2e3;
9945
- /** Cooldown period in ms between recovery attempts. */
9946
- const DEFAULT_RECOVERY_COOLDOWN_MS = 1e4;
9947
- /** Grace period in ms before treating ICE 'disconnected' as a failure. */
9948
- const DEFAULT_ICE_DISCONNECTED_GRACE_PERIOD_MS = 3e3;
9949
- /** Timeout for a single ICE restart attempt in milliseconds. */
9950
- const DEFAULT_ICE_RESTART_TIMEOUT_MS$1 = 5e3;
9951
- /** Maximum recovery attempts before emitting 'max_attempts_reached'. */
9952
- const DEFAULT_MAX_RECOVERY_ATTEMPTS = 3;
9953
- /** Upper bound in ms for waiting on iceGatheringState === 'complete' after an ICE restart. */
9954
- const ICE_GATHERING_COMPLETE_TIMEOUT_MS = 1e4;
9955
- /** Upper bound in ms for waiting on RTCPeerConnection.connectionState === 'connected' after a recovery ICE restart. */
9956
- const PEER_CONNECTION_RECOVERY_WAIT_MS = 5e3;
9957
- /** Polling interval in ms while waiting for RTCPeerConnection.connectionState to transition. */
9958
- const PEER_CONNECTION_RECOVERY_POLL_MS = 100;
9959
- /** Polling interval for LocalAudioPipeline.level$ (ms). ~30fps is smooth for meters. */
9960
- const AUDIO_LEVEL_POLL_INTERVAL_MS = 33;
9961
- /** RMS level threshold (0..1) above which the local participant is considered speaking. */
9962
- const VAD_THRESHOLD = .03;
9963
- /** Hold window in ms below the threshold before speaking$ flips back to false. */
9964
- const VAD_HOLD_MS = 250;
9965
- /** Whether to persist device selections to storage by default. */
9966
- const DEFAULT_PERSIST_DEVICE_SELECTION = true;
9967
- /** Whether to auto-apply device changes to active calls by default. */
9968
- const DEFAULT_SYNC_DEVICES_TO_ACTIVE_CALLS = true;
9969
- /** Storage keys for persisted device selections. */
9970
- const DEVICE_STORAGE_KEY_AUDIO_INPUT = "sw:device:audioinput";
9971
- const DEVICE_STORAGE_KEY_AUDIO_OUTPUT = "sw:device:audiooutput";
9972
- const DEVICE_STORAGE_KEY_VIDEO_INPUT = "sw:device:videoinput";
9973
- /** Whether to auto-mute video when the tab becomes hidden. */
9974
- const DEFAULT_AUTO_MUTE_VIDEO_ON_HIDDEN = false;
9975
- /** Whether to re-enumerate devices when the page becomes visible. */
9976
- const DEFAULT_REFRESH_DEVICES_ON_VISIBLE = true;
9977
- /** Whether to check peer connection health when the page becomes visible. */
9978
- const DEFAULT_CHECK_CONNECTION_ON_VISIBLE = true;
9979
- /** Whether automatic video degradation on low bandwidth is enabled. */
9980
- const DEFAULT_ENABLE_AUTO_DEGRADATION = true;
9981
- /** Bitrate in kbps below which video is automatically disabled. */
9982
- const DEFAULT_DEGRADATION_BITRATE_THRESHOLD_KBPS = 150;
9983
- /** Bitrate in kbps above which video is automatically re-enabled (hysteresis). */
9984
- const DEFAULT_DEGRADATION_RECOVERY_THRESHOLD_KBPS = 300;
9985
- /** Whether relay-only escalation is enabled as a last-resort recovery tier. */
9986
- const DEFAULT_ENABLE_RELAY_FALLBACK = true;
9987
- /** Whether to listen for browser online/offline/connection events. */
9988
- const DEFAULT_ENABLE_NETWORK_CHANGE_DETECTION = true;
9989
- /** Whether to intercept server-sent media-timeout hangups and attempt recovery. */
9990
- const DEFAULT_ENABLE_SERVER_HANGUP_INTERCEPTION = true;
9991
- /** Default video track constraints applied when video is enabled without explicit constraints. */
9992
- const DEFAULT_VIDEO_CONSTRAINTS = {
9993
- width: { ideal: 1280 },
9994
- height: { ideal: 720 },
9995
- aspectRatio: 16 / 9
9996
- };
9997
- /** Whether stereo Opus is enabled by default. */
9998
- const DEFAULT_STEREO_AUDIO = false;
9999
- /** Max average bitrate for stereo Opus in bits per second. */
10000
- const DEFAULT_STEREO_MAX_AVERAGE_BITRATE = 51e4;
10001
-
10002
10062
  //#endregion
10003
10063
  //#region src/utils/time.ts
10004
10064
  function fromSecToMs(seconds) {
@@ -10010,7 +10070,7 @@ function fromMsToSec(milliseconds) {
10010
10070
 
10011
10071
  //#endregion
10012
10072
  //#region src/containers/PreferencesContainer.ts
10013
- const logger$30 = getLogger();
10073
+ const logger$31 = getLogger();
10014
10074
  var PreferencesContainer = class PreferencesContainer {
10015
10075
  static get instance() {
10016
10076
  this._instance ??= new PreferencesContainer();
@@ -10672,7 +10732,7 @@ var ClientPreferences = class {
10672
10732
  if (!this._storage) return;
10673
10733
  const data = collectStoredPreferences();
10674
10734
  this._storage.setItem(PREFERENCES_STORAGE_KEY, data, "local").catch((error) => {
10675
- logger$30.error(`[ClientPreferences] Failed to save preferences: ${String(error)}`);
10735
+ logger$31.error(`[ClientPreferences] Failed to save preferences: ${String(error)}`);
10676
10736
  });
10677
10737
  }
10678
10738
  /** Loads preferences from storage and applies them to the container. */
@@ -10681,7 +10741,7 @@ var ClientPreferences = class {
10681
10741
  this._storage.getItem(PREFERENCES_STORAGE_KEY, "local").then((stored) => {
10682
10742
  if (stored) applyStoredPreferences(stored);
10683
10743
  }).catch((error) => {
10684
- logger$30.error(`[ClientPreferences] Failed to load preferences: ${String(error)}`);
10744
+ logger$31.error(`[ClientPreferences] Failed to load preferences: ${String(error)}`);
10685
10745
  });
10686
10746
  }
10687
10747
  };
@@ -10703,7 +10763,7 @@ function toError(value) {
10703
10763
  //#endregion
10704
10764
  //#region src/controllers/NavigatorDeviceController.ts
10705
10765
  var import_cjs$29 = require_cjs();
10706
- const logger$29 = getLogger();
10766
+ const logger$30 = getLogger();
10707
10767
  /** Maps a device kind to its storage key. */
10708
10768
  const DEVICE_STORAGE_KEYS = {
10709
10769
  audioinput: DEVICE_STORAGE_KEY_AUDIO_INPUT,
@@ -10725,7 +10785,7 @@ var NavigatorDeviceController = class extends Destroyable {
10725
10785
  super();
10726
10786
  this.webRTCApiProvider = webRTCApiProvider;
10727
10787
  this.deviceChangeHandler = () => {
10728
- logger$29.debug("[DeviceController] Device change detected");
10788
+ logger$30.debug("[DeviceController] Device change detected");
10729
10789
  this.enumerateDevices();
10730
10790
  };
10731
10791
  this._devicesState$ = this.createBehaviorSubject(initialDevicesState);
@@ -10790,13 +10850,13 @@ var NavigatorDeviceController = class extends Destroyable {
10790
10850
  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$)));
10791
10851
  }
10792
10852
  get selectedAudioInputDevice$() {
10793
- 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$29.debug("[DeviceController] Selected audio input device changed:", info))));
10853
+ 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))));
10794
10854
  }
10795
10855
  get selectedAudioOutputDevice$() {
10796
- 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$29.debug("[DeviceController] Selected audio output device changed:", info))));
10856
+ 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))));
10797
10857
  }
10798
10858
  get selectedVideoInputDevice$() {
10799
- 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$29.debug("[DeviceController] Selected video input device changed:", info))));
10859
+ 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))));
10800
10860
  }
10801
10861
  get selectedAudioInputDevice() {
10802
10862
  if (this._audioInputDisabled$.value) return null;
@@ -10871,7 +10931,7 @@ var NavigatorDeviceController = class extends Destroyable {
10871
10931
  if (device) this.persistDeviceSelection("audioinput", device);
10872
10932
  }
10873
10933
  selectVideoInputDevice(device) {
10874
- logger$29.debug("[DeviceController] Setting selected video input device:", device);
10934
+ logger$30.debug("[DeviceController] Setting selected video input device:", device);
10875
10935
  if (this._videoInputDisabled$.value && device) this._videoInputDisabled$.next(false);
10876
10936
  const previous = this._selectedDevicesState$.value.videoinput;
10877
10937
  if (previous && previous.deviceId !== device?.deviceId) this._deviceHistory.push("videoinput", previous);
@@ -10928,7 +10988,7 @@ var NavigatorDeviceController = class extends Destroyable {
10928
10988
  }
10929
10989
  const fromHistory = this._deviceHistory.findInHistory(kind, devices);
10930
10990
  if (fromHistory) {
10931
- logger$29.debug(`[DeviceController] Device disappeared, falling back to history: ${fromHistory.label}`);
10991
+ logger$30.debug(`[DeviceController] Device disappeared, falling back to history: ${fromHistory.label}`);
10932
10992
  this.emitDeviceRecovered(kind, selected, fromHistory, "device_disconnected");
10933
10993
  return fromHistory;
10934
10994
  }
@@ -10981,7 +11041,7 @@ var NavigatorDeviceController = class extends Destroyable {
10981
11041
  try {
10982
11042
  await this._storageManager.setItem(DEVICE_STORAGE_KEYS[kind], stored, "local");
10983
11043
  } catch (error) {
10984
- logger$29.error(`[DeviceController] Failed to persist device selection for ${kind}:`, error);
11044
+ logger$30.error(`[DeviceController] Failed to persist device selection for ${kind}:`, error);
10985
11045
  }
10986
11046
  }
10987
11047
  async loadPersistedDevices() {
@@ -10997,7 +11057,7 @@ var NavigatorDeviceController = class extends Destroyable {
10997
11057
  [kind]: stored
10998
11058
  };
10999
11059
  } catch (error) {
11000
- logger$29.error(`[DeviceController] Failed to load persisted device for ${kind}:`, error);
11060
+ logger$30.error(`[DeviceController] Failed to load persisted device for ${kind}:`, error);
11001
11061
  }
11002
11062
  }
11003
11063
  /** Clears device history, persisted selections, and re-enumerates devices. */
@@ -11015,7 +11075,7 @@ var NavigatorDeviceController = class extends Destroyable {
11015
11075
  this.disableDeviceMonitoring();
11016
11076
  this.webRTCApiProvider.mediaDevices.addEventListener("devicechange", this.deviceChangeHandler);
11017
11077
  if (PreferencesContainer.instance.devicePollingInterval > 0) this._devicesPoolingSubscription = (0, import_cjs$29.interval)(PreferencesContainer.instance.devicePollingInterval).subscribe(() => {
11018
- logger$29.debug("[DeviceController] Polling devices due to interval");
11078
+ logger$30.debug("[DeviceController] Polling devices due to interval");
11019
11079
  this.enumerateDevices();
11020
11080
  });
11021
11081
  this.enumerateDevices();
@@ -11041,13 +11101,13 @@ var NavigatorDeviceController = class extends Destroyable {
11041
11101
  videoinput: []
11042
11102
  });
11043
11103
  this._devicesState$.next(devicesByKind);
11044
- logger$29.debug("[DeviceController] Devices enumerated:", {
11104
+ logger$30.debug("[DeviceController] Devices enumerated:", {
11045
11105
  audioInputs: devicesByKind.audioinput.length,
11046
11106
  audioOutputs: devicesByKind.audiooutput.length,
11047
11107
  videoInputs: devicesByKind.videoinput.length
11048
11108
  });
11049
11109
  } catch (error) {
11050
- logger$29.error("[DeviceController] Failed to enumerate devices:", error);
11110
+ logger$30.error("[DeviceController] Failed to enumerate devices:", error);
11051
11111
  this._errors$.next(toError(error));
11052
11112
  }
11053
11113
  }
@@ -11063,7 +11123,7 @@ var NavigatorDeviceController = class extends Destroyable {
11063
11123
  stream.getTracks().forEach((t) => t.stop());
11064
11124
  return capabilities;
11065
11125
  } catch (error) {
11066
- logger$29.error("[DeviceController] Failed to get device capabilities:", error);
11126
+ logger$30.error("[DeviceController] Failed to get device capabilities:", error);
11067
11127
  this._errors$.next(toError(error));
11068
11128
  throw error;
11069
11129
  }
@@ -11314,7 +11374,7 @@ var DependencyContainer = class {
11314
11374
 
11315
11375
  //#endregion
11316
11376
  //#region src/controllers/CryptoController.ts
11317
- const logger$28 = getLogger();
11377
+ const logger$29 = getLogger();
11318
11378
  const DPOP_DB_NAME = "sw-dpop";
11319
11379
  const DPOP_DB_VERSION = 1;
11320
11380
  const DPOP_STORE_NAME = "keys";
@@ -11373,7 +11433,7 @@ async function loadKeyPairFromDB() {
11373
11433
  tx.oncomplete = () => db.close();
11374
11434
  });
11375
11435
  } catch (error) {
11376
- logger$28.warn("[DPoP] Failed to load key pair from IndexedDB:", error);
11436
+ logger$29.warn("[DPoP] Failed to load key pair from IndexedDB:", error);
11377
11437
  return null;
11378
11438
  }
11379
11439
  }
@@ -11393,7 +11453,7 @@ async function saveKeyPairToDB(keyPair) {
11393
11453
  };
11394
11454
  });
11395
11455
  } catch (error) {
11396
- logger$28.warn("[DPoP] Failed to save key pair to IndexedDB:", error);
11456
+ logger$29.warn("[DPoP] Failed to save key pair to IndexedDB:", error);
11397
11457
  }
11398
11458
  }
11399
11459
  async function deleteKeyPairFromDB() {
@@ -11412,7 +11472,7 @@ async function deleteKeyPairFromDB() {
11412
11472
  };
11413
11473
  });
11414
11474
  } catch (error) {
11415
- logger$28.warn("[DPoP] Failed to delete key pair from IndexedDB:", error);
11475
+ logger$29.warn("[DPoP] Failed to delete key pair from IndexedDB:", error);
11416
11476
  }
11417
11477
  }
11418
11478
  /**
@@ -11472,13 +11532,13 @@ var CryptoController = class {
11472
11532
  this._publicJwk = await crypto.subtle.exportKey("jwk", stored.publicKey);
11473
11533
  this._fingerprint = await computeJwkThumbprint(this._publicJwk);
11474
11534
  this._initialized = true;
11475
- logger$28.debug("[DPoP] Key pair restored from IndexedDB, fingerprint:", this._fingerprint);
11535
+ logger$29.debug("[DPoP] Key pair restored from IndexedDB, fingerprint:", this._fingerprint);
11476
11536
  return this._fingerprint;
11477
11537
  } catch (error) {
11478
- logger$28.warn("[DPoP] Stored key pair unusable, generating new one:", error);
11538
+ logger$29.warn("[DPoP] Stored key pair unusable, generating new one:", error);
11479
11539
  await deleteKeyPairFromDB();
11480
11540
  }
11481
- logger$28.debug("[DPoP] Generating RSA key pair");
11541
+ logger$29.debug("[DPoP] Generating RSA key pair");
11482
11542
  this._keyPair = await crypto.subtle.generateKey({
11483
11543
  name: "RSASSA-PKCS1-v1_5",
11484
11544
  modulusLength: 2048,
@@ -11493,7 +11553,7 @@ var CryptoController = class {
11493
11553
  this._fingerprint = await computeJwkThumbprint(this._publicJwk);
11494
11554
  this._initialized = true;
11495
11555
  await saveKeyPairToDB(this._keyPair);
11496
- logger$28.debug("[DPoP] Key pair generated and persisted, fingerprint:", this._fingerprint);
11556
+ logger$29.debug("[DPoP] Key pair generated and persisted, fingerprint:", this._fingerprint);
11497
11557
  return this._fingerprint;
11498
11558
  }
11499
11559
  /**
@@ -11559,7 +11619,7 @@ var CryptoController = class {
11559
11619
  this._fingerprint = null;
11560
11620
  this._initialized = false;
11561
11621
  deleteKeyPairFromDB();
11562
- logger$28.debug("[DPoP] Controller destroyed");
11622
+ logger$29.debug("[DPoP] Controller destroyed");
11563
11623
  }
11564
11624
  get publicJwk() {
11565
11625
  if (!this._publicJwk) throw new DPoPInitError("CryptoController not initialized. Call init() first.");
@@ -11583,7 +11643,7 @@ var CryptoController = class {
11583
11643
  //#endregion
11584
11644
  //#region src/controllers/NetworkMonitor.ts
11585
11645
  var import_cjs$28 = require_cjs();
11586
- const logger$27 = getLogger();
11646
+ const logger$28 = getLogger();
11587
11647
  /**
11588
11648
  * Safely check whether we are running in a browser environment
11589
11649
  * with `window` and the relevant event targets.
@@ -11640,7 +11700,7 @@ var NetworkMonitor = class extends Destroyable {
11640
11700
  }
11641
11701
  attachListeners() {
11642
11702
  if (!hasBrowserNetworkEvents()) {
11643
- logger$27.debug("NetworkMonitor: no browser environment detected, skipping event listeners");
11703
+ logger$28.debug("NetworkMonitor: no browser environment detected, skipping event listeners");
11644
11704
  return;
11645
11705
  }
11646
11706
  window.addEventListener("online", this._onOnline);
@@ -11648,7 +11708,7 @@ var NetworkMonitor = class extends Destroyable {
11648
11708
  const connection = getNetworkConnection();
11649
11709
  if (connection) connection.addEventListener("change", this._onConnectionChange);
11650
11710
  this._listenersAttached = true;
11651
- logger$27.debug("NetworkMonitor: event listeners attached");
11711
+ logger$28.debug("NetworkMonitor: event listeners attached");
11652
11712
  }
11653
11713
  removeListeners() {
11654
11714
  if (!this._listenersAttached) return;
@@ -11659,10 +11719,10 @@ var NetworkMonitor = class extends Destroyable {
11659
11719
  if (connection) connection.removeEventListener("change", this._onConnectionChange);
11660
11720
  }
11661
11721
  this._listenersAttached = false;
11662
- logger$27.debug("NetworkMonitor: event listeners removed");
11722
+ logger$28.debug("NetworkMonitor: event listeners removed");
11663
11723
  }
11664
11724
  handleOnline() {
11665
- logger$27.info("NetworkMonitor: browser went online");
11725
+ logger$28.info("NetworkMonitor: browser went online");
11666
11726
  this._isOnline$.next(true);
11667
11727
  this._networkChange$.next({
11668
11728
  type: "online",
@@ -11671,7 +11731,7 @@ var NetworkMonitor = class extends Destroyable {
11671
11731
  });
11672
11732
  }
11673
11733
  handleOffline() {
11674
- logger$27.info("NetworkMonitor: browser went offline");
11734
+ logger$28.info("NetworkMonitor: browser went offline");
11675
11735
  this._isOnline$.next(false);
11676
11736
  this._networkChange$.next({
11677
11737
  type: "offline",
@@ -11680,7 +11740,7 @@ var NetworkMonitor = class extends Destroyable {
11680
11740
  }
11681
11741
  handleConnectionChange() {
11682
11742
  const networkType = getNetworkType();
11683
- logger$27.info(`NetworkMonitor: connection changed — effectiveType=${networkType ?? "unknown"}`);
11743
+ logger$28.info(`NetworkMonitor: connection changed — effectiveType=${networkType ?? "unknown"}`);
11684
11744
  this._networkChange$.next({
11685
11745
  type: "connection_change",
11686
11746
  timestamp: Date.now(),
@@ -11796,7 +11856,7 @@ function getNavigatorMediaDevices() {
11796
11856
  //#endregion
11797
11857
  //#region src/controllers/PreflightRunner.ts
11798
11858
  var import_cjs$27 = require_cjs();
11799
- const logger$26 = getLogger();
11859
+ const logger$27 = getLogger();
11800
11860
  const DEFAULT_MEDIA_TEST_DURATION_S = 10;
11801
11861
  const ICE_GATHERING_TIMEOUT_MS = 1e4;
11802
11862
  const SIGNALING_RTT_TIMEOUT_MS = 5e3;
@@ -11845,7 +11905,7 @@ var PreflightRunner = class extends Destroyable {
11845
11905
  if (!this._options.skipMediaTest) try {
11846
11906
  bandwidth = await this.testMediaBandwidth(destination);
11847
11907
  } catch (error) {
11848
- logger$26.warn("[PreflightRunner] Media bandwidth test failed:", error);
11908
+ logger$27.warn("[PreflightRunner] Media bandwidth test failed:", error);
11849
11909
  warnings.push("Media bandwidth test failed");
11850
11910
  }
11851
11911
  return {
@@ -11857,7 +11917,7 @@ var PreflightRunner = class extends Destroyable {
11857
11917
  warnings
11858
11918
  };
11859
11919
  } catch (error) {
11860
- logger$26.error("[PreflightRunner] Preflight test failed:", error);
11920
+ logger$27.error("[PreflightRunner] Preflight test failed:", error);
11861
11921
  throw new PreflightError("preflight", error instanceof Error ? error : new Error(String(error)));
11862
11922
  } finally {
11863
11923
  this.destroy();
@@ -11888,7 +11948,7 @@ var PreflightRunner = class extends Destroyable {
11888
11948
  if (track.kind === "video" && track.readyState === "live") videoWorking = true;
11889
11949
  }
11890
11950
  } catch (error) {
11891
- logger$26.warn("[PreflightRunner] Device test failed:", error);
11951
+ logger$27.warn("[PreflightRunner] Device test failed:", error);
11892
11952
  } finally {
11893
11953
  if (audioStream) audioStream.getTracks().forEach((t) => t.stop());
11894
11954
  }
@@ -11946,7 +12006,7 @@ var PreflightRunner = class extends Destroyable {
11946
12006
  rttMs
11947
12007
  };
11948
12008
  } catch (error) {
11949
- logger$26.warn("[PreflightRunner] ICE connectivity test failed:", error);
12009
+ logger$27.warn("[PreflightRunner] ICE connectivity test failed:", error);
11950
12010
  return {
11951
12011
  type: "failed",
11952
12012
  turnReachable: false,
@@ -11994,7 +12054,7 @@ var PreflightRunner = class extends Destroyable {
11994
12054
  //#endregion
11995
12055
  //#region src/controllers/VisibilityController.ts
11996
12056
  var import_cjs$26 = require_cjs();
11997
- const logger$25 = getLogger();
12057
+ const logger$26 = getLogger();
11998
12058
  /**
11999
12059
  * Checks whether the document visibility API is available.
12000
12060
  */
@@ -12031,8 +12091,8 @@ var VisibilityController = class extends Destroyable {
12031
12091
  this._boundHandler = this._handleVisibilityChange.bind(this);
12032
12092
  if (this._hasVisibilityApi) {
12033
12093
  document.addEventListener("visibilitychange", this._boundHandler);
12034
- logger$25.debug("VisibilityController: listening for visibilitychange events");
12035
- } else logger$25.debug("VisibilityController: document visibility API not available, defaulting to visible");
12094
+ logger$26.debug("VisibilityController: listening for visibilitychange events");
12095
+ } else logger$26.debug("VisibilityController: document visibility API not available, defaulting to visible");
12036
12096
  }
12037
12097
  /**
12038
12098
  * Observable of the current visibility state.
@@ -12057,7 +12117,7 @@ var VisibilityController = class extends Destroyable {
12057
12117
  destroy() {
12058
12118
  if (this._hasVisibilityApi) {
12059
12119
  document.removeEventListener("visibilitychange", this._boundHandler);
12060
- logger$25.debug("VisibilityController: removed visibilitychange listener");
12120
+ logger$26.debug("VisibilityController: removed visibilitychange listener");
12061
12121
  }
12062
12122
  super.destroy();
12063
12123
  }
@@ -12075,7 +12135,7 @@ var VisibilityController = class extends Destroyable {
12075
12135
  timestamp: Date.now()
12076
12136
  };
12077
12137
  this._visibilityChange$.next(changeEvent);
12078
- logger$25.debug("VisibilityController: visibility changed", {
12138
+ logger$26.debug("VisibilityController: visibility changed", {
12079
12139
  from: previousState,
12080
12140
  to: newState
12081
12141
  });
@@ -12150,22 +12210,17 @@ function unsafeStringify(arr, offset = 0) {
12150
12210
 
12151
12211
  //#endregion
12152
12212
  //#region ../../node_modules/uuid/dist-node/rng.js
12153
- const rnds8Pool = new Uint8Array(256);
12154
- let poolPtr = rnds8Pool.length;
12213
+ const rnds8 = new Uint8Array(16);
12155
12214
  function rng() {
12156
- if (poolPtr > rnds8Pool.length - 16) {
12157
- randomFillSync(rnds8Pool);
12158
- poolPtr = 0;
12159
- }
12160
- return rnds8Pool.slice(poolPtr, poolPtr += 16);
12215
+ return crypto.getRandomValues(rnds8);
12161
12216
  }
12162
12217
 
12163
- //#endregion
12164
- //#region ../../node_modules/uuid/dist-node/native.js
12165
- var native_default = { randomUUID };
12166
-
12167
12218
  //#endregion
12168
12219
  //#region ../../node_modules/uuid/dist-node/v4.js
12220
+ function v4(options, buf, offset) {
12221
+ if (!buf && !options && crypto.randomUUID) return crypto.randomUUID();
12222
+ return _v4(options, buf, offset);
12223
+ }
12169
12224
  function _v4(options, buf, offset) {
12170
12225
  options = options || {};
12171
12226
  const rnds = options.random ?? options.rng?.() ?? rng();
@@ -12180,10 +12235,6 @@ function _v4(options, buf, offset) {
12180
12235
  }
12181
12236
  return unsafeStringify(rnds);
12182
12237
  }
12183
- function v4(options, buf, offset) {
12184
- if (native_default.randomUUID && !buf && !options) return native_default.randomUUID();
12185
- return _v4(options, buf, offset);
12186
- }
12187
12238
  var v4_default = v4;
12188
12239
 
12189
12240
  //#endregion
@@ -12325,7 +12376,7 @@ const RPCEventAckResponse = (id) => makeRPCResponse({
12325
12376
 
12326
12377
  //#endregion
12327
12378
  //#region src/managers/AttachManager.ts
12328
- const logger$24 = getLogger();
12379
+ const logger$25 = getLogger();
12329
12380
  var AttachManager = class {
12330
12381
  constructor(storage, deviceController, reconnectCallsTimeout, attachKey) {
12331
12382
  this.storage = storage;
@@ -12346,7 +12397,7 @@ var AttachManager = class {
12346
12397
  try {
12347
12398
  return await this.storage.getItem(this.attachKey) ?? {};
12348
12399
  } catch (error) {
12349
- logger$24.warn("[AttachManager] Failed to retrieve attached calls from storage", error);
12400
+ logger$25.warn("[AttachManager] Failed to retrieve attached calls from storage", error);
12350
12401
  return {};
12351
12402
  }
12352
12403
  }
@@ -12354,7 +12405,7 @@ var AttachManager = class {
12354
12405
  try {
12355
12406
  await this.storage.setItem(this.attachKey, attached);
12356
12407
  } catch (error) {
12357
- logger$24.warn("[AttachManager] Failed to write attached calls to storage", error);
12408
+ logger$25.warn("[AttachManager] Failed to write attached calls to storage", error);
12358
12409
  }
12359
12410
  }
12360
12411
  /**
@@ -12373,7 +12424,7 @@ var AttachManager = class {
12373
12424
  }
12374
12425
  async attach(call) {
12375
12426
  if (!call.to) {
12376
- logger$24.warn("[AttachManager] Skip attach for calls with no destination");
12427
+ logger$25.warn("[AttachManager] Skip attach for calls with no destination");
12377
12428
  return;
12378
12429
  }
12379
12430
  const destination = call.to;
@@ -12426,15 +12477,15 @@ var AttachManager = class {
12426
12477
  callId,
12427
12478
  ...options
12428
12479
  });
12429
- logger$24.info(`[AttachManager] Reattached call ${callId} (attempt ${attempt})`);
12480
+ logger$25.info(`[AttachManager] Reattached call ${callId} (attempt ${attempt})`);
12430
12481
  succeeded = true;
12431
12482
  break;
12432
12483
  } catch (error) {
12433
- logger$24.warn(`[AttachManager] Reattach attempt ${attempt}/3 failed for call ${callId}:`, error);
12484
+ logger$25.warn(`[AttachManager] Reattach attempt ${attempt}/3 failed for call ${callId}:`, error);
12434
12485
  if (attempt < 3) await new Promise((r) => setTimeout(r, (attempt + 1) * 1e3));
12435
12486
  }
12436
12487
  if (!succeeded) {
12437
- logger$24.warn(`[AttachManager] Reattach failed after 3 attempts for call ${callId}, removing reference`);
12488
+ logger$25.warn(`[AttachManager] Reattach failed after 3 attempts for call ${callId}, removing reference`);
12438
12489
  await this.detach({
12439
12490
  id: callId,
12440
12491
  mediaDirections: attachment.mediaDirections
@@ -13371,7 +13422,9 @@ const DEFAULT_MEMBER_CAPABILITIES = {
13371
13422
  position: false,
13372
13423
  meta: false,
13373
13424
  remove: false,
13374
- audioFlags: false
13425
+ audioFlags: false,
13426
+ denoise: false,
13427
+ lowbitrate: false
13375
13428
  };
13376
13429
  /**
13377
13430
  * Default call capabilities with no permissions
@@ -13444,7 +13497,9 @@ function computeMemberCapabilities(flags, memberType) {
13444
13497
  position: hasBooleanCapability(flags, memberType, null, "position"),
13445
13498
  meta: hasBooleanCapability(flags, memberType, null, "meta"),
13446
13499
  remove: hasBooleanCapability(flags, memberType, null, "remove"),
13447
- audioFlags: hasBooleanCapability(flags, memberType, null, "audioflags")
13500
+ audioFlags: hasBooleanCapability(flags, memberType, null, "audioflags"),
13501
+ denoise: hasBooleanCapability(flags, memberType, null, "denoise"),
13502
+ lowbitrate: hasBooleanCapability(flags, memberType, null, "lowbitrate")
13448
13503
  };
13449
13504
  }
13450
13505
  /**
@@ -13601,7 +13656,7 @@ function toggleHandraiseMethod(is) {
13601
13656
 
13602
13657
  //#endregion
13603
13658
  //#region src/core/entities/Participant.ts
13604
- const logger$23 = getLogger();
13659
+ const logger$24 = getLogger();
13605
13660
  const initialState = {};
13606
13661
  /**
13607
13662
  * Represents a participant in a call.
@@ -13611,9 +13666,9 @@ const initialState = {};
13611
13666
  * the local participant with additional device control.
13612
13667
  */
13613
13668
  var Participant = class extends Destroyable {
13614
- constructor(id, executeMethod, deviceController) {
13669
+ constructor(id, callExecuteMethod, deviceController) {
13615
13670
  super();
13616
- this.executeMethod = executeMethod;
13671
+ this.callExecuteMethod = callExecuteMethod;
13617
13672
  this.deviceController = deviceController;
13618
13673
  this._state$ = this.createBehaviorSubject(initialState);
13619
13674
  this.id = id;
@@ -13827,26 +13882,63 @@ var Participant = class extends Destroyable {
13827
13882
  get nodeId() {
13828
13883
  return this._state$.value.node_id;
13829
13884
  }
13885
+ /** Call ID for this participant's leg, or `undefined` if not available. */
13886
+ get callId() {
13887
+ return this._state$.value.call_id;
13888
+ }
13830
13889
  /** @internal */
13831
13890
  get value() {
13832
13891
  return this._state$.value;
13833
13892
  }
13893
+ /**
13894
+ * Target triple for member RPCs, built from the participant's own state.
13895
+ * The backend locates the member's session by the target `call_id`/`node_id`,
13896
+ * so this must always be the participant's own call context — never the
13897
+ * local call's id (issue #19400).
13898
+ *
13899
+ * Reading it doubles as a readiness probe: it throws until the first full
13900
+ * member event (`member.joined`/`member.updated` or the `call.joined`
13901
+ * roster) arrives, and never regresses afterwards.
13902
+ *
13903
+ * @throws {ParticipantNotReadyError} If the member state has not been
13904
+ * received yet (e.g. a participant first seen via `member.talking`) — an
13905
+ * empty call context can never address the member, so fail fast instead of
13906
+ * sending a doomed RPC.
13907
+ */
13908
+ get target() {
13909
+ const { call_id, node_id } = this._state$.value;
13910
+ if (!call_id || !node_id) throw new ParticipantNotReadyError(this.id);
13911
+ return {
13912
+ member_id: this.id,
13913
+ call_id,
13914
+ node_id
13915
+ };
13916
+ }
13917
+ /**
13918
+ * Executes a member RPC against this participant, injecting its own
13919
+ * {@link target} as the target.
13920
+ *
13921
+ * @throws {ParticipantNotReadyError} Via {@link target}, when the
13922
+ * member state has not been received yet.
13923
+ */
13924
+ async executeMethod(method, args) {
13925
+ return this.callExecuteMethod(this.target, method, args);
13926
+ }
13834
13927
  /** Toggles the deafened state (mutes/unmutes incoming audio). */
13835
13928
  async toggleDeaf() {
13836
- const method = toggleDeafMethod(this.deaf);
13837
- await this.executeMethod(this.id, method, {});
13929
+ await this.executeMethod(toggleDeafMethod(this.deaf), {});
13838
13930
  }
13839
13931
  /** Toggles the hand-raised state. */
13840
13932
  async toggleHandraise() {
13841
- await this.executeMethod(this.id, toggleHandraiseMethod(this.handraised), {});
13933
+ await this.executeMethod(toggleHandraiseMethod(this.handraised), {});
13842
13934
  }
13843
13935
  /** Mutes the participant's audio. */
13844
13936
  async mute() {
13845
- await this.executeMethod(this.id, "call.mute", { channels: ["audio"] });
13937
+ await this.executeMethod("call.mute", { channels: ["audio"] });
13846
13938
  }
13847
13939
  /** Unmutes the participant's audio. */
13848
13940
  async unmute() {
13849
- await this.executeMethod(this.id, "call.unmute", { channels: ["audio"] });
13941
+ await this.executeMethod("call.unmute", { channels: ["audio"] });
13850
13942
  }
13851
13943
  /** Toggles the participant's audio mute state. */
13852
13944
  async toggleMute() {
@@ -13854,11 +13946,11 @@ var Participant = class extends Destroyable {
13854
13946
  }
13855
13947
  /** Mutes the participant's video. */
13856
13948
  async muteVideo() {
13857
- await this.executeMethod(this.id, "call.mute", { channels: ["video"] });
13949
+ await this.executeMethod("call.mute", { channels: ["video"] });
13858
13950
  }
13859
13951
  /** Unmutes the participant's video. */
13860
13952
  async unmuteVideo() {
13861
- await this.executeMethod(this.id, "call.unmute", { channels: ["video"] });
13953
+ await this.executeMethod("call.unmute", { channels: ["video"] });
13862
13954
  }
13863
13955
  /** Toggles the participant's video mute state. */
13864
13956
  async toggleMuteVideo() {
@@ -13866,7 +13958,7 @@ var Participant = class extends Destroyable {
13866
13958
  }
13867
13959
  /** Toggles echo cancellation on the audio input. */
13868
13960
  async toggleEchoCancellation() {
13869
- await this.executeMethod(this.id, "call.audioflags.set", {
13961
+ await this.executeMethod("call.audioflags.set", {
13870
13962
  echo_cancellation: !this.echoCancellation,
13871
13963
  auto_gain: this.autoGain,
13872
13964
  noise_suppression: this.noiseSuppression
@@ -13874,7 +13966,7 @@ var Participant = class extends Destroyable {
13874
13966
  }
13875
13967
  /** Toggles automatic gain control on the audio input. */
13876
13968
  async toggleAudioInputAutoGain() {
13877
- await this.executeMethod(this.id, "call.audioflags.set", {
13969
+ await this.executeMethod("call.audioflags.set", {
13878
13970
  echo_cancellation: this.echoCancellation,
13879
13971
  auto_gain: !this.autoGain,
13880
13972
  noise_suppression: this.noiseSuppression
@@ -13882,14 +13974,15 @@ var Participant = class extends Destroyable {
13882
13974
  }
13883
13975
  /** Toggles noise suppression on the audio input. */
13884
13976
  async toggleNoiseSuppression() {
13885
- await this.executeMethod(this.id, "call.audioflags.set", {
13977
+ await this.executeMethod("call.audioflags.set", {
13886
13978
  echo_cancellation: this.echoCancellation,
13887
13979
  auto_gain: this.autoGain,
13888
13980
  noise_suppression: !this.noiseSuppression
13889
13981
  });
13890
13982
  }
13983
+ /** Toggles low-bitrate mode for this participant's media. */
13891
13984
  async toggleLowbitrate() {
13892
- throw new UnimplementedError();
13985
+ await this.executeMethod("call.lowbitrate.set", { lowbitrate: !this.lowbitrate });
13893
13986
  }
13894
13987
  /**
13895
13988
  * Adjusts the **conference-only** microphone energy gate / sensitivity level
@@ -13905,7 +13998,7 @@ var Participant = class extends Destroyable {
13905
13998
  * (integer, larger values are more sensitive).
13906
13999
  */
13907
14000
  async setAudioInputSensitivity(value) {
13908
- await this.executeMethod(this.id, "call.microphone.sensitivity.set", { sensitivity: value });
14001
+ await this.executeMethod("call.microphone.sensitivity.set", { sensitivity: value });
13909
14002
  }
13910
14003
  /**
13911
14004
  * Sets the **server-side** microphone volume on this participant's bridged
@@ -13918,7 +14011,7 @@ var Participant = class extends Destroyable {
13918
14011
  * @param value - Volume level (0-100).
13919
14012
  */
13920
14013
  async setAudioInputVolume(value) {
13921
- await this.executeMethod(this.id, "call.microphone.volume.set", { volume: value });
14014
+ await this.executeMethod("call.microphone.volume.set", { volume: value });
13922
14015
  }
13923
14016
  /**
13924
14017
  * Sets the **server-side** speaker volume on this participant's bridged call
@@ -13932,28 +14025,31 @@ var Participant = class extends Destroyable {
13932
14025
  * @param value - Volume level (0-100).
13933
14026
  */
13934
14027
  async setAudioOutputVolume(value) {
13935
- await this.executeMethod(this.id, "call.speaker.volume.set", { volume: value });
14028
+ await this.executeMethod("call.speaker.volume.set", { volume: value });
13936
14029
  }
13937
14030
  /**
13938
14031
  * Sets the participant's position in the video layout.
14032
+ *
14033
+ * Requires the `member.position` capability. The gateway requires a
14034
+ * `targets` array of `{ target, position }` entries (issue #19400). A
14035
+ * resolved promise does not guarantee a visible change: the backend silently
14036
+ * returns `200` (no-op) for non-conference targets.
14037
+ *
13939
14038
  * @param value - The {@link VideoPosition} to assign (e.g. `'auto'`, `'reserved-0'`).
13940
14039
  */
13941
14040
  async setPosition(value) {
13942
- await this.executeMethod(this.id, "call.member.position.set", { position: value });
14041
+ await this.executeMethod("call.member.position.set", { targets: [{
14042
+ target: this.target,
14043
+ position: value
14044
+ }] });
13943
14045
  }
13944
14046
  /** Removes this participant from the call. */
13945
14047
  async remove() {
13946
- const state = this._state$.value;
13947
- const target = {
13948
- member_id: this.id,
13949
- call_id: state.call_id ?? "",
13950
- node_id: state.node_id ?? ""
13951
- };
13952
- await this.executeMethod(target, "call.member.remove", {});
14048
+ await this.executeMethod("call.member.remove", { targets: [this.target] });
13953
14049
  }
13954
14050
  /** Ends the call for this participant. */
13955
14051
  async end() {
13956
- await this.executeMethod(this.id, "call.end", {});
14052
+ await this.executeMethod("call.end", {});
13957
14053
  }
13958
14054
  /**
13959
14055
  * Replaces custom metadata for this participant.
@@ -13973,7 +14069,7 @@ var Participant = class extends Destroyable {
13973
14069
  }
13974
14070
  /** Destroys the participant, releasing all subscriptions and references. */
13975
14071
  destroy() {
13976
- this.executeMethod = void 0;
14072
+ this.callExecuteMethod = void 0;
13977
14073
  super.destroy();
13978
14074
  }
13979
14075
  };
@@ -13985,8 +14081,8 @@ var Participant = class extends Destroyable {
13985
14081
  */
13986
14082
  var SelfParticipant = class extends Participant {
13987
14083
  /** @internal */
13988
- constructor(id, executeMethod, vertoManager, deviceController) {
13989
- super(id, executeMethod, deviceController);
14084
+ constructor(id, callExecuteMethod, vertoManager, deviceController) {
14085
+ super(id, callExecuteMethod, deviceController);
13990
14086
  this.vertoManager = vertoManager;
13991
14087
  this._studioAudio$ = this.createBehaviorSubject(false);
13992
14088
  this.capabilities = new SelfCapabilities();
@@ -14010,7 +14106,7 @@ var SelfParticipant = class extends Participant {
14010
14106
  async enableStudioAudio() {
14011
14107
  if (this._studioAudio$.value) return;
14012
14108
  this._studioAudio$.next(true);
14013
- await this.executeMethod(this.id, "call.audioflags.set", {
14109
+ await this.executeMethod("call.audioflags.set", {
14014
14110
  echo_cancellation: false,
14015
14111
  auto_gain: false,
14016
14112
  noise_suppression: false
@@ -14023,18 +14119,27 @@ var SelfParticipant = class extends Participant {
14023
14119
  async disableStudioAudio() {
14024
14120
  if (!this._studioAudio$.value) return;
14025
14121
  this._studioAudio$.next(false);
14026
- await this.executeMethod(this.id, "call.audioflags.set", {
14122
+ await this.executeMethod("call.audioflags.set", {
14027
14123
  echo_cancellation: true,
14028
14124
  auto_gain: true,
14029
14125
  noise_suppression: true
14030
14126
  });
14031
14127
  }
14032
- /** Starts sharing the local screen. */
14128
+ /**
14129
+ * Starts sharing the local screen.
14130
+ *
14131
+ * The call is unaffected when acquisition fails.
14132
+ *
14133
+ * @throws The raw `getDisplayMedia` error. A dismissed picker or a
14134
+ * permission denial rejects with a `NotAllowedError` `DOMException` —
14135
+ * inspect `error.name` to tell benign cancels apart from real failures.
14136
+ */
14033
14137
  async startScreenShare() {
14034
14138
  try {
14035
14139
  await this.vertoManager.addScreenMedia();
14036
14140
  } catch (error) {
14037
- logger$23.error("[Participant.startScreenShare] Screen share error:", error);
14141
+ logger$24.error("[Participant.startScreenShare] Screen share error:", error);
14142
+ throw error;
14038
14143
  }
14039
14144
  }
14040
14145
  /** Observable of the current screen share status. */
@@ -14049,12 +14154,20 @@ var SelfParticipant = class extends Participant {
14049
14154
  async stopScreenShare() {
14050
14155
  return this.vertoManager.removeScreenMedia();
14051
14156
  }
14052
- /** Adds an additional media input device to the call. */
14157
+ /**
14158
+ * Adds an additional media input device to the call.
14159
+ *
14160
+ * The call is unaffected when acquisition fails.
14161
+ *
14162
+ * @throws The raw `getUserMedia` error (e.g. `NotAllowedError` on
14163
+ * permission denial) — inspect `error.name` to decide how to react.
14164
+ */
14053
14165
  async addAdditionalDevice(options) {
14054
14166
  try {
14055
14167
  await this.vertoManager.addInputDevice(options);
14056
14168
  } catch (error) {
14057
- logger$23.error("[Participant.startScreenShare] Screen share error:", error);
14169
+ logger$24.error("[Participant.addAdditionalDevice] Additional device error:", error);
14170
+ throw error;
14058
14171
  }
14059
14172
  }
14060
14173
  /** Removes an additional media input device by ID. */
@@ -14116,7 +14229,7 @@ var SelfParticipant = class extends Participant {
14116
14229
  */
14117
14230
  exitStudioModeIfActive() {
14118
14231
  if (this._studioAudio$.value) {
14119
- logger$23.debug("[SelfParticipant] Exiting studio audio mode due to individual flag toggle");
14232
+ logger$24.debug("[SelfParticipant] Exiting studio audio mode due to individual flag toggle");
14120
14233
  this._studioAudio$.next(false);
14121
14234
  }
14122
14235
  }
@@ -14140,7 +14253,7 @@ var SelfParticipant = class extends Participant {
14140
14253
  try {
14141
14254
  await super.mute();
14142
14255
  } catch (error) {
14143
- logger$23.warn("[Participant.toggleAudioInput] Server Error while muting audio input, proceeding with local toggle anyway", error);
14256
+ logger$24.warn("[Participant.toggleAudioInput] Server Error while muting audio input, proceeding with local toggle anyway", error);
14144
14257
  } finally {
14145
14258
  this.vertoManager.muteMainAudioInputDevice();
14146
14259
  }
@@ -14150,7 +14263,7 @@ var SelfParticipant = class extends Participant {
14150
14263
  try {
14151
14264
  await super.unmute();
14152
14265
  } catch (error) {
14153
- logger$23.warn("[Participant.toggleAudioInput] Server Error while unmuting audio input, proceeding with local toggle anyway", error);
14266
+ logger$24.warn("[Participant.toggleAudioInput] Server Error while unmuting audio input, proceeding with local toggle anyway", error);
14154
14267
  } finally {
14155
14268
  await this.vertoManager.unmuteMainAudioInputDevice();
14156
14269
  }
@@ -14160,7 +14273,7 @@ var SelfParticipant = class extends Participant {
14160
14273
  try {
14161
14274
  await super.muteVideo();
14162
14275
  } catch (error) {
14163
- logger$23.warn("[Participant.toggleVideoInput] Server Error while muting video input, proceeding with local toggle anyway", error);
14276
+ logger$24.warn("[Participant.toggleVideoInput] Server Error while muting video input, proceeding with local toggle anyway", error);
14164
14277
  } finally {
14165
14278
  this.vertoManager.muteMainVideoInputDevice();
14166
14279
  }
@@ -14170,7 +14283,7 @@ var SelfParticipant = class extends Participant {
14170
14283
  try {
14171
14284
  await super.unmuteVideo();
14172
14285
  } catch (error) {
14173
- logger$23.warn("[Participant.toggleVideoInput] Server Error while unmuting video input, proceeding with local toggle anyway", error);
14286
+ logger$24.warn("[Participant.toggleVideoInput] Server Error while unmuting video input, proceeding with local toggle anyway", error);
14174
14287
  } finally {
14175
14288
  await this.vertoManager.unmuteMainVideoInputDevice();
14176
14289
  }
@@ -14375,7 +14488,7 @@ function filterAs(predicate, resultPath) {
14375
14488
  //#endregion
14376
14489
  //#region src/operators/throwOnRPCError.ts
14377
14490
  var import_cjs$21 = require_cjs();
14378
- const logger$22 = getLogger();
14491
+ const logger$23 = getLogger();
14379
14492
  /**
14380
14493
  * RxJS operator that throws a {@link JSONRPCError} when the RPC response contains an error.
14381
14494
  * Passes successful responses through unchanged.
@@ -14383,14 +14496,14 @@ const logger$22 = getLogger();
14383
14496
  function throwOnRPCError() {
14384
14497
  return (0, import_cjs$21.map)((response) => {
14385
14498
  if (response.error) {
14386
- logger$22.error("[throwOnRPCError] RPC error response:", {
14499
+ logger$23.error("[throwOnRPCError] RPC error response:", {
14387
14500
  code: response.error.code,
14388
14501
  message: response.error.message,
14389
14502
  data: response.error.data
14390
14503
  });
14391
14504
  throw new JSONRPCError(response.error.code, response.error.message, response.error.data);
14392
14505
  }
14393
- logger$22.debug("[throwOnRPCError] RPC successful response:", response);
14506
+ logger$23.debug("[throwOnRPCError] RPC successful response:", response);
14394
14507
  return response;
14395
14508
  });
14396
14509
  }
@@ -14398,7 +14511,7 @@ function throwOnRPCError() {
14398
14511
  //#endregion
14399
14512
  //#region src/managers/CallEventsManager.ts
14400
14513
  var import_cjs$20 = require_cjs();
14401
- const logger$21 = getLogger();
14514
+ const logger$22 = getLogger();
14402
14515
  const initialSessionState = {};
14403
14516
  /** @internal */
14404
14517
  var CallEventsManager = class extends Destroyable {
@@ -14502,7 +14615,7 @@ var CallEventsManager = class extends Destroyable {
14502
14615
  }
14503
14616
  initSubscriptions() {
14504
14617
  this.subscribeTo(this.callJoinedEvent$, (callJoinedEvent) => {
14505
- logger$21.debug("[CallEventsManager] Handling call.joined event for call/session IDs:", {
14618
+ logger$22.debug("[CallEventsManager] Handling call.joined event for call/session IDs:", {
14506
14619
  callId: callJoinedEvent.call_id,
14507
14620
  roomSessionId: callJoinedEvent.room_session_id
14508
14621
  });
@@ -14529,19 +14642,19 @@ var CallEventsManager = class extends Destroyable {
14529
14642
  if (this._self$.value?.capabilities.setLayout) this.updateLayouts();
14530
14643
  });
14531
14644
  this.subscribeTo(this.memberUpdates$, (member) => {
14532
- logger$21.debug("[CallEventsManager] Handling member update event for member ID:", member);
14645
+ logger$22.debug("[CallEventsManager] Handling member update event for member ID:", member);
14533
14646
  this.upsertParticipant(member);
14534
14647
  });
14535
14648
  this.subscribeTo(this.webRtcCallSession.memberLeft$, (memberLeftEvent) => {
14536
- logger$21.debug("[CallEventsManager] Handling member.left event for member ID:", memberLeftEvent.member.member_id);
14649
+ logger$22.debug("[CallEventsManager] Handling member.left event for member ID:", memberLeftEvent.member.member_id);
14537
14650
  const participants = { ...this._participants$.value };
14538
14651
  if (memberLeftEvent.member.member_id in participants) {
14539
14652
  delete participants[memberLeftEvent.member.member_id];
14540
14653
  this._participants$.next(participants);
14541
- } else logger$21.warn(`[CallEventsManager] Received member.left event for unknown member ID: ${memberLeftEvent.member.member_id}`);
14654
+ } else logger$22.warn(`[CallEventsManager] Received member.left event for unknown member ID: ${memberLeftEvent.member.member_id}`);
14542
14655
  });
14543
14656
  this.subscribeTo(this.webRtcCallSession.callUpdated$, (callUpdatedEvent) => {
14544
- logger$21.debug("[CallEventsManager] Handling call.updated event:", callUpdatedEvent);
14657
+ logger$22.debug("[CallEventsManager] Handling call.updated event:", callUpdatedEvent);
14545
14658
  const roomSession = callUpdatedEvent.room_session;
14546
14659
  this._sessionState$.next({
14547
14660
  ...this._sessionState$.value,
@@ -14556,7 +14669,7 @@ var CallEventsManager = class extends Destroyable {
14556
14669
  });
14557
14670
  });
14558
14671
  this.subscribeTo(this.layoutChangedEvent$, (layoutChangedEvent) => {
14559
- logger$21.debug("[CallEventsManager] Handling layout.changed event:", layoutChangedEvent);
14672
+ logger$22.debug("[CallEventsManager] Handling layout.changed event:", layoutChangedEvent);
14560
14673
  this._sessionState$.next({
14561
14674
  ...this._sessionState$.value,
14562
14675
  layout_name: layoutChangedEvent.id,
@@ -14566,10 +14679,10 @@ var CallEventsManager = class extends Destroyable {
14566
14679
  });
14567
14680
  }
14568
14681
  updateParticipantPositions(layoutChangedEvent) {
14569
- if (Object.keys(this._participants$.value).length > 0 && !layoutChangedEvent.layers.some((layer) => !!layer.member_id)) logger$21.warn("[CallEventsManager] No layers with member_id found in layout.changed event. Nothing to update.");
14682
+ 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.");
14570
14683
  layoutChangedEvent.layers.filter((layer) => !!layer.member_id).filter((layer) => {
14571
14684
  if (!(layer.member_id in this._participants$.value)) {
14572
- logger$21.warn(`[CallEventsManager] Skipping layout layer for unknown member_id: ${layer.member_id}`);
14685
+ logger$22.warn(`[CallEventsManager] Skipping layout layer for unknown member_id: ${layer.member_id}`);
14573
14686
  return false;
14574
14687
  }
14575
14688
  return true;
@@ -14592,7 +14705,7 @@ var CallEventsManager = class extends Destroyable {
14592
14705
  layouts: response.result.layouts
14593
14706
  });
14594
14707
  }).catch((error) => {
14595
- logger$21.error("[CallEventsManager] Error fetching layouts:", error);
14708
+ logger$22.error("[CallEventsManager] Error fetching layouts:", error);
14596
14709
  });
14597
14710
  }
14598
14711
  updateParticipants(members) {
@@ -14608,7 +14721,7 @@ var CallEventsManager = class extends Destroyable {
14608
14721
  }
14609
14722
  const participant = this._participants$.value[member.member_id];
14610
14723
  const oldValue = participant.value;
14611
- logger$21.debug("[CallEventsManager] Updating participant:", member.member_id, {
14724
+ logger$22.debug("[CallEventsManager] Updating participant:", member.member_id, {
14612
14725
  oldValue,
14613
14726
  newValue: member
14614
14727
  });
@@ -14621,17 +14734,17 @@ var CallEventsManager = class extends Destroyable {
14621
14734
  }
14622
14735
  get callJoinedEvent$() {
14623
14736
  return this.cachedObservable("callJoinedEvent$", () => this.webRtcCallSession.callEvent$.pipe((0, import_cjs$20.filter)(isCallJoinedPayload), (0, import_cjs$20.tap)((event) => {
14624
- logger$21.debug("[CallEventsManager] Call joined event:", event);
14737
+ logger$22.debug("[CallEventsManager] Call joined event:", event);
14625
14738
  })));
14626
14739
  }
14627
14740
  get layoutChangedEvent$() {
14628
14741
  return this.cachedObservable("layoutChangedEvent$", () => this.webRtcCallSession.callEvent$.pipe(filterAs(isLayoutChangedPayload, "layout"), (0, import_cjs$20.tap)((event) => {
14629
- logger$21.debug("[CallEventsManager] Layout changed event:", event);
14742
+ logger$22.debug("[CallEventsManager] Layout changed event:", event);
14630
14743
  })));
14631
14744
  }
14632
14745
  get memberUpdates$() {
14633
14746
  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) => {
14634
- logger$21.debug("[CallEventsManager] Member update event:", event);
14747
+ logger$22.debug("[CallEventsManager] Member update event:", event);
14635
14748
  })));
14636
14749
  }
14637
14750
  destroy() {
@@ -14888,7 +15001,7 @@ function appendStereoParams(fmtpLine, maxBitrate) {
14888
15001
  //#endregion
14889
15002
  //#region src/controllers/ICEGatheringController.ts
14890
15003
  var import_cjs$19 = require_cjs();
14891
- const logger$20 = getLogger();
15004
+ const logger$21 = getLogger();
14892
15005
  var ICEGatheringController = class extends Destroyable {
14893
15006
  constructor(peerConnection, peerConnectionControllerNegotiating$, options = {}) {
14894
15007
  super();
@@ -14896,23 +15009,23 @@ var ICEGatheringController = class extends Destroyable {
14896
15009
  this.peerConnectionControllerNegotiating$ = peerConnectionControllerNegotiating$;
14897
15010
  this.onicegatheringstatechangeHandler = () => {
14898
15011
  const { iceGatheringState } = this.peerConnection;
14899
- logger$20.debug(`[ICEGatheringController] ICE gathering state changed to: ${iceGatheringState}`);
15012
+ logger$21.debug(`[ICEGatheringController] ICE gathering state changed to: ${iceGatheringState}`);
14900
15013
  if (iceGatheringState === "gathering") this._iceCandidatesState.next({
14901
15014
  state: "gathering",
14902
15015
  validSDP: false
14903
15016
  });
14904
15017
  };
14905
15018
  this.onicecandidateHandler = (event) => {
14906
- logger$20.debug("[ICEGatheringController] ICE candidate event received:", event.candidate);
15019
+ logger$21.debug("[ICEGatheringController] ICE candidate event received:", event.candidate);
14907
15020
  this.removeTimer("iceCandidateTimer");
14908
15021
  if (event.candidate) this.iceCandidateTimer = setTimeout(() => {
14909
15022
  if (this.peerConnection.iceGatheringState !== "complete") {
14910
- logger$20.warn("[ICEGatheringController] ICE candidate timeout, using current SDP");
15023
+ logger$21.warn("[ICEGatheringController] ICE candidate timeout, using current SDP");
14911
15024
  this.handleICECandidateTimeout();
14912
15025
  }
14913
15026
  }, this.iceCandidateTimeout);
14914
15027
  else {
14915
- logger$20.debug("[ICEGatheringController] ICE gathering completed: null candidate received");
15028
+ logger$21.debug("[ICEGatheringController] ICE gathering completed: null candidate received");
14916
15029
  this.removeTimer("iceGatheringTimer");
14917
15030
  this.handleICEGatheringComplete();
14918
15031
  }
@@ -14930,7 +15043,7 @@ var ICEGatheringController = class extends Destroyable {
14930
15043
  this.setupEventListeners();
14931
15044
  this.iceGatheringTimer = setTimeout(() => {
14932
15045
  if (this.peerConnection.iceGatheringState !== "complete") {
14933
- logger$20.warn("[ICEGatheringController] ICE gathering timeout, using current SDP");
15046
+ logger$21.warn("[ICEGatheringController] ICE gathering timeout, using current SDP");
14934
15047
  this.handleICEGatheringTimeout();
14935
15048
  }
14936
15049
  }, this.iceGatheringTimeout);
@@ -14957,9 +15070,9 @@ var ICEGatheringController = class extends Destroyable {
14957
15070
  this.relayOnly = value;
14958
15071
  }
14959
15072
  handleICEGatheringComplete() {
14960
- logger$20.debug("[ICEGatheringController] Handling ICE gathering complete");
14961
- logger$20.debug(`[ICEGatheringController] Checking ICE gathering state: ${this.peerConnection.iceGatheringState}`);
14962
- logger$20.debug("[ICEGatheringController] ICE gathering complete");
15073
+ logger$21.debug("[ICEGatheringController] Handling ICE gathering complete");
15074
+ logger$21.debug(`[ICEGatheringController] Checking ICE gathering state: ${this.peerConnection.iceGatheringState}`);
15075
+ logger$21.debug("[ICEGatheringController] ICE gathering complete");
14963
15076
  this._iceCandidatesState.next({
14964
15077
  state: "complete",
14965
15078
  validSDP: this.hasValidLocalDescriptionSDP
@@ -14975,21 +15088,21 @@ var ICEGatheringController = class extends Destroyable {
14975
15088
  this.removeTimer("iceGatheringTimer");
14976
15089
  const validSDP = this.hasValidLocalDescriptionSDP;
14977
15090
  if (validSDP) {
14978
- logger$20.debug("[ICEGatheringController] Local SDP is valid");
15091
+ logger$21.debug("[ICEGatheringController] Local SDP is valid");
14979
15092
  this._iceCandidatesState.next({
14980
15093
  state: "timeout",
14981
15094
  validSDP
14982
15095
  });
14983
15096
  this.stopGathering();
14984
- } else logger$20.debug("### ICE gathering timeout\n", this.peerConnection.localDescription?.sdp);
15097
+ } else logger$21.debug("### ICE gathering timeout\n", this.peerConnection.localDescription?.sdp);
14985
15098
  }
14986
15099
  handleICECandidateTimeout() {
14987
15100
  if (this.iceCandidateTimer) this.removeTimer("iceCandidateTimer");
14988
- logger$20.warn("[ICEGatheringController] ICE candidate timeout");
15101
+ logger$21.warn("[ICEGatheringController] ICE candidate timeout");
14989
15102
  const validSDP = this.hasValidLocalDescriptionSDP;
14990
15103
  if (!validSDP && !this.relayOnly) this.restartICEGatheringWithRelayOnly();
14991
15104
  else {
14992
- logger$20.debug("[ICEGatheringController] Using current SDP due to ICE candidate timeout");
15105
+ logger$21.debug("[ICEGatheringController] Using current SDP due to ICE candidate timeout");
14993
15106
  this._iceCandidatesState.next({
14994
15107
  state: "timeout",
14995
15108
  validSDP
@@ -14998,7 +15111,7 @@ var ICEGatheringController = class extends Destroyable {
14998
15111
  }
14999
15112
  }
15000
15113
  restartICEGatheringWithRelayOnly() {
15001
- logger$20.debug("[ICEGatheringController] Restarting ICE gathering with relay-only candidates");
15114
+ logger$21.debug("[ICEGatheringController] Restarting ICE gathering with relay-only candidates");
15002
15115
  this.relayOnly = true;
15003
15116
  this.peerConnection.setConfiguration({
15004
15117
  ...this.peerConnection.getConfiguration(),
@@ -15013,7 +15126,7 @@ var ICEGatheringController = class extends Destroyable {
15013
15126
  }
15014
15127
  }
15015
15128
  clearAllTimers() {
15016
- logger$20.debug("[ICEGatheringController] Clearing all timers");
15129
+ logger$21.debug("[ICEGatheringController] Clearing all timers");
15017
15130
  this.removeTimer("iceGatheringTimer");
15018
15131
  this.removeTimer("iceCandidateTimer");
15019
15132
  }
@@ -15022,7 +15135,7 @@ var ICEGatheringController = class extends Destroyable {
15022
15135
  this.peerConnection.removeEventListener("icecandidate", this.onicecandidateHandler);
15023
15136
  }
15024
15137
  destroy() {
15025
- logger$20.debug("[ICEGatheringController] Destroying ICEGatheringController");
15138
+ logger$21.debug("[ICEGatheringController] Destroying ICEGatheringController");
15026
15139
  this.clearAllTimers();
15027
15140
  this.removeEventListeners();
15028
15141
  super.destroy();
@@ -15032,7 +15145,7 @@ var ICEGatheringController = class extends Destroyable {
15032
15145
  //#endregion
15033
15146
  //#region src/controllers/LocalAudioPipeline.ts
15034
15147
  var import_cjs$18 = require_cjs();
15035
- const logger$19 = getLogger();
15148
+ const logger$20 = getLogger();
15036
15149
  /**
15037
15150
  * Web Audio pipeline for the local microphone stream.
15038
15151
  *
@@ -15144,7 +15257,7 @@ var LocalAudioPipeline = class extends Destroyable {
15144
15257
  try {
15145
15258
  this._inputSource.disconnect();
15146
15259
  } catch (error) {
15147
- logger$19.debug("[LocalAudioPipeline] input disconnect warning:", error);
15260
+ logger$20.debug("[LocalAudioPipeline] input disconnect warning:", error);
15148
15261
  }
15149
15262
  this._inputSource = null;
15150
15263
  }
@@ -15154,7 +15267,7 @@ var LocalAudioPipeline = class extends Destroyable {
15154
15267
  this._inputSource = this._audioContext.createMediaStreamSource(this._inputStream);
15155
15268
  this._inputSource.connect(this._gainNode);
15156
15269
  if (this._audioContext.state === "suspended") this._audioContext.resume().catch((error) => {
15157
- logger$19.warn("[LocalAudioPipeline] AudioContext resume failed:", error);
15270
+ logger$20.warn("[LocalAudioPipeline] AudioContext resume failed:", error);
15158
15271
  });
15159
15272
  }
15160
15273
  destroy() {
@@ -15169,7 +15282,7 @@ var LocalAudioPipeline = class extends Destroyable {
15169
15282
  this._analyser.disconnect();
15170
15283
  } catch {}
15171
15284
  this._audioContext.close().catch((error) => {
15172
- logger$19.debug("[LocalAudioPipeline] audio context close warning:", error);
15285
+ logger$20.debug("[LocalAudioPipeline] audio context close warning:", error);
15173
15286
  });
15174
15287
  super.destroy();
15175
15288
  }
@@ -15196,7 +15309,7 @@ var LocalAudioPipeline = class extends Destroyable {
15196
15309
  //#endregion
15197
15310
  //#region src/controllers/LocalStreamController.ts
15198
15311
  var import_cjs$17 = require_cjs();
15199
- const logger$18 = getLogger();
15312
+ const logger$19 = getLogger();
15200
15313
  var LocalStreamController = class extends Destroyable {
15201
15314
  constructor(options) {
15202
15315
  super();
@@ -15234,28 +15347,30 @@ var LocalStreamController = class extends Destroyable {
15234
15347
  * Build the local media stream based on the provided options.
15235
15348
  */
15236
15349
  async buildLocalStream() {
15237
- logger$18.debug("[LocalStreamController] Building local media stream.");
15350
+ logger$19.debug("[LocalStreamController] Building local media stream.");
15238
15351
  let stream;
15239
15352
  if (this.options.inputAudioStream ?? this.options.inputVideoStream) {
15240
15353
  const tracks = [...this.options.inputAudioStream?.getTracks() ?? [], ...this.options.inputVideoStream?.getTracks() ?? []];
15241
15354
  stream = new MediaStream(tracks);
15242
15355
  } else if (this.options.propose === "screenshare") {
15243
- logger$18.debug("[LocalStreamController] Requesting display media for screen sharing with audio:", Boolean(this.options.inputAudioDeviceConstraints));
15356
+ logger$19.debug("[LocalStreamController] Requesting display media for screen sharing with audio:", Boolean(this.options.inputAudioDeviceConstraints));
15244
15357
  stream = await this.options.getDisplayMedia({
15245
15358
  video: true,
15246
15359
  audio: Boolean(this.options.inputAudioDeviceConstraints)
15247
15360
  });
15248
- logger$18.debug("[LocalStreamController] Screen share media obtained:", stream);
15361
+ logger$19.debug("[LocalStreamController] Screen share media obtained:", stream);
15249
15362
  } else {
15250
15363
  const constraints = {
15251
15364
  audio: this.options.inputAudioDeviceConstraints,
15252
15365
  video: this.options.inputVideoDeviceConstraints
15253
15366
  };
15254
- logger$18.debug("[LocalStreamController] Requesting user media with constraints:", constraints);
15367
+ logger$19.debug("[LocalStreamController] Requesting user media with constraints:", constraints);
15255
15368
  stream = await this.options.getUserMedia(constraints);
15256
- logger$18.debug("[LocalStreamController] User media obtained:", stream);
15369
+ logger$19.debug("[LocalStreamController] User media obtained:", stream);
15257
15370
  }
15258
15371
  this._localStream$.next(stream);
15372
+ this._localAudioTracks$.next(stream.getAudioTracks());
15373
+ this._localVideoTracks$.next(stream.getVideoTracks());
15259
15374
  return stream;
15260
15375
  }
15261
15376
  /**
@@ -15270,7 +15385,7 @@ var LocalStreamController = class extends Destroyable {
15270
15385
  this._localStream$.next(localStream);
15271
15386
  if (track.kind === "video") this._localVideoTracks$.next(localStream.getVideoTracks());
15272
15387
  else this._localAudioTracks$.next(localStream.getAudioTracks());
15273
- logger$18.debug(`[LocalStreamController] ${track.kind} track added:`, track.id);
15388
+ logger$19.debug(`[LocalStreamController] ${track.kind} track added:`, track.id);
15274
15389
  return localStream;
15275
15390
  }
15276
15391
  /**
@@ -15282,7 +15397,7 @@ var LocalStreamController = class extends Destroyable {
15282
15397
  const stream = this._localStream$.value;
15283
15398
  const track = stream?.getTracks().find((t) => t.id === trackId);
15284
15399
  if (!track) {
15285
- logger$18.debug(`[LocalStreamController] track not found: ${trackId}`);
15400
+ logger$19.debug(`[LocalStreamController] track not found: ${trackId}`);
15286
15401
  return;
15287
15402
  }
15288
15403
  track.removeEventListener("ended", this.mediaTrackEndedHandler);
@@ -15291,7 +15406,7 @@ var LocalStreamController = class extends Destroyable {
15291
15406
  this._localStream$.next(stream);
15292
15407
  if (track.kind === "video") this._localVideoTracks$.next(stream?.getVideoTracks() ?? []);
15293
15408
  else this._localAudioTracks$.next(stream?.getAudioTracks() ?? []);
15294
- logger$18.debug(`[LocalStreamController] ${track.kind} track removed:`, trackId);
15409
+ logger$19.debug(`[LocalStreamController] ${track.kind} track removed:`, trackId);
15295
15410
  return track;
15296
15411
  }
15297
15412
  /**
@@ -15326,7 +15441,7 @@ var LocalStreamController = class extends Destroyable {
15326
15441
  */
15327
15442
  stopAllTracks() {
15328
15443
  this._localStream$.value?.getTracks().forEach((track) => {
15329
- logger$18.debug(`[LocalStreamController] Stopping local track: ${track.kind}`);
15444
+ logger$19.debug(`[LocalStreamController] Stopping local track: ${track.kind}`);
15330
15445
  track.removeEventListener("ended", this.mediaTrackEndedHandler);
15331
15446
  track.stop();
15332
15447
  });
@@ -15342,7 +15457,7 @@ var LocalStreamController = class extends Destroyable {
15342
15457
 
15343
15458
  //#endregion
15344
15459
  //#region src/controllers/TransceiverController.ts
15345
- const logger$17 = getLogger();
15460
+ const logger$18 = getLogger();
15346
15461
  const getDirection = (send, recv) => {
15347
15462
  if (send && recv) return "sendrecv";
15348
15463
  else if (send && !recv) return "sendonly";
@@ -15423,6 +15538,12 @@ var TransceiverController = class extends Destroyable {
15423
15538
  scaleResolutionDownBy: Number(rid) * 6 || 1
15424
15539
  }));
15425
15540
  }
15541
+ /**
15542
+ * Resolve the current MediaTrackConstraints for an input kind, normalising
15543
+ * boolean shorthand to an empty object. Public so the surrounding
15544
+ * RTCPeerConnectionController can drive its own pipeline-aware getUserMedia
15545
+ * call with the same effective constraints the transceiver would have used.
15546
+ */
15426
15547
  getConstraintsFor(kind) {
15427
15548
  const constraints = kind === "audio" ? this.inputAudioDeviceConstraints : this.inputVideoDeviceConstraints;
15428
15549
  return typeof constraints === "boolean" ? {} : constraints;
@@ -15444,7 +15565,7 @@ var TransceiverController = class extends Destroyable {
15444
15565
  sendEncodings: isAudio ? void 0 : this.sendEncodings,
15445
15566
  streams: direction === "recvonly" ? void 0 : [localStream]
15446
15567
  };
15447
- logger$17.debug(`[TransceiverController] Setting up transceiver sender for local ${track.kind} track:`, {
15568
+ logger$18.debug(`[TransceiverController] Setting up transceiver sender for local ${track.kind} track:`, {
15448
15569
  transceiver,
15449
15570
  transceiverParams
15450
15571
  });
@@ -15452,11 +15573,11 @@ var TransceiverController = class extends Destroyable {
15452
15573
  await transceiver.sender.replaceTrack(track);
15453
15574
  transceiver.direction = transceiverParams.direction;
15454
15575
  if (transceiverParams.streams?.some((stream) => Boolean(stream))) {
15455
- logger$17.debug(`[TransceiverController] Setting streams for transceiver sender for local ${track.kind} track:`, transceiverParams.streams);
15576
+ logger$18.debug(`[TransceiverController] Setting streams for transceiver sender for local ${track.kind} track:`, transceiverParams.streams);
15456
15577
  transceiver.sender.setStreams(...transceiverParams.streams);
15457
15578
  }
15458
15579
  } else {
15459
- logger$17.debug(`[TransceiverController] Adding new transceiver for local ${track.kind} track:`, track.id);
15580
+ logger$18.debug(`[TransceiverController] Adding new transceiver for local ${track.kind} track:`, track.id);
15460
15581
  this.peerConnection.addTransceiver(track, transceiverParams);
15461
15582
  }
15462
15583
  }
@@ -15470,13 +15591,13 @@ var TransceiverController = class extends Destroyable {
15470
15591
  if (options.updateTransceiverDirection) transceiver.direction = "inactive";
15471
15592
  }
15472
15593
  } catch (error) {
15473
- logger$17.error("[TransceiverController] stopTrackSender error", kind, error);
15594
+ logger$18.error("[TransceiverController] stopTrackSender error", kind, error);
15474
15595
  this.options.onError?.(new MediaTrackError("stopTrackSender", kind, error));
15475
15596
  }
15476
15597
  }
15477
15598
  async restoreTrackSender(kind) {
15478
15599
  try {
15479
- logger$17.debug("[TransceiverController] restoreTrackSender called", kind);
15600
+ logger$18.debug("[TransceiverController] restoreTrackSender called", kind);
15480
15601
  const constraints = {};
15481
15602
  const transceivers = this.transceiverByKind(kind);
15482
15603
  for (const transceiver of transceivers) {
@@ -15486,23 +15607,23 @@ var TransceiverController = class extends Destroyable {
15486
15607
  if (trackKind === "audio" || trackKind === "video") constraints[trackKind] = this.getConstraintsFor(trackKind);
15487
15608
  }
15488
15609
  }
15489
- logger$17.debug("[TransceiverController] restoreTrackSender constraints:", constraints);
15610
+ logger$18.debug("[TransceiverController] restoreTrackSender constraints:", constraints);
15490
15611
  if (Object.keys(constraints).length === 0) {
15491
- logger$17.warn("[TransceiverController] restoreTrackSender: no tracks need restoration", kind);
15612
+ logger$18.warn("[TransceiverController] restoreTrackSender: no tracks need restoration", kind);
15492
15613
  return;
15493
15614
  }
15494
15615
  const newTracks = (await this.options.getUserMedia(constraints)).getTracks();
15495
- logger$17.debug("[TransceiverController] restoreTrackSender new tracks:", newTracks);
15616
+ logger$18.debug("[TransceiverController] restoreTrackSender new tracks:", newTracks);
15496
15617
  for (const newTrack of newTracks) {
15497
15618
  this.options.localStreamController.addTrack(newTrack);
15498
15619
  const trackKind = newTrack.kind;
15499
15620
  const transceiverOfKind = this.transceiverByKind(trackKind)[0];
15500
15621
  transceiverOfKind.direction = trackKind === "audio" ? this.audioDirection : this.videoDirection;
15501
- logger$17.debug("[TransceiverController] restoreTrackSender setting direction for", trackKind, transceiverOfKind.direction);
15622
+ logger$18.debug("[TransceiverController] restoreTrackSender setting direction for", trackKind, transceiverOfKind.direction);
15502
15623
  await transceiverOfKind.sender.replaceTrack(newTrack);
15503
15624
  }
15504
15625
  } catch (error) {
15505
- logger$17.error("[TransceiverController] restoreTrackSender error", kind, error);
15626
+ logger$18.error("[TransceiverController] restoreTrackSender error", kind, error);
15506
15627
  this.options.onError?.(new MediaTrackError("restoreTrackSender", kind, error));
15507
15628
  }
15508
15629
  }
@@ -15543,14 +15664,14 @@ var TransceiverController = class extends Destroyable {
15543
15664
  };
15544
15665
  try {
15545
15666
  await track.applyConstraints(constraintsToApply);
15546
- logger$17.debug(`[TransceiverController] Updated ${kind} sender constraints:`, constraintsToApply);
15547
- logger$17.debug(`[TransceiverController] Updated ${kind} sender constraints:`, track.getConstraints());
15667
+ logger$18.debug(`[TransceiverController] Updated ${kind} sender constraints:`, constraintsToApply);
15668
+ logger$18.debug(`[TransceiverController] Updated ${kind} sender constraints:`, track.getConstraints());
15548
15669
  } catch (error) {
15549
- logger$17.warn(`[TransceiverController] applyConstraints failed for ${kind} track ${track.id}, attempting track replacement fallback:`, error);
15670
+ logger$18.warn(`[TransceiverController] applyConstraints failed for ${kind} track ${track.id}, attempting track replacement fallback:`, error);
15550
15671
  try {
15551
15672
  await this.replaceTrackFallback(sender, track, kind, constraintsToApply);
15552
15673
  } catch (fallbackError) {
15553
- logger$17.warn(`[TransceiverController] Track replacement fallback also failed for ${kind} track:`, fallbackError);
15674
+ logger$18.warn(`[TransceiverController] Track replacement fallback also failed for ${kind} track:`, fallbackError);
15554
15675
  this.options.onError?.(new MediaTrackError("updateSendersConstraints", kind, fallbackError));
15555
15676
  }
15556
15677
  }
@@ -15578,7 +15699,7 @@ var TransceiverController = class extends Destroyable {
15578
15699
  if (!newTrack) throw new MediaTrackError("replaceTrackFallback", kind, /* @__PURE__ */ new Error("getUserMedia returned no track of the requested kind"));
15579
15700
  await sender.replaceTrack(newTrack);
15580
15701
  this.options.localStreamController.addTrack(newTrack);
15581
- logger$17.debug(`[TransceiverController] Track replacement fallback succeeded for ${kind}. New track: ${newTrack.id}`);
15702
+ logger$18.debug(`[TransceiverController] Track replacement fallback succeeded for ${kind}. New track: ${newTrack.id}`);
15582
15703
  }
15583
15704
  getMediaDirections() {
15584
15705
  if (this.peerConnection.connectionState === "connected") return this.peerConnection.getTransceivers().reduce((acc, transceiver) => {
@@ -15609,7 +15730,7 @@ var TransceiverController = class extends Destroyable {
15609
15730
  //#endregion
15610
15731
  //#region src/controllers/RTCPeerConnectionController.ts
15611
15732
  var import_cjs$16 = require_cjs();
15612
- const logger$16 = getLogger();
15733
+ const logger$17 = getLogger();
15613
15734
  var RTCPeerConnectionController = class extends Destroyable {
15614
15735
  constructor(options = {}, remoteSessionDescription, deviceController) {
15615
15736
  super();
@@ -15625,43 +15746,43 @@ var RTCPeerConnectionController = class extends Destroyable {
15625
15746
  this.oniceconnectionstatechangeHandler = () => {
15626
15747
  if (this.peerConnection) {
15627
15748
  const { iceConnectionState } = this.peerConnection;
15628
- logger$16.debug(`[RTCPeerConnectionController] ICE connection state changed to: ${iceConnectionState}`);
15749
+ logger$17.debug(`[RTCPeerConnectionController] ICE connection state changed to: ${iceConnectionState}`);
15629
15750
  this._iceConnectionState$.next(this.peerConnection.iceConnectionState);
15630
15751
  }
15631
15752
  };
15632
15753
  this.onconnectionstatechangeHandler = () => {
15633
15754
  if (this.peerConnection) {
15634
15755
  const { connectionState } = this.peerConnection;
15635
- logger$16.debug(`[RTCPeerConnectionController] Connection state changed to: ${connectionState}`);
15756
+ logger$17.debug(`[RTCPeerConnectionController] Connection state changed to: ${connectionState}`);
15636
15757
  if (connectionState === "connected") this.removeConnectionTimer();
15637
15758
  this._connectionState$.next(this.peerConnection.connectionState);
15638
15759
  }
15639
15760
  };
15640
15761
  this.onsignalingstatechangeHandler = () => {
15641
- logger$16.debug(`[RTCPeerConnectionController] Signaling state changed to: ${this.peerConnection?.signalingState}`);
15762
+ logger$17.debug(`[RTCPeerConnectionController] Signaling state changed to: ${this.peerConnection?.signalingState}`);
15642
15763
  };
15643
15764
  this.onicegatheringstatechangeHandler = () => {
15644
15765
  if (this.peerConnection) this._iceGatheringState$.next(this.peerConnection.iceGatheringState);
15645
15766
  };
15646
15767
  this.onnegotiationneededHandler = (event) => {
15647
- logger$16.debug("[RTCPeerConnectionController] Negotiation needed event received.", event);
15768
+ logger$17.debug("[RTCPeerConnectionController] Negotiation needed event received.", event);
15648
15769
  this.negotiationNeeded$.next();
15649
15770
  };
15650
15771
  this.updateSelectedInputDevice = async (kind, deviceInfo) => {
15651
15772
  try {
15652
15773
  const { localStream } = this;
15653
15774
  if (!localStream) {
15654
- logger$16.warn("[RTCPeerConnectionController] No local stream available to update input device.");
15775
+ logger$17.warn("[RTCPeerConnectionController] No local stream available to update input device.");
15655
15776
  return;
15656
15777
  }
15657
- logger$16.debug(`[RTCPeerConnectionController] Updating selected ${kind} input device:`, localStream.getTracks());
15778
+ logger$17.debug(`[RTCPeerConnectionController] Updating selected ${kind} input device:`, localStream.getTracks());
15658
15779
  const track = localStream.getTracks().find((track$1) => track$1.kind === kind);
15659
15780
  if (track) {
15660
15781
  this.transceiverController?.stopTrackSender(kind);
15661
15782
  this.localStreamController.removeTrack(track.id);
15662
- logger$16.debug(`[RTCPeerConnectionController] Stopped existing ${kind} track: ${track.id}`, localStream.getTracks());
15783
+ logger$17.debug(`[RTCPeerConnectionController] Stopped existing ${kind} track: ${track.id}`, localStream.getTracks());
15663
15784
  if (!deviceInfo) {
15664
- logger$16.debug(`[RTCPeerConnectionController] ${kind} input device selected: none`);
15785
+ logger$17.debug(`[RTCPeerConnectionController] ${kind} input device selected: none`);
15665
15786
  return;
15666
15787
  }
15667
15788
  const streamTrack = (await this.getUserMedia({ [kind]: {
@@ -15669,16 +15790,16 @@ var RTCPeerConnectionController = class extends Destroyable {
15669
15790
  ...this.deviceController.deviceInfoToConstraints(deviceInfo)
15670
15791
  } })).getTracks().find((t) => t.kind === kind);
15671
15792
  if (streamTrack) {
15672
- logger$16.debug(`[RTCPeerConnectionController] Adding new ${kind} track: ${streamTrack.id}`);
15793
+ logger$17.debug(`[RTCPeerConnectionController] Adding new ${kind} track: ${streamTrack.id}`);
15673
15794
  this.localStreamController.addTrack(streamTrack);
15674
15795
  await this.transceiverController?.replaceSenderTrack(kind, streamTrack);
15675
- logger$16.debug(`[RTCPeerConnectionController] Added new ${kind} track: ${streamTrack.id}`, this.localStream?.getTracks());
15796
+ logger$17.debug(`[RTCPeerConnectionController] Added new ${kind} track: ${streamTrack.id}`, this.localStream?.getTracks());
15676
15797
  }
15677
15798
  }
15678
- logger$16.debug(`[RTCPeerConnectionController] ${kind} input device selected:`, deviceInfo?.label);
15799
+ logger$17.debug(`[RTCPeerConnectionController] ${kind} input device selected:`, deviceInfo?.label);
15679
15800
  } catch (error) {
15680
- logger$16.error(`[RTCPeerConnectionController] Failed to select ${kind} input device:`, error);
15681
- this._errors$.next(toError(error));
15801
+ logger$17.error(`[RTCPeerConnectionController] Failed to select ${kind} input device:`, error);
15802
+ this._errors$.next(new MediaTrackError("updateSelectedInputDevice", kind, error));
15682
15803
  throw error;
15683
15804
  }
15684
15805
  };
@@ -15749,7 +15870,19 @@ var RTCPeerConnectionController = class extends Destroyable {
15749
15870
  return this._memberId;
15750
15871
  }
15751
15872
  stopTrackSender(kind, options = { updateTransceiverDirection: false }) {
15752
- this.transceiverController?.stopTrackSender(kind, options);
15873
+ const audioCovered = kind === "audio" || kind === "both";
15874
+ if (audioCovered && this._localAudioPipeline) this.stopRawAudioInputForPipeline();
15875
+ if (!audioCovered) this.transceiverController?.stopTrackSender(kind, options);
15876
+ else if (kind === "both") this.transceiverController?.stopTrackSender("video", options);
15877
+ else if (!this._localAudioPipeline) this.transceiverController?.stopTrackSender(kind, options);
15878
+ }
15879
+ stopRawAudioInputForPipeline() {
15880
+ const rawTracks = this.localStreamController.localAudioTracks;
15881
+ for (const track of rawTracks) if (track.readyState === "live") {
15882
+ track.stop();
15883
+ this.localStreamController.removeTrack(track.id);
15884
+ }
15885
+ this._localAudioPipeline?.setInputTrack(null);
15753
15886
  }
15754
15887
  get isNegotiating$() {
15755
15888
  return this._isNegotiating$.asObservable();
@@ -15888,7 +16021,7 @@ var RTCPeerConnectionController = class extends Destroyable {
15888
16021
  case "main":
15889
16022
  default: return {
15890
16023
  ...options,
15891
- offerToReceiveAudio: true,
16024
+ offerToReceiveAudio: this.options.receiveAudio ?? true,
15892
16025
  offerToReceiveVideo: this.options.receiveVideo ?? Boolean(this.inputVideoDeviceConstraints)
15893
16026
  };
15894
16027
  }
@@ -15915,15 +16048,15 @@ var RTCPeerConnectionController = class extends Destroyable {
15915
16048
  this.setupPeerConnection();
15916
16049
  this.subscribeTo(this.negotiationNeeded$.pipe((0, import_cjs$16.auditTime)(0), (0, import_cjs$16.exhaustMap)(async () => this.startNegotiation())), {
15917
16050
  next: () => {
15918
- logger$16.debug("[RTCPeerConnectionController] Start Negotiation completed successfully");
16051
+ logger$17.debug("[RTCPeerConnectionController] Start Negotiation completed successfully");
15919
16052
  },
15920
16053
  error: (error) => {
15921
- logger$16.error("[RTCPeerConnectionController] Start Negotiation error:", error);
16054
+ logger$17.error("[RTCPeerConnectionController] Start Negotiation error:", error);
15922
16055
  this._errors$.next(toError(error));
15923
16056
  }
15924
16057
  });
15925
16058
  this.subscribeTo((0, import_cjs$16.merge)(this.deviceController.selectedAudioInputDevice$.pipe((0, import_cjs$16.map)((deviceInfo) => ["audio", deviceInfo])), this.deviceController.selectedVideoInputDevice$.pipe((0, import_cjs$16.map)((deviceInfo) => ["video", deviceInfo]))).pipe((0, import_cjs$16.skipWhile)(() => !this.localStreamController.localStream)), async ([kind, deviceInfo]) => {
15926
- logger$16.debug(`[RTCPeerConnectionController] Selected input device changed for:`, {
16059
+ logger$17.debug(`[RTCPeerConnectionController] Selected input device changed for:`, {
15927
16060
  kind,
15928
16061
  deviceInfo
15929
16062
  });
@@ -15940,7 +16073,7 @@ var RTCPeerConnectionController = class extends Destroyable {
15940
16073
  this._initialized$.next(true);
15941
16074
  }
15942
16075
  } catch (error) {
15943
- logger$16.error("[RTCPeerConnectionController] Initialization error:", error);
16076
+ logger$17.error("[RTCPeerConnectionController] Initialization error:", error);
15944
16077
  this._errors$.next(toError(error));
15945
16078
  this.destroy();
15946
16079
  }
@@ -15972,22 +16105,22 @@ var RTCPeerConnectionController = class extends Destroyable {
15972
16105
  }
15973
16106
  async startNegotiation() {
15974
16107
  if (this.isNegotiating) {
15975
- logger$16.debug("[RTCPeerConnectionController] Negotiation already in progress, skipping.");
16108
+ logger$17.debug("[RTCPeerConnectionController] Negotiation already in progress, skipping.");
15976
16109
  return;
15977
16110
  }
15978
16111
  this.setupEventListeners();
15979
16112
  if (this.type === "answer") {
15980
- logger$16.debug("[RTCPeerConnectionController] This is an answer type still, skipping offer creation.");
16113
+ logger$17.debug("[RTCPeerConnectionController] This is an answer type still, skipping offer creation.");
15981
16114
  return;
15982
16115
  }
15983
16116
  this._isNegotiating$.next(true);
15984
- logger$16.debug("[RTCPeerConnectionController] Starting negotiation.");
16117
+ logger$17.debug("[RTCPeerConnectionController] Starting negotiation.");
15985
16118
  try {
15986
16119
  const { offerOptions } = this;
15987
- logger$16.debug("[RTCPeerConnectionController] Creating offer with options:", offerOptions);
16120
+ logger$17.debug("[RTCPeerConnectionController] Creating offer with options:", offerOptions);
15988
16121
  await this.createOffer(offerOptions);
15989
16122
  } catch (error) {
15990
- logger$16.error("[RTCPeerConnectionController] Error during negotiation:", error);
16123
+ logger$17.error("[RTCPeerConnectionController] Error during negotiation:", error);
15991
16124
  this._errors$.next(toError(error));
15992
16125
  }
15993
16126
  }
@@ -16003,14 +16136,14 @@ var RTCPeerConnectionController = class extends Destroyable {
16003
16136
  let readyToConnect = status !== "failed";
16004
16137
  try {
16005
16138
  if (status === "received" && sdp) {
16006
- logger$16.debug("[RTCPeerConnectionController] Received answer SDP:", sdp);
16139
+ logger$17.debug("[RTCPeerConnectionController] Received answer SDP:", sdp);
16007
16140
  await this._setRemoteDescription({
16008
16141
  type: "answer",
16009
16142
  sdp
16010
16143
  });
16011
16144
  }
16012
16145
  } catch (error) {
16013
- logger$16.error("[RTCPeerConnectionController] Error updating answer status:", error);
16146
+ logger$17.error("[RTCPeerConnectionController] Error updating answer status:", error);
16014
16147
  this._errors$.next(toError(error));
16015
16148
  readyToConnect = false;
16016
16149
  } finally {
@@ -16029,7 +16162,7 @@ var RTCPeerConnectionController = class extends Destroyable {
16029
16162
  await this.handleOfferReceived();
16030
16163
  break;
16031
16164
  case "failed":
16032
- logger$16.error("[RTCPeerConnectionController] Offer failed to be processed by remote.");
16165
+ logger$17.error("[RTCPeerConnectionController] Offer failed to be processed by remote.");
16033
16166
  break;
16034
16167
  case "sent":
16035
16168
  default:
@@ -16042,13 +16175,14 @@ var RTCPeerConnectionController = class extends Destroyable {
16042
16175
  */
16043
16176
  async acceptInbound(mediaOverrides) {
16044
16177
  if (mediaOverrides) {
16045
- const { audio, video, receiveAudio, receiveVideo } = mediaOverrides;
16178
+ const { audio, video, receiveAudio, receiveVideo, fallbackToReceiveOnly } = mediaOverrides;
16046
16179
  this.options = {
16047
16180
  ...this.options,
16048
16181
  ...audio !== void 0 ? { audio } : {},
16049
16182
  ...video !== void 0 ? { video } : {},
16050
16183
  ...receiveAudio !== void 0 ? { receiveAudio } : {},
16051
- ...receiveVideo !== void 0 ? { receiveVideo } : {}
16184
+ ...receiveVideo !== void 0 ? { receiveVideo } : {},
16185
+ ...fallbackToReceiveOnly !== void 0 ? { fallbackToReceiveOnly } : {}
16052
16186
  };
16053
16187
  this.transceiverController?.updateOptions({
16054
16188
  receiveAudio: this.receiveAudio,
@@ -16061,7 +16195,7 @@ var RTCPeerConnectionController = class extends Destroyable {
16061
16195
  }
16062
16196
  await this.setupLocalTracks();
16063
16197
  const { answerOptions } = this;
16064
- logger$16.debug("[RTCPeerConnectionController] Creating inbound answer with options:", answerOptions);
16198
+ logger$17.debug("[RTCPeerConnectionController] Creating inbound answer with options:", answerOptions);
16065
16199
  await this.createAnswer(answerOptions);
16066
16200
  }
16067
16201
  async handleOfferReceived() {
@@ -16069,7 +16203,7 @@ var RTCPeerConnectionController = class extends Destroyable {
16069
16203
  this._isNegotiating$.next(true);
16070
16204
  await this._setRemoteDescription(this.sdpInit);
16071
16205
  const { answerOptions } = this;
16072
- logger$16.debug("[RTCPeerConnectionController] Creating answer with options:", answerOptions);
16206
+ logger$17.debug("[RTCPeerConnectionController] Creating answer with options:", answerOptions);
16073
16207
  await this.createAnswer(answerOptions);
16074
16208
  }
16075
16209
  readyToConnect() {
@@ -16077,7 +16211,7 @@ var RTCPeerConnectionController = class extends Destroyable {
16077
16211
  this.connectionTimer = setTimeout(() => {
16078
16212
  this.removeConnectionTimer();
16079
16213
  if (this.peerConnection?.connectionState !== "connected") {
16080
- logger$16.debug("[RTCPeerConnectionController] Connection timeout, restarting ICE gathering with relay only.");
16214
+ logger$17.debug("[RTCPeerConnectionController] Connection timeout, restarting ICE gathering with relay only.");
16081
16215
  this.iceGatheringController.restartICEGatheringWithRelayOnly();
16082
16216
  }
16083
16217
  }, this.connectionTimeout);
@@ -16099,14 +16233,14 @@ var RTCPeerConnectionController = class extends Destroyable {
16099
16233
  const stereo = this.options.stereo ?? PreferencesContainer.instance.stereoAudio;
16100
16234
  if (preferredAudioCodecs.length > 0 || preferredVideoCodecs.length > 0) {
16101
16235
  result = setCodecPreferences(result, preferredAudioCodecs, preferredVideoCodecs);
16102
- logger$16.debug("[RTCPeerConnectionController] Applied codec preferences to SDP", {
16236
+ logger$17.debug("[RTCPeerConnectionController] Applied codec preferences to SDP", {
16103
16237
  preferredAudioCodecs,
16104
16238
  preferredVideoCodecs
16105
16239
  });
16106
16240
  }
16107
16241
  if (stereo) {
16108
16242
  result = enableStereoOpus(result);
16109
- logger$16.debug("[RTCPeerConnectionController] Applied stereo Opus to SDP");
16243
+ logger$17.debug("[RTCPeerConnectionController] Applied stereo Opus to SDP");
16110
16244
  }
16111
16245
  return Promise.resolve(result);
16112
16246
  }
@@ -16162,25 +16296,25 @@ var RTCPeerConnectionController = class extends Destroyable {
16162
16296
  ...this.peerConnection.getConfiguration(),
16163
16297
  iceTransportPolicy: "relay"
16164
16298
  });
16165
- logger$16.debug("[RTCPeerConnectionController] ICE transport policy set to relay-only");
16299
+ logger$17.debug("[RTCPeerConnectionController] ICE transport policy set to relay-only");
16166
16300
  } catch (error) {
16167
- logger$16.warn("[RTCPeerConnectionController] Failed to set relay-only policy:", error);
16301
+ logger$17.warn("[RTCPeerConnectionController] Failed to set relay-only policy:", error);
16168
16302
  }
16169
16303
  this.setupEventListeners();
16170
16304
  this._isNegotiating$.next(true);
16171
- logger$16.debug(`[RTCPeerConnectionController] Triggering ICE restart${relayOnly ? " (relay-only)" : ""}.`);
16305
+ logger$17.debug(`[RTCPeerConnectionController] Triggering ICE restart${relayOnly ? " (relay-only)" : ""}.`);
16172
16306
  try {
16173
16307
  const offer = await this.peerConnection.createOffer({ iceRestart: true });
16174
16308
  await this.setLocalDescription(offer);
16175
16309
  } catch (error) {
16176
- logger$16.error("[RTCPeerConnectionController] ICE restart offer failed:", error);
16310
+ logger$17.error("[RTCPeerConnectionController] ICE restart offer failed:", error);
16177
16311
  this._errors$.next(toError(error));
16178
16312
  this.negotiationEnded();
16179
16313
  if (policyChanged) this.restoreIceTransportPolicy();
16180
16314
  throw error;
16181
16315
  }
16182
16316
  if (policyChanged) (0, import_cjs$16.firstValueFrom)((0, import_cjs$16.race)(this._iceGatheringState$.pipe((0, import_cjs$16.filter)((state) => state === "complete"), (0, import_cjs$16.take)(1)), (0, import_cjs$16.timer)(ICE_GATHERING_COMPLETE_TIMEOUT_MS).pipe((0, import_cjs$16.map)(() => "timeout")))).then(() => this.restoreIceTransportPolicy()).catch((error) => {
16183
- logger$16.warn("[RTCPeerConnectionController] Error waiting for ICE gathering to complete:", error);
16317
+ logger$17.warn("[RTCPeerConnectionController] Error waiting for ICE gathering to complete:", error);
16184
16318
  this.restoreIceTransportPolicy();
16185
16319
  });
16186
16320
  }
@@ -16190,9 +16324,9 @@ var RTCPeerConnectionController = class extends Destroyable {
16190
16324
  ...this.peerConnection.getConfiguration(),
16191
16325
  iceTransportPolicy: this.options.relayOnly ? "relay" : "all"
16192
16326
  });
16193
- logger$16.debug("[RTCPeerConnectionController] ICE transport policy restored");
16327
+ logger$17.debug("[RTCPeerConnectionController] ICE transport policy restored");
16194
16328
  } catch (error) {
16195
- logger$16.warn("[RTCPeerConnectionController] Failed to restore ICE transport policy:", error);
16329
+ logger$17.warn("[RTCPeerConnectionController] Failed to restore ICE transport policy:", error);
16196
16330
  }
16197
16331
  }
16198
16332
  /**
@@ -16204,13 +16338,25 @@ var RTCPeerConnectionController = class extends Destroyable {
16204
16338
  await this.setupRemoteTracks();
16205
16339
  }
16206
16340
  async setupLocalTracks() {
16207
- logger$16.debug("[RTCPeerConnectionController] Setting up local tracks/transceivers.");
16208
- const localStream = this.localStream ?? await this.localStreamController.buildLocalStream();
16341
+ logger$17.debug("[RTCPeerConnectionController] Setting up local tracks/transceivers.");
16342
+ if (this.hasNoLocalMediaToSend()) {
16343
+ if (!this.receiveAudio && !this.receiveVideo) throw new InvalidParams("Call requests no media: enable audio/video or receiveAudio/receiveVideo");
16344
+ logger$17.debug("[RTCPeerConnectionController] No local media requested; negotiating receive-only.");
16345
+ this.setupReceiveOnlyTransceivers();
16346
+ return;
16347
+ }
16348
+ let localStream;
16349
+ try {
16350
+ localStream = this.localStream ?? await this.localStreamController.buildLocalStream();
16351
+ } catch (error) {
16352
+ this.handleLocalMediaFailure(error);
16353
+ return;
16354
+ }
16209
16355
  if (this.transceiverController?.useAddStream ?? false) {
16210
- logger$16.warn("[RTCPeerConnectionController] Using deprecated addStream API to add local stream.");
16356
+ logger$17.warn("[RTCPeerConnectionController] Using deprecated addStream API to add local stream.");
16211
16357
  this.peerConnection?.addStream(localStream);
16212
16358
  if (!this.isNegotiating) {
16213
- logger$16.debug("[RTCPeerConnectionController] Forcing negotiationneeded after local tracks setup.");
16359
+ logger$17.debug("[RTCPeerConnectionController] Forcing negotiationneeded after local tracks setup.");
16214
16360
  this.negotiationNeeded$.next();
16215
16361
  }
16216
16362
  return;
@@ -16226,12 +16372,54 @@ var RTCPeerConnectionController = class extends Destroyable {
16226
16372
  const transceivers = (kind === "audio" ? this.transceiverController?.audioTransceivers : this.transceiverController?.videoTransceivers) ?? [];
16227
16373
  await this.transceiverController?.setupTransceiverSender(track, localStream, transceivers[index]);
16228
16374
  } else {
16229
- logger$16.debug(`[RTCPeerConnectionController] Using addTrack for local ${kind} track:`, track.id);
16375
+ logger$17.debug(`[RTCPeerConnectionController] Using addTrack for local ${kind} track:`, track.id);
16230
16376
  this.peerConnection?.addTrack(track, localStream);
16231
16377
  }
16232
16378
  }
16233
16379
  }
16234
16380
  }
16381
+ /** True for a main connection with no local media to send. */
16382
+ hasNoLocalMediaToSend() {
16383
+ const hasInputStreams = Boolean(this.options.inputAudioStream ?? this.options.inputVideoStream);
16384
+ return this.propose === "main" && !this.localStream && !hasInputStreams && !this.inputAudioDeviceConstraints && !this.inputVideoDeviceConstraints;
16385
+ }
16386
+ /** The media kinds this connection wants to send: 'audiovideo' | 'video' | 'audio'. */
16387
+ get requestedMediaKinds() {
16388
+ const wantsAudio = Boolean(this.inputAudioDeviceConstraints);
16389
+ const wantsVideo = Boolean(this.inputVideoDeviceConstraints);
16390
+ if (wantsAudio && wantsVideo) return "audiovideo";
16391
+ return wantsVideo ? "video" : "audio";
16392
+ }
16393
+ /**
16394
+ * Handle a local media acquisition failure with a typed, semantically
16395
+ * accurate MediaAccessError created at the acquisition site:
16396
+ * - Auxiliary connections (screenshare / additional-device) throw a
16397
+ * non-fatal error — VertoManager surfaces it and the call is unaffected.
16398
+ * - The main connection degrades to receive-only when allowed (default),
16399
+ * otherwise fails with a fatal error.
16400
+ */
16401
+ handleLocalMediaFailure(error) {
16402
+ if (this.propose === "screenshare") throw new MediaAccessError("startScreenShare", "screen", error, false);
16403
+ if (this.propose === "additional-device") throw new MediaAccessError("addInputDevice", this.requestedMediaKinds, error, false);
16404
+ const canReceive = this.receiveAudio || this.receiveVideo;
16405
+ if (!((this.options.fallbackToReceiveOnly ?? true) && canReceive)) throw new MediaAccessError("acquireLocalMedia", this.requestedMediaKinds, error, true);
16406
+ logger$17.warn("[RTCPeerConnectionController] Local media unavailable; continuing receive-only:", error);
16407
+ this._errors$.next(new MediaAccessError("acquireLocalMedia", this.requestedMediaKinds, error, false));
16408
+ this.setupReceiveOnlyTransceivers();
16409
+ }
16410
+ /**
16411
+ * Negotiate receive-only m-lines when there are no local tracks to send.
16412
+ * Only offer-type connections add transceivers — answer-type connections
16413
+ * reuse the transceivers created from the remote offer.
16414
+ */
16415
+ setupReceiveOnlyTransceivers() {
16416
+ if (this.type !== "offer") return;
16417
+ if (this.transceiverController?.useAddTransceivers ?? false) {
16418
+ this.peerConnection?.addTransceiver("audio", { direction: this.receiveAudio ? "recvonly" : "inactive" });
16419
+ this.peerConnection?.addTransceiver("video", { direction: this.receiveVideo ? "recvonly" : "inactive" });
16420
+ }
16421
+ if (!this.isNegotiating) this.negotiationNeeded$.next();
16422
+ }
16235
16423
  async getUserMedia(constraints) {
16236
16424
  return (this.options.webRTCApiProvider?.mediaDevices ?? navigator.mediaDevices).getUserMedia(constraints);
16237
16425
  }
@@ -16243,7 +16431,7 @@ var RTCPeerConnectionController = class extends Destroyable {
16243
16431
  async setupRemoteTracks() {
16244
16432
  if (!this.peerConnection) throw new DependencyError("RTCPeerConnection is not initialized");
16245
16433
  this.peerConnection.ontrack = (event) => {
16246
- logger$16.debug("[RTCPeerConnectionController] Remote track received:", event.track.kind);
16434
+ logger$17.debug("[RTCPeerConnectionController] Remote track received:", event.track.kind);
16247
16435
  if (event.streams[0]) this._remoteStream$.next(event.streams[0]);
16248
16436
  else {
16249
16437
  const existingTracks = this._remoteStream$.value?.getTracks() ?? [];
@@ -16254,8 +16442,27 @@ var RTCPeerConnectionController = class extends Destroyable {
16254
16442
  await this.transceiverController?.setupRemoteTransceivers(this.type);
16255
16443
  }
16256
16444
  async restoreTrackSender(kind) {
16257
- await this.transceiverController?.restoreTrackSender(kind);
16258
- if (kind !== "video" && this._localAudioPipeline) await this.applyLocalAudioPipelineToSender();
16445
+ const audioCovered = kind === "audio" || kind === "both";
16446
+ if (audioCovered && this._localAudioPipeline) await this.restoreRawAudioInputForPipeline();
16447
+ if (!audioCovered) await this.transceiverController?.restoreTrackSender(kind);
16448
+ else if (kind === "both") await this.transceiverController?.restoreTrackSender("video");
16449
+ else if (!this._localAudioPipeline) await this.transceiverController?.restoreTrackSender(kind);
16450
+ }
16451
+ async restoreRawAudioInputForPipeline() {
16452
+ if (!this._localAudioPipeline) return;
16453
+ const constraints = this.transceiverController?.getConstraintsFor("audio") ?? {};
16454
+ let stream;
16455
+ try {
16456
+ stream = await this.getUserMedia({ audio: constraints });
16457
+ } catch (error) {
16458
+ logger$17.error("[RTCPeerConnectionController] Failed to re-acquire mic for pipeline restore:", error);
16459
+ this._errors$.next(new MediaTrackError("restoreAudioPipelineInput", "audio", error));
16460
+ return;
16461
+ }
16462
+ const newTrack = stream.getAudioTracks().at(0);
16463
+ if (!newTrack) return;
16464
+ this.localStreamController.addTrack(newTrack);
16465
+ this._localAudioPipeline.setInputTrack(newTrack);
16259
16466
  }
16260
16467
  /**
16261
16468
  * Return the lazily-created {@link LocalAudioPipeline}, constructing it on
@@ -16270,7 +16477,7 @@ var RTCPeerConnectionController = class extends Destroyable {
16270
16477
  try {
16271
16478
  this._localAudioPipeline = new LocalAudioPipeline();
16272
16479
  } catch (error) {
16273
- logger$16.warn("[RTCPeerConnectionController] Failed to create LocalAudioPipeline:", error);
16480
+ logger$17.warn("[RTCPeerConnectionController] Failed to create LocalAudioPipeline:", error);
16274
16481
  return null;
16275
16482
  }
16276
16483
  this.subscribeTo(this.localStreamController.localAudioTracks$, () => {
@@ -16292,7 +16499,7 @@ var RTCPeerConnectionController = class extends Destroyable {
16292
16499
  try {
16293
16500
  await sender.replaceTrack(this._localAudioPipeline.outputTrack);
16294
16501
  } catch (error) {
16295
- logger$16.warn("[RTCPeerConnectionController] Failed to route audio sender through pipeline:", error);
16502
+ logger$17.warn("[RTCPeerConnectionController] Failed to route audio sender through pipeline:", error);
16296
16503
  }
16297
16504
  }
16298
16505
  /**
@@ -16308,10 +16515,10 @@ var RTCPeerConnectionController = class extends Destroyable {
16308
16515
  try {
16309
16516
  const localStream = this.localStreamController.addTrack(track);
16310
16517
  this.peerConnection.addTrack(track, localStream);
16311
- logger$16.debug(`[RTCPeerConnectionController] ${track.kind} track added:`, track.id);
16518
+ logger$17.debug(`[RTCPeerConnectionController] ${track.kind} track added:`, track.id);
16312
16519
  } catch (error) {
16313
- logger$16.error(`[RTCPeerConnectionController] Failed to add ${track.kind} track:`, error);
16314
- this._errors$.next(toError(error));
16520
+ logger$17.error(`[RTCPeerConnectionController] Failed to add ${track.kind} track:`, error);
16521
+ this._errors$.next(new MediaTrackError("addLocalTrack", track.kind, error));
16315
16522
  throw error;
16316
16523
  }
16317
16524
  }
@@ -16327,16 +16534,16 @@ var RTCPeerConnectionController = class extends Destroyable {
16327
16534
  }
16328
16535
  const sender = this.peerConnection.getSenders().find((sender$1) => sender$1.track?.id === trackId);
16329
16536
  if (!sender) {
16330
- logger$16.debug(`[RTCPeerConnectionController] track not found: ${trackId}`);
16537
+ logger$17.debug(`[RTCPeerConnectionController] track not found: ${trackId}`);
16331
16538
  return;
16332
16539
  }
16333
16540
  try {
16334
16541
  this.peerConnection.removeTrack(sender);
16335
16542
  this.localStreamController.removeTrack(trackId);
16336
- logger$16.debug(`[RTCPeerConnectionController] ${sender.track?.kind} track removed:`, trackId);
16543
+ logger$17.debug(`[RTCPeerConnectionController] ${sender.track?.kind} track removed:`, trackId);
16337
16544
  } catch (error) {
16338
- logger$16.error(`[RTCPeerConnectionController] Failed to remove ${sender.track?.kind} track:`, error);
16339
- this._errors$.next(toError(error));
16545
+ logger$17.error(`[RTCPeerConnectionController] Failed to remove ${sender.track?.kind} track:`, error);
16546
+ this._errors$.next(new MediaTrackError("removeLocalTrack", sender.track?.kind ?? "unknown", error));
16340
16547
  throw error;
16341
16548
  }
16342
16549
  }
@@ -16362,7 +16569,7 @@ var RTCPeerConnectionController = class extends Destroyable {
16362
16569
  async replaceAudioTrackWithConstraints(constraints) {
16363
16570
  const senders = this.peerConnection?.getSenders().filter((s) => s.track?.kind === "audio" && s.track.readyState === "live");
16364
16571
  if (!senders || senders.length === 0) {
16365
- logger$16.warn("[RTCPeerConnectionController] No live audio sender to replace");
16572
+ logger$17.warn("[RTCPeerConnectionController] No live audio sender to replace");
16366
16573
  return;
16367
16574
  }
16368
16575
  for (const sender of senders) {
@@ -16380,7 +16587,7 @@ var RTCPeerConnectionController = class extends Destroyable {
16380
16587
  const newTrack = (await this.getUserMedia({ audio: mergedConstraints })).getAudioTracks()[0];
16381
16588
  await sender.replaceTrack(newTrack);
16382
16589
  this.localStreamController.addTrack(newTrack);
16383
- logger$16.debug(`[RTCPeerConnectionController] Audio track replaced for server-pushed params. New track: ${newTrack.id}`);
16590
+ logger$17.debug(`[RTCPeerConnectionController] Audio track replaced for server-pushed params. New track: ${newTrack.id}`);
16384
16591
  }
16385
16592
  }
16386
16593
  /**
@@ -16388,7 +16595,7 @@ var RTCPeerConnectionController = class extends Destroyable {
16388
16595
  * Completes all observables to prevent memory leaks.
16389
16596
  */
16390
16597
  destroy() {
16391
- logger$16.debug(`[RTCPeerConnectionController] Destroying RTCPeerConnectionController. ${this.propose}`);
16598
+ logger$17.debug(`[RTCPeerConnectionController] Destroying RTCPeerConnectionController. ${this.propose}`);
16392
16599
  this.removeConnectionTimer();
16393
16600
  this._iceGatheringController?.destroy();
16394
16601
  this._localAudioPipeline?.destroy();
@@ -16414,7 +16621,7 @@ var RTCPeerConnectionController = class extends Destroyable {
16414
16621
  }
16415
16622
  stopRemoteTracks() {
16416
16623
  this._remoteStream$.value?.getTracks().forEach((track) => {
16417
- logger$16.debug(`[RTCPeerConnectionController] Stopping remote track: ${track.kind}`);
16624
+ logger$17.debug(`[RTCPeerConnectionController] Stopping remote track: ${track.kind}`);
16418
16625
  track.stop();
16419
16626
  });
16420
16627
  }
@@ -16431,7 +16638,7 @@ var RTCPeerConnectionController = class extends Destroyable {
16431
16638
  ...params,
16432
16639
  sdp: finalRemote
16433
16640
  };
16434
- logger$16.debug("[RTCPeerConnectionController] Setting remote description:", answer);
16641
+ logger$17.debug("[RTCPeerConnectionController] Setting remote description:", answer);
16435
16642
  return this.peerConnection.setRemoteDescription(answer);
16436
16643
  }
16437
16644
  };
@@ -16446,7 +16653,11 @@ function isVertoInviteMessage(value) {
16446
16653
  const msg = value;
16447
16654
  return msg.method === "verto.invite" && isObject(msg.params) && hasProperty(msg.params, "sdp") && hasProperty(msg.params, "callID");
16448
16655
  }
16449
- function isVertoByeMessage(value) {
16656
+ /**
16657
+ * Guards inbound `verto.bye` frames (server → client). Outbound bye messages are
16658
+ * built locally and never type-guarded, so only the inbound shape is asserted.
16659
+ */
16660
+ function isVertoByeInboundMessage(value) {
16450
16661
  if (!isVertoMethodMessage(value)) return false;
16451
16662
  return value.method === "verto.bye";
16452
16663
  }
@@ -16466,11 +16677,19 @@ function isVertoMediaParamsInnerParams(value) {
16466
16677
  function isVertoPingInnerParams(value) {
16467
16678
  return isObject(value) && hasProperty(value, "jsonrpc") && value.jsonrpc === "2.0" && hasProperty(value, "method") && value.method === "verto.ping";
16468
16679
  }
16680
+ /**
16681
+ * Guards the params of an inbound `verto.bye` frame (top-level `callID`). Use
16682
+ * this to recognise the bye payload extracted by `vertoBye$`, e.g. when racing
16683
+ * it against a boolean answer/reject signal.
16684
+ */
16685
+ function isVertoByeInboundParamsGuard(value) {
16686
+ return isObject(value) && hasProperty(value, "callID") && typeof value.callID === "string";
16687
+ }
16469
16688
 
16470
16689
  //#endregion
16471
16690
  //#region src/managers/VertoManager.ts
16472
16691
  var import_cjs$15 = require_cjs();
16473
- const logger$15 = getLogger();
16692
+ const logger$16 = getLogger();
16474
16693
  /**
16475
16694
  * Decide what value goes on the `node_id` field of a `webrtc.verto` envelope.
16476
16695
  *
@@ -16526,7 +16745,7 @@ var WebRTCVertoManager = class extends VertoManager {
16526
16745
  try {
16527
16746
  await this.executeVerto(vertoModifyMessage);
16528
16747
  } catch (error) {
16529
- logger$15.warn("[WebRTCManager] Call might already be disconnected, error sending Verto hold:", error);
16748
+ logger$16.warn("[WebRTCManager] Call might already be disconnected, error sending Verto hold:", error);
16530
16749
  throw error;
16531
16750
  }
16532
16751
  }
@@ -16539,7 +16758,7 @@ var WebRTCVertoManager = class extends VertoManager {
16539
16758
  try {
16540
16759
  await this.executeVerto(vertoModifyMessage);
16541
16760
  } catch (error) {
16542
- logger$15.warn("[WebRTCManager] Call might already be disconnected, error sending Verto unhold:", error);
16761
+ logger$16.warn("[WebRTCManager] Call might already be disconnected, error sending Verto unhold:", error);
16543
16762
  throw error;
16544
16763
  }
16545
16764
  }
@@ -16592,25 +16811,25 @@ var WebRTCVertoManager = class extends VertoManager {
16592
16811
  if (event.member_id) this.setSelfIdIfNull(event.member_id);
16593
16812
  });
16594
16813
  this.subscribeTo(this.vertoMedia$, (event) => {
16595
- logger$15.debug("[WebRTCManager] Received Verto media event (early media SDP):", event);
16596
- this._signalingStatus$.next("ringing");
16814
+ logger$16.debug("[WebRTCManager] Received Verto media event (early media SDP):", event);
16597
16815
  const { sdp, callID } = event;
16816
+ this.emitMainSignalingStatus(callID, "ringing");
16598
16817
  this._rtcPeerConnectionsMap.get(callID)?.updateAnswerStatus({
16599
16818
  status: "received",
16600
16819
  sdp
16601
16820
  });
16602
16821
  });
16603
16822
  this.subscribeTo(this.vertoAnswer$, (event) => {
16604
- logger$15.debug("[WebRTCManager] Received Verto answer event:", event);
16605
- this._signalingStatus$.next("connecting");
16823
+ logger$16.debug("[WebRTCManager] Received Verto answer event:", event);
16606
16824
  const { sdp, callID } = event;
16825
+ this.emitMainSignalingStatus(callID, "connecting");
16607
16826
  this._rtcPeerConnectionsMap.get(callID)?.updateAnswerStatus({
16608
16827
  status: "received",
16609
16828
  sdp
16610
16829
  });
16611
16830
  });
16612
16831
  this.subscribeTo(this.vertoMediaParams$, (event) => {
16613
- logger$15.debug("[WebRTCManager] Received Verto mediaParams event:", event);
16832
+ logger$16.debug("[WebRTCManager] Received Verto mediaParams event:", event);
16614
16833
  const { mediaParams, callID } = event;
16615
16834
  const rtcPeerConnController = this._rtcPeerConnectionsMap.get(callID);
16616
16835
  const { audio, video } = mediaParams;
@@ -16624,7 +16843,7 @@ var WebRTCVertoManager = class extends VertoManager {
16624
16843
  timestamp: Date.now()
16625
16844
  });
16626
16845
  } catch (error) {
16627
- logger$15.warn("[WebRTCManager] Error applying server-pushed media params:", error);
16846
+ logger$16.warn("[WebRTCManager] Error applying server-pushed media params:", error);
16628
16847
  this.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
16629
16848
  }
16630
16849
  })();
@@ -16646,13 +16865,13 @@ var WebRTCVertoManager = class extends VertoManager {
16646
16865
  */
16647
16866
  setNodeIdIfNull(nodeId) {
16648
16867
  if (!this._nodeId$.value && nodeId) {
16649
- logger$15.debug(`[WebRTCManager] Early node_id set: ${nodeId}`);
16868
+ logger$16.debug(`[WebRTCManager] Early node_id set: ${nodeId}`);
16650
16869
  this._nodeId$.next(nodeId);
16651
16870
  }
16652
16871
  }
16653
16872
  setSelfIdIfNull(selfId) {
16654
16873
  if (!this._selfId$.value && selfId) {
16655
- logger$15.debug(`[WebRTCManager] Early selfId set: ${selfId}`);
16874
+ logger$16.debug(`[WebRTCManager] Early selfId set: ${selfId}`);
16656
16875
  this._selfId$.next(selfId);
16657
16876
  }
16658
16877
  }
@@ -16661,7 +16880,7 @@ var WebRTCVertoManager = class extends VertoManager {
16661
16880
  const vertoPongMessage = VertoPong({ ...vertoPing });
16662
16881
  await this.executeVerto(vertoPongMessage);
16663
16882
  } catch (error) {
16664
- logger$15.warn("[WebRTCManager] Call might disconnect, error sending Verto pong:", error);
16883
+ logger$16.warn("[WebRTCManager] Call might disconnect, error sending Verto pong:", error);
16665
16884
  this.onError?.(new VertoPongError(error));
16666
16885
  }
16667
16886
  }
@@ -16671,7 +16890,7 @@ var WebRTCVertoManager = class extends VertoManager {
16671
16890
  if (audio) await this.mainPeerConnection.updateSendersConstraints("audio", audio);
16672
16891
  if (video) await this.mainPeerConnection.updateSendersConstraints("video", video);
16673
16892
  } catch (error) {
16674
- logger$15.warn("[WebRTCManager] Error updating media constraints:", error);
16893
+ logger$16.warn("[WebRTCManager] Error updating media constraints:", error);
16675
16894
  this.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
16676
16895
  throw error;
16677
16896
  }
@@ -16701,20 +16920,20 @@ var WebRTCVertoManager = class extends VertoManager {
16701
16920
  try {
16702
16921
  const pc = this.mainPeerConnection.peerConnection;
16703
16922
  if (!pc) {
16704
- logger$15.warn("[WebRTCManager] No peer connection for keyframe request");
16923
+ logger$16.warn("[WebRTCManager] No peer connection for keyframe request");
16705
16924
  return;
16706
16925
  }
16707
16926
  const videoReceiver = pc.getReceivers().find((r) => r.track.kind === "video");
16708
16927
  if (!videoReceiver) {
16709
- logger$15.warn("[WebRTCManager] No video receiver for keyframe request");
16928
+ logger$16.warn("[WebRTCManager] No video receiver for keyframe request");
16710
16929
  return;
16711
16930
  }
16712
16931
  if (typeof videoReceiver.requestKeyFrame === "function") {
16713
16932
  videoReceiver.requestKeyFrame();
16714
- logger$15.debug("[WebRTCManager] Keyframe requested via RTCRtpReceiver.requestKeyFrame()");
16715
- } else logger$15.debug("[WebRTCManager] requestKeyFrame() not supported, skipping");
16933
+ logger$16.debug("[WebRTCManager] Keyframe requested via RTCRtpReceiver.requestKeyFrame()");
16934
+ } else logger$16.debug("[WebRTCManager] requestKeyFrame() not supported, skipping");
16716
16935
  } catch (error) {
16717
- logger$15.warn("[WebRTCManager] Keyframe request failed (non-fatal):", error);
16936
+ logger$16.warn("[WebRTCManager] Keyframe request failed (non-fatal):", error);
16718
16937
  }
16719
16938
  }
16720
16939
  /**
@@ -16732,13 +16951,13 @@ var WebRTCVertoManager = class extends VertoManager {
16732
16951
  try {
16733
16952
  const controller = this.mainPeerConnection;
16734
16953
  if (!controller.peerConnection) {
16735
- logger$15.warn("[WebRTCManager] No peer connection for ICE restart");
16954
+ logger$16.warn("[WebRTCManager] No peer connection for ICE restart");
16736
16955
  return;
16737
16956
  }
16738
16957
  await controller.triggerIceRestart(relayOnly);
16739
- logger$15.info(`[WebRTCManager] ICE restart initiated${relayOnly ? " (relay-only)" : ""}`);
16958
+ logger$16.info(`[WebRTCManager] ICE restart initiated${relayOnly ? " (relay-only)" : ""}`);
16740
16959
  } catch (error) {
16741
- logger$15.error("[WebRTCManager] ICE restart failed:", error);
16960
+ logger$16.error("[WebRTCManager] ICE restart failed:", error);
16742
16961
  throw error;
16743
16962
  }
16744
16963
  }
@@ -16756,13 +16975,13 @@ var WebRTCVertoManager = class extends VertoManager {
16756
16975
  const entries = Array.from(this._rtcPeerConnectionsMap.entries());
16757
16976
  for (const [id, controller] of entries) try {
16758
16977
  if (!controller.peerConnection) {
16759
- logger$15.debug(`[WebRTCManager] No peer connection for leg ${id}, skipping ICE restart`);
16978
+ logger$16.debug(`[WebRTCManager] No peer connection for leg ${id}, skipping ICE restart`);
16760
16979
  continue;
16761
16980
  }
16762
16981
  await controller.triggerIceRestart(relayOnly);
16763
- logger$15.info(`[WebRTCManager] ICE restart initiated for leg ${id}${relayOnly ? " (relay-only)" : ""}`);
16982
+ logger$16.info(`[WebRTCManager] ICE restart initiated for leg ${id}${relayOnly ? " (relay-only)" : ""}`);
16764
16983
  } catch (error) {
16765
- logger$15.warn(`[WebRTCManager] ICE restart failed for leg ${id}:`, error);
16984
+ logger$16.warn(`[WebRTCManager] ICE restart failed for leg ${id}:`, error);
16766
16985
  }
16767
16986
  }
16768
16987
  /**
@@ -16774,7 +16993,7 @@ var WebRTCVertoManager = class extends VertoManager {
16774
16993
  requestKeyframeAll() {
16775
16994
  for (const [id, controller] of this._rtcPeerConnectionsMap) {
16776
16995
  if (controller.isScreenShare) {
16777
- logger$15.debug(`[WebRTCManager] Skipping keyframe for send-only screen share leg ${id}`);
16996
+ logger$16.debug(`[WebRTCManager] Skipping keyframe for send-only screen share leg ${id}`);
16778
16997
  continue;
16779
16998
  }
16780
16999
  try {
@@ -16784,10 +17003,10 @@ var WebRTCVertoManager = class extends VertoManager {
16784
17003
  if (!videoReceiver) continue;
16785
17004
  if (typeof videoReceiver.requestKeyFrame === "function") {
16786
17005
  videoReceiver.requestKeyFrame();
16787
- logger$15.debug(`[WebRTCManager] Keyframe requested for leg ${id}`);
17006
+ logger$16.debug(`[WebRTCManager] Keyframe requested for leg ${id}`);
16788
17007
  }
16789
17008
  } catch (error) {
16790
- logger$15.warn(`[WebRTCManager] Keyframe request failed for leg ${id} (non-fatal):`, error);
17009
+ logger$16.warn(`[WebRTCManager] Keyframe request failed for leg ${id} (non-fatal):`, error);
16791
17010
  }
16792
17011
  }
16793
17012
  }
@@ -16804,7 +17023,7 @@ var WebRTCVertoManager = class extends VertoManager {
16804
17023
  return this.cachedObservable("vertoMediaParams$", () => this.webRtcCallSession.webrtcMessages$.pipe(filterAs(isVertoMediaParamsInnerParams, "params"), (0, import_cjs$15.takeUntil)(this.destroyed$)));
16805
17024
  }
16806
17025
  get vertoBye$() {
16807
- return this.cachedObservable("vertoBye$", () => this.webRtcCallSession.webrtcMessages$.pipe(filterAs(isVertoByeMessage, "params"), (0, import_cjs$15.takeUntil)(this.destroyed$)));
17026
+ return this.cachedObservable("vertoBye$", () => this.webRtcCallSession.webrtcMessages$.pipe(filterAs(isVertoByeInboundMessage, "params"), (0, import_cjs$15.takeUntil)(this.destroyed$)));
16808
17027
  }
16809
17028
  get vertoAttach$() {
16810
17029
  return this.cachedObservable("vertoAttach$", () => this.webRtcCallSession.webrtcMessages$.pipe(filterAs(isVertoAttachMessage, "params"), (0, import_cjs$15.takeUntil)(this.destroyed$)));
@@ -16848,7 +17067,7 @@ var WebRTCVertoManager = class extends VertoManager {
16848
17067
  default:
16849
17068
  }
16850
17069
  } catch (error) {
16851
- logger$15.error(`[WebRTCManager] Error sending Verto ${vertoMethod}:`, error);
17070
+ logger$16.error(`[WebRTCManager] Error sending Verto ${vertoMethod}:`, error);
16852
17071
  this.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
16853
17072
  if (vertoMethod === "verto.modify") this.onModifyFailed?.();
16854
17073
  }
@@ -16863,19 +17082,33 @@ var WebRTCVertoManager = class extends VertoManager {
16863
17082
  sdp
16864
17083
  });
16865
17084
  } catch (error) {
16866
- logger$15.warn("[WebRTCManager] Error processing modify response:", error);
17085
+ logger$16.warn("[WebRTCManager] Error processing modify response:", error);
16867
17086
  const modifyError = error instanceof Error ? error : new Error(String(error), { cause: error });
16868
17087
  this.onError?.(modifyError);
16869
17088
  }
16870
17089
  }
16871
17090
  }
17091
+ emitMainSignalingStatus(callId, status) {
17092
+ const rtcPeerConnController = this._rtcPeerConnectionsMap.get(callId);
17093
+ if (!rtcPeerConnController) {
17094
+ const signalingError = new DependencyError(`Cannot emit signaling status, RTCPeerConnectionController not found for callID: ${callId}`);
17095
+ logger$16.error("[WebRTCManager] Failed to emit signaling status:", {
17096
+ callId,
17097
+ status,
17098
+ signalingError
17099
+ });
17100
+ this.onError?.(signalingError);
17101
+ return;
17102
+ }
17103
+ if (rtcPeerConnController.isMainDevice) this._signalingStatus$.next(status);
17104
+ }
16872
17105
  processInviteResponse(response, rtcPeerConnController) {
16873
17106
  if (!response.error && getValueFrom(response, "result.result.result.message") === "CALL CREATED") {
16874
- this._signalingStatus$.next("trying");
17107
+ this.emitMainSignalingStatus(rtcPeerConnController.id, "trying");
16875
17108
  this._nodeId$.next(getValueFrom(response, "result.node_id") ?? null);
16876
17109
  const memberId = getValueFrom(response, "result.result.result.memberID") ?? null;
16877
17110
  const callId = getValueFrom(response, "result.result.result.callID") ?? null;
16878
- logger$15.debug("[WebRTCManager] Verto invite response:", {
17111
+ logger$16.debug("[WebRTCManager] Verto invite response:", {
16879
17112
  callId,
16880
17113
  memberId,
16881
17114
  response
@@ -16885,14 +17118,14 @@ var WebRTCVertoManager = class extends VertoManager {
16885
17118
  if (callId) {
16886
17119
  this.webRtcCallSession.addCallId(callId);
16887
17120
  this.attachManager.attach(this.buildAttachableCall(callId));
16888
- } else logger$15.warn("[WebRTCManager] Cannot attach call, missing callId:", {
17121
+ } else logger$16.warn("[WebRTCManager] Cannot attach call, missing callId:", {
16889
17122
  nodeId: this.nodeId,
16890
17123
  callId
16891
17124
  });
16892
- logger$15.info("[WebRTCManager] Verto invite successful");
16893
- logger$15.debug(`[WebRTCManager] nodeid: ${this._nodeId$.value}, selfId: ${this._selfId$.value}`);
17125
+ logger$16.info("[WebRTCManager] Verto invite successful");
17126
+ logger$16.debug(`[WebRTCManager] nodeid: ${this._nodeId$.value}, selfId: ${this._selfId$.value}`);
16894
17127
  } else {
16895
- logger$15.error("[WebRTCManager] Verto invite failed:", response);
17128
+ logger$16.error("[WebRTCManager] Verto invite failed:", response);
16896
17129
  const inviteError = response.error ? new JSONRPCError(response.error.code, response.error.message, response.error.data) : /* @__PURE__ */ new Error("Verto invite failed: unexpected response");
16897
17130
  this.onError?.(inviteError);
16898
17131
  }
@@ -16919,6 +17152,7 @@ var WebRTCVertoManager = class extends VertoManager {
16919
17152
  inputVideoStream: options.inputVideoStream,
16920
17153
  receiveAudio: options.receiveAudio,
16921
17154
  receiveVideo: options.receiveVideo,
17155
+ fallbackToReceiveOnly: options.fallbackToReceiveOnly,
16922
17156
  webRTCApiProvider: this.webRTCApiProvider,
16923
17157
  preferredVideoCodecs: options.preferredVideoCodecs,
16924
17158
  preferredAudioCodecs: options.preferredAudioCodecs,
@@ -16937,17 +17171,17 @@ var WebRTCVertoManager = class extends VertoManager {
16937
17171
  if (options.initOffer) this.handleInboundAnswer(rtcPeerConnController);
16938
17172
  }
16939
17173
  async handleInboundAnswer(rtcPeerConnController) {
16940
- logger$15.debug("[WebRTCManager] Waiting for inbound call to be accepted or rejected");
17174
+ logger$16.debug("[WebRTCManager] Waiting for inbound call to be accepted or rejected");
16941
17175
  const vertoByeOrAccepted = await (0, import_cjs$15.firstValueFrom)((0, import_cjs$15.race)(this.vertoBye$, this.webRtcCallSession.answered$).pipe((0, import_cjs$15.takeUntil)(this.destroyed$))).catch(() => null);
16942
17176
  if (vertoByeOrAccepted === null) {
16943
- logger$15.debug("[WebRTCManager] Inbound answer handler aborted (destroyed).");
17177
+ logger$16.debug("[WebRTCManager] Inbound answer handler aborted (destroyed).");
16944
17178
  return;
16945
17179
  }
16946
- if (isVertoByeMessage(vertoByeOrAccepted)) {
16947
- logger$15.info("[WebRTCManager] Inbound call ended by remote before answer.");
17180
+ if (isVertoByeInboundParamsGuard(vertoByeOrAccepted)) {
17181
+ logger$16.info("[WebRTCManager] Inbound call ended by remote before answer.");
16948
17182
  this.callSession?.destroy();
16949
17183
  } else if (!vertoByeOrAccepted) {
16950
- logger$15.info("[WebRTCManager] Inbound call rejected by user.");
17184
+ logger$16.info("[WebRTCManager] Inbound call rejected by user.");
16951
17185
  try {
16952
17186
  await this.bye("USER_BUSY");
16953
17187
  } finally {
@@ -16955,19 +17189,19 @@ var WebRTCVertoManager = class extends VertoManager {
16955
17189
  this.callSession?.destroy();
16956
17190
  }
16957
17191
  } else {
16958
- logger$15.debug("[WebRTCManager] Inbound call accepted, creating SDP answer");
17192
+ logger$16.debug("[WebRTCManager] Inbound call accepted, creating SDP answer");
16959
17193
  const answerOptions = this.webRtcCallSession.answerMediaOptions;
16960
17194
  try {
16961
17195
  await rtcPeerConnController.acceptInbound(answerOptions);
16962
17196
  } catch (error) {
16963
- logger$15.error("[WebRTCManager] Error creating inbound answer:", error);
17197
+ logger$16.error("[WebRTCManager] Error creating inbound answer:", error);
16964
17198
  this.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
16965
17199
  }
16966
17200
  }
16967
17201
  }
16968
17202
  setupVertoAttachHandler() {
16969
17203
  this.subscribeTo(this.vertoAttach$, async (vertoAttach) => {
16970
- logger$15.debug("[WebRTCManager] Received Verto attach event for existing call:", vertoAttach);
17204
+ logger$16.debug("[WebRTCManager] Received Verto attach event for existing call:", vertoAttach);
16971
17205
  const { callID } = vertoAttach;
16972
17206
  await this.attachManager.attach({
16973
17207
  nodeId: this.nodeId ?? void 0,
@@ -17039,17 +17273,17 @@ var WebRTCVertoManager = class extends VertoManager {
17039
17273
  };
17040
17274
  }
17041
17275
  async sendLocalDescriptionOnceAccepted(vertoMessageRequest, rtcPeerConnectionController) {
17042
- logger$15.debug("[WebRTCManager] Waiting for call to be accepted or ended before sending answer");
17276
+ logger$16.debug("[WebRTCManager] Waiting for call to be accepted or ended before sending answer");
17043
17277
  const vertoByeOrAccepted = await (0, import_cjs$15.firstValueFrom)((0, import_cjs$15.race)(this.vertoBye$, this.webRtcCallSession.answered$).pipe((0, import_cjs$15.takeUntil)(this.destroyed$))).catch(() => null);
17044
17278
  if (vertoByeOrAccepted === null) {
17045
- logger$15.debug("[WebRTCManager] Destroyed while waiting for call acceptance");
17279
+ logger$16.debug("[WebRTCManager] Destroyed while waiting for call acceptance");
17046
17280
  return;
17047
17281
  }
17048
- if (isVertoByeMessage(vertoByeOrAccepted)) {
17049
- logger$15.info("[WebRTCManager] Call ended before answer was sent.");
17282
+ if (isVertoByeInboundParamsGuard(vertoByeOrAccepted)) {
17283
+ logger$16.info("[WebRTCManager] Call ended before answer was sent.");
17050
17284
  this.callSession?.destroy();
17051
17285
  } else if (!vertoByeOrAccepted) {
17052
- logger$15.info("[WebRTCManager] Call was not accepted, sending verto.bye.");
17286
+ logger$16.info("[WebRTCManager] Call was not accepted, sending verto.bye.");
17053
17287
  try {
17054
17288
  await this.bye("USER_BUSY");
17055
17289
  } finally {
@@ -17057,14 +17291,14 @@ var WebRTCVertoManager = class extends VertoManager {
17057
17291
  this.callSession?.destroy();
17058
17292
  }
17059
17293
  } else {
17060
- logger$15.debug("[WebRTCManager] Call accepted, sending answer");
17294
+ logger$16.debug("[WebRTCManager] Call accepted, sending answer");
17061
17295
  try {
17062
- this._signalingStatus$.next("connecting");
17296
+ this.emitMainSignalingStatus(rtcPeerConnectionController.id, "connecting");
17063
17297
  await this.sendLocalDescription(vertoMessageRequest, rtcPeerConnectionController);
17064
17298
  await rtcPeerConnectionController.updateAnswerStatus({ status: "sent" });
17065
17299
  await this.attachManager.attach(this.buildAttachableCall());
17066
17300
  } catch (error) {
17067
- logger$15.error("[WebRTCManager] Error sending Verto answer:", error);
17301
+ logger$16.error("[WebRTCManager] Error sending Verto answer:", error);
17068
17302
  this.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
17069
17303
  await rtcPeerConnectionController.updateAnswerStatus({ status: "failed" });
17070
17304
  }
@@ -17145,9 +17379,11 @@ var WebRTCVertoManager = class extends VertoManager {
17145
17379
  await this.initAdditionalPeerConnection("screenshare", options);
17146
17380
  }
17147
17381
  async initAdditionalPeerConnection(propose, options) {
17382
+ const isScreenShare = propose === "screenshare";
17383
+ let firstPeerConnectionError;
17148
17384
  let rtcPeerConnController = null;
17149
17385
  try {
17150
- this._screenShareStatus$.next("starting");
17386
+ if (isScreenShare) this._screenShareStatus$.next("starting");
17151
17387
  rtcPeerConnController = new RTCPeerConnectionController({
17152
17388
  ...options,
17153
17389
  ...this.RTCPeerConnectionConfig,
@@ -17155,21 +17391,27 @@ var WebRTCVertoManager = class extends VertoManager {
17155
17391
  webRTCApiProvider: this.webRTCApiProvider
17156
17392
  }, void 0, this.deviceController);
17157
17393
  this.setupLocalDescriptionHandler(rtcPeerConnController);
17158
- if (propose === "screenshare") this._screenShareId = rtcPeerConnController.id;
17394
+ if (isScreenShare) this._screenShareId = rtcPeerConnController.id;
17159
17395
  this._rtcPeerConnectionsMap.set(rtcPeerConnController.id, rtcPeerConnController);
17160
17396
  this._rtcPeerConnections$.next(Array.from(this._rtcPeerConnectionsMap.values()));
17161
17397
  this.subscribeTo(rtcPeerConnController.errors$, (error) => {
17162
- this.onError?.(error);
17398
+ firstPeerConnectionError ??= error;
17399
+ this.onError?.(error, { fatal: false });
17163
17400
  });
17164
17401
  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$)));
17165
- this._screenShareStatus$.next("started");
17166
- logger$15.info("[WebRTCManager] Screen share started successfully.");
17402
+ if (isScreenShare) this._screenShareStatus$.next("started");
17403
+ logger$16.info(`[WebRTCManager] Additional peer connection connected (${propose}).`);
17167
17404
  return rtcPeerConnController.id;
17168
17405
  } catch (error) {
17169
- logger$15.warn("[WebRTCManager] Error initializing additional peer connection:", error);
17170
- this.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
17406
+ logger$16.warn("[WebRTCManager] Error initializing additional peer connection:", error);
17171
17407
  if (rtcPeerConnController) rtcPeerConnController.destroy();
17172
- this._screenShareStatus$.next("none");
17408
+ if (isScreenShare) this._screenShareStatus$.next("none");
17409
+ if (firstPeerConnectionError) throw firstPeerConnectionError instanceof MediaAccessError && firstPeerConnectionError.originalError instanceof Error ? firstPeerConnectionError.originalError : firstPeerConnectionError;
17410
+ if (error instanceof import_cjs$15.EmptyError) {
17411
+ logger$16.debug("[WebRTCManager] Additional peer connection aborted before connecting.");
17412
+ return;
17413
+ }
17414
+ throw error instanceof Error ? error : new Error(String(error), { cause: error });
17173
17415
  }
17174
17416
  }
17175
17417
  async removeInputDevices(id) {
@@ -17185,9 +17427,9 @@ var WebRTCVertoManager = class extends VertoManager {
17185
17427
  if (removeTrack) return this.mainPeerConnection.stopTrackSender(removeTrack, { updateTransceiverDirection: true });
17186
17428
  }
17187
17429
  async removeScreenMedia() {
17188
- if (!["starting", "started"].includes(this._screenShareStatus$.value)) logger$15.warn("[WebRTCManager] No active screen share to stop.");
17430
+ if (!["starting", "started"].includes(this._screenShareStatus$.value)) logger$16.warn("[WebRTCManager] No active screen share to stop.");
17189
17431
  if (!this._screenShareId) {
17190
- logger$15.debug("[WebRTCManager] No screen share peer connection found.");
17432
+ logger$16.debug("[WebRTCManager] No screen share peer connection found.");
17191
17433
  return;
17192
17434
  }
17193
17435
  this._screenShareStatus$.next("stopping");
@@ -17216,7 +17458,7 @@ var WebRTCVertoManager = class extends VertoManager {
17216
17458
  dialogParams: this.dialogParams(rtcPeerConnController)
17217
17459
  }));
17218
17460
  } catch (error) {
17219
- logger$15.warn("[WebRTCManager] Call might already be disconnected, error sending Verto bye:", error);
17461
+ logger$16.warn("[WebRTCManager] Call might already be disconnected, error sending Verto bye:", error);
17220
17462
  throw error;
17221
17463
  }
17222
17464
  }
@@ -17234,7 +17476,7 @@ var WebRTCVertoManager = class extends VertoManager {
17234
17476
  try {
17235
17477
  await this.executeVerto(vertoInfoMessage);
17236
17478
  } catch (error) {
17237
- logger$15.warn("[WebRTCManager] Error sending DTMF digits:", error);
17479
+ logger$16.warn("[WebRTCManager] Error sending DTMF digits:", error);
17238
17480
  throw error;
17239
17481
  }
17240
17482
  }
@@ -17245,10 +17487,10 @@ var WebRTCVertoManager = class extends VertoManager {
17245
17487
  action: "transfer"
17246
17488
  });
17247
17489
  try {
17248
- logger$15.debug("[WebRTCManager] Transferring call with options:", options);
17490
+ logger$16.debug("[WebRTCManager] Transferring call with options:", options);
17249
17491
  await this.executeVerto(message);
17250
17492
  } catch (error) {
17251
- logger$15.error("[WebRTCManager] Error transferring call:", error);
17493
+ logger$16.error("[WebRTCManager] Error transferring call:", error);
17252
17494
  throw error;
17253
17495
  }
17254
17496
  }
@@ -17265,7 +17507,7 @@ var WebRTCVertoManager = class extends VertoManager {
17265
17507
  //#endregion
17266
17508
  //#region src/controllers/RemoteAudioMeter.ts
17267
17509
  var import_cjs$14 = require_cjs();
17268
- const logger$14 = getLogger();
17510
+ const logger$15 = getLogger();
17269
17511
  /**
17270
17512
  * Read-only audio level meter for a remote MediaStream. Attaches an
17271
17513
  * AnalyserNode to a MediaStreamAudioSourceNode so it observes the stream
@@ -17300,7 +17542,7 @@ var RemoteAudioMeter = class extends Destroyable {
17300
17542
  try {
17301
17543
  this._source.disconnect();
17302
17544
  } catch (error) {
17303
- logger$14.debug("[RemoteAudioMeter] source disconnect warning:", error);
17545
+ logger$15.debug("[RemoteAudioMeter] source disconnect warning:", error);
17304
17546
  }
17305
17547
  this._source = null;
17306
17548
  this._stream = null;
@@ -17317,7 +17559,7 @@ var RemoteAudioMeter = class extends Destroyable {
17317
17559
  this._source = null;
17318
17560
  }
17319
17561
  this._audioContext.close().catch((error) => {
17320
- logger$14.debug("[RemoteAudioMeter] audio context close warning:", error);
17562
+ logger$15.debug("[RemoteAudioMeter] audio context close warning:", error);
17321
17563
  });
17322
17564
  super.destroy();
17323
17565
  }
@@ -17336,7 +17578,7 @@ var RemoteAudioMeter = class extends Destroyable {
17336
17578
  //#endregion
17337
17579
  //#region src/controllers/RTCStatsMonitor.ts
17338
17580
  var import_cjs$13 = require_cjs();
17339
- const logger$13 = getLogger();
17581
+ const logger$14 = getLogger();
17340
17582
  const DEFAULT_POLLING_INTERVAL_MS = 1e3;
17341
17583
  const DEFAULT_BASELINE_SAMPLES = 10;
17342
17584
  const DEFAULT_NO_AUDIO_PACKET_THRESHOLD_MS = 2e3;
@@ -17426,9 +17668,9 @@ var RTCStatsMonitor = class extends Destroyable {
17426
17668
  const now = Date.now();
17427
17669
  this.lastAudioPacketChangeTime = now;
17428
17670
  this.lastVideoPacketChangeTime = now;
17429
- logger$13.debug("[RTCStatsMonitor] Starting stats monitoring");
17671
+ logger$14.debug("[RTCStatsMonitor] Starting stats monitoring");
17430
17672
  this.subscribeTo((0, import_cjs$13.interval)(this.pollingIntervalMs).pipe((0, import_cjs$13.filter)(() => this.running), (0, import_cjs$13.switchMap)(() => (0, import_cjs$13.from)(this.peerConnection.getStats()).pipe((0, import_cjs$13.catchError)((err) => {
17431
- logger$13.warn("[RTCStatsMonitor] Failed to get stats:", err);
17673
+ logger$14.warn("[RTCStatsMonitor] Failed to get stats:", err);
17432
17674
  return import_cjs$13.EMPTY;
17433
17675
  }))), (0, import_cjs$13.filter)(() => this.running), (0, import_cjs$13.map)((report) => this.extractSample(report))), (sample$1) => this._sample$.next(sample$1));
17434
17676
  this.subscribeTo(this._sample$.pipe((0, import_cjs$13.take)(this.baselineSampleCount), (0, import_cjs$13.toArray)(), (0, import_cjs$13.map)((samples) => ({
@@ -17436,7 +17678,7 @@ var RTCStatsMonitor = class extends Destroyable {
17436
17678
  jitter: samples.reduce((s, b) => s + b.audioJitter, 0) / samples.length,
17437
17679
  ready: true
17438
17680
  }))), (baseline) => {
17439
- logger$13.debug(`[RTCStatsMonitor] Baseline established: rtt=${baseline.rtt.toFixed(1)}ms, jitter=${baseline.jitter.toFixed(1)}ms`);
17681
+ logger$14.debug(`[RTCStatsMonitor] Baseline established: rtt=${baseline.rtt.toFixed(1)}ms, jitter=${baseline.jitter.toFixed(1)}ms`);
17440
17682
  this._baseline$.next(baseline);
17441
17683
  });
17442
17684
  this.subscribeTo(this._sample$.pipe((0, import_cjs$13.scan)((acc, sample$1) => ({
@@ -17473,10 +17715,10 @@ var RTCStatsMonitor = class extends Destroyable {
17473
17715
  stop() {
17474
17716
  if (!this.running) return;
17475
17717
  this.running = false;
17476
- logger$13.debug("[RTCStatsMonitor] Stopping stats monitoring");
17718
+ logger$14.debug("[RTCStatsMonitor] Stopping stats monitoring");
17477
17719
  }
17478
17720
  destroy() {
17479
- logger$13.debug("[RTCStatsMonitor] Destroying RTCStatsMonitor");
17721
+ logger$14.debug("[RTCStatsMonitor] Destroying RTCStatsMonitor");
17480
17722
  this.stop();
17481
17723
  super.destroy();
17482
17724
  }
@@ -17605,7 +17847,7 @@ var RTCStatsMonitor = class extends Destroyable {
17605
17847
  //#endregion
17606
17848
  //#region src/managers/CallRecoveryManager.ts
17607
17849
  var import_cjs$12 = require_cjs();
17608
- const logger$12 = getLogger();
17850
+ const logger$13 = getLogger();
17609
17851
  const DEFAULT_DEBOUNCE_TIME_MS = 2e3;
17610
17852
  const DEFAULT_COOLDOWN_MS = 1e4;
17611
17853
  const DEFAULT_ICE_GRACE_PERIOD_MS = 3e3;
@@ -17702,10 +17944,10 @@ var CallRecoveryManager = class extends Destroyable {
17702
17944
  */
17703
17945
  async requestIceRestart() {
17704
17946
  if (this._recoveryState$.value === "recovering") {
17705
- logger$12.info("CallRecoveryManager: manual ICE restart skipped — recovery already in progress");
17947
+ logger$13.info("CallRecoveryManager: manual ICE restart skipped — recovery already in progress");
17706
17948
  return;
17707
17949
  }
17708
- logger$12.info("CallRecoveryManager: manual ICE restart requested");
17950
+ logger$13.info("CallRecoveryManager: manual ICE restart requested");
17709
17951
  this.transitionTo("recovering");
17710
17952
  await this.executeIceRestart(false);
17711
17953
  this.startCooldown();
@@ -17721,7 +17963,7 @@ var CallRecoveryManager = class extends Destroyable {
17721
17963
  * WebSocket reconnect or call state recovers to 'connected'.
17722
17964
  */
17723
17965
  reset() {
17724
- logger$12.info("CallRecoveryManager: resetting counters");
17966
+ logger$13.info("CallRecoveryManager: resetting counters");
17725
17967
  this._attemptCount = 0;
17726
17968
  this._keyframeBurstCount = 0;
17727
17969
  this._keyframeBurstStart = 0;
@@ -17736,7 +17978,7 @@ var CallRecoveryManager = class extends Destroyable {
17736
17978
  */
17737
17979
  notifyModifyFailed() {
17738
17980
  if (this._recoveryState$.value === "cooldown" || this._recoveryState$.value === "idle") {
17739
- logger$12.info("CallRecoveryManager: verto.modify failed — re-entering recovery");
17981
+ logger$13.info("CallRecoveryManager: verto.modify failed — re-entering recovery");
17740
17982
  this._cooldownUntil = 0;
17741
17983
  this.transitionTo("idle");
17742
17984
  this.pushTrigger({
@@ -17760,7 +18002,7 @@ var CallRecoveryManager = class extends Destroyable {
17760
18002
  reason: `bandwidth ${bitrateKbps}kbps below threshold ${this._config.degradationBitrateThreshold}kbps`,
17761
18003
  timestamp: Date.now()
17762
18004
  });
17763
- logger$12.warn(`CallRecoveryManager: disabling video — bandwidth ${bitrateKbps}kbps < ${this._config.degradationBitrateThreshold}kbps`);
18005
+ logger$13.warn(`CallRecoveryManager: disabling video — bandwidth ${bitrateKbps}kbps < ${this._config.degradationBitrateThreshold}kbps`);
17764
18006
  } else if (wasConstrained && bitrateKbps >= this._config.degradationRecoveryThreshold) {
17765
18007
  this._bandwidthConstrained$.next(false);
17766
18008
  this._callbacks.enableVideo();
@@ -17769,7 +18011,7 @@ var CallRecoveryManager = class extends Destroyable {
17769
18011
  reason: `bandwidth ${bitrateKbps}kbps recovered above ${this._config.degradationRecoveryThreshold}kbps`,
17770
18012
  timestamp: Date.now()
17771
18013
  });
17772
- logger$12.info(`CallRecoveryManager: restoring video — bandwidth ${bitrateKbps}kbps >= ${this._config.degradationRecoveryThreshold}kbps`);
18014
+ logger$13.info(`CallRecoveryManager: restoring video — bandwidth ${bitrateKbps}kbps >= ${this._config.degradationRecoveryThreshold}kbps`);
17773
18015
  }
17774
18016
  }
17775
18017
  /**
@@ -17788,14 +18030,14 @@ var CallRecoveryManager = class extends Destroyable {
17788
18030
  handleWebSocketReconnect() {
17789
18031
  const pcState = this._callbacks.getPeerConnectionState();
17790
18032
  if (pcState === "connected" || pcState === "completed") {
17791
- logger$12.info("CallRecoveryManager: signal-only reconnect — peer connection still alive");
18033
+ logger$13.info("CallRecoveryManager: signal-only reconnect — peer connection still alive");
17792
18034
  this.emitEvent({
17793
18035
  action: "signal_reconnect",
17794
18036
  reason: "WebSocket reconnected, peer connection still connected",
17795
18037
  timestamp: Date.now()
17796
18038
  });
17797
18039
  } else {
17798
- logger$12.info("CallRecoveryManager: full reconnect — peer connection also down");
18040
+ logger$13.info("CallRecoveryManager: full reconnect — peer connection also down");
17799
18041
  this.emitEvent({
17800
18042
  action: "full_reconnect",
17801
18043
  reason: "WebSocket reconnected, peer connection not connected — ICE restart needed",
@@ -17819,7 +18061,7 @@ var CallRecoveryManager = class extends Destroyable {
17819
18061
  }), (0, import_cjs$12.debounceTime)(this._config.debounceTimeMs), (0, import_cjs$12.withLatestFrom)(this._inputs.signalingReady$), (0, import_cjs$12.filter)(([, signalingReady]) => this.passGateChecks(signalingReady)), (0, import_cjs$12.map)(([trigger]) => trigger), (0, import_cjs$12.exhaustMap)((trigger) => this.executeTieredRecovery(trigger)), (0, import_cjs$12.takeUntil)((0, import_cjs$12.merge)(this._destroyed$, this._pipelineStop$))), {
17820
18062
  next: () => {},
17821
18063
  error: (err) => {
17822
- logger$12.error("CallRecoveryManager: pipeline error", err);
18064
+ logger$13.error("CallRecoveryManager: pipeline error", err);
17823
18065
  this.transitionTo("idle");
17824
18066
  }
17825
18067
  });
@@ -17846,27 +18088,27 @@ var CallRecoveryManager = class extends Destroyable {
17846
18088
  reason: `no packet loss for ${this._config.packetLossRecoveryDelaySec}s — restoring video`,
17847
18089
  timestamp: Date.now()
17848
18090
  });
17849
- logger$12.info(`CallRecoveryManager: restoring video — no packet loss for ${this._config.packetLossRecoveryDelaySec}s`);
18091
+ logger$13.info(`CallRecoveryManager: restoring video — no packet loss for ${this._config.packetLossRecoveryDelaySec}s`);
17850
18092
  });
17851
18093
  }
17852
18094
  passGateChecks(signalingReady) {
17853
18095
  if (this._callbacks.isNegotiating()) {
17854
- logger$12.debug("CallRecoveryManager: gate blocked — negotiation in progress");
18096
+ logger$13.debug("CallRecoveryManager: gate blocked — negotiation in progress");
17855
18097
  this.transitionTo("idle");
17856
18098
  return false;
17857
18099
  }
17858
18100
  if (!signalingReady) {
17859
- logger$12.debug("CallRecoveryManager: gate blocked — signaling not ready");
18101
+ logger$13.debug("CallRecoveryManager: gate blocked — signaling not ready");
17860
18102
  this.transitionTo("idle");
17861
18103
  return false;
17862
18104
  }
17863
18105
  if (!this._callbacks.isCallConnected()) {
17864
- logger$12.debug("CallRecoveryManager: gate blocked — call not connected");
18106
+ logger$13.debug("CallRecoveryManager: gate blocked — call not connected");
17865
18107
  this.transitionTo("idle");
17866
18108
  return false;
17867
18109
  }
17868
18110
  if (this.isCooldownActive()) {
17869
- logger$12.debug("CallRecoveryManager: gate blocked — cooldown active");
18111
+ logger$13.debug("CallRecoveryManager: gate blocked — cooldown active");
17870
18112
  this.transitionTo("cooldown");
17871
18113
  return false;
17872
18114
  }
@@ -17877,9 +18119,9 @@ var CallRecoveryManager = class extends Destroyable {
17877
18119
  }
17878
18120
  executeTieredRecovery(trigger) {
17879
18121
  this.transitionTo("recovering");
17880
- logger$12.info(`CallRecoveryManager: starting tiered recovery — source=${trigger.source} detail=${trigger.detail}`);
18122
+ logger$13.info(`CallRecoveryManager: starting tiered recovery — source=${trigger.source} detail=${trigger.detail}`);
17881
18123
  return (0, import_cjs$12.from)(this.runTiers(trigger)).pipe((0, import_cjs$12.tap)(() => this.startCooldown()), (0, import_cjs$12.catchError)((err) => {
17882
- logger$12.error("CallRecoveryManager: tiered recovery failed", err);
18124
+ logger$13.error("CallRecoveryManager: tiered recovery failed", err);
17883
18125
  this.startCooldown();
17884
18126
  return import_cjs$12.EMPTY;
17885
18127
  }));
@@ -17887,7 +18129,7 @@ var CallRecoveryManager = class extends Destroyable {
17887
18129
  async runTiers(trigger) {
17888
18130
  this.executeKeyframe(trigger.detail);
17889
18131
  if (trigger.issueType && DEGRADATION_ONLY_ISSUES.has(trigger.issueType)) {
17890
- logger$12.debug(`CallRecoveryManager: degradation-only issue (${trigger.issueType}) — Tier 1 only, skipping ICE restart`);
18132
+ logger$13.debug(`CallRecoveryManager: degradation-only issue (${trigger.issueType}) — Tier 1 only, skipping ICE restart`);
17891
18133
  return;
17892
18134
  }
17893
18135
  if (this._attemptCount < this._config.maxAttempts) {
@@ -17904,13 +18146,13 @@ var CallRecoveryManager = class extends Destroyable {
17904
18146
  maxAttempts: this._config.maxAttempts,
17905
18147
  timestamp: Date.now()
17906
18148
  });
17907
- logger$12.warn("CallRecoveryManager: max recovery attempts reached");
18149
+ logger$13.warn("CallRecoveryManager: max recovery attempts reached");
17908
18150
  }
17909
18151
  }
17910
18152
  executeKeyframe(reason) {
17911
18153
  const now = Date.now();
17912
18154
  if (now < this._keyframeCooldownUntil) {
17913
- logger$12.debug("CallRecoveryManager: keyframe request skipped — cooldown active");
18155
+ logger$13.debug("CallRecoveryManager: keyframe request skipped — cooldown active");
17914
18156
  return;
17915
18157
  }
17916
18158
  if (now - this._keyframeBurstStart > this._config.keyframeBurstWindowMs) {
@@ -17919,7 +18161,7 @@ var CallRecoveryManager = class extends Destroyable {
17919
18161
  }
17920
18162
  if (this._keyframeBurstCount >= this._config.keyframeMaxBurst) {
17921
18163
  this._keyframeCooldownUntil = now + this._config.keyframeCooldownMs;
17922
- logger$12.debug(`CallRecoveryManager: keyframe burst limit reached (${this._config.keyframeMaxBurst}), cooldown until ${this._keyframeCooldownUntil}`);
18164
+ logger$13.debug(`CallRecoveryManager: keyframe burst limit reached (${this._config.keyframeMaxBurst}), cooldown until ${this._keyframeCooldownUntil}`);
17923
18165
  return;
17924
18166
  }
17925
18167
  this._keyframeBurstCount += 1;
@@ -17929,12 +18171,12 @@ var CallRecoveryManager = class extends Destroyable {
17929
18171
  reason,
17930
18172
  timestamp: now
17931
18173
  });
17932
- logger$12.debug(`CallRecoveryManager: keyframe requested (burst ${this._keyframeBurstCount}/${this._config.keyframeMaxBurst})`);
18174
+ logger$13.debug(`CallRecoveryManager: keyframe requested (burst ${this._keyframeBurstCount}/${this._config.keyframeMaxBurst})`);
17933
18175
  }
17934
18176
  async executeIceRestart(relayOnly) {
17935
18177
  this._attemptCount += 1;
17936
18178
  const tier = relayOnly ? "Tier 3 (relay-only)" : "Tier 2 (standard)";
17937
- logger$12.info(`CallRecoveryManager: ${tier} ICE restart — attempt ${this._attemptCount}/${this._config.maxAttempts}`);
18179
+ logger$13.info(`CallRecoveryManager: ${tier} ICE restart — attempt ${this._attemptCount}/${this._config.maxAttempts}`);
17938
18180
  this.emitEvent({
17939
18181
  action: "reinvite_started",
17940
18182
  reason: `${tier} ICE restart`,
@@ -17951,7 +18193,7 @@ var CallRecoveryManager = class extends Destroyable {
17951
18193
  maxAttempts: this._config.maxAttempts,
17952
18194
  timestamp: Date.now()
17953
18195
  });
17954
- logger$12.info(`CallRecoveryManager: ${tier} ICE restart succeeded`);
18196
+ logger$13.info(`CallRecoveryManager: ${tier} ICE restart succeeded`);
17955
18197
  this._attemptCount = 0;
17956
18198
  return true;
17957
18199
  }
@@ -17962,7 +18204,7 @@ var CallRecoveryManager = class extends Destroyable {
17962
18204
  maxAttempts: this._config.maxAttempts,
17963
18205
  timestamp: Date.now()
17964
18206
  });
17965
- logger$12.warn(`CallRecoveryManager: ${tier} ICE restart failed`);
18207
+ logger$13.warn(`CallRecoveryManager: ${tier} ICE restart failed`);
17966
18208
  return false;
17967
18209
  } catch {
17968
18210
  this.emitEvent({
@@ -17972,7 +18214,7 @@ var CallRecoveryManager = class extends Destroyable {
17972
18214
  maxAttempts: this._config.maxAttempts,
17973
18215
  timestamp: Date.now()
17974
18216
  });
17975
- logger$12.warn(`CallRecoveryManager: ${tier} ICE restart timed out`);
18217
+ logger$13.warn(`CallRecoveryManager: ${tier} ICE restart timed out`);
17976
18218
  return false;
17977
18219
  }
17978
18220
  }
@@ -17993,7 +18235,7 @@ var CallRecoveryManager = class extends Destroyable {
17993
18235
  transitionTo(state) {
17994
18236
  const prev = this._recoveryState$.value;
17995
18237
  if (prev !== state) {
17996
- logger$12.debug(`CallRecoveryManager: state ${prev} -> ${state}`);
18238
+ logger$13.debug(`CallRecoveryManager: state ${prev} -> ${state}`);
17997
18239
  this._recoveryState$.next(state);
17998
18240
  }
17999
18241
  }
@@ -18096,7 +18338,7 @@ function mosToQualityLevel(mos) {
18096
18338
  //#endregion
18097
18339
  //#region src/core/entities/Call.ts
18098
18340
  var import_cjs$11 = require_cjs();
18099
- const logger$11 = getLogger();
18341
+ const logger$12 = getLogger();
18100
18342
  /**
18101
18343
  * Ratio between the critical and warning RTT spike multipliers.
18102
18344
  * Warning threshold = baseline * warningMultiplier (default 3x)
@@ -18114,7 +18356,7 @@ const fromDestinationParams = (destination) => {
18114
18356
  });
18115
18357
  return params;
18116
18358
  } catch (error) {
18117
- logger$11.warn(`Failed to parse destination URI: ${destination}`, error);
18359
+ logger$12.warn(`Failed to parse destination URI: ${destination}`, error);
18118
18360
  return {};
18119
18361
  }
18120
18362
  };
@@ -18248,7 +18490,7 @@ var WebRTCCall = class extends Destroyable {
18248
18490
  /** Toggles the call lock state, preventing or allowing new participants from joining. */
18249
18491
  async toggleLock() {
18250
18492
  const method = this.locked ? "call.unlock" : "call.lock";
18251
- await this.executeMethod(this.selfId ?? "", method, {});
18493
+ await this.executeMethod(this.callSelf, method, {});
18252
18494
  }
18253
18495
  /**
18254
18496
  * Toggles the hold state of the call (pauses/resumes local media transmission).
@@ -18297,14 +18539,24 @@ var WebRTCCall = class extends Destroyable {
18297
18539
  *
18298
18540
  * Constructs call context (node_id, call_id, member_id) and sends the RPC request.
18299
18541
  *
18300
- * @param target - Target member ID string, or a {@link MemberTarget} object.
18542
+ * @param target - Target {@link MemberTarget} triple, or the local member's
18543
+ * ID string for self-operations (any other string is rejected — a bare
18544
+ * member id cannot carry the remote member's own call context).
18301
18545
  * @param method - Verto method name (e.g. `'call.mute'`, `'call.member.remove'`).
18302
18546
  * @param args - Parameters for the RPC method.
18303
18547
  * @returns The RPC response.
18548
+ * @throws {CallNotReadyError} If the call has no self member context yet.
18549
+ * @throws {InvalidParams} If a string target is not the local member's ID.
18304
18550
  * @throws {JSONRPCError} If the RPC call returns an error.
18305
18551
  */
18306
18552
  async executeMethod(target, method, args) {
18307
- const params = this.buildMethodParams(target, args);
18553
+ const self = this.callSelf;
18554
+ 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}`);
18555
+ const params = {
18556
+ ...args,
18557
+ self,
18558
+ target: typeof target === "string" ? self : target
18559
+ };
18308
18560
  const request = buildRPCRequest({
18309
18561
  method,
18310
18562
  params
@@ -18314,29 +18566,27 @@ var WebRTCCall = class extends Destroyable {
18314
18566
  if (isJSONRPCErrorResponse(response)) throw new JSONRPCError(parseInt(response.result?.code ?? "0"), `Error response from method ${method}: ${response.result?.code} ${response.result?.message}`, void 0, void 0, request.id);
18315
18567
  return response;
18316
18568
  } catch (error) {
18317
- logger$11.error(`[Call] Error executing method ${method} with params`, params, error);
18569
+ logger$12.error(`[Call] Error executing method ${method} with params`, params, error);
18318
18570
  throw error;
18319
18571
  }
18320
18572
  }
18321
- buildMethodParams(target, args) {
18322
- const self = {
18323
- node_id: this.nodeId ?? "",
18324
- call_id: this.id,
18325
- member_id: this.vertoManager.selfId ?? ""
18326
- };
18327
- if (typeof target === "object") return {
18328
- ...args,
18329
- self,
18330
- targets: [target]
18331
- };
18573
+ /**
18574
+ * The local leg's member triple — sent as `self` in every member RPC
18575
+ * envelope, and as the `target` of call-scoped self-operations (e.g. lock,
18576
+ * layout).
18577
+ *
18578
+ * @throws {CallNotReadyError} Before `call.joined` delivers the self member
18579
+ * context (`selfId`/`nodeId`) an RPC without it cannot be routed, so fail
18580
+ * fast instead of sending a doomed request.
18581
+ */
18582
+ get callSelf() {
18583
+ const node_id = this.nodeId;
18584
+ const member_id = this.vertoManager.selfId;
18585
+ if (!node_id || !member_id) throw new CallNotReadyError(this.id);
18332
18586
  return {
18333
- ...args,
18334
- self,
18335
- target: {
18336
- node_id: this.nodeId ?? "",
18337
- call_id: this.id,
18338
- member_id: target
18339
- }
18587
+ node_id,
18588
+ call_id: this.id,
18589
+ member_id
18340
18590
  };
18341
18591
  }
18342
18592
  /** Observable of the current call status (e.g. `'ringing'`, `'connected'`). */
@@ -18517,9 +18767,9 @@ var WebRTCCall = class extends Destroyable {
18517
18767
  */
18518
18768
  initResilienceSubsystems() {
18519
18769
  const pc = this.rtcPeerConnection;
18520
- logger$11.debug(`[Call] initResilienceSubsystems: pc=${pc ? "exists" : "undefined"}, connectionState=${pc?.connectionState}`);
18770
+ logger$12.debug(`[Call] initResilienceSubsystems: pc=${pc ? "exists" : "undefined"}, connectionState=${pc?.connectionState}`);
18521
18771
  if (!pc) {
18522
- logger$11.warn("[Call] No peer connection available, skipping resilience init");
18772
+ logger$12.warn("[Call] No peer connection available, skipping resilience init");
18523
18773
  return;
18524
18774
  }
18525
18775
  try {
@@ -18554,14 +18804,14 @@ var WebRTCCall = class extends Destroyable {
18554
18804
  disableVideo: () => {
18555
18805
  try {
18556
18806
  this.vertoManager.muteMainVideoInputDevice();
18557
- logger$11.debug("[Call] Recovery manager disabled video");
18807
+ logger$12.debug("[Call] Recovery manager disabled video");
18558
18808
  } catch {
18559
- logger$11.debug("[Call] Recovery manager failed to disable video");
18809
+ logger$12.debug("[Call] Recovery manager failed to disable video");
18560
18810
  }
18561
18811
  },
18562
18812
  enableVideo: () => {
18563
18813
  this.vertoManager.unmuteMainVideoInputDevice().catch(() => {
18564
- logger$11.debug("[Call] Recovery manager failed to enable video");
18814
+ logger$12.debug("[Call] Recovery manager failed to enable video");
18565
18815
  });
18566
18816
  },
18567
18817
  isNegotiating: () => this.vertoManager.mainPeerConnection.isNegotiating,
@@ -18611,7 +18861,7 @@ var WebRTCCall = class extends Destroyable {
18611
18861
  this.subscribeTo(this._recoveryManager.recoveryEvent$, (event) => {
18612
18862
  this._recoveryEvent$.next(event);
18613
18863
  if (event.action === "max_attempts_reached") {
18614
- logger$11.warn("[Call] All recovery attempts exhausted, terminating call");
18864
+ logger$12.warn("[Call] All recovery attempts exhausted, terminating call");
18615
18865
  this.emitError({
18616
18866
  kind: "network",
18617
18867
  fatal: true,
@@ -18631,13 +18881,13 @@ var WebRTCCall = class extends Destroyable {
18631
18881
  else if (event.type === "online") this._recoveryManager?.handleWebSocketReconnect();
18632
18882
  });
18633
18883
  this.subscribeTo(this.clientSession.authenticated$.pipe((0, import_cjs$11.skip)(1), (0, import_cjs$11.filter)(Boolean)), () => {
18634
- logger$11.debug("[Call] WebSocket reconnected — notifying recovery manager");
18884
+ logger$12.debug("[Call] WebSocket reconnected — notifying recovery manager");
18635
18885
  this._recoveryManager?.handleWebSocketReconnect();
18636
18886
  });
18637
18887
  this._statsMonitor.start();
18638
- logger$11.debug("[Call] Resilience subsystems initialized for call", this.id);
18888
+ logger$12.debug("[Call] Resilience subsystems initialized for call", this.id);
18639
18889
  } catch (error) {
18640
- logger$11.warn("[Call] Failed to initialize resilience subsystems:", error);
18890
+ logger$12.warn("[Call] Failed to initialize resilience subsystems:", error);
18641
18891
  }
18642
18892
  }
18643
18893
  /**
@@ -18720,19 +18970,19 @@ var WebRTCCall = class extends Destroyable {
18720
18970
  }
18721
18971
  isCallSessionEvent(event) {
18722
18972
  try {
18723
- logger$11.debug("[Call] Checking if event is for this call session:", event);
18973
+ logger$12.debug("[Call] Checking if event is for this call session:", event);
18724
18974
  const callId = getValueFrom(event, "params.params.callID") ?? getValueFrom(event, "params.call_id");
18725
18975
  const roomSessionId = getValueFrom(event, "params.room_session_id");
18726
- logger$11.debug(`[Call] Extracted session identifiers callID: ${callId} and roomSessionID: ${roomSessionId} from event:`);
18976
+ logger$12.debug(`[Call] Extracted session identifiers callID: ${callId} and roomSessionID: ${roomSessionId} from event:`);
18727
18977
  return callId === this.id || !!callId && this.callEventsManager.isCallIdValid(callId) || !!roomSessionId && this.callEventsManager.isRoomSessionIdValid(roomSessionId);
18728
18978
  } catch (error) {
18729
- logger$11.error("[Call] Error checking if event is for this call session:", error);
18979
+ logger$12.error("[Call] Error checking if event is for this call session:", error);
18730
18980
  return false;
18731
18981
  }
18732
18982
  }
18733
18983
  get callSessionEvents$() {
18734
18984
  return this.cachedObservable("callSessionEvents$", () => this.clientSession.signalingEvent$.pipe((0, import_cjs$11.filter)((event) => this.isCallSessionEvent(event)), (0, import_cjs$11.tap)((event) => {
18735
- logger$11.debug("[Call] Received call session event:", event);
18985
+ logger$12.debug("[Call] Received call session event:", event);
18736
18986
  }), (0, import_cjs$11.takeUntil)(this.destroyed$), (0, import_cjs$11.share)()));
18737
18987
  }
18738
18988
  /** Observable of call-updated events. */
@@ -18802,16 +19052,16 @@ var WebRTCCall = class extends Destroyable {
18802
19052
  this._customSubscriptions.set(eventType, filtered$);
18803
19053
  }, (error) => {
18804
19054
  this._customSubscriptions.delete(eventType);
18805
- logger$11.warn(`[Call] verto.subscribe for '${eventType}' failed, not caching:`, error);
19055
+ logger$12.warn(`[Call] verto.subscribe for '${eventType}' failed, not caching:`, error);
18806
19056
  });
18807
19057
  this._customSubscriptions.set(eventType, filtered$);
18808
19058
  return filtered$;
18809
19059
  }
18810
19060
  get webrtcMessages$() {
18811
- return this.cachedObservable("webrtcMessages$", () => this.callSessionEvents$.pipe(filterAs(isWebrtcMessageMetadata, "params"), (0, import_cjs$11.tap)((event) => logger$11.debug("[Call] Event is a WebRTC message event:", event)), (0, import_cjs$11.takeUntil)(this.destroyed$), (0, import_cjs$11.share)()));
19061
+ return this.cachedObservable("webrtcMessages$", () => this.callSessionEvents$.pipe(filterAs(isWebrtcMessageMetadata, "params"), (0, import_cjs$11.tap)((event) => logger$12.debug("[Call] Event is a WebRTC message event:", event)), (0, import_cjs$11.takeUntil)(this.destroyed$), (0, import_cjs$11.share)()));
18812
19062
  }
18813
19063
  get callEvent$() {
18814
- return this.cachedObservable("callEvent$", () => this.callSessionEvents$.pipe(filterAs(isSignalwireCallMetadata, "params"), (0, import_cjs$11.tap)((event) => logger$11.debug("[Call] Event is a call event:", event)), (0, import_cjs$11.takeUntil)(this.destroyed$), (0, import_cjs$11.share)()));
19064
+ return this.cachedObservable("callEvent$", () => this.callSessionEvents$.pipe(filterAs(isSignalwireCallMetadata, "params"), (0, import_cjs$11.tap)((event) => logger$12.debug("[Call] Event is a call event:", event)), (0, import_cjs$11.takeUntil)(this.destroyed$), (0, import_cjs$11.share)()));
18815
19065
  }
18816
19066
  get layoutEvent$() {
18817
19067
  return this.cachedObservable("layoutEvent$", () => this.callEvent$.pipe(filterAs(isLayoutChangedMetadata, "params")));
@@ -18889,11 +19139,27 @@ var WebRTCCall = class extends Destroyable {
18889
19139
  return this.deferEmission(this._answered$.asObservable());
18890
19140
  }
18891
19141
  /**
18892
- * Sets the call layout and participant positions.
19142
+ * Sets the call layout and, optionally, individual participant positions.
19143
+ *
19144
+ * The gateway `call.layout.set` DTO has **no** `positions` member, so when
19145
+ * `positions` is provided this method issues a `call.member.position.set`
19146
+ * request per member (via {@link Participant.setPosition}, which keys each
19147
+ * position by that member's own call context) alongside `call.layout.set`
19148
+ * (issue #19400, Flag #6).
19149
+ *
19150
+ * **These operations are NOT atomic.** The layout is applied first, then each
19151
+ * member position sequentially, so members may briefly flash into their
19152
+ * default slots before being moved to the requested positions. Targeted
19153
+ * members are validated upfront, though: when any of them has no
19154
+ * {@link Participant.target | member call context} yet, the whole call
19155
+ * rejects before any request is sent and the layout is left unchanged.
18893
19156
  *
18894
19157
  * @param layout - Layout name (must be one of {@link layouts}).
18895
- * @param positions - Map of member IDs to {@link VideoPosition} values.
19158
+ * @param positions - Optional map of member IDs to {@link VideoPosition} values.
19159
+ * When omitted or empty, only the layout is changed.
18896
19160
  * @throws {InvalidParams} If the layout is not in the available {@link layouts}.
19161
+ * @throws {ParticipantNotReadyError} If a targeted member's call context has
19162
+ * not been received yet — thrown before any request is sent.
18897
19163
  *
18898
19164
  * @example
18899
19165
  * ```ts
@@ -18904,11 +19170,19 @@ var WebRTCCall = class extends Destroyable {
18904
19170
  */
18905
19171
  async setLayout(layout, positions) {
18906
19172
  if (!this.layouts.includes(layout)) throw new InvalidParams(`Layout ${layout} is not available in the current call layouts: ${this.layouts.join(", ")}`);
19173
+ const targets = [];
19174
+ for (const [memberId, position] of Object.entries(positions ?? {})) {
19175
+ const participant = this.participants.find((p) => p.id === memberId);
19176
+ if (!participant) {
19177
+ logger$12.warn(`[Call] setLayout: member ${memberId} not found in participants; skipping position ${position}`);
19178
+ continue;
19179
+ }
19180
+ participant.target;
19181
+ targets.push([participant, position]);
19182
+ }
18907
19183
  const selfId = await (0, import_cjs$11.firstValueFrom)(this.selfId$.pipe((0, import_cjs$11.filter)((id) => id !== null)));
18908
- await this.executeMethod(selfId, "call.layout.set", {
18909
- layout,
18910
- positions
18911
- });
19184
+ await this.executeMethod(selfId, "call.layout.set", { layout });
19185
+ for (const [participant, position] of targets) await participant.setPosition(position);
18912
19186
  }
18913
19187
  /**
18914
19188
  * Transfers the call to another destination.
@@ -18940,7 +19214,7 @@ var WebRTCCall = class extends Destroyable {
18940
19214
  setLocalMicrophoneGain(value) {
18941
19215
  const pipeline = this.vertoManager.ensureLocalAudioPipeline();
18942
19216
  if (!pipeline) {
18943
- logger$11.warn("[Call] setLocalMicrophoneGain: audio pipeline unavailable");
19217
+ logger$12.warn("[Call] setLocalMicrophoneGain: audio pipeline unavailable");
18944
19218
  return;
18945
19219
  }
18946
19220
  const percent = Math.max(0, Math.min(200, value));
@@ -18985,7 +19259,7 @@ var WebRTCCall = class extends Destroyable {
18985
19259
  enablePushToTalk() {
18986
19260
  const pipeline = this.vertoManager.ensureLocalAudioPipeline();
18987
19261
  if (!pipeline) {
18988
- logger$11.warn("[Call] enablePushToTalk: audio pipeline unavailable");
19262
+ logger$12.warn("[Call] enablePushToTalk: audio pipeline unavailable");
18989
19263
  return;
18990
19264
  }
18991
19265
  pipeline.setPTTActive(false);
@@ -19082,6 +19356,7 @@ function inferCallErrorKind(error) {
19082
19356
  if (error instanceof RPCTimeoutError) return "timeout";
19083
19357
  if (error instanceof JSONRPCError) return "signaling";
19084
19358
  if (error instanceof MediaTrackError) return "media";
19359
+ if (error instanceof MediaAccessError) return "media";
19085
19360
  if (error instanceof WebSocketConnectionError || error instanceof TransportConnectionError) return "network";
19086
19361
  return "internal";
19087
19362
  }
@@ -19098,6 +19373,7 @@ const RECOVERABLE_RPC_CODES = new Set([
19098
19373
  function isFatalError(error) {
19099
19374
  if (error instanceof VertoPongError) return false;
19100
19375
  if (error instanceof MediaTrackError) return false;
19376
+ if (error instanceof MediaAccessError) return error.fatal;
19101
19377
  if (error instanceof RPCTimeoutError) return false;
19102
19378
  if (error instanceof JSONRPCError && RECOVERABLE_RPC_CODES.has(error.code)) return false;
19103
19379
  return true;
@@ -19123,10 +19399,10 @@ var CallFactory = class {
19123
19399
  return {
19124
19400
  vertoManager: new WebRTCVertoManager(callInstance, this.attachManager, this.deviceController, this.webRTCApiProvider, {
19125
19401
  nodeId: options.nodeId,
19126
- onError: (error) => {
19402
+ onError: (error, options$1) => {
19127
19403
  const callError = {
19128
19404
  kind: inferCallErrorKind(error),
19129
- fatal: isFatalError(error),
19405
+ fatal: options$1?.fatal ?? isFatalError(error),
19130
19406
  error,
19131
19407
  callId: callInstance.id
19132
19408
  };
@@ -19148,7 +19424,7 @@ var CallFactory = class {
19148
19424
  //#endregion
19149
19425
  //#region src/behaviors/Collection.ts
19150
19426
  var import_cjs$10 = require_cjs();
19151
- const logger$10 = getLogger();
19427
+ const logger$11 = getLogger();
19152
19428
  var Fetcher = class {
19153
19429
  constructor(endpoint, params, http) {
19154
19430
  this.endpoint = endpoint;
@@ -19172,7 +19448,7 @@ var Fetcher = class {
19172
19448
  this.hasMore = !!this.nextUrl;
19173
19449
  return result.data.filter(this.filter).map(this.mapper);
19174
19450
  }
19175
- logger$10.error("Failed to fetch entity");
19451
+ logger$11.error("Failed to fetch entity");
19176
19452
  return [];
19177
19453
  }
19178
19454
  async id(v) {
@@ -19248,7 +19524,7 @@ var EntityCollection = class extends Destroyable {
19248
19524
  this._hasMore$.next(this.fetchController.hasMore ?? false);
19249
19525
  this._loading$.next(false);
19250
19526
  } catch (error) {
19251
- logger$10.error(`Failed to fetch initial collection data`, error);
19527
+ logger$11.error(`Failed to fetch initial collection data`, error);
19252
19528
  this._hasMore$.next(this.fetchController.hasMore ?? false);
19253
19529
  this._loading$.next(false);
19254
19530
  this.onError?.(new CollectionFetchError("fetchMore", error));
@@ -19262,7 +19538,7 @@ var EntityCollection = class extends Destroyable {
19262
19538
  if (data) this.upsertData(data);
19263
19539
  return data;
19264
19540
  } catch (error) {
19265
- logger$10.error(`Failed to fetch data for (${String(key)}:${String(value)}) :`, error);
19541
+ logger$11.error(`Failed to fetch data for (${String(key)}:${String(value)}) :`, error);
19266
19542
  this._loading$.next(false);
19267
19543
  this.onError?.(new CollectionFetchError(`tryFetch(${String(key)})`, error));
19268
19544
  }
@@ -19511,13 +19787,13 @@ var Address = class extends Destroyable {
19511
19787
  //#endregion
19512
19788
  //#region src/core/utils.ts
19513
19789
  var import_cjs$8 = require_cjs();
19514
- const logger$9 = getLogger();
19790
+ const logger$10 = getLogger();
19515
19791
  const isRPCConnectResult = (e) => {
19516
- logger$9.debug("isRPCConnectResult check:", e);
19792
+ logger$10.debug("isRPCConnectResult check:", e);
19517
19793
  if (!e || typeof e !== "object") return false;
19518
19794
  const result = e;
19519
19795
  const is = typeof result.identity === "string" && typeof result.protocol === "string" && typeof result.authorization === "object" && typeof result.authorization.jti === "string" && typeof result.authorization.project_id === "string" && typeof result.authorization.fabric_subscriber === "object";
19520
- logger$9.debug("isRPCConnectResult check result:", is);
19796
+ logger$10.debug("isRPCConnectResult check result:", is);
19521
19797
  return is;
19522
19798
  };
19523
19799
  var PendingRPC = class PendingRPC {
@@ -19526,7 +19802,7 @@ var PendingRPC = class PendingRPC {
19526
19802
  }
19527
19803
  constructor(request, responses$, options) {
19528
19804
  this.id = v4_default();
19529
- logger$9.debug(`[PendingRPC(${this.id}) request:${request.id}: method:${request.method}] Creating PendingRPC`);
19805
+ logger$10.debug(`[PendingRPC(${this.id}) request:${request.id}: method:${request.method}] Creating PendingRPC`);
19530
19806
  this.request = request;
19531
19807
  const timeoutMs = options?.timeoutMs ?? PendingRPC.defaultTimeoutMs;
19532
19808
  const signal = options?.signal;
@@ -19552,22 +19828,22 @@ var PendingRPC = class PendingRPC {
19552
19828
  isSettled = true;
19553
19829
  if (response.error) {
19554
19830
  const rpcError = new JSONRPCError(response.error.code, response.error.message, response.error.data, void 0, request.id);
19555
- logger$9.debug(`[PendingRPC(${this.id}) request:${request.id}] Rejecting promise with RPC error:`, rpcError);
19831
+ logger$10.debug(`[PendingRPC(${this.id}) request:${request.id}] Rejecting promise with RPC error:`, rpcError);
19556
19832
  reject(rpcError);
19557
19833
  } else {
19558
- logger$9.debug(`[PendingRPC(${this.id}) request:${request.id}] Resolving promise with response:`, response);
19834
+ logger$10.debug(`[PendingRPC(${this.id}) request:${request.id}] Resolving promise with response:`, response);
19559
19835
  resolve(response);
19560
19836
  }
19561
19837
  subscription.unsubscribe();
19562
19838
  },
19563
19839
  error: (error) => {
19564
- logger$9.debug(`[PendingRPC(${this.id}) request:${request.id}] Rejecting promise with error:`, error);
19840
+ logger$10.debug(`[PendingRPC(${this.id}) request:${request.id}] Rejecting promise with error:`, error);
19565
19841
  isSettled = true;
19566
19842
  reject(error);
19567
19843
  subscription.unsubscribe();
19568
19844
  },
19569
19845
  complete: () => {
19570
- logger$9.debug(`[PendingRPC(${this.id}) request:${request.id}] Observable completed`);
19846
+ logger$10.debug(`[PendingRPC(${this.id}) request:${request.id}] Observable completed`);
19571
19847
  if (!isSettled) reject(new RPCTimeoutError(request.id, timeoutMs));
19572
19848
  subscription.unsubscribe();
19573
19849
  }
@@ -19588,7 +19864,18 @@ var PendingRPC = class PendingRPC {
19588
19864
  //#endregion
19589
19865
  //#region src/managers/ClientSessionManager.ts
19590
19866
  var import_cjs$7 = require_cjs();
19591
- const logger$8 = getLogger();
19867
+ const logger$9 = getLogger();
19868
+ /**
19869
+ * Decide whether an error emitted on `call.errors$` during dial should
19870
+ * abort the dial. A non-fatal MediaAccessError means the call degraded to
19871
+ * receive-only and still connects — everything else rejects `dial()` with
19872
+ * the real cause.
19873
+ *
19874
+ * Pure function — exported for unit testing.
19875
+ */
19876
+ function shouldAbortDial(callError) {
19877
+ return callError.fatal || !(callError.error instanceof MediaAccessError);
19878
+ }
19592
19879
  const getAddressSearchURI = (options) => {
19593
19880
  const to = options.to?.split("?")[0];
19594
19881
  const from$9 = options.from?.startsWith("subscriber://") ? options.from.replace("subscriber://", "") : options.from;
@@ -19686,7 +19973,7 @@ var ClientSessionManager = class extends Destroyable {
19686
19973
  try {
19687
19974
  return await this.transport.execute(request, options);
19688
19975
  } catch (error) {
19689
- logger$8.debug("[Session] Execute Error", error);
19976
+ logger$9.debug("[Session] Execute Error", error);
19690
19977
  this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
19691
19978
  throw error;
19692
19979
  }
@@ -19700,13 +19987,13 @@ var ClientSessionManager = class extends Destroyable {
19700
19987
  return true;
19701
19988
  }
19702
19989
  setupMessageHandlers() {
19703
- logger$8.debug("[Session] Setting up message handlers");
19990
+ logger$9.debug("[Session] Setting up message handlers");
19704
19991
  this.subscribeTo(this.authStateEvent$, async (authStateEvent) => {
19705
- logger$8.debug("[Session] Authorization state event received:", authStateEvent);
19992
+ logger$9.debug("[Session] Authorization state event received:", authStateEvent);
19706
19993
  try {
19707
19994
  await this.updateAuthorizationStateInStorage(authStateEvent.authorization_state);
19708
19995
  } catch (error) {
19709
- logger$8.error("[Session] Failed to handle authorization state update:", error);
19996
+ logger$9.error("[Session] Failed to handle authorization state update:", error);
19710
19997
  this._errors$.next(new AuthStateHandlerError(error));
19711
19998
  }
19712
19999
  });
@@ -19714,29 +20001,29 @@ var ClientSessionManager = class extends Destroyable {
19714
20001
  if (this._authState$.value.kind === "authenticated") this._authState$.next({ kind: "unauthenticated" });
19715
20002
  });
19716
20003
  this.subscribeTo(this.transport.connectionStatus$.pipe((0, import_cjs$7.filter)((status) => status === "connected"), (0, import_cjs$7.exhaustMap)(() => {
19717
- logger$8.debug("[Session] Connection established, initiating authentication");
20004
+ logger$9.debug("[Session] Connection established, initiating authentication");
19718
20005
  return (0, import_cjs$7.from)(this.authenticate()).pipe((0, import_cjs$7.catchError)((error) => {
19719
20006
  this.handleAuthenticationError(error).catch((err) => {
19720
- logger$8.error("[Session] Error handling authentication failure:", err);
20007
+ logger$9.error("[Session] Error handling authentication failure:", err);
19721
20008
  });
19722
20009
  return import_cjs$7.EMPTY;
19723
20010
  }));
19724
20011
  })), void 0);
19725
20012
  this.subscribeTo(this.vertoInvite$, async (invite) => {
19726
- logger$8.debug("[Session] Verto invite received:", invite);
20013
+ logger$9.debug("[Session] Verto invite received:", invite);
19727
20014
  try {
19728
20015
  await this.createInboundCall(invite);
19729
20016
  } catch (error) {
19730
- logger$8.error("[Session] Error handling Verto invite:", error);
20017
+ logger$9.error("[Session] Error handling Verto invite:", error);
19731
20018
  this._errors$.next(new VertoInviteHandlerError(error));
19732
20019
  }
19733
20020
  });
19734
20021
  this.subscribeTo(this.vertoAttach$, async (attach) => {
19735
- logger$8.debug("[Session] Verto attach received:", attach);
20022
+ logger$9.debug("[Session] Verto attach received:", attach);
19736
20023
  try {
19737
20024
  await this.handleVertoAttach(attach);
19738
20025
  } catch (error) {
19739
- logger$8.error("[Session] Error handling Verto attach:", error);
20026
+ logger$9.error("[Session] Error handling Verto attach:", error);
19740
20027
  this._errors$.next(new VertoAttachHandlerError(error));
19741
20028
  }
19742
20029
  });
@@ -19746,36 +20033,36 @@ var ClientSessionManager = class extends Destroyable {
19746
20033
  const storedState = await this.storage.getItem(this.authorizationStateKey);
19747
20034
  this.authorizationState$.next(storedState ?? void 0);
19748
20035
  } catch (error) {
19749
- logger$8.error("Failed to retrieve authorization state from storage:", error);
20036
+ logger$9.error("Failed to retrieve authorization state from storage:", error);
19750
20037
  this.authorizationState$.next(void 0);
19751
20038
  }
19752
20039
  }
19753
20040
  async updateAuthorizationStateInStorage(authorizationState) {
19754
20041
  if (!authorizationState) {
19755
- logger$8.debug("[Session] Removing authorization state from storage");
20042
+ logger$9.debug("[Session] Removing authorization state from storage");
19756
20043
  try {
19757
20044
  await this.storage.removeItem(this.authorizationStateKey);
19758
20045
  this.authorizationState$.next(void 0);
19759
20046
  } catch (error) {
19760
- logger$8.error("Failed to remove authorization state from storage:", error);
20047
+ logger$9.error("Failed to remove authorization state from storage:", error);
19761
20048
  throw error;
19762
20049
  }
19763
20050
  return;
19764
20051
  }
19765
20052
  try {
19766
- logger$8.debug("[Session] Updating authorization state in storage");
20053
+ logger$9.debug("[Session] Updating authorization state in storage");
19767
20054
  await this.storage.setItem(this.authorizationStateKey, authorizationState);
19768
20055
  this.authorizationState$.next(authorizationState);
19769
20056
  } catch (error) {
19770
- logger$8.error("Failed to retrieve authorization state from storage:", error);
20057
+ logger$9.error("Failed to retrieve authorization state from storage:", error);
19771
20058
  throw error;
19772
20059
  }
19773
20060
  }
19774
20061
  get authStateEvent$() {
19775
20062
  return this.cachedObservable("authStateEvent$", () => this.signalingEvent$.pipe((0, import_cjs$7.tap)((msg) => {
19776
- logger$8.debug("[Session] Received incoming message:", msg);
20063
+ logger$9.debug("[Session] Received incoming message:", msg);
19777
20064
  }), filterAs(isSignalwireAuthorizationStateMetadata, "params"), (0, import_cjs$7.tap)((event) => {
19778
- logger$8.debug("[Session] Authorization state event received:", event.authorization_state);
20065
+ logger$9.debug("[Session] Authorization state event received:", event.authorization_state);
19779
20066
  })));
19780
20067
  }
19781
20068
  get signalingEvent$() {
@@ -19813,42 +20100,72 @@ var ClientSessionManager = class extends Destroyable {
19813
20100
  await (0, import_cjs$7.firstValueFrom)(this.authenticated$.pipe((0, import_cjs$7.takeUntil)(this.destroyed$), (0, import_cjs$7.filter)(Boolean), (0, import_cjs$7.take)(1), (0, import_cjs$7.timeout)({ first: 15e3 })));
19814
20101
  }
19815
20102
  async handleAuthenticationError(error) {
19816
- logger$8.error("Authentication error:", error);
19817
- const isRecoverableAuthError = error instanceof JSONRPCError && (error.code === RPC_ERROR_REQUESTER_VALIDATION_FAILED || error.code === RPC_ERROR_INVALID_PARAMS || error.code === RPC_ERROR_AUTHENTICATION_FAILED);
20103
+ logger$9.error("Authentication error:", error);
20104
+ 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);
19818
20105
  const hasStoredState = await (0, import_cjs$7.firstValueFrom)(this.authorizationState$.pipe((0, import_cjs$7.take)(1))) !== void 0;
19819
- if (isRecoverableAuthError && hasStoredState) {
19820
- logger$8.debug("[Session] Recoverable auth error — cleaning up stored state and reconnecting fresh");
20106
+ if (isRecoverableAuthError$1 && hasStoredState) {
20107
+ logger$9.debug("[Session] Recoverable auth error — cleaning up stored state and reconnecting fresh");
19821
20108
  try {
19822
20109
  await this.cleanupStoredConnectionParams();
19823
20110
  } catch (cleanupError) {
19824
- logger$8.error("Failed to cleanup stored connection params:", cleanupError);
20111
+ logger$9.error("Failed to cleanup stored connection params:", cleanupError);
19825
20112
  } finally {
19826
20113
  this.transport.reconnect();
19827
20114
  }
19828
20115
  } else this._errors$.next(error);
19829
20116
  }
20117
+ /**
20118
+ * Clear the resume state (authorization_state + protocol) only.
20119
+ *
20120
+ * This is the stale-auth-state recovery helper used by handleAuthError:
20121
+ * the server rejected a reconnect, so the resume state is discarded and a
20122
+ * fresh connect follows. Attach records are deliberately preserved — the
20123
+ * session lives on through the reconnect and reattachCalls() needs the
20124
+ * stored call references afterwards. Do NOT add detachAll() here.
20125
+ *
20126
+ * For public teardown (disconnect/destroy), use {@link teardownSessionState}
20127
+ * instead, which clears the attach records as well.
20128
+ */
19830
20129
  async cleanupStoredConnectionParams() {
19831
20130
  await this.transport.setProtocol(void 0);
19832
20131
  await this.updateAuthorizationStateInStorage(void 0);
19833
20132
  this._authorization$.next(void 0);
19834
20133
  }
20134
+ /**
20135
+ * Public-teardown helper for disconnect()/destroy(). Clears the resume
20136
+ * state (authorization_state + protocol) AND the attach records as one
20137
+ * atomic unit.
20138
+ *
20139
+ * The two stores are coupled: the backend only honors attach records
20140
+ * within the session identified by the resume state, so ending the
20141
+ * session must clear both. Clearing one without the other strands records
20142
+ * no future session can honor (disconnect) or revives a session the
20143
+ * developer explicitly ended (destroy).
20144
+ *
20145
+ * Distinct from {@link cleanupStoredConnectionParams}, which keeps the
20146
+ * attach records for the stale-auth-state recovery path.
20147
+ */
20148
+ async teardownSessionState() {
20149
+ await this.cleanupStoredConnectionParams();
20150
+ await this.attachManager.detachAll();
20151
+ }
19835
20152
  async updateAuthState(authorization_state) {
19836
20153
  try {
19837
20154
  await this.storage.setItem(this.authorizationStateKey, authorization_state);
19838
20155
  } catch (error) {
19839
- logger$8.error("Failed to update authorization state in storage:", error);
20156
+ logger$9.error("Failed to update authorization state in storage:", error);
19840
20157
  this._errors$.next(new AuthStateHandlerError(error));
19841
20158
  }
19842
20159
  }
19843
20160
  async reauthenticate(token, dpopToken, options) {
19844
- logger$8.debug("[Session] Re-authenticating session");
20161
+ logger$9.debug("[Session] Re-authenticating session");
19845
20162
  try {
19846
20163
  let resolvedDpopToken = dpopToken;
19847
20164
  if (!resolvedDpopToken && this.dpopManager?.initialized) try {
19848
20165
  resolvedDpopToken = await this.dpopManager.createRpcProof({ method: "signalwire.reauthenticate" });
19849
20166
  } catch (error) {
19850
20167
  if (this.clientBound) throw error;
19851
- logger$8.warn("[Session] Failed to create DPoP proof for reauthenticate:", error);
20168
+ logger$9.warn("[Session] Failed to create DPoP proof for reauthenticate:", error);
19852
20169
  }
19853
20170
  const request = RPCReauthenticate({
19854
20171
  project: this._authorization$.value?.project_id ?? "",
@@ -19856,24 +20173,24 @@ var ClientSessionManager = class extends Destroyable {
19856
20173
  ...resolvedDpopToken ? { dpop_token: resolvedDpopToken } : {}
19857
20174
  });
19858
20175
  await (0, import_cjs$7.lastValueFrom)((0, import_cjs$7.from)(this.transport.execute(request)).pipe(throwOnRPCError(), (0, import_cjs$7.take)(1), (0, import_cjs$7.catchError)((err) => {
19859
- logger$8.error("[Session] Re-authentication RPC failed:", err);
20176
+ logger$9.error("[Session] Re-authentication RPC failed:", err);
19860
20177
  throw err;
19861
20178
  })));
19862
20179
  if (options?.clientBound) this._wasClientBound = true;
19863
- logger$8.debug("[Session] Re-authentication successful, updating stored auth state");
20180
+ logger$9.debug("[Session] Re-authentication successful, updating stored auth state");
19864
20181
  } catch (error) {
19865
- logger$8.error("[Session] Re-authentication failed:", error);
20182
+ logger$9.error("[Session] Re-authentication failed:", error);
19866
20183
  this._errors$.next(new AuthStateHandlerError(error));
19867
20184
  throw error;
19868
20185
  }
19869
20186
  }
19870
20187
  async authenticate() {
19871
- logger$8.debug("[Session] Starting authentication process");
20188
+ logger$9.debug("[Session] Starting authentication process");
19872
20189
  const persistedParams = await (0, import_cjs$7.firstValueFrom)((0, import_cjs$7.combineLatest)({
19873
20190
  protocol: this.transport.protocol$,
19874
20191
  authorization_state: this.authorizationState$
19875
20192
  }).pipe((0, import_cjs$7.take)(1)));
19876
- logger$8.debug("[Session] Persisted params:\n", {
20193
+ logger$9.debug("[Session] Persisted params:\n", {
19877
20194
  protocol: persistedParams.protocol,
19878
20195
  authStateLength: persistedParams.authorization_state?.length
19879
20196
  });
@@ -19881,16 +20198,20 @@ var ClientSessionManager = class extends Destroyable {
19881
20198
  const storedToken = this.getCredential().token;
19882
20199
  const isReconnect = hasReconnectState && storedToken;
19883
20200
  let dpopToken;
19884
- if (isReconnect) logger$8.debug("[Session] Reconnecting with stored jwt_token + authorization_state");
19885
- else if (this.onBeforeReconnect && this.clientBound) {
19886
- logger$8.debug("[Session] Refreshing credentials before fresh connect");
19887
- await this.onBeforeReconnect();
20201
+ if (isReconnect) logger$9.debug("[Session] Reconnecting with stored jwt_token + authorization_state");
20202
+ else {
20203
+ const credential = this.getCredential();
20204
+ const credentialExpired = credential.expiry_at !== void 0 && credential.expiry_at <= Date.now() + CREDENTIAL_EXPIRY_SKEW_MS;
20205
+ if (this.onBeforeReconnect && (this.clientBound || credentialExpired)) {
20206
+ logger$9.debug("[Session] Refreshing credentials before fresh connect");
20207
+ await this.onBeforeReconnect();
20208
+ }
19888
20209
  }
19889
- if ((!isReconnect || this.clientBound) && this.dpopManager?.initialized) try {
20210
+ if (this.dpopManager?.initialized) try {
19890
20211
  dpopToken = await this.dpopManager.createRpcProof({ method: "signalwire.connect" });
19891
20212
  } catch (error) {
19892
20213
  if (this.clientBound) throw error;
19893
- logger$8.warn("[Session] Failed to create DPoP proof for connect, proceeding without:", error);
20214
+ logger$9.warn("[Session] Failed to create DPoP proof for connect, proceeding without:", error);
19894
20215
  }
19895
20216
  const rpcConnectRequest = RPCConnect({
19896
20217
  authentication: isReconnect ? { jwt_token: storedToken } : this.authentication,
@@ -19907,26 +20228,27 @@ var ClientSessionManager = class extends Destroyable {
19907
20228
  } : {}
19908
20229
  });
19909
20230
  const response = await (0, import_cjs$7.lastValueFrom)((0, import_cjs$7.from)(this.transport.execute(rpcConnectRequest)).pipe(throwOnRPCError(), (0, import_cjs$7.map)((res) => res.result), (0, import_cjs$7.filter)(isRPCConnectResult), (0, import_cjs$7.tap)(() => {
19910
- logger$8.debug("[Session] Response passed filter, processing authentication result");
20231
+ logger$9.debug("[Session] Response passed filter, processing authentication result");
19911
20232
  }), (0, import_cjs$7.take)(1), (0, import_cjs$7.catchError)((err) => {
19912
- logger$8.error("[Session] Authentication RPC failed:", err);
20233
+ logger$9.error("[Session] Authentication RPC failed:", err);
19913
20234
  throw err;
19914
20235
  })));
19915
- logger$8.debug("[Session] Processing authentication result:", {
20236
+ logger$9.debug("[Session] Processing authentication result:", {
19916
20237
  hasProtocol: !!response.protocol,
19917
20238
  hasAuthorization: !!response.authorization,
19918
20239
  hasIceServers: !!response.ice_servers
19919
20240
  });
19920
20241
  if (response.protocol) await this.transport.setProtocol(response.protocol);
19921
20242
  this._authorization$.next(response.authorization);
20243
+ if (response.authorization.cnf?.jkt) this._wasClientBound = true;
19922
20244
  this._iceServers$.next(response.ice_servers ?? []);
19923
20245
  this._authState$.next({ kind: "authenticated" });
19924
- logger$8.debug("[Session] Authentication completed successfully");
20246
+ logger$9.debug("[Session] Authentication completed successfully");
19925
20247
  }
19926
20248
  async disconnect() {
19927
20249
  this.transport.disconnect();
19928
20250
  this._authState$.next({ kind: "unauthenticated" });
19929
- await this.cleanupStoredConnectionParams();
20251
+ await this.teardownSessionState();
19930
20252
  }
19931
20253
  async createInboundCall(invite) {
19932
20254
  const callSession = await this.createCall({
@@ -19957,11 +20279,11 @@ var ClientSessionManager = class extends Destroyable {
19957
20279
  async handleVertoAttach(attach) {
19958
20280
  const { callID } = attach;
19959
20281
  if (callID in this._calls$.value) {
19960
- logger$8.debug(`[Session] Verto attach for existing call ${callID}, deferring to per-call handler`);
20282
+ logger$9.debug(`[Session] Verto attach for existing call ${callID}, deferring to per-call handler`);
19961
20283
  return;
19962
20284
  }
19963
20285
  const storedOptions = await this.attachManager.consumePendingAttachment(callID);
19964
- logger$8.debug(`[Session] Creating reattached call for callID: ${callID}`);
20286
+ logger$9.debug(`[Session] Creating reattached call for callID: ${callID}`);
19965
20287
  const callSession = await this.createCall({
19966
20288
  nodeId: attach.node_id,
19967
20289
  callId: callID,
@@ -19985,14 +20307,14 @@ var ClientSessionManager = class extends Destroyable {
19985
20307
  to: destinationURI,
19986
20308
  ...options
19987
20309
  });
19988
- 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)))));
20310
+ 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.filter)(shouldAbortDial), (0, import_cjs$7.take)(1), (0, import_cjs$7.switchMap)((callError) => (0, import_cjs$7.throwError)(() => callError.error)))));
19989
20311
  this._calls$.next({
19990
20312
  [`${callSession.id}`]: callSession,
19991
20313
  ...this._calls$.value
19992
20314
  });
19993
20315
  return callSession;
19994
20316
  } catch (error) {
19995
- logger$8.error("[Session] Error creating outbound call:", error);
20317
+ logger$9.error("[Session] Error creating outbound call:", error);
19996
20318
  callSession?.destroy();
19997
20319
  const callError = new CallCreateError(error instanceof import_cjs$7.TimeoutError ? "Call create timeout" : "Call creation failed", error, "outbound");
19998
20320
  this._errors$.next(callError);
@@ -20010,7 +20332,7 @@ var ClientSessionManager = class extends Destroyable {
20010
20332
  address = this._directory.get(addressId);
20011
20333
  if (!address) throw new DependencyError(`Address ID: ${addressId} not found`);
20012
20334
  } catch {
20013
- logger$8.warn(`[Session] Directory lookup failed for ${addressURI}, proceeding with raw URI`);
20335
+ logger$9.warn(`[Session] Directory lookup failed for ${addressURI}, proceeding with raw URI`);
20014
20336
  }
20015
20337
  const callSession = this.callFactory.createCall(address, { ...options });
20016
20338
  this.subscribeTo(callSession.status$.pipe((0, import_cjs$7.filter)((status) => status === "destroyed"), (0, import_cjs$7.take)(1)), () => {
@@ -20019,7 +20341,7 @@ var ClientSessionManager = class extends Destroyable {
20019
20341
  });
20020
20342
  return callSession;
20021
20343
  } catch (error) {
20022
- logger$8.error("[Session] Error creating call session:", error);
20344
+ logger$9.error("[Session] Error creating call session:", error);
20023
20345
  throw new CallCreateError("Call create error", error, options.initOffer ? "inbound" : "outbound");
20024
20346
  }
20025
20347
  }
@@ -20038,6 +20360,14 @@ var ClientSessionWrapper = class {
20038
20360
  get authenticated() {
20039
20361
  return this.clientSessionManager.authenticated;
20040
20362
  }
20363
+ /**
20364
+ * Whether the session is using a Client Bound SAT (DPoP). Sticky — set
20365
+ * when the binding is established or restored from a resumed session's
20366
+ * server authorization.
20367
+ */
20368
+ get clientBound() {
20369
+ return this.clientSessionManager.clientBound;
20370
+ }
20041
20371
  get signalingEvent$() {
20042
20372
  return this.clientSessionManager.signalingEvent$;
20043
20373
  }
@@ -20068,7 +20398,7 @@ const isString = (obj) => typeof obj === "string";
20068
20398
  //#endregion
20069
20399
  //#region src/managers/ConversationsManager.ts
20070
20400
  var import_cjs$6 = require_cjs();
20071
- const logger$7 = getLogger();
20401
+ const logger$8 = getLogger();
20072
20402
  var ConversationMessagesFetcher = class extends Fetcher {
20073
20403
  constructor(groupId, http) {
20074
20404
  super(`/api/fabric/conversations/${groupId}/messages`, "page_size=100", http);
@@ -20108,13 +20438,13 @@ var ConversationsManager = class {
20108
20438
  }
20109
20439
  throw new ConversationError("Join Failed - Unexpected response");
20110
20440
  } catch (error) {
20111
- logger$7.error("[ConversationsManager] Failed to join conversation:", error);
20441
+ logger$8.error("[ConversationsManager] Failed to join conversation:", error);
20112
20442
  throw error;
20113
20443
  }
20114
20444
  }
20115
20445
  async getConversationMessageCollection(addressId) {
20116
20446
  const groupId = this.groupIds.get(addressId) ?? await this.join(addressId);
20117
- return Promise.resolve(new ConversationMessageCollection(groupId, this.clientSession.signalingEvent$.pipe(filterAs(isConversationMessageMetadata, "params"), (0, import_cjs$6.tap)((event) => logger$7.debug("[ConversationsManager ] Conversation Event:", event)), (0, import_cjs$6.map)((params) => ({ ...params }))), this.http, this.onError));
20447
+ return Promise.resolve(new ConversationMessageCollection(groupId, this.clientSession.signalingEvent$.pipe(filterAs(isConversationMessageMetadata, "params"), (0, import_cjs$6.tap)((event) => logger$8.debug("[ConversationsManager ] Conversation Event:", event)), (0, import_cjs$6.map)((params) => ({ ...params }))), this.http, this.onError));
20118
20448
  }
20119
20449
  async sendText(text, destinationAddressId) {
20120
20450
  const groupId = this.groupIds.get(destinationAddressId) ?? await this.join(destinationAddressId);
@@ -20131,7 +20461,7 @@ var ConversationsManager = class {
20131
20461
  })).ok) return;
20132
20462
  throw new ConversationError("Send Text Failed - Unexpected response");
20133
20463
  } catch (error) {
20134
- logger$7.error("[ConversationsManager] Failed to send text message:", error);
20464
+ logger$8.error("[ConversationsManager] Failed to send text message:", error);
20135
20465
  throw error;
20136
20466
  }
20137
20467
  }
@@ -20140,7 +20470,7 @@ var ConversationsManager = class {
20140
20470
  //#endregion
20141
20471
  //#region src/managers/DeviceTokenManager.ts
20142
20472
  var import_cjs$5 = require_cjs();
20143
- const logger$6 = getLogger();
20473
+ const logger$7 = getLogger();
20144
20474
  /**
20145
20475
  * Resolves the token expiry timestamp (epoch seconds) using a 3-tier priority chain:
20146
20476
  * 1. `data.expires_at` — server-provided absolute timestamp
@@ -20150,7 +20480,7 @@ const logger$6 = getLogger();
20150
20480
  function resolveExpiresAt(data) {
20151
20481
  if (data.expires_at) return data.expires_at;
20152
20482
  if (data.expires_in) return Math.floor(Date.now() / 1e3) + data.expires_in;
20153
- logger$6.warn("[DeviceToken] Could not determine token expiry, using default");
20483
+ logger$7.warn("[DeviceToken] Could not determine token expiry, using default");
20154
20484
  return Math.floor(Date.now() / 1e3) + DEVICE_TOKEN_DEFAULT_EXPIRE_IN;
20155
20485
  }
20156
20486
  /**
@@ -20183,11 +20513,12 @@ var DeviceTokenManager = class extends Destroyable {
20183
20513
  this.getCredential = getCredential;
20184
20514
  this._currentToken$ = this.createBehaviorSubject(null);
20185
20515
  this._refreshInProgress = false;
20516
+ this._paused = false;
20186
20517
  this._effectiveExpireIn = DEVICE_TOKEN_DEFAULT_EXPIRE_IN;
20187
20518
  this.subscribeTo(this._currentToken$.pipe((0, import_cjs$5.filter)(Boolean), (0, import_cjs$5.switchMap)((tokenData) => {
20188
20519
  const expiresAt = resolveExpiresAt(tokenData);
20189
20520
  const refreshIn = Math.max(expiresAt * 1e3 - Date.now() - DEVICE_TOKEN_REFRESH_BUFFER_MS, 1e3);
20190
- logger$6.debug(`[DeviceToken] Scheduling Client Bound SAT refresh in ${refreshIn}ms`);
20521
+ logger$7.debug(`[DeviceToken] Scheduling Client Bound SAT refresh in ${refreshIn}ms`);
20191
20522
  return (0, import_cjs$5.timer)(refreshIn);
20192
20523
  })), () => {
20193
20524
  this.executeRefresh();
@@ -20201,7 +20532,12 @@ var DeviceTokenManager = class extends Destroyable {
20201
20532
  * Activates the Client Bound SAT flow when the user's token has
20202
20533
  * `sat:refresh` scope.
20203
20534
  *
20204
- * Steps:
20535
+ * Returns an {@link ActivationResult} indicating whether the manager
20536
+ * took ownership of refresh duties. The caller must use the `activated`
20537
+ * boolean to decide whether to keep its own refresh path armed — when
20538
+ * `activated` is `false`, the caller is responsible for refresh.
20539
+ *
20540
+ * Steps on success:
20205
20541
  * 1. Check user's `sat_claims` for `sat:refresh` scope
20206
20542
  * 2. Call `/api/fabric/subscriber/devices/token` with a DPoP proof
20207
20543
  * 3. Reauthenticate the session with the Client Bound SAT + DPoP proof
@@ -20210,32 +20546,66 @@ var DeviceTokenManager = class extends Destroyable {
20210
20546
  async activate(user, session, updateCredential) {
20211
20547
  const { satClaims } = user;
20212
20548
  if (!satClaims?.scope?.includes(SAT_REFRESH_SCOPE)) {
20213
- logger$6.debug("[DeviceToken] No sat:refresh scope, skipping Client Bound SAT activation");
20214
- return;
20549
+ logger$7.debug("[DeviceToken] No sat:refresh scope, skipping Client Bound SAT activation");
20550
+ return {
20551
+ activated: false,
20552
+ reason: "no-scope"
20553
+ };
20215
20554
  }
20216
20555
  this._session = session;
20217
20556
  this._updateCredential = updateCredential;
20218
20557
  try {
20558
+ const cached = this._currentToken$.value;
20559
+ if (cached && this.isTokenFresh(cached)) {
20560
+ logger$7.debug("[DeviceToken] Reusing cached Client Bound SAT — skipping /devices/token");
20561
+ const rpcProof$1 = await this.dpopManager.createRpcProof({ method: "signalwire.reauthenticate" });
20562
+ await session.reauthenticate(cached.token, rpcProof$1, { clientBound: true });
20563
+ updateCredential({ token: cached.token });
20564
+ return { activated: true };
20565
+ }
20219
20566
  const tokenData = await this.obtainToken();
20220
20567
  if (!tokenData.expires_at && !tokenData.expires_in && satClaims.expires_at) tokenData.expires_at = satClaims.expires_at;
20221
20568
  this._effectiveExpireIn = resolveExpireIn(tokenData);
20222
20569
  const rpcProof = await this.dpopManager.createRpcProof({ method: "signalwire.reauthenticate" });
20223
20570
  await session.reauthenticate(tokenData.token, rpcProof, { clientBound: true });
20224
20571
  updateCredential({ token: tokenData.token });
20225
- logger$6.info("[DeviceToken] Client Bound SAT activated successfully");
20226
- this._currentToken$.next(tokenData);
20572
+ logger$7.info("[DeviceToken] Client Bound SAT activated successfully");
20573
+ this.emitCurrentToken(tokenData);
20574
+ return { activated: true };
20227
20575
  } catch (error) {
20228
- logger$6.error("[DeviceToken] Failed to activate Client Bound SAT:", error);
20576
+ logger$7.error("[DeviceToken] Failed to activate Client Bound SAT:", error);
20229
20577
  this.errorHandler(new DPoPInitError(error, "Failed to activate Client Bound SAT"));
20230
- const credential = this.getCredential();
20231
- const expiresAt = satClaims.expires_at ?? (credential.expiry_at ? credential.expiry_at / 1e3 : Date.now() / 1e3 + DEVICE_TOKEN_DEFAULT_EXPIRE_IN / 1e3);
20232
- this._currentToken$.next({
20233
- token: credential.token ?? "",
20234
- expires_at: expiresAt
20235
- });
20578
+ return {
20579
+ activated: false,
20580
+ reason: "endpoint-failed"
20581
+ };
20236
20582
  }
20237
20583
  }
20238
20584
  /**
20585
+ * Emit a freshly received token to the reactive pipeline, stamping an
20586
+ * absolute `expires_at` when the response carried only `expires_in`.
20587
+ * Resolving the expiry at RECEIVE time (not at read time) is what lets
20588
+ * {@link refreshNowIfDue} detect due-ness on resume: a bare `expires_in`
20589
+ * re-resolved later would always compute a full TTL from "now" and never
20590
+ * cross the refresh buffer.
20591
+ */
20592
+ emitCurrentToken(token) {
20593
+ const stamped = token.expires_at ? token : {
20594
+ ...token,
20595
+ expires_at: resolveExpiresAt(token)
20596
+ };
20597
+ this._currentToken$.next(stamped);
20598
+ }
20599
+ /**
20600
+ * Returns true when the cached token has enough headroom before expiry to
20601
+ * be safely reused on reactivation. The headroom matches the refresh
20602
+ * buffer, so a token within the refresh window is treated as stale (the
20603
+ * reactive pipeline is about to refresh it anyway).
20604
+ */
20605
+ isTokenFresh(token) {
20606
+ return resolveExpiresAt(token) * 1e3 - Date.now() > DEVICE_TOKEN_REFRESH_BUFFER_MS;
20607
+ }
20608
+ /**
20239
20609
  * Obtains a Client Bound SAT from `/api/fabric/subscriber/devices/token`.
20240
20610
  * Returns the full {@link DeviceTokenResponse} including expiry metadata.
20241
20611
  */
@@ -20265,7 +20635,7 @@ var DeviceTokenManager = class extends Destroyable {
20265
20635
  * handled by the reactive pipeline).
20266
20636
  */
20267
20637
  async refreshToken(session, currentToken, updateCredential) {
20268
- logger$6.debug("[DeviceToken] Refreshing Client Bound SAT");
20638
+ logger$7.debug("[DeviceToken] Refreshing Client Bound SAT");
20269
20639
  const dpopProof = await this.dpopManager.createHttpProof({
20270
20640
  method: "POST",
20271
20641
  uri: DEVICE_REFRESH_ENDPOINT,
@@ -20287,7 +20657,7 @@ var DeviceTokenManager = class extends Destroyable {
20287
20657
  const rpcProof = await this.dpopManager.createRpcProof({ method: "signalwire.reauthenticate" });
20288
20658
  await session.reauthenticate(data.token, rpcProof);
20289
20659
  updateCredential({ token: data.token });
20290
- logger$6.info("[DeviceToken] Client Bound SAT refreshed successfully");
20660
+ logger$7.info("[DeviceToken] Client Bound SAT refreshed successfully");
20291
20661
  return data;
20292
20662
  }
20293
20663
  /**
@@ -20296,18 +20666,22 @@ var DeviceTokenManager = class extends Destroyable {
20296
20666
  * On all retries exhausted, emits to `errorHandler`.
20297
20667
  */
20298
20668
  async executeRefresh() {
20669
+ if (this._paused) {
20670
+ logger$7.debug("[DeviceToken] Manager paused, skipping refresh");
20671
+ return;
20672
+ }
20299
20673
  if (this._refreshInProgress) {
20300
- logger$6.debug("[DeviceToken] Refresh already in progress, skipping");
20674
+ logger$7.debug("[DeviceToken] Refresh already in progress, skipping");
20301
20675
  return;
20302
20676
  }
20303
20677
  const session = this._session;
20304
20678
  const updateCredential = this._updateCredential;
20305
20679
  if (!session || !updateCredential) {
20306
- logger$6.warn("[DeviceToken] Cannot refresh: session or updateCredential not set");
20680
+ logger$7.warn("[DeviceToken] Cannot refresh: session or updateCredential not set");
20307
20681
  return;
20308
20682
  }
20309
20683
  if (!session.authenticated) {
20310
- logger$6.debug("[DeviceToken] Session not authenticated, deferring refresh");
20684
+ logger$7.debug("[DeviceToken] Session not authenticated, deferring refresh");
20311
20685
  return;
20312
20686
  }
20313
20687
  this._refreshInProgress = true;
@@ -20315,9 +20689,9 @@ var DeviceTokenManager = class extends Destroyable {
20315
20689
  const currentToken = this.getCredential().token;
20316
20690
  if (!currentToken) throw new TokenRefreshError("No current token available for refresh");
20317
20691
  const newTokenData = await this.retryRefresh(session, currentToken, updateCredential);
20318
- this._currentToken$.next(newTokenData);
20692
+ this.emitCurrentToken(newTokenData);
20319
20693
  } catch (error) {
20320
- logger$6.error("[DeviceToken] Automatic Client Bound SAT refresh failed:", error);
20694
+ logger$7.error("[DeviceToken] Automatic Client Bound SAT refresh failed:", error);
20321
20695
  this.errorHandler(error instanceof TokenRefreshError ? error : new TokenRefreshError("Automatic token refresh failed", error));
20322
20696
  } finally {
20323
20697
  this._refreshInProgress = false;
@@ -20335,18 +20709,324 @@ var DeviceTokenManager = class extends Destroyable {
20335
20709
  lastError = error;
20336
20710
  if (attempt < DEVICE_TOKEN_REFRESH_MAX_RETRIES - 1) {
20337
20711
  const delay$1 = DEVICE_TOKEN_REFRESH_RETRY_BASE_MS * Math.pow(2, attempt);
20338
- logger$6.warn(`[DeviceToken] Refresh attempt ${attempt + 1} failed, retrying in ${delay$1}ms`);
20712
+ logger$7.warn(`[DeviceToken] Refresh attempt ${attempt + 1} failed, retrying in ${delay$1}ms`);
20339
20713
  await new Promise((resolve) => setTimeout(resolve, delay$1));
20340
20714
  }
20341
20715
  }
20342
20716
  throw lastError instanceof Error ? lastError : new TokenRefreshError("All refresh retries exhausted", lastError);
20343
20717
  }
20718
+ /**
20719
+ * Force an immediate refresh when the cached Client Bound SAT is already
20720
+ * past its refresh window. Called on resume from suspension where
20721
+ * background-tab throttling can delay the reactive timer past the buffer.
20722
+ * A no-op when no token is cached or it still has headroom; the normal
20723
+ * {@link executeRefresh} guards (paused / in-progress / unauthenticated)
20724
+ * still apply.
20725
+ */
20726
+ refreshNowIfDue() {
20727
+ const token = this._currentToken$.value;
20728
+ if (!token) return;
20729
+ if (resolveExpiresAt(token) * 1e3 - Date.now() <= DEVICE_TOKEN_REFRESH_BUFFER_MS) {
20730
+ logger$7.debug("[DeviceToken] Resume: cached SAT past refresh window; refreshing now");
20731
+ this.executeRefresh();
20732
+ }
20733
+ }
20734
+ /**
20735
+ * Stops the reactive refresh pipeline from firing. Use when the underlying
20736
+ * session is being torn down (e.g., during {@link SignalWire.disconnect})
20737
+ * so a scheduled refresh cannot fire against a destroyed session.
20738
+ *
20739
+ * The manager's state (cached token, effective TTL, subscriptions) is
20740
+ * preserved — call {@link resume} to re-enable firing after reconnect.
20741
+ */
20742
+ pause() {
20743
+ this._paused = true;
20744
+ }
20745
+ /** Re-enables the reactive refresh pipeline after a previous {@link pause}. */
20746
+ resume() {
20747
+ this._paused = false;
20748
+ }
20344
20749
  /** Cleans up the manager, cancelling the reactive pipeline and all subscriptions. */
20345
20750
  destroy() {
20346
20751
  super.destroy();
20347
20752
  }
20348
20753
  };
20349
20754
 
20755
+ //#endregion
20756
+ //#region src/managers/CredentialRefreshCoordinator.ts
20757
+ const logger$6 = getLogger();
20758
+ const defaultDeviceTokenManagerFactory = (dpopManager, http, errorHandler, getCredential) => new DeviceTokenManager(dpopManager, http, errorHandler, getCredential);
20759
+ /**
20760
+ * Centralizes credential-refresh ownership across the two competing
20761
+ * mechanisms — developer-provided `CredentialProvider.refresh()` and the
20762
+ * Client Bound SAT path via {@link DeviceTokenManager}.
20763
+ *
20764
+ * Maintains the invariant: **at most one refresh mechanism is armed at a
20765
+ * time, and at least one is armed whenever the current credential has an
20766
+ * `expiry_at`**.
20767
+ *
20768
+ * Replaces the previous design where refresh state was distributed across
20769
+ * `SignalWire` (timer field, scheduler method, activation helper) and
20770
+ * `DeviceTokenManager` (reactive pipeline). Centralizing the invariant in
20771
+ * one component eliminates the bug class that produced issue #19074.
20772
+ *
20773
+ * Race-safety:
20774
+ * - `_activating` flag prevents overlapping `activate()` calls from racing.
20775
+ * - `_activationGeneration` lets late resolutions detect they've been
20776
+ * preempted by a newer activation (e.g., reconnect during in-flight
20777
+ * `obtainToken`).
20778
+ */
20779
+ var CredentialRefreshCoordinator = class extends Destroyable {
20780
+ constructor(dpopManager, deps) {
20781
+ super();
20782
+ this.deps = deps;
20783
+ this._activating = false;
20784
+ this._activationGeneration = 0;
20785
+ this._developerRefreshInProgress = false;
20786
+ if (dpopManager?.initialized) this._deviceTokenManager = (deps.deviceTokenManagerFactory ?? defaultDeviceTokenManagerFactory)(dpopManager, deps.http, (error) => deps.notifier.onError(error), () => deps.store.read());
20787
+ }
20788
+ /** True when the Client Bound SAT path is available (DPoP initialized). */
20789
+ get clientBoundSATAvailable() {
20790
+ return this._deviceTokenManager !== void 0;
20791
+ }
20792
+ /** True when the developer-provided refresh timer is currently armed. */
20793
+ get developerRefreshArmed() {
20794
+ return this._developerTimerId !== void 0;
20795
+ }
20796
+ /**
20797
+ * Arms the developer-provided refresh timer to fire shortly before
20798
+ * `expiresAt`. Replaces any previously scheduled developer refresh.
20799
+ *
20800
+ * Idempotent — multiple calls just reschedule. On retry exhaustion,
20801
+ * invokes `deps.onRefreshExhausted` so the orchestrator can disconnect.
20802
+ */
20803
+ scheduleDeveloperRefresh(provider, expiresAt, attempt = 0) {
20804
+ this._activeProvider = provider;
20805
+ if (this._developerTimerId !== void 0) clearTimeout(this._developerTimerId);
20806
+ 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);
20807
+ this._developerTimerId = setTimeout(() => {
20808
+ this._developerTimerId = void 0;
20809
+ this.executeDeveloperRefresh(provider, expiresAt, attempt);
20810
+ }, refreshInterval);
20811
+ }
20812
+ /**
20813
+ * Runs the developer-provided refresh once: mints a new credential, stores
20814
+ * and persists it, reauthenticates the live session (via the notifier), and
20815
+ * reschedules against the new expiry. On failure retries with backoff up to
20816
+ * {@link CREDENTIAL_REFRESH_MAX_RETRIES}, then signals exhaustion.
20817
+ *
20818
+ * Shared by the scheduled timer tick and {@link forceRefreshIfDue}. The
20819
+ * `_developerRefreshInProgress` guard prevents the two from overlapping.
20820
+ */
20821
+ async executeDeveloperRefresh(provider, expiresAt, attempt) {
20822
+ if (this._developerRefreshInProgress) {
20823
+ logger$6.debug("[Coordinator] Developer refresh already in progress; skipping");
20824
+ return;
20825
+ }
20826
+ this._developerRefreshInProgress = true;
20827
+ try {
20828
+ const newCredentials = await this.refreshCredential(provider);
20829
+ this.deps.store.write(newCredentials);
20830
+ this.deps.store.persist(newCredentials);
20831
+ try {
20832
+ await this.deps.notifier.onCredentialRefreshed(newCredentials);
20833
+ } catch (reauthError) {
20834
+ logger$6.warn("[Coordinator] onCredentialRefreshed rejected (non-fatal):", reauthError);
20835
+ }
20836
+ logger$6.info("[Coordinator] Credentials refreshed successfully.");
20837
+ if (newCredentials.expiry_at) this.scheduleDeveloperRefresh(provider, newCredentials.expiry_at, 0);
20838
+ } catch (error) {
20839
+ const nextAttempt = attempt + 1;
20840
+ logger$6.error(`[Coordinator] Credential refresh failed (attempt ${nextAttempt}/${CREDENTIAL_REFRESH_MAX_RETRIES}):`, error);
20841
+ this.deps.notifier.onError(error instanceof Error ? error : new Error(String(error), { cause: error }));
20842
+ if (nextAttempt < CREDENTIAL_REFRESH_MAX_RETRIES) this.scheduleDeveloperRefresh(provider, expiresAt, nextAttempt);
20843
+ else {
20844
+ logger$6.error("[Coordinator] Credential refresh exhausted all retries. Disconnecting.");
20845
+ this.deps.notifier.onError(new TokenRefreshError("Credential refresh failed after max retries"));
20846
+ this.deps.notifier.onRefreshExhausted();
20847
+ }
20848
+ } finally {
20849
+ this._developerRefreshInProgress = false;
20850
+ }
20851
+ }
20852
+ /**
20853
+ * Force an immediate refresh when the current credential is already past its
20854
+ * scheduled refresh window. Called on resume from suspension, where
20855
+ * background-tab timer throttling can delay the armed refresh well past
20856
+ * expiry, leaving the live session stale.
20857
+ *
20858
+ * Routes to whichever mechanism is armed: the developer timer if armed,
20859
+ * otherwise the Client Bound SAT pipeline. A no-op when nothing is due.
20860
+ */
20861
+ forceRefreshIfDue() {
20862
+ if (this._developerTimerId !== void 0 && this._activeProvider) {
20863
+ const expiry = this.deps.store.read().expiry_at;
20864
+ if (expiry !== void 0 && Date.now() >= expiry - CREDENTIAL_REFRESH_BUFFER_MS) {
20865
+ logger$6.debug("[Coordinator] Resume: credential past refresh window; forcing refresh");
20866
+ clearTimeout(this._developerTimerId);
20867
+ this._developerTimerId = void 0;
20868
+ this.executeDeveloperRefresh(this._activeProvider, expiry, 0);
20869
+ }
20870
+ return;
20871
+ }
20872
+ this._deviceTokenManager?.refreshNowIfDue();
20873
+ }
20874
+ /**
20875
+ * Sync the credential's expiry from the server-provided authorization (the
20876
+ * `signalwire.connect` result). SATs are opaque JWE, so
20877
+ * `fabric_subscriber.expires_at` is the authoritative expiry of the token
20878
+ * the session actually connected with — the provider-reported `expiry_at`
20879
+ * is only a hint (and may be wrong or absent). Corrects the stored
20880
+ * credential and re-arms the developer refresh timer against the real
20881
+ * deadline when the provider supports `refresh()`.
20882
+ */
20883
+ syncExpiryFromAuthorization(authorization, provider) {
20884
+ const expiresAtSec = authorization?.fabric_subscriber?.expires_at;
20885
+ if (!expiresAtSec) return;
20886
+ const expiryAt = expiresAtSec * 1e3;
20887
+ const credential = this.deps.store.read();
20888
+ if (credential.expiry_at === expiryAt) return;
20889
+ logger$6.debug(`[Coordinator] Correcting credential expiry from server authorization: ${new Date(expiryAt).toISOString()}`);
20890
+ const updated = {
20891
+ ...credential,
20892
+ expiry_at: expiryAt
20893
+ };
20894
+ this.deps.store.write(updated);
20895
+ this.deps.store.persist(updated);
20896
+ if (provider?.refresh) this.scheduleDeveloperRefresh(provider, expiryAt);
20897
+ }
20898
+ /**
20899
+ * Invoke `provider.refresh()` deduped against any concurrent developer
20900
+ * refresh. Concurrent callers — the scheduled tick, a resume-forced refresh,
20901
+ * and the orchestrator's -32003 recovery / reconnect re-mint — share one
20902
+ * in-flight promise, so a provider backed by one-time-use rotating refresh
20903
+ * tokens is never invoked twice in parallel.
20904
+ *
20905
+ * The caller owns applying the returned credential (store write, session
20906
+ * reauth, rescheduling); this method only serializes the network call.
20907
+ */
20908
+ async refreshCredential(provider) {
20909
+ if (this._refreshInFlight) return this._refreshInFlight;
20910
+ if (!provider.refresh) throw new InvalidCredentialsError("Credential provider does not support refresh");
20911
+ const run = provider.refresh();
20912
+ this._refreshInFlight = run;
20913
+ const clear = () => {
20914
+ if (this._refreshInFlight === run) this._refreshInFlight = void 0;
20915
+ };
20916
+ run.then(clear, clear);
20917
+ return run;
20918
+ }
20919
+ /**
20920
+ * Cancels any scheduled developer-provided refresh. Idempotent.
20921
+ *
20922
+ * @internal Used by the coordinator's own activation flow. External
20923
+ * callers should use {@link suspend} for disconnect-time quiescence —
20924
+ * `suspend()` also pauses the internal Client Bound SAT pipeline.
20925
+ */
20926
+ cancelDeveloperRefresh() {
20927
+ if (this._developerTimerId !== void 0) {
20928
+ clearTimeout(this._developerTimerId);
20929
+ this._developerTimerId = void 0;
20930
+ }
20931
+ }
20932
+ /**
20933
+ * Suspends both refresh paths — cancels the developer timer and pauses
20934
+ * the internal reactive pipeline. Use when the underlying session is
20935
+ * being torn down (e.g., {@link SignalWire.disconnect}). The next
20936
+ * {@link activate} call re-enables the internal pipeline.
20937
+ *
20938
+ * The internal manager's cached token survives — see
20939
+ * {@link DeviceTokenManager.pause} — so a subsequent reconnect can
20940
+ * skip the `/devices/token` exchange entirely.
20941
+ */
20942
+ suspend() {
20943
+ this.cancelDeveloperRefresh();
20944
+ this._deviceTokenManager?.pause();
20945
+ }
20946
+ /**
20947
+ * Asks the Client Bound SAT path to take over refresh. If it accepts,
20948
+ * the developer-provided timer (if any) is cancelled. If it declines, the
20949
+ * developer timer remains armed and a `credential_refresh_fallback`
20950
+ * warning is emitted.
20951
+ *
20952
+ * **Idempotent** — re-entrant calls during an in-flight activation are
20953
+ * dropped. Use `_activationGeneration` to detect stale resolutions
20954
+ * (e.g., a reconnect-triggered activate() that races with an earlier one).
20955
+ */
20956
+ async activate(user, session) {
20957
+ if (this._activating) {
20958
+ logger$6.debug("[Coordinator] activate() in flight; ignoring re-entrant call");
20959
+ return;
20960
+ }
20961
+ if (!this._deviceTokenManager) return;
20962
+ this._deviceTokenManager.resume();
20963
+ const generation = ++this._activationGeneration;
20964
+ this._activating = true;
20965
+ try {
20966
+ const result = await this.withActivationTimeout(this._deviceTokenManager.activate(user, session, (cred) => this.deps.store.merge(cred)));
20967
+ if (generation !== this._activationGeneration) {
20968
+ logger$6.debug("[Coordinator] activate() result discarded (preempted by newer activation)");
20969
+ return;
20970
+ }
20971
+ if (result.activated) {
20972
+ this.cancelDeveloperRefresh();
20973
+ logger$6.debug("[Coordinator] Developer refresh disabled — Client Bound SAT owns refresh");
20974
+ return;
20975
+ }
20976
+ logger$6.warn(`[SignalWire] [SW-REFRESH-FALLBACK] Client Bound SAT declined (reason=${result.reason}); using developer-provided refresh handler.`);
20977
+ this.deps.notifier.onWarning({
20978
+ code: "credential_refresh_fallback",
20979
+ source: "CredentialProvider",
20980
+ reason: result.reason,
20981
+ message: `Client Bound SAT activation declined (${result.reason}); using developer-provided refresh.`
20982
+ });
20983
+ } finally {
20984
+ this._activating = false;
20985
+ }
20986
+ }
20987
+ destroy() {
20988
+ this.cancelDeveloperRefresh();
20989
+ this._deviceTokenManager?.destroy();
20990
+ this._deviceTokenManager = void 0;
20991
+ super.destroy();
20992
+ }
20993
+ /**
20994
+ * Races the manager's `activate()` against a hard timeout. A wedged HTTP
20995
+ * layer (e.g., proxy issues) could otherwise hang the activation
20996
+ * indefinitely, leaving the session with no refresh mechanism while the
20997
+ * `_activating` guard blocks subsequent retries.
20998
+ *
20999
+ * On timeout, the manager is paused immediately. The inner `activate()`
21000
+ * may still complete in the background and emit to `_currentToken$`,
21001
+ * which would normally arm the reactive refresh pipeline; pausing
21002
+ * prevents that pipeline from firing while the developer refresh path
21003
+ * is the active mechanism — preserving the "at most one mechanism
21004
+ * armed" invariant. The next `activate()` call resumes the manager.
21005
+ */
21006
+ async withActivationTimeout(inner) {
21007
+ return new Promise((resolve) => {
21008
+ const timer$4 = setTimeout(() => {
21009
+ this._deviceTokenManager?.pause();
21010
+ resolve({
21011
+ activated: false,
21012
+ reason: "activation-timeout"
21013
+ });
21014
+ }, CREDENTIAL_ACTIVATE_TIMEOUT_MS);
21015
+ inner.then((result) => {
21016
+ clearTimeout(timer$4);
21017
+ resolve(result);
21018
+ }, (error) => {
21019
+ clearTimeout(timer$4);
21020
+ this.deps.notifier.onError(error instanceof Error ? error : new Error(String(error)));
21021
+ resolve({
21022
+ activated: false,
21023
+ reason: "endpoint-failed"
21024
+ });
21025
+ });
21026
+ });
21027
+ }
21028
+ };
21029
+
20350
21030
  //#endregion
20351
21031
  //#region src/managers/DiagnosticsCollector.ts
20352
21032
  const logger$5 = getLogger();
@@ -20867,6 +21547,10 @@ var TransportManager = class extends Destroyable {
20867
21547
  if (!isSignalwireRequest(message)) return true;
20868
21548
  const eventChannel = message.params.event_channel;
20869
21549
  if (!eventChannel) return true;
21550
+ if (message.params.event_type.startsWith("conversation.")) {
21551
+ logger$2.debug(`[Transport] Received conversation event: ${message.params.event_type} (event_channel: ${eventChannel})`);
21552
+ return true;
21553
+ }
20870
21554
  const currentProtocol = this._currentProtocol;
20871
21555
  if (!currentProtocol) return true;
20872
21556
  if (!eventChannel.includes(currentProtocol)) {
@@ -21010,6 +21694,33 @@ var TransportManager = class extends Destroyable {
21010
21694
  }
21011
21695
  };
21012
21696
 
21697
+ //#endregion
21698
+ //#region src/utils/authRecovery.ts
21699
+ /**
21700
+ * Walk an error's `error`/`cause` chain looking for a {@link JSONRPCError}.
21701
+ * Errors thrown by call creation are wrapped (e.g. `CallCreateError`), so the
21702
+ * underlying signaling error is nested. Bounded by a visited set to guard
21703
+ * against cyclic causes.
21704
+ */
21705
+ function findJSONRPCError(error) {
21706
+ const seen = /* @__PURE__ */ new Set();
21707
+ let current = error;
21708
+ while (current instanceof Error && !seen.has(current)) {
21709
+ seen.add(current);
21710
+ if (current instanceof JSONRPCError) return current;
21711
+ current = current.error ?? current.cause;
21712
+ }
21713
+ }
21714
+ /**
21715
+ * Whether an error is a session-recoverable authentication failure
21716
+ * (`-32002` authentication failed or `-32003` requester validation failed)
21717
+ * that a credential re-mint + retry can heal.
21718
+ */
21719
+ function isRecoverableAuthError(error) {
21720
+ const rpcError = findJSONRPCError(error);
21721
+ return rpcError !== void 0 && (rpcError.code === RPC_ERROR_REQUESTER_VALIDATION_FAILED || rpcError.code === RPC_ERROR_AUTHENTICATION_FAILED);
21722
+ }
21723
+
21013
21724
  //#endregion
21014
21725
  //#region src/clients/SignalWire.ts
21015
21726
  var import_cjs$1 = require_cjs();
@@ -21060,6 +21771,7 @@ var SignalWire = class extends Destroyable {
21060
21771
  this._isConnected$ = this.createBehaviorSubject(false);
21061
21772
  this._isRegistered$ = this.createBehaviorSubject(false);
21062
21773
  this._errors$ = this.createReplaySubject(1);
21774
+ this._warnings$ = this.createReplaySubject(10);
21063
21775
  this._options = {};
21064
21776
  this._deps = new DependencyContainer();
21065
21777
  this._credentialProvider = credentialProvider;
@@ -21119,6 +21831,29 @@ var SignalWire = class extends Destroyable {
21119
21831
  */
21120
21832
  async resolveCredentials() {
21121
21833
  const fingerprint = await this.initDPoP();
21834
+ this._refreshCoordinator = new CredentialRefreshCoordinator(this._dpopManager, {
21835
+ http: this._deps.http,
21836
+ notifier: {
21837
+ onError: (error) => this._errors$.next(error),
21838
+ onWarning: (warning) => this._warnings$.next(warning),
21839
+ onRefreshExhausted: () => void this.disconnect(),
21840
+ onCredentialRefreshed: async (credential) => this.reauthenticateLiveSession(credential)
21841
+ },
21842
+ store: {
21843
+ read: () => this._deps.credential,
21844
+ write: (credential) => {
21845
+ this._deps.credential = credential;
21846
+ },
21847
+ merge: (partial) => {
21848
+ this._deps.credential = {
21849
+ ...this._deps.credential,
21850
+ ...partial
21851
+ };
21852
+ this.persistCredential(this._deps.credential);
21853
+ },
21854
+ persist: (credential) => this.persistCredential(credential)
21855
+ }
21856
+ });
21122
21857
  if (this._credentialProvider) return this.validateCredentials(this._credentialProvider, void 0, fingerprint);
21123
21858
  for (const scope of this._deps.persistSession ? ["local", "session"] : ["session"]) try {
21124
21859
  const cached = await this._deps.storage.getItem("sw:cached_credential", scope);
@@ -21148,11 +21883,32 @@ var SignalWire = class extends Destroyable {
21148
21883
  logger$1.error("[SignalWire] Provided credentials have expired.");
21149
21884
  throw new InvalidCredentialsError("Provided credentials have expired.");
21150
21885
  }
21151
- if (_credentials.expiry_at && credentialProvider?.refresh) this.scheduleCredentialRefresh(credentialProvider, _credentials.expiry_at);
21886
+ if (_credentials.expiry_at && credentialProvider?.refresh) this._refreshCoordinator?.scheduleDeveloperRefresh(credentialProvider, _credentials.expiry_at);
21887
+ else if (_credentials.expiry_at && !credentialProvider?.refresh) {
21888
+ logger$1.warn(`[SignalWire] [SW-NO-REFRESH-HANDLER] Credential has expiry_at=${_credentials.expiry_at} but no refresh handler. Session will terminate at expiry unless the SAT has 'sat:refresh' scope.`);
21889
+ this._warnings$.next({
21890
+ code: "credential_no_refresh_handler",
21891
+ source: "CredentialProvider",
21892
+ message: "Credential has expiry_at but no refresh handler. Session will terminate at expiry unless the SAT carries 'sat:refresh' scope.",
21893
+ expiresAt: _credentials.expiry_at
21894
+ });
21895
+ }
21152
21896
  this._deps.credential = _credentials;
21153
21897
  this.persistCredential(_credentials);
21154
- if (this.isConnected && this._clientSession.authenticated && _credentials.token) try {
21155
- await this._clientSession.reauthenticate(_credentials.token);
21898
+ await this.reauthenticateLiveSession(_credentials);
21899
+ }
21900
+ /**
21901
+ * Reauthenticate the currently-open session with a freshly obtained
21902
+ * credential so the new token takes effect on the live socket immediately —
21903
+ * not just on the next reconnect. No-op when the session is not
21904
+ * connected/authenticated or the credential carries no token (e.g. an
21905
+ * authorization-state-only refresh). Non-fatal: reauth failures surface on
21906
+ * `errors$` without aborting the refresh that triggered this.
21907
+ */
21908
+ async reauthenticateLiveSession(credential) {
21909
+ if (!this.isConnected || !this._clientSession.authenticated || !credential.token) return;
21910
+ try {
21911
+ await this._clientSession.reauthenticate(credential.token);
21156
21912
  logger$1.info("[SignalWire] Session refreshed with new credentials.");
21157
21913
  } catch (error) {
21158
21914
  logger$1.error("[SignalWire] Failed to refresh session with new credentials:", error);
@@ -21160,33 +21916,101 @@ var SignalWire = class extends Destroyable {
21160
21916
  }
21161
21917
  }
21162
21918
  /**
21163
- * Schedules credential refresh with exponential backoff retry on failure.
21164
- * On success, resets attempt counter and schedules the next refresh.
21165
- * After exhausting retries, emits TokenRefreshError and disconnects.
21919
+ * One-shot recovery for a live session that failed with a recoverable auth
21920
+ * error (`-32002`/`-32003`) because its token went stale e.g. a refresh
21921
+ * timer that was throttled while the tab was backgrounded.
21922
+ *
21923
+ * Two steps, first that succeeds wins:
21924
+ * 1. Reauthenticate with the in-memory token (a fresh DPoP proof is
21925
+ * generated automatically). Heals a session whose token was already
21926
+ * refreshed by the coordinator but never applied to the open socket,
21927
+ * and a client-bound session whose server-side auth merely drifted.
21928
+ * 2. Re-mint via the developer provider's `refresh()` and reauthenticate.
21929
+ * Skipped for client-bound sessions (the DeviceTokenManager owns their
21930
+ * refresh; re-minting a base SAT here would drop the DPoP binding).
21931
+ *
21932
+ * @param allowRemint - Whether step 2 (provider re-mint) may run. Callers
21933
+ * pass `false` for non-auth failures so a transient network error never
21934
+ * escalates to a token re-mint; step 1 (in-memory reauth) always runs.
21935
+ * @returns `true` if the session was reauthenticated, `false` otherwise.
21166
21936
  */
21167
- scheduleCredentialRefresh(credentialProvider, expiresAt, attempt = 0) {
21168
- if (this._refreshTimerId !== void 0) clearTimeout(this._refreshTimerId);
21169
- 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);
21170
- this._refreshTimerId = setTimeout(async () => {
21171
- try {
21172
- if (!credentialProvider.refresh) throw new InvalidCredentialsError("Credential provider does not support refresh");
21173
- const newCredentials = await credentialProvider.refresh();
21174
- this._deps.credential = newCredentials;
21175
- this.persistCredential(newCredentials);
21176
- logger$1.info("[SignalWire] Credentials refreshed successfully.");
21177
- if (newCredentials.expiry_at) this.scheduleCredentialRefresh(credentialProvider, newCredentials.expiry_at, 0);
21178
- } catch (error) {
21179
- const nextAttempt = attempt + 1;
21180
- logger$1.error(`[SignalWire] Credential refresh failed (attempt ${nextAttempt}/${CREDENTIAL_REFRESH_MAX_RETRIES}):`, error);
21181
- this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
21182
- if (nextAttempt < CREDENTIAL_REFRESH_MAX_RETRIES) this.scheduleCredentialRefresh(credentialProvider, expiresAt, nextAttempt);
21183
- else {
21184
- logger$1.error("[SignalWire] Credential refresh exhausted all retries. Disconnecting.");
21185
- this._errors$.next(new TokenRefreshError("Credential refresh failed after max retries"));
21186
- this.disconnect();
21187
- }
21937
+ async recoverStaleCredential(allowRemint = true) {
21938
+ const currentToken = this._deps.credential.token;
21939
+ if (currentToken) try {
21940
+ await this._clientSession.reauthenticate(currentToken);
21941
+ return true;
21942
+ } catch (error) {
21943
+ logger$1.debug("[SignalWire] In-memory reauth failed during recovery:", error);
21944
+ }
21945
+ if (!allowRemint || this._clientSession.clientBound || !this._credentialProvider?.refresh) return false;
21946
+ try {
21947
+ const newCredentials = await this.remintCredential(this._credentialProvider);
21948
+ if (!newCredentials.token) return false;
21949
+ this._deps.credential = newCredentials;
21950
+ this.persistCredential(newCredentials);
21951
+ await this._clientSession.reauthenticate(newCredentials.token);
21952
+ if (newCredentials.expiry_at) this._refreshCoordinator?.scheduleDeveloperRefresh(this._credentialProvider, newCredentials.expiry_at);
21953
+ return true;
21954
+ } catch (error) {
21955
+ logger$1.error("[SignalWire] Credential recovery failed:", error);
21956
+ return false;
21957
+ }
21958
+ }
21959
+ /**
21960
+ * Re-mint a credential via `provider.refresh()`, routed through the
21961
+ * coordinator's shared in-flight guard so concurrent re-mint paths (a
21962
+ * scheduled/resume refresh, -32003 recovery, and reconnect) never fire a
21963
+ * second `provider.refresh()` in parallel — which rotating one-time-use
21964
+ * refresh tokens reject. Falls back to a direct call only if the coordinator
21965
+ * has not been constructed yet.
21966
+ */
21967
+ async remintCredential(provider) {
21968
+ if (this._refreshCoordinator) return this._refreshCoordinator.refreshCredential(provider);
21969
+ if (!provider.refresh) throw new InvalidCredentialsError("Credential provider does not support refresh");
21970
+ return provider.refresh();
21971
+ }
21972
+ /**
21973
+ * Re-mint credentials before a fresh (re)connect (`onBeforeReconnect` hook).
21974
+ * The session invokes this only when it is client-bound OR the in-memory
21975
+ * token is expired. The re-mint mechanism depends on the binding:
21976
+ * - Client-bound: `authenticate()` with the DPoP fingerprint to obtain a
21977
+ * fresh base SAT the upcoming reconnect can re-bind (the
21978
+ * DeviceTokenManager re-activates afterwards).
21979
+ * - Unbound: the developer's non-interactive `refresh()` handler.
21980
+ * `authenticate()` is deliberately NOT used here — it may be interactive
21981
+ * (a login prompt) and must not fire on a background reconnect.
21982
+ *
21983
+ * Rejects on failure so the session aborts the reconnect rather than
21984
+ * replaying a stale token.
21985
+ */
21986
+ async refreshCredentialForReconnect() {
21987
+ if (!this._credentialProvider) return;
21988
+ try {
21989
+ let newCredentials;
21990
+ if (this._clientSession.clientBound) {
21991
+ const fingerprint = this._dpopManager?.initialized ? this._dpopManager.fingerprint : void 0;
21992
+ logger$1.debug("[SignalWire] Re-minting client-bound base SAT before reconnect");
21993
+ newCredentials = await this._credentialProvider.authenticate(fingerprint ? { fingerprint } : void 0);
21994
+ } else if (this._credentialProvider.refresh) {
21995
+ logger$1.debug("[SignalWire] Refreshing unbound credential before reconnect");
21996
+ newCredentials = await this.remintCredential(this._credentialProvider);
21997
+ } else {
21998
+ logger$1.warn("[SignalWire] [SW-NO-REFRESH-HANDLER] Token expired on reconnect but no refresh handler; reconnecting with the existing token.");
21999
+ return;
21188
22000
  }
21189
- }, refreshInterval);
22001
+ if (!newCredentials.token) {
22002
+ logger$1.warn("[SignalWire] Re-minted credential has no token; keeping the existing credential for reconnect.");
22003
+ return;
22004
+ }
22005
+ this._deps.credential = newCredentials;
22006
+ this.persistCredential(newCredentials);
22007
+ if (newCredentials.expiry_at && this._credentialProvider.refresh) this._refreshCoordinator?.scheduleDeveloperRefresh(this._credentialProvider, newCredentials.expiry_at);
22008
+ logger$1.debug("[SignalWire] Credential refreshed successfully for reconnect");
22009
+ } catch (error) {
22010
+ logger$1.error("[SignalWire] Failed to refresh credentials for reconnect:", error);
22011
+ this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
22012
+ throw error;
22013
+ }
21190
22014
  }
21191
22015
  /** Persist credential to localStorage when persistSession is enabled. */
21192
22016
  persistCredential(credential) {
@@ -21274,51 +22098,20 @@ var SignalWire = class extends Destroyable {
21274
22098
  this._attachManager = new AttachManager(this._deps.storage, this._deps.deviceController, PreferencesContainer.instance.reconnectCallsTimeout, this._deps.attachedCallsKey);
21275
22099
  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$);
21276
22100
  this._publicSession = new ClientSessionWrapper(this._clientSession);
21277
- this._clientSession.onBeforeReconnect = async () => {
21278
- if (!this._credentialProvider) return;
21279
- try {
21280
- const fingerprint = this._dpopManager?.initialized ? this._dpopManager.fingerprint : void 0;
21281
- logger$1.debug("[SignalWire] Credential expired, refreshing before reconnect");
21282
- const newCredentials = await this._credentialProvider.authenticate(fingerprint ? { fingerprint } : void 0);
21283
- this._deps.credential = newCredentials;
21284
- logger$1.debug("[SignalWire] Credential refreshed successfully for reconnect");
21285
- } catch (error) {
21286
- logger$1.error("[SignalWire] Failed to refresh credentials for reconnect:", error);
21287
- this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
21288
- throw error;
21289
- }
21290
- };
22101
+ this._clientSession.onBeforeReconnect = async () => this.refreshCredentialForReconnect();
21291
22102
  this.subscribeTo(this._clientSession.errors$, (error) => {
21292
22103
  this._errors$.next(error);
21293
22104
  });
22105
+ this.subscribeTo(this._clientSession.authorization$, (authorization) => {
22106
+ this._refreshCoordinator?.syncExpiryFromAuthorization(authorization, this._credentialProvider);
22107
+ });
21294
22108
  await this._clientSession.connect();
21295
- if (this._dpopManager?.initialized) {
21296
- if (this._refreshTimerId) {
21297
- clearTimeout(this._refreshTimerId);
21298
- this._refreshTimerId = void 0;
21299
- logger$1.debug("[SignalWire] Developer refresh disabled — Client Bound SAT activation starting");
21300
- }
21301
- this._deviceTokenManager = new DeviceTokenManager(this._dpopManager, this._deps.http, (error) => this._errors$.next(error), () => this._deps.credential);
21302
- await this._deviceTokenManager.activate(this._deps.user, this._clientSession, (cred) => {
21303
- this._deps.credential = {
21304
- ...this._deps.credential,
21305
- ...cred
21306
- };
21307
- });
21308
- }
22109
+ await this._refreshCoordinator?.activate(this._deps.user, this._clientSession);
21309
22110
  this.subscribeTo(this._clientSession.authenticated$.pipe((0, import_cjs$1.skip)(1), (0, import_cjs$1.filter)(Boolean)), async () => {
21310
22111
  try {
21311
- if (this._deviceTokenManager) {
21312
- await this._deviceTokenManager.activate(this._deps.user, this._clientSession, (cred) => {
21313
- this._deps.credential = {
21314
- ...this._deps.credential,
21315
- ...cred
21316
- };
21317
- });
21318
- logger$1.debug("[SignalWire] Client Bound SAT re-activated after reconnect");
21319
- }
22112
+ await this._refreshCoordinator?.activate(this._deps.user, this._clientSession);
21320
22113
  } catch (error) {
21321
- logger$1.error("[SignalWire] Client Bound SAT re-activation failed (non-fatal):", error);
22114
+ logger$1.error("[SignalWire] Refresh re-arm after reconnect failed (non-fatal):", error);
21322
22115
  this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
21323
22116
  }
21324
22117
  try {
@@ -21404,6 +22197,19 @@ var SignalWire = class extends Destroyable {
21404
22197
  get errors$() {
21405
22198
  return this.deferEmission(this._errors$.asObservable());
21406
22199
  }
22200
+ /**
22201
+ * Observable stream of non-fatal SDK warnings.
22202
+ *
22203
+ * Subscribe to detect SDK behaviors that affect session liveness or developer-facing
22204
+ * contracts but do not warrant disconnection — e.g., a fallback from Client Bound SAT
22205
+ * refresh to the developer-provided `refresh()` because the SAT lacks `sat:refresh`
22206
+ * scope. Discriminated by `code`.
22207
+ *
22208
+ * Independent from {@link errors$}: existing error consumers are not notified.
22209
+ */
22210
+ get warnings$() {
22211
+ return this.deferEmission(this._warnings$.asObservable());
22212
+ }
21407
22213
  /** Platform WebRTC capabilities detected at construction time. */
21408
22214
  get platformCapabilities() {
21409
22215
  this._platformCapabilities ??= detectPlatformCapabilities(this._options.webRTCApiProvider);
@@ -21461,6 +22267,13 @@ var SignalWire = class extends Destroyable {
21461
22267
  }
21462
22268
  try {
21463
22269
  this._visibilityController = new VisibilityController();
22270
+ this.subscribeTo(this._visibilityController.visibilityChange$.pipe((0, import_cjs$1.filter)((event) => event.to === "visible")), () => {
22271
+ try {
22272
+ this._refreshCoordinator?.forceRefreshIfDue();
22273
+ } catch (error) {
22274
+ logger$1.warn("[SignalWire] Resume credential revalidation failed (non-fatal):", error);
22275
+ }
22276
+ });
21464
22277
  this.subscribeTo(this._visibilityController.visibilityChange$.pipe((0, import_cjs$1.filter)((event) => event.to === "visible" && PreferencesContainer.instance.refreshDevicesOnVisible)), () => {
21465
22278
  logger$1.debug("[SignalWire] Page visible, re-enumerating devices");
21466
22279
  try {
@@ -21472,7 +22285,7 @@ var SignalWire = class extends Destroyable {
21472
22285
  logger$1.warn("[SignalWire] Failed to initialize VisibilityController:", error);
21473
22286
  }
21474
22287
  try {
21475
- this._diagnosticsCollector = new DiagnosticsCollector({ sdkVersion: "3.30.0" });
22288
+ this._diagnosticsCollector = new DiagnosticsCollector({ sdkVersion: "4.0.0-rc.2" });
21476
22289
  } catch (error) {
21477
22290
  logger$1.warn("[SignalWire] Failed to initialize DiagnosticsCollector:", error);
21478
22291
  }
@@ -21480,14 +22293,19 @@ var SignalWire = class extends Destroyable {
21480
22293
  /**
21481
22294
  * Disconnects the WebSocket and tears down the current session.
21482
22295
  *
22296
+ * Ends the session identified by the protocol and clears its persisted
22297
+ * resume state (`authorization_state` + protocol) and attach records
22298
+ * together — a later {@link connect} with the same credentials starts a
22299
+ * fresh session and cannot reattach to the ended session's calls.
22300
+ * Credentials and device preferences are preserved. To temporarily stop
22301
+ * receiving inbound calls while keeping the session alive, use
22302
+ * `unregister()` instead.
22303
+ *
21483
22304
  * The client can be reconnected by calling {@link connect} again,
21484
22305
  * which creates a fresh transport and session.
21485
22306
  */
21486
22307
  async disconnect() {
21487
- if (this._refreshTimerId) {
21488
- clearTimeout(this._refreshTimerId);
21489
- this._refreshTimerId = void 0;
21490
- }
22308
+ this._refreshCoordinator?.suspend();
21491
22309
  this._diagnosticsCollector?.record("connection", "disconnected");
21492
22310
  await this.teardownTransportAndSession();
21493
22311
  this._isConnected$.next(false);
@@ -21538,21 +22356,23 @@ var SignalWire = class extends Destroyable {
21538
22356
  this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
21539
22357
  throw error;
21540
22358
  }
21541
- logger$1.debug("[SignalWire] Failed to register user, trying reauthentication...");
21542
- try {
21543
- await this._clientSession.reauthenticate(this._deps.credential.token);
21544
- logger$1.debug("[SignalWire] Reauthentication successful, retrying register()");
22359
+ logger$1.debug("[SignalWire] Failed to register user, attempting credential recovery...");
22360
+ let failureCause = error;
22361
+ if (await this.recoverStaleCredential(isRecoverableAuthError(error))) try {
22362
+ logger$1.debug("[SignalWire] Recovery succeeded, retrying register()");
21545
22363
  await this._transport.execute(RPCExecute({
21546
22364
  method: "subscriber.online",
21547
22365
  params: {}
21548
22366
  }));
21549
22367
  this._isRegistered$.next(true);
21550
- } catch (reauthError) {
21551
- logger$1.error("[SignalWire] Reauthentication failed during register():", reauthError);
21552
- 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 }) });
21553
- this._errors$.next(registerError);
21554
- throw registerError;
22368
+ return;
22369
+ } catch (retryError) {
22370
+ logger$1.error("[SignalWire] Register retry after recovery failed:", retryError);
22371
+ failureCause = retryError;
21555
22372
  }
22373
+ 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 }) });
22374
+ this._errors$.next(registerError);
22375
+ throw registerError;
21556
22376
  }
21557
22377
  }
21558
22378
  /**
@@ -21605,7 +22425,15 @@ var SignalWire = class extends Destroyable {
21605
22425
  };
21606
22426
  await this.waitAuthentication();
21607
22427
  logger$1.debug("[SignalWire] Dialing with options:", computed_options);
21608
- return this._clientSession.createOutboundCall(destination, computed_options);
22428
+ try {
22429
+ return await this._clientSession.createOutboundCall(destination, computed_options);
22430
+ } catch (error) {
22431
+ if (!isRecoverableAuthError(error)) throw error;
22432
+ logger$1.debug("[SignalWire] Dial hit a recoverable auth error; recovering credential and retrying once");
22433
+ if (!await this.recoverStaleCredential()) throw error;
22434
+ await this.waitAuthentication();
22435
+ return this._clientSession.createOutboundCall(destination, computed_options);
22436
+ }
21609
22437
  }
21610
22438
  /**
21611
22439
  * Runs a multi-phase connectivity test against the given destination.
@@ -21874,17 +22702,23 @@ var SignalWire = class extends Destroyable {
21874
22702
  prefs.preferredVideoInput = null;
21875
22703
  await this._deviceController.clearDeviceState();
21876
22704
  }
21877
- /** Destroys the client, clearing timers and releasing all resources. */
22705
+ /**
22706
+ * Destroys the client, clearing timers and releasing all resources.
22707
+ *
22708
+ * Intentionally destroying the client ends its session: the resume state
22709
+ * (`authorization_state` + protocol) and the attach records are both
22710
+ * cleared. Credentials and device preferences are preserved — use
22711
+ * {@link resetToDefaults} for a full wipe. To temporarily stop receiving
22712
+ * inbound calls while keeping the session alive, use `unregister()`.
22713
+ */
21878
22714
  destroy() {
21879
- if (this._refreshTimerId) {
21880
- clearTimeout(this._refreshTimerId);
21881
- this._refreshTimerId = void 0;
21882
- }
21883
- this._deviceTokenManager?.destroy();
22715
+ this._refreshCoordinator?.destroy();
22716
+ this._refreshCoordinator = void 0;
21884
22717
  this._dpopManager?.destroy();
21885
- if (this._attachManager) this._attachManager.detachAll();
21886
- this._transport.destroy();
21887
- this._clientSession.destroy();
22718
+ const session = this._clientSession;
22719
+ session?.teardownSessionState();
22720
+ this._transport?.destroy();
22721
+ session?.destroy();
21888
22722
  try {
21889
22723
  this._networkMonitor?.destroy();
21890
22724
  } catch {}
@@ -21997,7 +22831,7 @@ var StaticCredentialProvider = class {
21997
22831
  /**
21998
22832
  * Library version from package.json, injected at build time.
21999
22833
  */
22000
- const version = "3.30.0";
22834
+ const version = "4.0.0-rc.2";
22001
22835
  /**
22002
22836
  * Flag indicating the library has been loaded and is ready to use.
22003
22837
  * For UMD builds: `window.SignalWire.ready`
@@ -22019,7 +22853,7 @@ const ready = true;
22019
22853
  */
22020
22854
  const emitReadyEvent = () => {
22021
22855
  if (typeof window !== "undefined") {
22022
- const event = new CustomEvent("signalwire:js:ready", { detail: { version: "3.30.0" } });
22856
+ const event = new CustomEvent("signalwire:js:ready", { detail: { version: "4.0.0-rc.2" } });
22023
22857
  window.dispatchEvent(event);
22024
22858
  }
22025
22859
  };
@@ -22043,5 +22877,5 @@ emitReadyEvent();
22043
22877
  if (typeof process === "undefined") globalThis.process = { env: { NODE_ENV: "production" } };
22044
22878
 
22045
22879
  //#endregion
22046
- export { Address, CallCreateError, ClientPreferences, CollectionFetchError, DPoPInitError, DeviceTokenError, EmbedTokenCredentialProvider, InvalidCredentialsError, MediaTrackError, MessageParseError, OverconstrainedFallbackError, Participant, PreflightError, RecoveryError, SelfCapabilities, SelfParticipant, SignalWire, StaticCredentialProvider, TokenRefreshError, UnexpectedError, User, VertoPongError, WebRTCCall, embeddableCall, getLogger, isSelfParticipant, ready, setDebugOptions, setLogLevel, setLogger, version };
22880
+ export { Address, CallCreateError, CallNotReadyError, ClientPreferences, CollectionFetchError, DPoPInitError, DeviceTokenError, EmbedTokenCredentialProvider, InvalidCredentialsError, MediaAccessError, MediaTrackError, MessageParseError, OverconstrainedFallbackError, Participant, ParticipantNotReadyError, PreflightError, RecoveryError, SelfCapabilities, SelfParticipant, SignalWire, StaticCredentialProvider, TokenRefreshError, UnexpectedError, User, VertoPongError, WebRTCCall, embeddableCall, getLogger, isSelfParticipant, ready, setDebugOptions, setLogLevel, setLogger, version };
22047
22881
  //# sourceMappingURL=browser.mjs.map