@signalwire/js 4.0.0-dev-20260515133934 → 4.0.0-dev-20260714201720

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.
@@ -9021,6 +9021,143 @@ var Destroyable = class {
9021
9021
  }
9022
9022
  };
9023
9023
 
9024
+ //#endregion
9025
+ //#region src/core/constants.ts
9026
+ const INVITE_VERSION = 1e3;
9027
+ const DEFAULT_ICE_CANDIDATE_TIMEOUT_MS = 600;
9028
+ const DEFAULT_ICE_GATHERING_TIMEOUT_MS = 6e3;
9029
+ const DEFAULT_RECONNECT_CALLS_TIMEOUT_MS = 300 * 1e3;
9030
+ const DEFAULT_CONNECTION_TIMEOUT_MS = 1e4;
9031
+ const DEFAULT_RECONNECT_DELAY_MIN_MS = 100;
9032
+ const DEFAULT_RECONNECT_DELAY_MAX_MS = 3e3;
9033
+ const DEFAULT_DEVICE_DEBOUNCE_TIME_MS = 1500;
9034
+ const DEFAULT_DEVICE_POLLING_INTERVAL_MS = 0;
9035
+ const PREFERENCES_STORAGE_KEY = "sw:preferences";
9036
+ /** Scope value that enables automatic token refresh. */
9037
+ const SAT_REFRESH_SCOPE = "sat:refresh";
9038
+ /** API endpoints for device token operations. */
9039
+ const DEVICE_TOKEN_ENDPOINT = "/api/fabric/subscriber/devices/token";
9040
+ const DEVICE_REFRESH_ENDPOINT = "/api/fabric/subscriber/devices/refresh";
9041
+ /** Default device token TTL in seconds (15 minutes). */
9042
+ const DEVICE_TOKEN_DEFAULT_EXPIRE_IN = 900;
9043
+ /** Buffer time in milliseconds before expiry to trigger refresh. */
9044
+ const DEVICE_TOKEN_REFRESH_BUFFER_MS = 3e4;
9045
+ /** Maximum retry attempts for device token refresh on transient failure. */
9046
+ const DEVICE_TOKEN_REFRESH_MAX_RETRIES = 3;
9047
+ /** Base delay in milliseconds for exponential backoff on refresh retry. */
9048
+ const DEVICE_TOKEN_REFRESH_RETRY_BASE_MS = 1e3;
9049
+ /** Maximum retry attempts for developer credential refresh on transient failure. */
9050
+ const CREDENTIAL_REFRESH_MAX_RETRIES = 5;
9051
+ /** Base delay in milliseconds for exponential backoff on credential refresh retry. */
9052
+ const CREDENTIAL_REFRESH_RETRY_BASE_MS = 1e3;
9053
+ /** Maximum delay in milliseconds for credential refresh backoff. */
9054
+ const CREDENTIAL_REFRESH_MAX_DELAY_MS = 3e4;
9055
+ /** Buffer in milliseconds before token expiry to trigger refresh. */
9056
+ const CREDENTIAL_REFRESH_BUFFER_MS = 5e3;
9057
+ /**
9058
+ * Maximum time the coordinator will wait for `DeviceTokenManager.activate()`
9059
+ * to resolve before treating the activation as failed and falling back to
9060
+ * the developer-provided refresh path. Prevents a wedged HTTP layer from
9061
+ * leaving the session with no active refresh mechanism.
9062
+ */
9063
+ const CREDENTIAL_ACTIVATE_TIMEOUT_MS = 3e4;
9064
+ /** JSON-RPC error code for requester validation failure (corrupted auth state). */
9065
+ const RPC_ERROR_REQUESTER_VALIDATION_FAILED = -32003;
9066
+ /** JSON-RPC error code for invalid params (e.g., missing authentication block). */
9067
+ const RPC_ERROR_INVALID_PARAMS = -32602;
9068
+ /** JSON-RPC error code for authentication failure (invalid token, missing DPoP, etc.). */
9069
+ const RPC_ERROR_AUTHENTICATION_FAILED = -32002;
9070
+ /** Error names browsers use for a media permission denial (user or policy). */
9071
+ const MEDIA_ACCESS_DENIAL_NAMES = [
9072
+ "NotAllowedError",
9073
+ "SecurityError",
9074
+ "PermissionDeniedError"
9075
+ ];
9076
+ /** Default polling interval for RTCPeerConnection.getStats() in milliseconds. */
9077
+ const DEFAULT_STATS_POLLING_INTERVAL_MS = 1e3;
9078
+ /** Number of initial samples used to build a baseline for spike detection. */
9079
+ const DEFAULT_STATS_BASELINE_SAMPLES = 10;
9080
+ /** Duration in ms with no inbound audio packets before emitting a critical issue. */
9081
+ const DEFAULT_STATS_NO_PACKET_THRESHOLD_MS = 2e3;
9082
+ /** Multiplier applied to baseline RTT to detect a warning-level RTT spike. */
9083
+ const DEFAULT_STATS_RTT_SPIKE_MULTIPLIER = 3;
9084
+ /** Packet loss fraction (0-1) above which a warning is emitted. */
9085
+ const DEFAULT_STATS_PACKET_LOSS_THRESHOLD = .05;
9086
+ /** Multiplier applied to baseline jitter to detect a jitter spike. */
9087
+ const DEFAULT_STATS_JITTER_SPIKE_MULTIPLIER = 4;
9088
+ /** Number of seconds of metrics history to retain. */
9089
+ const DEFAULT_STATS_HISTORY_SIZE = 30;
9090
+ /** Maximum keyframe requests allowed within a single burst window. */
9091
+ const DEFAULT_KEYFRAME_MAX_BURST$1 = 3;
9092
+ /** Duration of the keyframe burst window in milliseconds. */
9093
+ const DEFAULT_KEYFRAME_BURST_WINDOW_MS$1 = 3e3;
9094
+ /** Cooldown period in ms after burst limit is reached before allowing more keyframes. */
9095
+ const DEFAULT_KEYFRAME_COOLDOWN_MS$1 = 1e4;
9096
+ /** Minimum time between re-INVITE attempts in milliseconds. */
9097
+ const DEFAULT_REINVITE_DEBOUNCE_TIME_MS = 1e4;
9098
+ /** Maximum number of re-INVITE attempts per call. */
9099
+ const DEFAULT_REINVITE_MAX_ATTEMPTS = 3;
9100
+ /** Timeout for a single re-INVITE attempt in milliseconds. */
9101
+ const DEFAULT_REINVITE_TIMEOUT_MS = 5e3;
9102
+ /** Debounce window in ms to collapse multiple detection signals into one trigger. */
9103
+ const DEFAULT_RECOVERY_DEBOUNCE_TIME_MS = 2e3;
9104
+ /** Cooldown period in ms between recovery attempts. */
9105
+ const DEFAULT_RECOVERY_COOLDOWN_MS = 1e4;
9106
+ /** Grace period in ms before treating ICE 'disconnected' as a failure. */
9107
+ const DEFAULT_ICE_DISCONNECTED_GRACE_PERIOD_MS = 3e3;
9108
+ /** Timeout for a single ICE restart attempt in milliseconds. */
9109
+ const DEFAULT_ICE_RESTART_TIMEOUT_MS$1 = 5e3;
9110
+ /** Maximum recovery attempts before emitting 'max_attempts_reached'. */
9111
+ const DEFAULT_MAX_RECOVERY_ATTEMPTS = 3;
9112
+ /** Upper bound in ms for waiting on iceGatheringState === 'complete' after an ICE restart. */
9113
+ const ICE_GATHERING_COMPLETE_TIMEOUT_MS = 1e4;
9114
+ /** Upper bound in ms for waiting on RTCPeerConnection.connectionState === 'connected' after a recovery ICE restart. */
9115
+ const PEER_CONNECTION_RECOVERY_WAIT_MS = 5e3;
9116
+ /** Polling interval in ms while waiting for RTCPeerConnection.connectionState to transition. */
9117
+ const PEER_CONNECTION_RECOVERY_POLL_MS = 100;
9118
+ /** Polling interval for LocalAudioPipeline.level$ (ms). ~30fps is smooth for meters. */
9119
+ const AUDIO_LEVEL_POLL_INTERVAL_MS = 33;
9120
+ /** RMS level threshold (0..1) above which the local participant is considered speaking. */
9121
+ const VAD_THRESHOLD = .03;
9122
+ /** Hold window in ms below the threshold before speaking$ flips back to false. */
9123
+ const VAD_HOLD_MS = 250;
9124
+ /** Whether to persist device selections to storage by default. */
9125
+ const DEFAULT_PERSIST_DEVICE_SELECTION = true;
9126
+ /** Whether to auto-apply device changes to active calls by default. */
9127
+ const DEFAULT_SYNC_DEVICES_TO_ACTIVE_CALLS = true;
9128
+ /** Storage keys for persisted device selections. */
9129
+ const DEVICE_STORAGE_KEY_AUDIO_INPUT = "sw:device:audioinput";
9130
+ const DEVICE_STORAGE_KEY_AUDIO_OUTPUT = "sw:device:audiooutput";
9131
+ const DEVICE_STORAGE_KEY_VIDEO_INPUT = "sw:device:videoinput";
9132
+ /** Whether to auto-mute video when the tab becomes hidden. */
9133
+ const DEFAULT_AUTO_MUTE_VIDEO_ON_HIDDEN = false;
9134
+ /** Whether to re-enumerate devices when the page becomes visible. */
9135
+ const DEFAULT_REFRESH_DEVICES_ON_VISIBLE = true;
9136
+ /** Whether to check peer connection health when the page becomes visible. */
9137
+ const DEFAULT_CHECK_CONNECTION_ON_VISIBLE = true;
9138
+ /** Whether automatic video degradation on low bandwidth is enabled. */
9139
+ const DEFAULT_ENABLE_AUTO_DEGRADATION = true;
9140
+ /** Bitrate in kbps below which video is automatically disabled. */
9141
+ const DEFAULT_DEGRADATION_BITRATE_THRESHOLD_KBPS = 150;
9142
+ /** Bitrate in kbps above which video is automatically re-enabled (hysteresis). */
9143
+ const DEFAULT_DEGRADATION_RECOVERY_THRESHOLD_KBPS = 300;
9144
+ /** Whether relay-only escalation is enabled as a last-resort recovery tier. */
9145
+ const DEFAULT_ENABLE_RELAY_FALLBACK = true;
9146
+ /** Whether to listen for browser online/offline/connection events. */
9147
+ const DEFAULT_ENABLE_NETWORK_CHANGE_DETECTION = true;
9148
+ /** Whether to intercept server-sent media-timeout hangups and attempt recovery. */
9149
+ const DEFAULT_ENABLE_SERVER_HANGUP_INTERCEPTION = true;
9150
+ /** Default video track constraints applied when video is enabled without explicit constraints. */
9151
+ const DEFAULT_VIDEO_CONSTRAINTS = {
9152
+ width: { ideal: 1280 },
9153
+ height: { ideal: 720 },
9154
+ aspectRatio: 16 / 9
9155
+ };
9156
+ /** Whether stereo Opus is enabled by default. */
9157
+ const DEFAULT_STEREO_AUDIO = false;
9158
+ /** Max average bitrate for stereo Opus in bits per second. */
9159
+ const DEFAULT_STEREO_MAX_AVERAGE_BITRATE = 51e4;
9160
+
9024
9161
  //#endregion
9025
9162
  //#region src/core/errors.ts
9026
9163
  var UnexpectedError = class extends Error {
@@ -9230,6 +9367,33 @@ var MediaTrackError = class extends Error {
9230
9367
  this.name = "MediaTrackError";
9231
9368
  }
9232
9369
  };
9370
+ /** True when a `getUserMedia`/`getDisplayMedia` rejection is a permission denial. */
9371
+ function isMediaAccessDenial(originalError) {
9372
+ return originalError instanceof Error && MEDIA_ACCESS_DENIAL_NAMES.includes(originalError.name);
9373
+ }
9374
+ /**
9375
+ * Failure to acquire local media (camera, microphone, or screen capture)
9376
+ * via `getUserMedia`/`getDisplayMedia`.
9377
+ *
9378
+ * Non-fatal by default: screenshare and additional-device failures never
9379
+ * end the call, and main-connection failures degrade to receive-only when
9380
+ * possible. The wrapping site sets `fatal` to `true` only when the call
9381
+ * cannot continue (receive-only fallback disabled or no receive intent).
9382
+ */
9383
+ var MediaAccessError = class extends Error {
9384
+ constructor(operation, media, originalError, fatal = false) {
9385
+ super(`Media access ${isMediaAccessDenial(originalError) ? "denied" : "failed"} for ${operation} (${media})`, { cause: originalError instanceof Error ? originalError : void 0 });
9386
+ this.operation = operation;
9387
+ this.media = media;
9388
+ this.originalError = originalError;
9389
+ this.fatal = fatal;
9390
+ this.name = "MediaAccessError";
9391
+ }
9392
+ /** True when the underlying failure is a permission denial (user or policy). */
9393
+ get denied() {
9394
+ return isMediaAccessDenial(this.originalError);
9395
+ }
9396
+ };
9233
9397
  var DPoPInitError = class extends Error {
9234
9398
  constructor(originalError, message = "Failed to initialize DPoP key pair") {
9235
9399
  super(message, { cause: originalError instanceof Error ? originalError : void 0 });
@@ -9470,9 +9634,9 @@ var require_loglevel = /* @__PURE__ */ __commonJSMin(((exports, module) => {
9470
9634
  defaultLogger$1 = new Logger();
9471
9635
  defaultLogger$1.getLogger = function getLogger$1(name) {
9472
9636
  if (typeof name !== "symbol" && typeof name !== "string" || name === "") throw new TypeError("You must supply a name when creating a logger.");
9473
- var logger$32 = _loggersByName[name];
9474
- if (!logger$32) logger$32 = _loggersByName[name] = new Logger(name, defaultLogger$1.methodFactory);
9475
- return logger$32;
9637
+ var logger$33 = _loggersByName[name];
9638
+ if (!logger$33) logger$33 = _loggersByName[name] = new Logger(name, defaultLogger$1.methodFactory);
9639
+ return logger$33;
9476
9640
  };
9477
9641
  var _log = typeof window !== undefinedType ? window.log : void 0;
9478
9642
  defaultLogger$1.noConflict = function() {
@@ -9508,8 +9672,8 @@ const defaultLoggerLevel = defaultLogger.levels.WARN;
9508
9672
  defaultLogger.setLevel(defaultLoggerLevel);
9509
9673
  let userLogger = null;
9510
9674
  /** Replace the built-in logger with a custom implementation. Pass `null` to restore defaults. */
9511
- const setLogger = (logger$32) => {
9512
- userLogger = logger$32;
9675
+ const setLogger = (logger$33) => {
9676
+ userLogger = logger$33;
9513
9677
  };
9514
9678
  let debugOptions = {};
9515
9679
  /** Configure debug options (e.g., `{ logWsTraffic: true }`). */
@@ -9553,8 +9717,8 @@ const wsTraffic = (options) => {
9553
9717
  loggerInstance.debug(`${options.type.toUpperCase()}: \n`, msg, "\n");
9554
9718
  };
9555
9719
  const getLogger = () => {
9556
- const logger$32 = getLoggerInstance();
9557
- return new Proxy(logger$32, { get(_target, prop, _receiver) {
9720
+ const logger$33 = getLoggerInstance();
9721
+ return new Proxy(logger$33, { get(_target, prop, _receiver) {
9558
9722
  if (prop === "wsTraffic") return wsTraffic;
9559
9723
  const instance = getLoggerInstance();
9560
9724
  const value = Reflect.get(instance, prop);
@@ -9606,7 +9770,7 @@ const asyncRetry = async ({ asyncCallable, maxRetries: retries = DEFAULT_MAX_RET
9606
9770
 
9607
9771
  //#endregion
9608
9772
  //#region src/controllers/HTTPRequestController.ts
9609
- const logger$31 = getLogger();
9773
+ const logger$32 = getLogger();
9610
9774
  const GET_PARAMS = {
9611
9775
  method: "GET",
9612
9776
  headers: { Accept: "application/json" }
@@ -9670,7 +9834,7 @@ var HTTPRequestController = class HTTPRequestController extends Destroyable {
9670
9834
  this._responses$.next(response);
9671
9835
  return response;
9672
9836
  } catch (error) {
9673
- logger$31.error("[HTTPRequestController] Request error:", error);
9837
+ logger$32.error("[HTTPRequestController] Request error:", error);
9674
9838
  this._status$.next("error");
9675
9839
  const err = error instanceof Error ? error : new Error("HTTP request failed", { cause: error });
9676
9840
  this._errors$.next(err);
@@ -9697,7 +9861,7 @@ var HTTPRequestController = class HTTPRequestController extends Destroyable {
9697
9861
  const url = this.buildURL(request.url);
9698
9862
  const headers = this.buildHeaders(request.headers);
9699
9863
  const timeout$5 = request.timeout ?? this.requestTimeout;
9700
- logger$31.debug("[HTTPRequestController] Executing request:", {
9864
+ logger$32.debug("[HTTPRequestController] Executing request:", {
9701
9865
  method: request.method,
9702
9866
  url,
9703
9867
  headers: Object.keys(headers).reduce((acc, key) => {
@@ -9717,7 +9881,7 @@ var HTTPRequestController = class HTTPRequestController extends Destroyable {
9717
9881
  });
9718
9882
  clearTimeout(timeoutId);
9719
9883
  const httpResponse = await this.convertResponse(response);
9720
- logger$31.debug("[HTTPRequestController] Response received:", {
9884
+ logger$32.debug("[HTTPRequestController] Response received:", {
9721
9885
  status: response.status,
9722
9886
  statusText: response.statusText,
9723
9887
  headers: [...response.headers.entries()],
@@ -9727,7 +9891,7 @@ var HTTPRequestController = class HTTPRequestController extends Destroyable {
9727
9891
  } catch (error) {
9728
9892
  clearTimeout(timeoutId);
9729
9893
  if (error instanceof Error && error.name === "AbortError") throw new RequestTimeoutError(`Request timeout after ${timeout$5}ms`, { cause: error });
9730
- logger$31.error("[HTTPRequestController] Request failed:", error);
9894
+ logger$32.error("[HTTPRequestController] Request failed:", error);
9731
9895
  throw error;
9732
9896
  }
9733
9897
  }
@@ -9741,8 +9905,8 @@ var HTTPRequestController = class HTTPRequestController extends Destroyable {
9741
9905
  const credential = this.getCredential();
9742
9906
  if (credential.token) {
9743
9907
  headers.Authorization = `Bearer ${credential.token}`;
9744
- logger$31.debug("[HTTPRequestController] Using Bearer token auth, token length:", credential.token.length);
9745
- } else logger$31.warn("[HTTPRequestController] No credentials available for authentication");
9908
+ logger$32.debug("[HTTPRequestController] Using Bearer token auth, token length:", credential.token.length);
9909
+ } else logger$32.warn("[HTTPRequestController] No credentials available for authentication");
9746
9910
  return headers;
9747
9911
  }
9748
9912
  /**
@@ -9879,130 +10043,6 @@ var DeviceHistoryManager = class {
9879
10043
  }
9880
10044
  };
9881
10045
 
9882
- //#endregion
9883
- //#region src/core/constants.ts
9884
- const INVITE_VERSION = 1e3;
9885
- const DEFAULT_ICE_CANDIDATE_TIMEOUT_MS = 600;
9886
- const DEFAULT_ICE_GATHERING_TIMEOUT_MS = 6e3;
9887
- const DEFAULT_RECONNECT_CALLS_TIMEOUT_MS = 300 * 1e3;
9888
- const DEFAULT_CONNECTION_TIMEOUT_MS = 1e4;
9889
- const DEFAULT_RECONNECT_DELAY_MIN_MS = 100;
9890
- const DEFAULT_RECONNECT_DELAY_MAX_MS = 3e3;
9891
- const DEFAULT_DEVICE_DEBOUNCE_TIME_MS = 1500;
9892
- const DEFAULT_DEVICE_POLLING_INTERVAL_MS = 0;
9893
- const PREFERENCES_STORAGE_KEY = "sw:preferences";
9894
- /** Scope value that enables automatic token refresh. */
9895
- const SAT_REFRESH_SCOPE = "sat:refresh";
9896
- /** API endpoints for device token operations. */
9897
- const DEVICE_TOKEN_ENDPOINT = "/api/fabric/subscriber/devices/token";
9898
- const DEVICE_REFRESH_ENDPOINT = "/api/fabric/subscriber/devices/refresh";
9899
- /** Default device token TTL in seconds (15 minutes). */
9900
- const DEVICE_TOKEN_DEFAULT_EXPIRE_IN = 900;
9901
- /** Buffer time in milliseconds before expiry to trigger refresh. */
9902
- const DEVICE_TOKEN_REFRESH_BUFFER_MS = 3e4;
9903
- /** Maximum retry attempts for device token refresh on transient failure. */
9904
- const DEVICE_TOKEN_REFRESH_MAX_RETRIES = 3;
9905
- /** Base delay in milliseconds for exponential backoff on refresh retry. */
9906
- const DEVICE_TOKEN_REFRESH_RETRY_BASE_MS = 1e3;
9907
- /** Maximum retry attempts for developer credential refresh on transient failure. */
9908
- const CREDENTIAL_REFRESH_MAX_RETRIES = 5;
9909
- /** Base delay in milliseconds for exponential backoff on credential refresh retry. */
9910
- const CREDENTIAL_REFRESH_RETRY_BASE_MS = 1e3;
9911
- /** Maximum delay in milliseconds for credential refresh backoff. */
9912
- const CREDENTIAL_REFRESH_MAX_DELAY_MS = 3e4;
9913
- /** Buffer in milliseconds before token expiry to trigger refresh. */
9914
- const CREDENTIAL_REFRESH_BUFFER_MS = 5e3;
9915
- /** JSON-RPC error code for requester validation failure (corrupted auth state). */
9916
- const RPC_ERROR_REQUESTER_VALIDATION_FAILED = -32003;
9917
- /** JSON-RPC error code for invalid params (e.g., missing authentication block). */
9918
- const RPC_ERROR_INVALID_PARAMS = -32602;
9919
- /** JSON-RPC error code for authentication failure (invalid token, missing DPoP, etc.). */
9920
- const RPC_ERROR_AUTHENTICATION_FAILED = -32002;
9921
- /** Default polling interval for RTCPeerConnection.getStats() in milliseconds. */
9922
- const DEFAULT_STATS_POLLING_INTERVAL_MS = 1e3;
9923
- /** Number of initial samples used to build a baseline for spike detection. */
9924
- const DEFAULT_STATS_BASELINE_SAMPLES = 10;
9925
- /** Duration in ms with no inbound audio packets before emitting a critical issue. */
9926
- const DEFAULT_STATS_NO_PACKET_THRESHOLD_MS = 2e3;
9927
- /** Multiplier applied to baseline RTT to detect a warning-level RTT spike. */
9928
- const DEFAULT_STATS_RTT_SPIKE_MULTIPLIER = 3;
9929
- /** Packet loss fraction (0-1) above which a warning is emitted. */
9930
- const DEFAULT_STATS_PACKET_LOSS_THRESHOLD = .05;
9931
- /** Multiplier applied to baseline jitter to detect a jitter spike. */
9932
- const DEFAULT_STATS_JITTER_SPIKE_MULTIPLIER = 4;
9933
- /** Number of seconds of metrics history to retain. */
9934
- const DEFAULT_STATS_HISTORY_SIZE = 30;
9935
- /** Maximum keyframe requests allowed within a single burst window. */
9936
- const DEFAULT_KEYFRAME_MAX_BURST$1 = 3;
9937
- /** Duration of the keyframe burst window in milliseconds. */
9938
- const DEFAULT_KEYFRAME_BURST_WINDOW_MS$1 = 3e3;
9939
- /** Cooldown period in ms after burst limit is reached before allowing more keyframes. */
9940
- const DEFAULT_KEYFRAME_COOLDOWN_MS$1 = 1e4;
9941
- /** Minimum time between re-INVITE attempts in milliseconds. */
9942
- const DEFAULT_REINVITE_DEBOUNCE_TIME_MS = 1e4;
9943
- /** Maximum number of re-INVITE attempts per call. */
9944
- const DEFAULT_REINVITE_MAX_ATTEMPTS = 3;
9945
- /** Timeout for a single re-INVITE attempt in milliseconds. */
9946
- const DEFAULT_REINVITE_TIMEOUT_MS = 5e3;
9947
- /** Debounce window in ms to collapse multiple detection signals into one trigger. */
9948
- const DEFAULT_RECOVERY_DEBOUNCE_TIME_MS = 2e3;
9949
- /** Cooldown period in ms between recovery attempts. */
9950
- const DEFAULT_RECOVERY_COOLDOWN_MS = 1e4;
9951
- /** Grace period in ms before treating ICE 'disconnected' as a failure. */
9952
- const DEFAULT_ICE_DISCONNECTED_GRACE_PERIOD_MS = 3e3;
9953
- /** Timeout for a single ICE restart attempt in milliseconds. */
9954
- const DEFAULT_ICE_RESTART_TIMEOUT_MS$1 = 5e3;
9955
- /** Maximum recovery attempts before emitting 'max_attempts_reached'. */
9956
- const DEFAULT_MAX_RECOVERY_ATTEMPTS = 3;
9957
- /** Upper bound in ms for waiting on iceGatheringState === 'complete' after an ICE restart. */
9958
- const ICE_GATHERING_COMPLETE_TIMEOUT_MS = 1e4;
9959
- /** Upper bound in ms for waiting on RTCPeerConnection.connectionState === 'connected' after a recovery ICE restart. */
9960
- const PEER_CONNECTION_RECOVERY_WAIT_MS = 5e3;
9961
- /** Polling interval in ms while waiting for RTCPeerConnection.connectionState to transition. */
9962
- const PEER_CONNECTION_RECOVERY_POLL_MS = 100;
9963
- /** Polling interval for LocalAudioPipeline.level$ (ms). ~30fps is smooth for meters. */
9964
- const AUDIO_LEVEL_POLL_INTERVAL_MS = 33;
9965
- /** RMS level threshold (0..1) above which the local participant is considered speaking. */
9966
- const VAD_THRESHOLD = .03;
9967
- /** Hold window in ms below the threshold before speaking$ flips back to false. */
9968
- const VAD_HOLD_MS = 250;
9969
- /** Whether to persist device selections to storage by default. */
9970
- const DEFAULT_PERSIST_DEVICE_SELECTION = true;
9971
- /** Whether to auto-apply device changes to active calls by default. */
9972
- const DEFAULT_SYNC_DEVICES_TO_ACTIVE_CALLS = true;
9973
- /** Storage keys for persisted device selections. */
9974
- const DEVICE_STORAGE_KEY_AUDIO_INPUT = "sw:device:audioinput";
9975
- const DEVICE_STORAGE_KEY_AUDIO_OUTPUT = "sw:device:audiooutput";
9976
- const DEVICE_STORAGE_KEY_VIDEO_INPUT = "sw:device:videoinput";
9977
- /** Whether to auto-mute video when the tab becomes hidden. */
9978
- const DEFAULT_AUTO_MUTE_VIDEO_ON_HIDDEN = false;
9979
- /** Whether to re-enumerate devices when the page becomes visible. */
9980
- const DEFAULT_REFRESH_DEVICES_ON_VISIBLE = true;
9981
- /** Whether to check peer connection health when the page becomes visible. */
9982
- const DEFAULT_CHECK_CONNECTION_ON_VISIBLE = true;
9983
- /** Whether automatic video degradation on low bandwidth is enabled. */
9984
- const DEFAULT_ENABLE_AUTO_DEGRADATION = true;
9985
- /** Bitrate in kbps below which video is automatically disabled. */
9986
- const DEFAULT_DEGRADATION_BITRATE_THRESHOLD_KBPS = 150;
9987
- /** Bitrate in kbps above which video is automatically re-enabled (hysteresis). */
9988
- const DEFAULT_DEGRADATION_RECOVERY_THRESHOLD_KBPS = 300;
9989
- /** Whether relay-only escalation is enabled as a last-resort recovery tier. */
9990
- const DEFAULT_ENABLE_RELAY_FALLBACK = true;
9991
- /** Whether to listen for browser online/offline/connection events. */
9992
- const DEFAULT_ENABLE_NETWORK_CHANGE_DETECTION = true;
9993
- /** Whether to intercept server-sent media-timeout hangups and attempt recovery. */
9994
- const DEFAULT_ENABLE_SERVER_HANGUP_INTERCEPTION = true;
9995
- /** Default video track constraints applied when video is enabled without explicit constraints. */
9996
- const DEFAULT_VIDEO_CONSTRAINTS = {
9997
- width: { ideal: 1280 },
9998
- height: { ideal: 720 },
9999
- aspectRatio: 16 / 9
10000
- };
10001
- /** Whether stereo Opus is enabled by default. */
10002
- const DEFAULT_STEREO_AUDIO = false;
10003
- /** Max average bitrate for stereo Opus in bits per second. */
10004
- const DEFAULT_STEREO_MAX_AVERAGE_BITRATE = 51e4;
10005
-
10006
10046
  //#endregion
10007
10047
  //#region src/utils/time.ts
10008
10048
  function fromSecToMs(seconds) {
@@ -10014,7 +10054,7 @@ function fromMsToSec(milliseconds) {
10014
10054
 
10015
10055
  //#endregion
10016
10056
  //#region src/containers/PreferencesContainer.ts
10017
- const logger$30 = getLogger();
10057
+ const logger$31 = getLogger();
10018
10058
  var PreferencesContainer = class PreferencesContainer {
10019
10059
  static get instance() {
10020
10060
  this._instance ??= new PreferencesContainer();
@@ -10676,7 +10716,7 @@ var ClientPreferences = class {
10676
10716
  if (!this._storage) return;
10677
10717
  const data = collectStoredPreferences();
10678
10718
  this._storage.setItem(PREFERENCES_STORAGE_KEY, data, "local").catch((error) => {
10679
- logger$30.error(`[ClientPreferences] Failed to save preferences: ${String(error)}`);
10719
+ logger$31.error(`[ClientPreferences] Failed to save preferences: ${String(error)}`);
10680
10720
  });
10681
10721
  }
10682
10722
  /** Loads preferences from storage and applies them to the container. */
@@ -10685,7 +10725,7 @@ var ClientPreferences = class {
10685
10725
  this._storage.getItem(PREFERENCES_STORAGE_KEY, "local").then((stored) => {
10686
10726
  if (stored) applyStoredPreferences(stored);
10687
10727
  }).catch((error) => {
10688
- logger$30.error(`[ClientPreferences] Failed to load preferences: ${String(error)}`);
10728
+ logger$31.error(`[ClientPreferences] Failed to load preferences: ${String(error)}`);
10689
10729
  });
10690
10730
  }
10691
10731
  };
@@ -10707,7 +10747,7 @@ function toError(value) {
10707
10747
  //#endregion
10708
10748
  //#region src/controllers/NavigatorDeviceController.ts
10709
10749
  var import_cjs$29 = require_cjs();
10710
- const logger$29 = getLogger();
10750
+ const logger$30 = getLogger();
10711
10751
  /** Maps a device kind to its storage key. */
10712
10752
  const DEVICE_STORAGE_KEYS = {
10713
10753
  audioinput: DEVICE_STORAGE_KEY_AUDIO_INPUT,
@@ -10729,7 +10769,7 @@ var NavigatorDeviceController = class extends Destroyable {
10729
10769
  super();
10730
10770
  this.webRTCApiProvider = webRTCApiProvider;
10731
10771
  this.deviceChangeHandler = () => {
10732
- logger$29.debug("[DeviceController] Device change detected");
10772
+ logger$30.debug("[DeviceController] Device change detected");
10733
10773
  this.enumerateDevices();
10734
10774
  };
10735
10775
  this._devicesState$ = this.createBehaviorSubject(initialDevicesState);
@@ -10794,13 +10834,13 @@ var NavigatorDeviceController = class extends Destroyable {
10794
10834
  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$)));
10795
10835
  }
10796
10836
  get selectedAudioInputDevice$() {
10797
- 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))));
10837
+ 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))));
10798
10838
  }
10799
10839
  get selectedAudioOutputDevice$() {
10800
- 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))));
10840
+ 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))));
10801
10841
  }
10802
10842
  get selectedVideoInputDevice$() {
10803
- 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))));
10843
+ 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))));
10804
10844
  }
10805
10845
  get selectedAudioInputDevice() {
10806
10846
  if (this._audioInputDisabled$.value) return null;
@@ -10875,7 +10915,7 @@ var NavigatorDeviceController = class extends Destroyable {
10875
10915
  if (device) this.persistDeviceSelection("audioinput", device);
10876
10916
  }
10877
10917
  selectVideoInputDevice(device) {
10878
- logger$29.debug("[DeviceController] Setting selected video input device:", device);
10918
+ logger$30.debug("[DeviceController] Setting selected video input device:", device);
10879
10919
  if (this._videoInputDisabled$.value && device) this._videoInputDisabled$.next(false);
10880
10920
  const previous = this._selectedDevicesState$.value.videoinput;
10881
10921
  if (previous && previous.deviceId !== device?.deviceId) this._deviceHistory.push("videoinput", previous);
@@ -10932,7 +10972,7 @@ var NavigatorDeviceController = class extends Destroyable {
10932
10972
  }
10933
10973
  const fromHistory = this._deviceHistory.findInHistory(kind, devices);
10934
10974
  if (fromHistory) {
10935
- logger$29.debug(`[DeviceController] Device disappeared, falling back to history: ${fromHistory.label}`);
10975
+ logger$30.debug(`[DeviceController] Device disappeared, falling back to history: ${fromHistory.label}`);
10936
10976
  this.emitDeviceRecovered(kind, selected, fromHistory, "device_disconnected");
10937
10977
  return fromHistory;
10938
10978
  }
@@ -10985,7 +11025,7 @@ var NavigatorDeviceController = class extends Destroyable {
10985
11025
  try {
10986
11026
  await this._storageManager.setItem(DEVICE_STORAGE_KEYS[kind], stored, "local");
10987
11027
  } catch (error) {
10988
- logger$29.error(`[DeviceController] Failed to persist device selection for ${kind}:`, error);
11028
+ logger$30.error(`[DeviceController] Failed to persist device selection for ${kind}:`, error);
10989
11029
  }
10990
11030
  }
10991
11031
  async loadPersistedDevices() {
@@ -11001,7 +11041,7 @@ var NavigatorDeviceController = class extends Destroyable {
11001
11041
  [kind]: stored
11002
11042
  };
11003
11043
  } catch (error) {
11004
- logger$29.error(`[DeviceController] Failed to load persisted device for ${kind}:`, error);
11044
+ logger$30.error(`[DeviceController] Failed to load persisted device for ${kind}:`, error);
11005
11045
  }
11006
11046
  }
11007
11047
  /** Clears device history, persisted selections, and re-enumerates devices. */
@@ -11019,7 +11059,7 @@ var NavigatorDeviceController = class extends Destroyable {
11019
11059
  this.disableDeviceMonitoring();
11020
11060
  this.webRTCApiProvider.mediaDevices.addEventListener("devicechange", this.deviceChangeHandler);
11021
11061
  if (PreferencesContainer.instance.devicePollingInterval > 0) this._devicesPoolingSubscription = (0, import_cjs$29.interval)(PreferencesContainer.instance.devicePollingInterval).subscribe(() => {
11022
- logger$29.debug("[DeviceController] Polling devices due to interval");
11062
+ logger$30.debug("[DeviceController] Polling devices due to interval");
11023
11063
  this.enumerateDevices();
11024
11064
  });
11025
11065
  this.enumerateDevices();
@@ -11045,13 +11085,13 @@ var NavigatorDeviceController = class extends Destroyable {
11045
11085
  videoinput: []
11046
11086
  });
11047
11087
  this._devicesState$.next(devicesByKind);
11048
- logger$29.debug("[DeviceController] Devices enumerated:", {
11088
+ logger$30.debug("[DeviceController] Devices enumerated:", {
11049
11089
  audioInputs: devicesByKind.audioinput.length,
11050
11090
  audioOutputs: devicesByKind.audiooutput.length,
11051
11091
  videoInputs: devicesByKind.videoinput.length
11052
11092
  });
11053
11093
  } catch (error) {
11054
- logger$29.error("[DeviceController] Failed to enumerate devices:", error);
11094
+ logger$30.error("[DeviceController] Failed to enumerate devices:", error);
11055
11095
  this._errors$.next(toError(error));
11056
11096
  }
11057
11097
  }
@@ -11067,7 +11107,7 @@ var NavigatorDeviceController = class extends Destroyable {
11067
11107
  stream.getTracks().forEach((t) => t.stop());
11068
11108
  return capabilities;
11069
11109
  } catch (error) {
11070
- logger$29.error("[DeviceController] Failed to get device capabilities:", error);
11110
+ logger$30.error("[DeviceController] Failed to get device capabilities:", error);
11071
11111
  this._errors$.next(toError(error));
11072
11112
  throw error;
11073
11113
  }
@@ -11318,7 +11358,7 @@ var DependencyContainer = class {
11318
11358
 
11319
11359
  //#endregion
11320
11360
  //#region src/controllers/CryptoController.ts
11321
- const logger$28 = getLogger();
11361
+ const logger$29 = getLogger();
11322
11362
  const DPOP_DB_NAME = "sw-dpop";
11323
11363
  const DPOP_DB_VERSION = 1;
11324
11364
  const DPOP_STORE_NAME = "keys";
@@ -11377,7 +11417,7 @@ async function loadKeyPairFromDB() {
11377
11417
  tx.oncomplete = () => db.close();
11378
11418
  });
11379
11419
  } catch (error) {
11380
- logger$28.warn("[DPoP] Failed to load key pair from IndexedDB:", error);
11420
+ logger$29.warn("[DPoP] Failed to load key pair from IndexedDB:", error);
11381
11421
  return null;
11382
11422
  }
11383
11423
  }
@@ -11397,7 +11437,7 @@ async function saveKeyPairToDB(keyPair) {
11397
11437
  };
11398
11438
  });
11399
11439
  } catch (error) {
11400
- logger$28.warn("[DPoP] Failed to save key pair to IndexedDB:", error);
11440
+ logger$29.warn("[DPoP] Failed to save key pair to IndexedDB:", error);
11401
11441
  }
11402
11442
  }
11403
11443
  async function deleteKeyPairFromDB() {
@@ -11416,7 +11456,7 @@ async function deleteKeyPairFromDB() {
11416
11456
  };
11417
11457
  });
11418
11458
  } catch (error) {
11419
- logger$28.warn("[DPoP] Failed to delete key pair from IndexedDB:", error);
11459
+ logger$29.warn("[DPoP] Failed to delete key pair from IndexedDB:", error);
11420
11460
  }
11421
11461
  }
11422
11462
  /**
@@ -11476,13 +11516,13 @@ var CryptoController = class {
11476
11516
  this._publicJwk = await crypto.subtle.exportKey("jwk", stored.publicKey);
11477
11517
  this._fingerprint = await computeJwkThumbprint(this._publicJwk);
11478
11518
  this._initialized = true;
11479
- logger$28.debug("[DPoP] Key pair restored from IndexedDB, fingerprint:", this._fingerprint);
11519
+ logger$29.debug("[DPoP] Key pair restored from IndexedDB, fingerprint:", this._fingerprint);
11480
11520
  return this._fingerprint;
11481
11521
  } catch (error) {
11482
- logger$28.warn("[DPoP] Stored key pair unusable, generating new one:", error);
11522
+ logger$29.warn("[DPoP] Stored key pair unusable, generating new one:", error);
11483
11523
  await deleteKeyPairFromDB();
11484
11524
  }
11485
- logger$28.debug("[DPoP] Generating RSA key pair");
11525
+ logger$29.debug("[DPoP] Generating RSA key pair");
11486
11526
  this._keyPair = await crypto.subtle.generateKey({
11487
11527
  name: "RSASSA-PKCS1-v1_5",
11488
11528
  modulusLength: 2048,
@@ -11497,7 +11537,7 @@ var CryptoController = class {
11497
11537
  this._fingerprint = await computeJwkThumbprint(this._publicJwk);
11498
11538
  this._initialized = true;
11499
11539
  await saveKeyPairToDB(this._keyPair);
11500
- logger$28.debug("[DPoP] Key pair generated and persisted, fingerprint:", this._fingerprint);
11540
+ logger$29.debug("[DPoP] Key pair generated and persisted, fingerprint:", this._fingerprint);
11501
11541
  return this._fingerprint;
11502
11542
  }
11503
11543
  /**
@@ -11563,7 +11603,7 @@ var CryptoController = class {
11563
11603
  this._fingerprint = null;
11564
11604
  this._initialized = false;
11565
11605
  deleteKeyPairFromDB();
11566
- logger$28.debug("[DPoP] Controller destroyed");
11606
+ logger$29.debug("[DPoP] Controller destroyed");
11567
11607
  }
11568
11608
  get publicJwk() {
11569
11609
  if (!this._publicJwk) throw new DPoPInitError("CryptoController not initialized. Call init() first.");
@@ -11587,7 +11627,7 @@ var CryptoController = class {
11587
11627
  //#endregion
11588
11628
  //#region src/controllers/NetworkMonitor.ts
11589
11629
  var import_cjs$28 = require_cjs();
11590
- const logger$27 = getLogger();
11630
+ const logger$28 = getLogger();
11591
11631
  /**
11592
11632
  * Safely check whether we are running in a browser environment
11593
11633
  * with `window` and the relevant event targets.
@@ -11644,7 +11684,7 @@ var NetworkMonitor = class extends Destroyable {
11644
11684
  }
11645
11685
  attachListeners() {
11646
11686
  if (!hasBrowserNetworkEvents()) {
11647
- logger$27.debug("NetworkMonitor: no browser environment detected, skipping event listeners");
11687
+ logger$28.debug("NetworkMonitor: no browser environment detected, skipping event listeners");
11648
11688
  return;
11649
11689
  }
11650
11690
  window.addEventListener("online", this._onOnline);
@@ -11652,7 +11692,7 @@ var NetworkMonitor = class extends Destroyable {
11652
11692
  const connection = getNetworkConnection();
11653
11693
  if (connection) connection.addEventListener("change", this._onConnectionChange);
11654
11694
  this._listenersAttached = true;
11655
- logger$27.debug("NetworkMonitor: event listeners attached");
11695
+ logger$28.debug("NetworkMonitor: event listeners attached");
11656
11696
  }
11657
11697
  removeListeners() {
11658
11698
  if (!this._listenersAttached) return;
@@ -11663,10 +11703,10 @@ var NetworkMonitor = class extends Destroyable {
11663
11703
  if (connection) connection.removeEventListener("change", this._onConnectionChange);
11664
11704
  }
11665
11705
  this._listenersAttached = false;
11666
- logger$27.debug("NetworkMonitor: event listeners removed");
11706
+ logger$28.debug("NetworkMonitor: event listeners removed");
11667
11707
  }
11668
11708
  handleOnline() {
11669
- logger$27.info("NetworkMonitor: browser went online");
11709
+ logger$28.info("NetworkMonitor: browser went online");
11670
11710
  this._isOnline$.next(true);
11671
11711
  this._networkChange$.next({
11672
11712
  type: "online",
@@ -11675,7 +11715,7 @@ var NetworkMonitor = class extends Destroyable {
11675
11715
  });
11676
11716
  }
11677
11717
  handleOffline() {
11678
- logger$27.info("NetworkMonitor: browser went offline");
11718
+ logger$28.info("NetworkMonitor: browser went offline");
11679
11719
  this._isOnline$.next(false);
11680
11720
  this._networkChange$.next({
11681
11721
  type: "offline",
@@ -11684,7 +11724,7 @@ var NetworkMonitor = class extends Destroyable {
11684
11724
  }
11685
11725
  handleConnectionChange() {
11686
11726
  const networkType = getNetworkType();
11687
- logger$27.info(`NetworkMonitor: connection changed — effectiveType=${networkType ?? "unknown"}`);
11727
+ logger$28.info(`NetworkMonitor: connection changed — effectiveType=${networkType ?? "unknown"}`);
11688
11728
  this._networkChange$.next({
11689
11729
  type: "connection_change",
11690
11730
  timestamp: Date.now(),
@@ -11800,7 +11840,7 @@ function getNavigatorMediaDevices() {
11800
11840
  //#endregion
11801
11841
  //#region src/controllers/PreflightRunner.ts
11802
11842
  var import_cjs$27 = require_cjs();
11803
- const logger$26 = getLogger();
11843
+ const logger$27 = getLogger();
11804
11844
  const DEFAULT_MEDIA_TEST_DURATION_S = 10;
11805
11845
  const ICE_GATHERING_TIMEOUT_MS = 1e4;
11806
11846
  const SIGNALING_RTT_TIMEOUT_MS = 5e3;
@@ -11849,7 +11889,7 @@ var PreflightRunner = class extends Destroyable {
11849
11889
  if (!this._options.skipMediaTest) try {
11850
11890
  bandwidth = await this.testMediaBandwidth(destination);
11851
11891
  } catch (error) {
11852
- logger$26.warn("[PreflightRunner] Media bandwidth test failed:", error);
11892
+ logger$27.warn("[PreflightRunner] Media bandwidth test failed:", error);
11853
11893
  warnings.push("Media bandwidth test failed");
11854
11894
  }
11855
11895
  return {
@@ -11861,7 +11901,7 @@ var PreflightRunner = class extends Destroyable {
11861
11901
  warnings
11862
11902
  };
11863
11903
  } catch (error) {
11864
- logger$26.error("[PreflightRunner] Preflight test failed:", error);
11904
+ logger$27.error("[PreflightRunner] Preflight test failed:", error);
11865
11905
  throw new PreflightError("preflight", error instanceof Error ? error : new Error(String(error)));
11866
11906
  } finally {
11867
11907
  this.destroy();
@@ -11892,7 +11932,7 @@ var PreflightRunner = class extends Destroyable {
11892
11932
  if (track.kind === "video" && track.readyState === "live") videoWorking = true;
11893
11933
  }
11894
11934
  } catch (error) {
11895
- logger$26.warn("[PreflightRunner] Device test failed:", error);
11935
+ logger$27.warn("[PreflightRunner] Device test failed:", error);
11896
11936
  } finally {
11897
11937
  if (audioStream) audioStream.getTracks().forEach((t) => t.stop());
11898
11938
  }
@@ -11950,7 +11990,7 @@ var PreflightRunner = class extends Destroyable {
11950
11990
  rttMs
11951
11991
  };
11952
11992
  } catch (error) {
11953
- logger$26.warn("[PreflightRunner] ICE connectivity test failed:", error);
11993
+ logger$27.warn("[PreflightRunner] ICE connectivity test failed:", error);
11954
11994
  return {
11955
11995
  type: "failed",
11956
11996
  turnReachable: false,
@@ -11998,7 +12038,7 @@ var PreflightRunner = class extends Destroyable {
11998
12038
  //#endregion
11999
12039
  //#region src/controllers/VisibilityController.ts
12000
12040
  var import_cjs$26 = require_cjs();
12001
- const logger$25 = getLogger();
12041
+ const logger$26 = getLogger();
12002
12042
  /**
12003
12043
  * Checks whether the document visibility API is available.
12004
12044
  */
@@ -12035,8 +12075,8 @@ var VisibilityController = class extends Destroyable {
12035
12075
  this._boundHandler = this._handleVisibilityChange.bind(this);
12036
12076
  if (this._hasVisibilityApi) {
12037
12077
  document.addEventListener("visibilitychange", this._boundHandler);
12038
- logger$25.debug("VisibilityController: listening for visibilitychange events");
12039
- } else logger$25.debug("VisibilityController: document visibility API not available, defaulting to visible");
12078
+ logger$26.debug("VisibilityController: listening for visibilitychange events");
12079
+ } else logger$26.debug("VisibilityController: document visibility API not available, defaulting to visible");
12040
12080
  }
12041
12081
  /**
12042
12082
  * Observable of the current visibility state.
@@ -12061,7 +12101,7 @@ var VisibilityController = class extends Destroyable {
12061
12101
  destroy() {
12062
12102
  if (this._hasVisibilityApi) {
12063
12103
  document.removeEventListener("visibilitychange", this._boundHandler);
12064
- logger$25.debug("VisibilityController: removed visibilitychange listener");
12104
+ logger$26.debug("VisibilityController: removed visibilitychange listener");
12065
12105
  }
12066
12106
  super.destroy();
12067
12107
  }
@@ -12079,7 +12119,7 @@ var VisibilityController = class extends Destroyable {
12079
12119
  timestamp: Date.now()
12080
12120
  };
12081
12121
  this._visibilityChange$.next(changeEvent);
12082
- logger$25.debug("VisibilityController: visibility changed", {
12122
+ logger$26.debug("VisibilityController: visibility changed", {
12083
12123
  from: previousState,
12084
12124
  to: newState
12085
12125
  });
@@ -12320,7 +12360,7 @@ const RPCEventAckResponse = (id) => makeRPCResponse({
12320
12360
 
12321
12361
  //#endregion
12322
12362
  //#region src/managers/AttachManager.ts
12323
- const logger$24 = getLogger();
12363
+ const logger$25 = getLogger();
12324
12364
  var AttachManager = class {
12325
12365
  constructor(storage, deviceController, reconnectCallsTimeout, attachKey) {
12326
12366
  this.storage = storage;
@@ -12341,7 +12381,7 @@ var AttachManager = class {
12341
12381
  try {
12342
12382
  return await this.storage.getItem(this.attachKey) ?? {};
12343
12383
  } catch (error) {
12344
- logger$24.warn("[AttachManager] Failed to retrieve attached calls from storage", error);
12384
+ logger$25.warn("[AttachManager] Failed to retrieve attached calls from storage", error);
12345
12385
  return {};
12346
12386
  }
12347
12387
  }
@@ -12349,7 +12389,7 @@ var AttachManager = class {
12349
12389
  try {
12350
12390
  await this.storage.setItem(this.attachKey, attached);
12351
12391
  } catch (error) {
12352
- logger$24.warn("[AttachManager] Failed to write attached calls to storage", error);
12392
+ logger$25.warn("[AttachManager] Failed to write attached calls to storage", error);
12353
12393
  }
12354
12394
  }
12355
12395
  /**
@@ -12368,7 +12408,7 @@ var AttachManager = class {
12368
12408
  }
12369
12409
  async attach(call) {
12370
12410
  if (!call.to) {
12371
- logger$24.warn("[AttachManager] Skip attach for calls with no destination");
12411
+ logger$25.warn("[AttachManager] Skip attach for calls with no destination");
12372
12412
  return;
12373
12413
  }
12374
12414
  const destination = call.to;
@@ -12421,15 +12461,15 @@ var AttachManager = class {
12421
12461
  callId,
12422
12462
  ...options
12423
12463
  });
12424
- logger$24.info(`[AttachManager] Reattached call ${callId} (attempt ${attempt})`);
12464
+ logger$25.info(`[AttachManager] Reattached call ${callId} (attempt ${attempt})`);
12425
12465
  succeeded = true;
12426
12466
  break;
12427
12467
  } catch (error) {
12428
- logger$24.warn(`[AttachManager] Reattach attempt ${attempt}/3 failed for call ${callId}:`, error);
12468
+ logger$25.warn(`[AttachManager] Reattach attempt ${attempt}/3 failed for call ${callId}:`, error);
12429
12469
  if (attempt < 3) await new Promise((r) => setTimeout(r, (attempt + 1) * 1e3));
12430
12470
  }
12431
12471
  if (!succeeded) {
12432
- logger$24.warn(`[AttachManager] Reattach failed after 3 attempts for call ${callId}, removing reference`);
12472
+ logger$25.warn(`[AttachManager] Reattach failed after 3 attempts for call ${callId}, removing reference`);
12433
12473
  await this.detach({
12434
12474
  id: callId,
12435
12475
  mediaDirections: attachment.mediaDirections
@@ -13366,7 +13406,9 @@ const DEFAULT_MEMBER_CAPABILITIES = {
13366
13406
  position: false,
13367
13407
  meta: false,
13368
13408
  remove: false,
13369
- audioFlags: false
13409
+ audioFlags: false,
13410
+ denoise: false,
13411
+ lowbitrate: false
13370
13412
  };
13371
13413
  /**
13372
13414
  * Default call capabilities with no permissions
@@ -13439,7 +13481,9 @@ function computeMemberCapabilities(flags, memberType) {
13439
13481
  position: hasBooleanCapability(flags, memberType, null, "position"),
13440
13482
  meta: hasBooleanCapability(flags, memberType, null, "meta"),
13441
13483
  remove: hasBooleanCapability(flags, memberType, null, "remove"),
13442
- audioFlags: hasBooleanCapability(flags, memberType, null, "audioflags")
13484
+ audioFlags: hasBooleanCapability(flags, memberType, null, "audioflags"),
13485
+ denoise: hasBooleanCapability(flags, memberType, null, "denoise"),
13486
+ lowbitrate: hasBooleanCapability(flags, memberType, null, "lowbitrate")
13443
13487
  };
13444
13488
  }
13445
13489
  /**
@@ -13596,7 +13640,7 @@ function toggleHandraiseMethod(is) {
13596
13640
 
13597
13641
  //#endregion
13598
13642
  //#region src/core/entities/Participant.ts
13599
- const logger$23 = getLogger();
13643
+ const logger$24 = getLogger();
13600
13644
  const initialState = {};
13601
13645
  /**
13602
13646
  * Represents a participant in a call.
@@ -13822,6 +13866,10 @@ var Participant = class extends Destroyable {
13822
13866
  get nodeId() {
13823
13867
  return this._state$.value.node_id;
13824
13868
  }
13869
+ /** Call ID for this participant's leg, or `undefined` if not available. */
13870
+ get callId() {
13871
+ return this._state$.value.call_id;
13872
+ }
13825
13873
  /** @internal */
13826
13874
  get value() {
13827
13875
  return this._state$.value;
@@ -13883,8 +13931,9 @@ var Participant = class extends Destroyable {
13883
13931
  noise_suppression: !this.noiseSuppression
13884
13932
  });
13885
13933
  }
13934
+ /** Toggles low-bitrate mode for this participant's media. */
13886
13935
  async toggleLowbitrate() {
13887
- throw new UnimplementedError();
13936
+ await this.executeMethod(this.id, "call.lowbitrate.set", { lowbitrate: !this.lowbitrate });
13888
13937
  }
13889
13938
  /**
13890
13939
  * Adjusts the **conference-only** microphone energy gate / sensitivity level
@@ -13931,10 +13980,27 @@ var Participant = class extends Destroyable {
13931
13980
  }
13932
13981
  /**
13933
13982
  * Sets the participant's position in the video layout.
13983
+ *
13984
+ * Requires the `member.position` capability. The gateway keys positions by the
13985
+ * **target member's own** `call_id`/`node_id` (see issue #19400 and the legacy
13986
+ * `setPositions` implementation), so this sends the participant's own call
13987
+ * context — matching {@link Participant.remove}. A resolved promise does not
13988
+ * guarantee a visible change: the backend silently returns `200` (no-op) for
13989
+ * non-conference targets.
13990
+ *
13934
13991
  * @param value - The {@link VideoPosition} to assign (e.g. `'auto'`, `'reserved-0'`).
13935
13992
  */
13936
13993
  async setPosition(value) {
13937
- await this.executeMethod(this.id, "call.member.position.set", { position: value });
13994
+ const state = this._state$.value;
13995
+ const target = {
13996
+ member_id: this.id,
13997
+ call_id: state.call_id ?? "",
13998
+ node_id: state.node_id ?? ""
13999
+ };
14000
+ await this.executeMethod(target, "call.member.position.set", { targets: [{
14001
+ target,
14002
+ position: value
14003
+ }] });
13938
14004
  }
13939
14005
  /** Removes this participant from the call. */
13940
14006
  async remove() {
@@ -14024,12 +14090,21 @@ var SelfParticipant = class extends Participant {
14024
14090
  noise_suppression: true
14025
14091
  });
14026
14092
  }
14027
- /** Starts sharing the local screen. */
14093
+ /**
14094
+ * Starts sharing the local screen.
14095
+ *
14096
+ * The call is unaffected when acquisition fails.
14097
+ *
14098
+ * @throws The raw `getDisplayMedia` error. A dismissed picker or a
14099
+ * permission denial rejects with a `NotAllowedError` `DOMException` —
14100
+ * inspect `error.name` to tell benign cancels apart from real failures.
14101
+ */
14028
14102
  async startScreenShare() {
14029
14103
  try {
14030
14104
  await this.vertoManager.addScreenMedia();
14031
14105
  } catch (error) {
14032
- logger$23.error("[Participant.startScreenShare] Screen share error:", error);
14106
+ logger$24.error("[Participant.startScreenShare] Screen share error:", error);
14107
+ throw error;
14033
14108
  }
14034
14109
  }
14035
14110
  /** Observable of the current screen share status. */
@@ -14044,12 +14119,20 @@ var SelfParticipant = class extends Participant {
14044
14119
  async stopScreenShare() {
14045
14120
  return this.vertoManager.removeScreenMedia();
14046
14121
  }
14047
- /** Adds an additional media input device to the call. */
14122
+ /**
14123
+ * Adds an additional media input device to the call.
14124
+ *
14125
+ * The call is unaffected when acquisition fails.
14126
+ *
14127
+ * @throws The raw `getUserMedia` error (e.g. `NotAllowedError` on
14128
+ * permission denial) — inspect `error.name` to decide how to react.
14129
+ */
14048
14130
  async addAdditionalDevice(options) {
14049
14131
  try {
14050
14132
  await this.vertoManager.addInputDevice(options);
14051
14133
  } catch (error) {
14052
- logger$23.error("[Participant.startScreenShare] Screen share error:", error);
14134
+ logger$24.error("[Participant.addAdditionalDevice] Additional device error:", error);
14135
+ throw error;
14053
14136
  }
14054
14137
  }
14055
14138
  /** Removes an additional media input device by ID. */
@@ -14111,7 +14194,7 @@ var SelfParticipant = class extends Participant {
14111
14194
  */
14112
14195
  exitStudioModeIfActive() {
14113
14196
  if (this._studioAudio$.value) {
14114
- logger$23.debug("[SelfParticipant] Exiting studio audio mode due to individual flag toggle");
14197
+ logger$24.debug("[SelfParticipant] Exiting studio audio mode due to individual flag toggle");
14115
14198
  this._studioAudio$.next(false);
14116
14199
  }
14117
14200
  }
@@ -14135,7 +14218,7 @@ var SelfParticipant = class extends Participant {
14135
14218
  try {
14136
14219
  await super.mute();
14137
14220
  } catch (error) {
14138
- logger$23.warn("[Participant.toggleAudioInput] Server Error while muting audio input, proceeding with local toggle anyway", error);
14221
+ logger$24.warn("[Participant.toggleAudioInput] Server Error while muting audio input, proceeding with local toggle anyway", error);
14139
14222
  } finally {
14140
14223
  this.vertoManager.muteMainAudioInputDevice();
14141
14224
  }
@@ -14145,7 +14228,7 @@ var SelfParticipant = class extends Participant {
14145
14228
  try {
14146
14229
  await super.unmute();
14147
14230
  } catch (error) {
14148
- logger$23.warn("[Participant.toggleAudioInput] Server Error while unmuting audio input, proceeding with local toggle anyway", error);
14231
+ logger$24.warn("[Participant.toggleAudioInput] Server Error while unmuting audio input, proceeding with local toggle anyway", error);
14149
14232
  } finally {
14150
14233
  await this.vertoManager.unmuteMainAudioInputDevice();
14151
14234
  }
@@ -14155,7 +14238,7 @@ var SelfParticipant = class extends Participant {
14155
14238
  try {
14156
14239
  await super.muteVideo();
14157
14240
  } catch (error) {
14158
- logger$23.warn("[Participant.toggleVideoInput] Server Error while muting video input, proceeding with local toggle anyway", error);
14241
+ logger$24.warn("[Participant.toggleVideoInput] Server Error while muting video input, proceeding with local toggle anyway", error);
14159
14242
  } finally {
14160
14243
  this.vertoManager.muteMainVideoInputDevice();
14161
14244
  }
@@ -14165,7 +14248,7 @@ var SelfParticipant = class extends Participant {
14165
14248
  try {
14166
14249
  await super.unmuteVideo();
14167
14250
  } catch (error) {
14168
- logger$23.warn("[Participant.toggleVideoInput] Server Error while unmuting video input, proceeding with local toggle anyway", error);
14251
+ logger$24.warn("[Participant.toggleVideoInput] Server Error while unmuting video input, proceeding with local toggle anyway", error);
14169
14252
  } finally {
14170
14253
  await this.vertoManager.unmuteMainVideoInputDevice();
14171
14254
  }
@@ -14370,7 +14453,7 @@ function filterAs(predicate, resultPath) {
14370
14453
  //#endregion
14371
14454
  //#region src/operators/throwOnRPCError.ts
14372
14455
  var import_cjs$21 = require_cjs();
14373
- const logger$22 = getLogger();
14456
+ const logger$23 = getLogger();
14374
14457
  /**
14375
14458
  * RxJS operator that throws a {@link JSONRPCError} when the RPC response contains an error.
14376
14459
  * Passes successful responses through unchanged.
@@ -14378,14 +14461,14 @@ const logger$22 = getLogger();
14378
14461
  function throwOnRPCError() {
14379
14462
  return (0, import_cjs$21.map)((response) => {
14380
14463
  if (response.error) {
14381
- logger$22.error("[throwOnRPCError] RPC error response:", {
14464
+ logger$23.error("[throwOnRPCError] RPC error response:", {
14382
14465
  code: response.error.code,
14383
14466
  message: response.error.message,
14384
14467
  data: response.error.data
14385
14468
  });
14386
14469
  throw new JSONRPCError(response.error.code, response.error.message, response.error.data);
14387
14470
  }
14388
- logger$22.debug("[throwOnRPCError] RPC successful response:", response);
14471
+ logger$23.debug("[throwOnRPCError] RPC successful response:", response);
14389
14472
  return response;
14390
14473
  });
14391
14474
  }
@@ -14393,7 +14476,7 @@ function throwOnRPCError() {
14393
14476
  //#endregion
14394
14477
  //#region src/managers/CallEventsManager.ts
14395
14478
  var import_cjs$20 = require_cjs();
14396
- const logger$21 = getLogger();
14479
+ const logger$22 = getLogger();
14397
14480
  const initialSessionState = {};
14398
14481
  /** @internal */
14399
14482
  var CallEventsManager = class extends Destroyable {
@@ -14497,7 +14580,7 @@ var CallEventsManager = class extends Destroyable {
14497
14580
  }
14498
14581
  initSubscriptions() {
14499
14582
  this.subscribeTo(this.callJoinedEvent$, (callJoinedEvent) => {
14500
- logger$21.debug("[CallEventsManager] Handling call.joined event for call/session IDs:", {
14583
+ logger$22.debug("[CallEventsManager] Handling call.joined event for call/session IDs:", {
14501
14584
  callId: callJoinedEvent.call_id,
14502
14585
  roomSessionId: callJoinedEvent.room_session_id
14503
14586
  });
@@ -14524,19 +14607,19 @@ var CallEventsManager = class extends Destroyable {
14524
14607
  if (this._self$.value?.capabilities.setLayout) this.updateLayouts();
14525
14608
  });
14526
14609
  this.subscribeTo(this.memberUpdates$, (member) => {
14527
- logger$21.debug("[CallEventsManager] Handling member update event for member ID:", member);
14610
+ logger$22.debug("[CallEventsManager] Handling member update event for member ID:", member);
14528
14611
  this.upsertParticipant(member);
14529
14612
  });
14530
14613
  this.subscribeTo(this.webRtcCallSession.memberLeft$, (memberLeftEvent) => {
14531
- logger$21.debug("[CallEventsManager] Handling member.left event for member ID:", memberLeftEvent.member.member_id);
14614
+ logger$22.debug("[CallEventsManager] Handling member.left event for member ID:", memberLeftEvent.member.member_id);
14532
14615
  const participants = { ...this._participants$.value };
14533
14616
  if (memberLeftEvent.member.member_id in participants) {
14534
14617
  delete participants[memberLeftEvent.member.member_id];
14535
14618
  this._participants$.next(participants);
14536
- } else logger$21.warn(`[CallEventsManager] Received member.left event for unknown member ID: ${memberLeftEvent.member.member_id}`);
14619
+ } else logger$22.warn(`[CallEventsManager] Received member.left event for unknown member ID: ${memberLeftEvent.member.member_id}`);
14537
14620
  });
14538
14621
  this.subscribeTo(this.webRtcCallSession.callUpdated$, (callUpdatedEvent) => {
14539
- logger$21.debug("[CallEventsManager] Handling call.updated event:", callUpdatedEvent);
14622
+ logger$22.debug("[CallEventsManager] Handling call.updated event:", callUpdatedEvent);
14540
14623
  const roomSession = callUpdatedEvent.room_session;
14541
14624
  this._sessionState$.next({
14542
14625
  ...this._sessionState$.value,
@@ -14551,7 +14634,7 @@ var CallEventsManager = class extends Destroyable {
14551
14634
  });
14552
14635
  });
14553
14636
  this.subscribeTo(this.layoutChangedEvent$, (layoutChangedEvent) => {
14554
- logger$21.debug("[CallEventsManager] Handling layout.changed event:", layoutChangedEvent);
14637
+ logger$22.debug("[CallEventsManager] Handling layout.changed event:", layoutChangedEvent);
14555
14638
  this._sessionState$.next({
14556
14639
  ...this._sessionState$.value,
14557
14640
  layout_name: layoutChangedEvent.id,
@@ -14561,10 +14644,10 @@ var CallEventsManager = class extends Destroyable {
14561
14644
  });
14562
14645
  }
14563
14646
  updateParticipantPositions(layoutChangedEvent) {
14564
- 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.");
14647
+ 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.");
14565
14648
  layoutChangedEvent.layers.filter((layer) => !!layer.member_id).filter((layer) => {
14566
14649
  if (!(layer.member_id in this._participants$.value)) {
14567
- logger$21.warn(`[CallEventsManager] Skipping layout layer for unknown member_id: ${layer.member_id}`);
14650
+ logger$22.warn(`[CallEventsManager] Skipping layout layer for unknown member_id: ${layer.member_id}`);
14568
14651
  return false;
14569
14652
  }
14570
14653
  return true;
@@ -14587,7 +14670,7 @@ var CallEventsManager = class extends Destroyable {
14587
14670
  layouts: response.result.layouts
14588
14671
  });
14589
14672
  }).catch((error) => {
14590
- logger$21.error("[CallEventsManager] Error fetching layouts:", error);
14673
+ logger$22.error("[CallEventsManager] Error fetching layouts:", error);
14591
14674
  });
14592
14675
  }
14593
14676
  updateParticipants(members) {
@@ -14603,7 +14686,7 @@ var CallEventsManager = class extends Destroyable {
14603
14686
  }
14604
14687
  const participant = this._participants$.value[member.member_id];
14605
14688
  const oldValue = participant.value;
14606
- logger$21.debug("[CallEventsManager] Updating participant:", member.member_id, {
14689
+ logger$22.debug("[CallEventsManager] Updating participant:", member.member_id, {
14607
14690
  oldValue,
14608
14691
  newValue: member
14609
14692
  });
@@ -14616,17 +14699,17 @@ var CallEventsManager = class extends Destroyable {
14616
14699
  }
14617
14700
  get callJoinedEvent$() {
14618
14701
  return this.cachedObservable("callJoinedEvent$", () => this.webRtcCallSession.callEvent$.pipe((0, import_cjs$20.filter)(isCallJoinedPayload), (0, import_cjs$20.tap)((event) => {
14619
- logger$21.debug("[CallEventsManager] Call joined event:", event);
14702
+ logger$22.debug("[CallEventsManager] Call joined event:", event);
14620
14703
  })));
14621
14704
  }
14622
14705
  get layoutChangedEvent$() {
14623
14706
  return this.cachedObservable("layoutChangedEvent$", () => this.webRtcCallSession.callEvent$.pipe(filterAs(isLayoutChangedPayload, "layout"), (0, import_cjs$20.tap)((event) => {
14624
- logger$21.debug("[CallEventsManager] Layout changed event:", event);
14707
+ logger$22.debug("[CallEventsManager] Layout changed event:", event);
14625
14708
  })));
14626
14709
  }
14627
14710
  get memberUpdates$() {
14628
14711
  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) => {
14629
- logger$21.debug("[CallEventsManager] Member update event:", event);
14712
+ logger$22.debug("[CallEventsManager] Member update event:", event);
14630
14713
  })));
14631
14714
  }
14632
14715
  destroy() {
@@ -14883,7 +14966,7 @@ function appendStereoParams(fmtpLine, maxBitrate) {
14883
14966
  //#endregion
14884
14967
  //#region src/controllers/ICEGatheringController.ts
14885
14968
  var import_cjs$19 = require_cjs();
14886
- const logger$20 = getLogger();
14969
+ const logger$21 = getLogger();
14887
14970
  var ICEGatheringController = class extends Destroyable {
14888
14971
  constructor(peerConnection, peerConnectionControllerNegotiating$, options = {}) {
14889
14972
  super();
@@ -14891,23 +14974,23 @@ var ICEGatheringController = class extends Destroyable {
14891
14974
  this.peerConnectionControllerNegotiating$ = peerConnectionControllerNegotiating$;
14892
14975
  this.onicegatheringstatechangeHandler = () => {
14893
14976
  const { iceGatheringState } = this.peerConnection;
14894
- logger$20.debug(`[ICEGatheringController] ICE gathering state changed to: ${iceGatheringState}`);
14977
+ logger$21.debug(`[ICEGatheringController] ICE gathering state changed to: ${iceGatheringState}`);
14895
14978
  if (iceGatheringState === "gathering") this._iceCandidatesState.next({
14896
14979
  state: "gathering",
14897
14980
  validSDP: false
14898
14981
  });
14899
14982
  };
14900
14983
  this.onicecandidateHandler = (event) => {
14901
- logger$20.debug("[ICEGatheringController] ICE candidate event received:", event.candidate);
14984
+ logger$21.debug("[ICEGatheringController] ICE candidate event received:", event.candidate);
14902
14985
  this.removeTimer("iceCandidateTimer");
14903
14986
  if (event.candidate) this.iceCandidateTimer = setTimeout(() => {
14904
14987
  if (this.peerConnection.iceGatheringState !== "complete") {
14905
- logger$20.warn("[ICEGatheringController] ICE candidate timeout, using current SDP");
14988
+ logger$21.warn("[ICEGatheringController] ICE candidate timeout, using current SDP");
14906
14989
  this.handleICECandidateTimeout();
14907
14990
  }
14908
14991
  }, this.iceCandidateTimeout);
14909
14992
  else {
14910
- logger$20.debug("[ICEGatheringController] ICE gathering completed: null candidate received");
14993
+ logger$21.debug("[ICEGatheringController] ICE gathering completed: null candidate received");
14911
14994
  this.removeTimer("iceGatheringTimer");
14912
14995
  this.handleICEGatheringComplete();
14913
14996
  }
@@ -14925,7 +15008,7 @@ var ICEGatheringController = class extends Destroyable {
14925
15008
  this.setupEventListeners();
14926
15009
  this.iceGatheringTimer = setTimeout(() => {
14927
15010
  if (this.peerConnection.iceGatheringState !== "complete") {
14928
- logger$20.warn("[ICEGatheringController] ICE gathering timeout, using current SDP");
15011
+ logger$21.warn("[ICEGatheringController] ICE gathering timeout, using current SDP");
14929
15012
  this.handleICEGatheringTimeout();
14930
15013
  }
14931
15014
  }, this.iceGatheringTimeout);
@@ -14952,9 +15035,9 @@ var ICEGatheringController = class extends Destroyable {
14952
15035
  this.relayOnly = value;
14953
15036
  }
14954
15037
  handleICEGatheringComplete() {
14955
- logger$20.debug("[ICEGatheringController] Handling ICE gathering complete");
14956
- logger$20.debug(`[ICEGatheringController] Checking ICE gathering state: ${this.peerConnection.iceGatheringState}`);
14957
- logger$20.debug("[ICEGatheringController] ICE gathering complete");
15038
+ logger$21.debug("[ICEGatheringController] Handling ICE gathering complete");
15039
+ logger$21.debug(`[ICEGatheringController] Checking ICE gathering state: ${this.peerConnection.iceGatheringState}`);
15040
+ logger$21.debug("[ICEGatheringController] ICE gathering complete");
14958
15041
  this._iceCandidatesState.next({
14959
15042
  state: "complete",
14960
15043
  validSDP: this.hasValidLocalDescriptionSDP
@@ -14970,21 +15053,21 @@ var ICEGatheringController = class extends Destroyable {
14970
15053
  this.removeTimer("iceGatheringTimer");
14971
15054
  const validSDP = this.hasValidLocalDescriptionSDP;
14972
15055
  if (validSDP) {
14973
- logger$20.debug("[ICEGatheringController] Local SDP is valid");
15056
+ logger$21.debug("[ICEGatheringController] Local SDP is valid");
14974
15057
  this._iceCandidatesState.next({
14975
15058
  state: "timeout",
14976
15059
  validSDP
14977
15060
  });
14978
15061
  this.stopGathering();
14979
- } else logger$20.debug("### ICE gathering timeout\n", this.peerConnection.localDescription?.sdp);
15062
+ } else logger$21.debug("### ICE gathering timeout\n", this.peerConnection.localDescription?.sdp);
14980
15063
  }
14981
15064
  handleICECandidateTimeout() {
14982
15065
  if (this.iceCandidateTimer) this.removeTimer("iceCandidateTimer");
14983
- logger$20.warn("[ICEGatheringController] ICE candidate timeout");
15066
+ logger$21.warn("[ICEGatheringController] ICE candidate timeout");
14984
15067
  const validSDP = this.hasValidLocalDescriptionSDP;
14985
15068
  if (!validSDP && !this.relayOnly) this.restartICEGatheringWithRelayOnly();
14986
15069
  else {
14987
- logger$20.debug("[ICEGatheringController] Using current SDP due to ICE candidate timeout");
15070
+ logger$21.debug("[ICEGatheringController] Using current SDP due to ICE candidate timeout");
14988
15071
  this._iceCandidatesState.next({
14989
15072
  state: "timeout",
14990
15073
  validSDP
@@ -14993,7 +15076,7 @@ var ICEGatheringController = class extends Destroyable {
14993
15076
  }
14994
15077
  }
14995
15078
  restartICEGatheringWithRelayOnly() {
14996
- logger$20.debug("[ICEGatheringController] Restarting ICE gathering with relay-only candidates");
15079
+ logger$21.debug("[ICEGatheringController] Restarting ICE gathering with relay-only candidates");
14997
15080
  this.relayOnly = true;
14998
15081
  this.peerConnection.setConfiguration({
14999
15082
  ...this.peerConnection.getConfiguration(),
@@ -15008,7 +15091,7 @@ var ICEGatheringController = class extends Destroyable {
15008
15091
  }
15009
15092
  }
15010
15093
  clearAllTimers() {
15011
- logger$20.debug("[ICEGatheringController] Clearing all timers");
15094
+ logger$21.debug("[ICEGatheringController] Clearing all timers");
15012
15095
  this.removeTimer("iceGatheringTimer");
15013
15096
  this.removeTimer("iceCandidateTimer");
15014
15097
  }
@@ -15017,7 +15100,7 @@ var ICEGatheringController = class extends Destroyable {
15017
15100
  this.peerConnection.removeEventListener("icecandidate", this.onicecandidateHandler);
15018
15101
  }
15019
15102
  destroy() {
15020
- logger$20.debug("[ICEGatheringController] Destroying ICEGatheringController");
15103
+ logger$21.debug("[ICEGatheringController] Destroying ICEGatheringController");
15021
15104
  this.clearAllTimers();
15022
15105
  this.removeEventListeners();
15023
15106
  super.destroy();
@@ -15027,7 +15110,7 @@ var ICEGatheringController = class extends Destroyable {
15027
15110
  //#endregion
15028
15111
  //#region src/controllers/LocalAudioPipeline.ts
15029
15112
  var import_cjs$18 = require_cjs();
15030
- const logger$19 = getLogger();
15113
+ const logger$20 = getLogger();
15031
15114
  /**
15032
15115
  * Web Audio pipeline for the local microphone stream.
15033
15116
  *
@@ -15139,7 +15222,7 @@ var LocalAudioPipeline = class extends Destroyable {
15139
15222
  try {
15140
15223
  this._inputSource.disconnect();
15141
15224
  } catch (error) {
15142
- logger$19.debug("[LocalAudioPipeline] input disconnect warning:", error);
15225
+ logger$20.debug("[LocalAudioPipeline] input disconnect warning:", error);
15143
15226
  }
15144
15227
  this._inputSource = null;
15145
15228
  }
@@ -15149,7 +15232,7 @@ var LocalAudioPipeline = class extends Destroyable {
15149
15232
  this._inputSource = this._audioContext.createMediaStreamSource(this._inputStream);
15150
15233
  this._inputSource.connect(this._gainNode);
15151
15234
  if (this._audioContext.state === "suspended") this._audioContext.resume().catch((error) => {
15152
- logger$19.warn("[LocalAudioPipeline] AudioContext resume failed:", error);
15235
+ logger$20.warn("[LocalAudioPipeline] AudioContext resume failed:", error);
15153
15236
  });
15154
15237
  }
15155
15238
  destroy() {
@@ -15164,7 +15247,7 @@ var LocalAudioPipeline = class extends Destroyable {
15164
15247
  this._analyser.disconnect();
15165
15248
  } catch {}
15166
15249
  this._audioContext.close().catch((error) => {
15167
- logger$19.debug("[LocalAudioPipeline] audio context close warning:", error);
15250
+ logger$20.debug("[LocalAudioPipeline] audio context close warning:", error);
15168
15251
  });
15169
15252
  super.destroy();
15170
15253
  }
@@ -15191,7 +15274,7 @@ var LocalAudioPipeline = class extends Destroyable {
15191
15274
  //#endregion
15192
15275
  //#region src/controllers/LocalStreamController.ts
15193
15276
  var import_cjs$17 = require_cjs();
15194
- const logger$18 = getLogger();
15277
+ const logger$19 = getLogger();
15195
15278
  var LocalStreamController = class extends Destroyable {
15196
15279
  constructor(options) {
15197
15280
  super();
@@ -15229,26 +15312,26 @@ var LocalStreamController = class extends Destroyable {
15229
15312
  * Build the local media stream based on the provided options.
15230
15313
  */
15231
15314
  async buildLocalStream() {
15232
- logger$18.debug("[LocalStreamController] Building local media stream.");
15315
+ logger$19.debug("[LocalStreamController] Building local media stream.");
15233
15316
  let stream;
15234
15317
  if (this.options.inputAudioStream ?? this.options.inputVideoStream) {
15235
15318
  const tracks = [...this.options.inputAudioStream?.getTracks() ?? [], ...this.options.inputVideoStream?.getTracks() ?? []];
15236
15319
  stream = new MediaStream(tracks);
15237
15320
  } else if (this.options.propose === "screenshare") {
15238
- logger$18.debug("[LocalStreamController] Requesting display media for screen sharing with audio:", Boolean(this.options.inputAudioDeviceConstraints));
15321
+ logger$19.debug("[LocalStreamController] Requesting display media for screen sharing with audio:", Boolean(this.options.inputAudioDeviceConstraints));
15239
15322
  stream = await this.options.getDisplayMedia({
15240
15323
  video: true,
15241
15324
  audio: Boolean(this.options.inputAudioDeviceConstraints)
15242
15325
  });
15243
- logger$18.debug("[LocalStreamController] Screen share media obtained:", stream);
15326
+ logger$19.debug("[LocalStreamController] Screen share media obtained:", stream);
15244
15327
  } else {
15245
15328
  const constraints = {
15246
15329
  audio: this.options.inputAudioDeviceConstraints,
15247
15330
  video: this.options.inputVideoDeviceConstraints
15248
15331
  };
15249
- logger$18.debug("[LocalStreamController] Requesting user media with constraints:", constraints);
15332
+ logger$19.debug("[LocalStreamController] Requesting user media with constraints:", constraints);
15250
15333
  stream = await this.options.getUserMedia(constraints);
15251
- logger$18.debug("[LocalStreamController] User media obtained:", stream);
15334
+ logger$19.debug("[LocalStreamController] User media obtained:", stream);
15252
15335
  }
15253
15336
  this._localStream$.next(stream);
15254
15337
  this._localAudioTracks$.next(stream.getAudioTracks());
@@ -15267,7 +15350,7 @@ var LocalStreamController = class extends Destroyable {
15267
15350
  this._localStream$.next(localStream);
15268
15351
  if (track.kind === "video") this._localVideoTracks$.next(localStream.getVideoTracks());
15269
15352
  else this._localAudioTracks$.next(localStream.getAudioTracks());
15270
- logger$18.debug(`[LocalStreamController] ${track.kind} track added:`, track.id);
15353
+ logger$19.debug(`[LocalStreamController] ${track.kind} track added:`, track.id);
15271
15354
  return localStream;
15272
15355
  }
15273
15356
  /**
@@ -15279,7 +15362,7 @@ var LocalStreamController = class extends Destroyable {
15279
15362
  const stream = this._localStream$.value;
15280
15363
  const track = stream?.getTracks().find((t) => t.id === trackId);
15281
15364
  if (!track) {
15282
- logger$18.debug(`[LocalStreamController] track not found: ${trackId}`);
15365
+ logger$19.debug(`[LocalStreamController] track not found: ${trackId}`);
15283
15366
  return;
15284
15367
  }
15285
15368
  track.removeEventListener("ended", this.mediaTrackEndedHandler);
@@ -15288,7 +15371,7 @@ var LocalStreamController = class extends Destroyable {
15288
15371
  this._localStream$.next(stream);
15289
15372
  if (track.kind === "video") this._localVideoTracks$.next(stream?.getVideoTracks() ?? []);
15290
15373
  else this._localAudioTracks$.next(stream?.getAudioTracks() ?? []);
15291
- logger$18.debug(`[LocalStreamController] ${track.kind} track removed:`, trackId);
15374
+ logger$19.debug(`[LocalStreamController] ${track.kind} track removed:`, trackId);
15292
15375
  return track;
15293
15376
  }
15294
15377
  /**
@@ -15323,7 +15406,7 @@ var LocalStreamController = class extends Destroyable {
15323
15406
  */
15324
15407
  stopAllTracks() {
15325
15408
  this._localStream$.value?.getTracks().forEach((track) => {
15326
- logger$18.debug(`[LocalStreamController] Stopping local track: ${track.kind}`);
15409
+ logger$19.debug(`[LocalStreamController] Stopping local track: ${track.kind}`);
15327
15410
  track.removeEventListener("ended", this.mediaTrackEndedHandler);
15328
15411
  track.stop();
15329
15412
  });
@@ -15339,7 +15422,7 @@ var LocalStreamController = class extends Destroyable {
15339
15422
 
15340
15423
  //#endregion
15341
15424
  //#region src/controllers/TransceiverController.ts
15342
- const logger$17 = getLogger();
15425
+ const logger$18 = getLogger();
15343
15426
  const getDirection = (send, recv) => {
15344
15427
  if (send && recv) return "sendrecv";
15345
15428
  else if (send && !recv) return "sendonly";
@@ -15447,7 +15530,7 @@ var TransceiverController = class extends Destroyable {
15447
15530
  sendEncodings: isAudio ? void 0 : this.sendEncodings,
15448
15531
  streams: direction === "recvonly" ? void 0 : [localStream]
15449
15532
  };
15450
- logger$17.debug(`[TransceiverController] Setting up transceiver sender for local ${track.kind} track:`, {
15533
+ logger$18.debug(`[TransceiverController] Setting up transceiver sender for local ${track.kind} track:`, {
15451
15534
  transceiver,
15452
15535
  transceiverParams
15453
15536
  });
@@ -15455,11 +15538,11 @@ var TransceiverController = class extends Destroyable {
15455
15538
  await transceiver.sender.replaceTrack(track);
15456
15539
  transceiver.direction = transceiverParams.direction;
15457
15540
  if (transceiverParams.streams?.some((stream) => Boolean(stream))) {
15458
- logger$17.debug(`[TransceiverController] Setting streams for transceiver sender for local ${track.kind} track:`, transceiverParams.streams);
15541
+ logger$18.debug(`[TransceiverController] Setting streams for transceiver sender for local ${track.kind} track:`, transceiverParams.streams);
15459
15542
  transceiver.sender.setStreams(...transceiverParams.streams);
15460
15543
  }
15461
15544
  } else {
15462
- logger$17.debug(`[TransceiverController] Adding new transceiver for local ${track.kind} track:`, track.id);
15545
+ logger$18.debug(`[TransceiverController] Adding new transceiver for local ${track.kind} track:`, track.id);
15463
15546
  this.peerConnection.addTransceiver(track, transceiverParams);
15464
15547
  }
15465
15548
  }
@@ -15473,13 +15556,13 @@ var TransceiverController = class extends Destroyable {
15473
15556
  if (options.updateTransceiverDirection) transceiver.direction = "inactive";
15474
15557
  }
15475
15558
  } catch (error) {
15476
- logger$17.error("[TransceiverController] stopTrackSender error", kind, error);
15559
+ logger$18.error("[TransceiverController] stopTrackSender error", kind, error);
15477
15560
  this.options.onError?.(new MediaTrackError("stopTrackSender", kind, error));
15478
15561
  }
15479
15562
  }
15480
15563
  async restoreTrackSender(kind) {
15481
15564
  try {
15482
- logger$17.debug("[TransceiverController] restoreTrackSender called", kind);
15565
+ logger$18.debug("[TransceiverController] restoreTrackSender called", kind);
15483
15566
  const constraints = {};
15484
15567
  const transceivers = this.transceiverByKind(kind);
15485
15568
  for (const transceiver of transceivers) {
@@ -15489,23 +15572,23 @@ var TransceiverController = class extends Destroyable {
15489
15572
  if (trackKind === "audio" || trackKind === "video") constraints[trackKind] = this.getConstraintsFor(trackKind);
15490
15573
  }
15491
15574
  }
15492
- logger$17.debug("[TransceiverController] restoreTrackSender constraints:", constraints);
15575
+ logger$18.debug("[TransceiverController] restoreTrackSender constraints:", constraints);
15493
15576
  if (Object.keys(constraints).length === 0) {
15494
- logger$17.warn("[TransceiverController] restoreTrackSender: no tracks need restoration", kind);
15577
+ logger$18.warn("[TransceiverController] restoreTrackSender: no tracks need restoration", kind);
15495
15578
  return;
15496
15579
  }
15497
15580
  const newTracks = (await this.options.getUserMedia(constraints)).getTracks();
15498
- logger$17.debug("[TransceiverController] restoreTrackSender new tracks:", newTracks);
15581
+ logger$18.debug("[TransceiverController] restoreTrackSender new tracks:", newTracks);
15499
15582
  for (const newTrack of newTracks) {
15500
15583
  this.options.localStreamController.addTrack(newTrack);
15501
15584
  const trackKind = newTrack.kind;
15502
15585
  const transceiverOfKind = this.transceiverByKind(trackKind)[0];
15503
15586
  transceiverOfKind.direction = trackKind === "audio" ? this.audioDirection : this.videoDirection;
15504
- logger$17.debug("[TransceiverController] restoreTrackSender setting direction for", trackKind, transceiverOfKind.direction);
15587
+ logger$18.debug("[TransceiverController] restoreTrackSender setting direction for", trackKind, transceiverOfKind.direction);
15505
15588
  await transceiverOfKind.sender.replaceTrack(newTrack);
15506
15589
  }
15507
15590
  } catch (error) {
15508
- logger$17.error("[TransceiverController] restoreTrackSender error", kind, error);
15591
+ logger$18.error("[TransceiverController] restoreTrackSender error", kind, error);
15509
15592
  this.options.onError?.(new MediaTrackError("restoreTrackSender", kind, error));
15510
15593
  }
15511
15594
  }
@@ -15546,14 +15629,14 @@ var TransceiverController = class extends Destroyable {
15546
15629
  };
15547
15630
  try {
15548
15631
  await track.applyConstraints(constraintsToApply);
15549
- logger$17.debug(`[TransceiverController] Updated ${kind} sender constraints:`, constraintsToApply);
15550
- logger$17.debug(`[TransceiverController] Updated ${kind} sender constraints:`, track.getConstraints());
15632
+ logger$18.debug(`[TransceiverController] Updated ${kind} sender constraints:`, constraintsToApply);
15633
+ logger$18.debug(`[TransceiverController] Updated ${kind} sender constraints:`, track.getConstraints());
15551
15634
  } catch (error) {
15552
- logger$17.warn(`[TransceiverController] applyConstraints failed for ${kind} track ${track.id}, attempting track replacement fallback:`, error);
15635
+ logger$18.warn(`[TransceiverController] applyConstraints failed for ${kind} track ${track.id}, attempting track replacement fallback:`, error);
15553
15636
  try {
15554
15637
  await this.replaceTrackFallback(sender, track, kind, constraintsToApply);
15555
15638
  } catch (fallbackError) {
15556
- logger$17.warn(`[TransceiverController] Track replacement fallback also failed for ${kind} track:`, fallbackError);
15639
+ logger$18.warn(`[TransceiverController] Track replacement fallback also failed for ${kind} track:`, fallbackError);
15557
15640
  this.options.onError?.(new MediaTrackError("updateSendersConstraints", kind, fallbackError));
15558
15641
  }
15559
15642
  }
@@ -15581,7 +15664,7 @@ var TransceiverController = class extends Destroyable {
15581
15664
  if (!newTrack) throw new MediaTrackError("replaceTrackFallback", kind, /* @__PURE__ */ new Error("getUserMedia returned no track of the requested kind"));
15582
15665
  await sender.replaceTrack(newTrack);
15583
15666
  this.options.localStreamController.addTrack(newTrack);
15584
- logger$17.debug(`[TransceiverController] Track replacement fallback succeeded for ${kind}. New track: ${newTrack.id}`);
15667
+ logger$18.debug(`[TransceiverController] Track replacement fallback succeeded for ${kind}. New track: ${newTrack.id}`);
15585
15668
  }
15586
15669
  getMediaDirections() {
15587
15670
  if (this.peerConnection.connectionState === "connected") return this.peerConnection.getTransceivers().reduce((acc, transceiver) => {
@@ -15612,7 +15695,7 @@ var TransceiverController = class extends Destroyable {
15612
15695
  //#endregion
15613
15696
  //#region src/controllers/RTCPeerConnectionController.ts
15614
15697
  var import_cjs$16 = require_cjs();
15615
- const logger$16 = getLogger();
15698
+ const logger$17 = getLogger();
15616
15699
  var RTCPeerConnectionController = class extends Destroyable {
15617
15700
  constructor(options = {}, remoteSessionDescription, deviceController) {
15618
15701
  super();
@@ -15628,43 +15711,43 @@ var RTCPeerConnectionController = class extends Destroyable {
15628
15711
  this.oniceconnectionstatechangeHandler = () => {
15629
15712
  if (this.peerConnection) {
15630
15713
  const { iceConnectionState } = this.peerConnection;
15631
- logger$16.debug(`[RTCPeerConnectionController] ICE connection state changed to: ${iceConnectionState}`);
15714
+ logger$17.debug(`[RTCPeerConnectionController] ICE connection state changed to: ${iceConnectionState}`);
15632
15715
  this._iceConnectionState$.next(this.peerConnection.iceConnectionState);
15633
15716
  }
15634
15717
  };
15635
15718
  this.onconnectionstatechangeHandler = () => {
15636
15719
  if (this.peerConnection) {
15637
15720
  const { connectionState } = this.peerConnection;
15638
- logger$16.debug(`[RTCPeerConnectionController] Connection state changed to: ${connectionState}`);
15721
+ logger$17.debug(`[RTCPeerConnectionController] Connection state changed to: ${connectionState}`);
15639
15722
  if (connectionState === "connected") this.removeConnectionTimer();
15640
15723
  this._connectionState$.next(this.peerConnection.connectionState);
15641
15724
  }
15642
15725
  };
15643
15726
  this.onsignalingstatechangeHandler = () => {
15644
- logger$16.debug(`[RTCPeerConnectionController] Signaling state changed to: ${this.peerConnection?.signalingState}`);
15727
+ logger$17.debug(`[RTCPeerConnectionController] Signaling state changed to: ${this.peerConnection?.signalingState}`);
15645
15728
  };
15646
15729
  this.onicegatheringstatechangeHandler = () => {
15647
15730
  if (this.peerConnection) this._iceGatheringState$.next(this.peerConnection.iceGatheringState);
15648
15731
  };
15649
15732
  this.onnegotiationneededHandler = (event) => {
15650
- logger$16.debug("[RTCPeerConnectionController] Negotiation needed event received.", event);
15733
+ logger$17.debug("[RTCPeerConnectionController] Negotiation needed event received.", event);
15651
15734
  this.negotiationNeeded$.next();
15652
15735
  };
15653
15736
  this.updateSelectedInputDevice = async (kind, deviceInfo) => {
15654
15737
  try {
15655
15738
  const { localStream } = this;
15656
15739
  if (!localStream) {
15657
- logger$16.warn("[RTCPeerConnectionController] No local stream available to update input device.");
15740
+ logger$17.warn("[RTCPeerConnectionController] No local stream available to update input device.");
15658
15741
  return;
15659
15742
  }
15660
- logger$16.debug(`[RTCPeerConnectionController] Updating selected ${kind} input device:`, localStream.getTracks());
15743
+ logger$17.debug(`[RTCPeerConnectionController] Updating selected ${kind} input device:`, localStream.getTracks());
15661
15744
  const track = localStream.getTracks().find((track$1) => track$1.kind === kind);
15662
15745
  if (track) {
15663
15746
  this.transceiverController?.stopTrackSender(kind);
15664
15747
  this.localStreamController.removeTrack(track.id);
15665
- logger$16.debug(`[RTCPeerConnectionController] Stopped existing ${kind} track: ${track.id}`, localStream.getTracks());
15748
+ logger$17.debug(`[RTCPeerConnectionController] Stopped existing ${kind} track: ${track.id}`, localStream.getTracks());
15666
15749
  if (!deviceInfo) {
15667
- logger$16.debug(`[RTCPeerConnectionController] ${kind} input device selected: none`);
15750
+ logger$17.debug(`[RTCPeerConnectionController] ${kind} input device selected: none`);
15668
15751
  return;
15669
15752
  }
15670
15753
  const streamTrack = (await this.getUserMedia({ [kind]: {
@@ -15672,16 +15755,16 @@ var RTCPeerConnectionController = class extends Destroyable {
15672
15755
  ...this.deviceController.deviceInfoToConstraints(deviceInfo)
15673
15756
  } })).getTracks().find((t) => t.kind === kind);
15674
15757
  if (streamTrack) {
15675
- logger$16.debug(`[RTCPeerConnectionController] Adding new ${kind} track: ${streamTrack.id}`);
15758
+ logger$17.debug(`[RTCPeerConnectionController] Adding new ${kind} track: ${streamTrack.id}`);
15676
15759
  this.localStreamController.addTrack(streamTrack);
15677
15760
  await this.transceiverController?.replaceSenderTrack(kind, streamTrack);
15678
- logger$16.debug(`[RTCPeerConnectionController] Added new ${kind} track: ${streamTrack.id}`, this.localStream?.getTracks());
15761
+ logger$17.debug(`[RTCPeerConnectionController] Added new ${kind} track: ${streamTrack.id}`, this.localStream?.getTracks());
15679
15762
  }
15680
15763
  }
15681
- logger$16.debug(`[RTCPeerConnectionController] ${kind} input device selected:`, deviceInfo?.label);
15764
+ logger$17.debug(`[RTCPeerConnectionController] ${kind} input device selected:`, deviceInfo?.label);
15682
15765
  } catch (error) {
15683
- logger$16.error(`[RTCPeerConnectionController] Failed to select ${kind} input device:`, error);
15684
- this._errors$.next(toError(error));
15766
+ logger$17.error(`[RTCPeerConnectionController] Failed to select ${kind} input device:`, error);
15767
+ this._errors$.next(new MediaTrackError("updateSelectedInputDevice", kind, error));
15685
15768
  throw error;
15686
15769
  }
15687
15770
  };
@@ -15903,7 +15986,7 @@ var RTCPeerConnectionController = class extends Destroyable {
15903
15986
  case "main":
15904
15987
  default: return {
15905
15988
  ...options,
15906
- offerToReceiveAudio: true,
15989
+ offerToReceiveAudio: this.options.receiveAudio ?? true,
15907
15990
  offerToReceiveVideo: this.options.receiveVideo ?? Boolean(this.inputVideoDeviceConstraints)
15908
15991
  };
15909
15992
  }
@@ -15930,15 +16013,15 @@ var RTCPeerConnectionController = class extends Destroyable {
15930
16013
  this.setupPeerConnection();
15931
16014
  this.subscribeTo(this.negotiationNeeded$.pipe((0, import_cjs$16.auditTime)(0), (0, import_cjs$16.exhaustMap)(async () => this.startNegotiation())), {
15932
16015
  next: () => {
15933
- logger$16.debug("[RTCPeerConnectionController] Start Negotiation completed successfully");
16016
+ logger$17.debug("[RTCPeerConnectionController] Start Negotiation completed successfully");
15934
16017
  },
15935
16018
  error: (error) => {
15936
- logger$16.error("[RTCPeerConnectionController] Start Negotiation error:", error);
16019
+ logger$17.error("[RTCPeerConnectionController] Start Negotiation error:", error);
15937
16020
  this._errors$.next(toError(error));
15938
16021
  }
15939
16022
  });
15940
16023
  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]) => {
15941
- logger$16.debug(`[RTCPeerConnectionController] Selected input device changed for:`, {
16024
+ logger$17.debug(`[RTCPeerConnectionController] Selected input device changed for:`, {
15942
16025
  kind,
15943
16026
  deviceInfo
15944
16027
  });
@@ -15955,7 +16038,7 @@ var RTCPeerConnectionController = class extends Destroyable {
15955
16038
  this._initialized$.next(true);
15956
16039
  }
15957
16040
  } catch (error) {
15958
- logger$16.error("[RTCPeerConnectionController] Initialization error:", error);
16041
+ logger$17.error("[RTCPeerConnectionController] Initialization error:", error);
15959
16042
  this._errors$.next(toError(error));
15960
16043
  this.destroy();
15961
16044
  }
@@ -15987,22 +16070,22 @@ var RTCPeerConnectionController = class extends Destroyable {
15987
16070
  }
15988
16071
  async startNegotiation() {
15989
16072
  if (this.isNegotiating) {
15990
- logger$16.debug("[RTCPeerConnectionController] Negotiation already in progress, skipping.");
16073
+ logger$17.debug("[RTCPeerConnectionController] Negotiation already in progress, skipping.");
15991
16074
  return;
15992
16075
  }
15993
16076
  this.setupEventListeners();
15994
16077
  if (this.type === "answer") {
15995
- logger$16.debug("[RTCPeerConnectionController] This is an answer type still, skipping offer creation.");
16078
+ logger$17.debug("[RTCPeerConnectionController] This is an answer type still, skipping offer creation.");
15996
16079
  return;
15997
16080
  }
15998
16081
  this._isNegotiating$.next(true);
15999
- logger$16.debug("[RTCPeerConnectionController] Starting negotiation.");
16082
+ logger$17.debug("[RTCPeerConnectionController] Starting negotiation.");
16000
16083
  try {
16001
16084
  const { offerOptions } = this;
16002
- logger$16.debug("[RTCPeerConnectionController] Creating offer with options:", offerOptions);
16085
+ logger$17.debug("[RTCPeerConnectionController] Creating offer with options:", offerOptions);
16003
16086
  await this.createOffer(offerOptions);
16004
16087
  } catch (error) {
16005
- logger$16.error("[RTCPeerConnectionController] Error during negotiation:", error);
16088
+ logger$17.error("[RTCPeerConnectionController] Error during negotiation:", error);
16006
16089
  this._errors$.next(toError(error));
16007
16090
  }
16008
16091
  }
@@ -16018,14 +16101,14 @@ var RTCPeerConnectionController = class extends Destroyable {
16018
16101
  let readyToConnect = status !== "failed";
16019
16102
  try {
16020
16103
  if (status === "received" && sdp) {
16021
- logger$16.debug("[RTCPeerConnectionController] Received answer SDP:", sdp);
16104
+ logger$17.debug("[RTCPeerConnectionController] Received answer SDP:", sdp);
16022
16105
  await this._setRemoteDescription({
16023
16106
  type: "answer",
16024
16107
  sdp
16025
16108
  });
16026
16109
  }
16027
16110
  } catch (error) {
16028
- logger$16.error("[RTCPeerConnectionController] Error updating answer status:", error);
16111
+ logger$17.error("[RTCPeerConnectionController] Error updating answer status:", error);
16029
16112
  this._errors$.next(toError(error));
16030
16113
  readyToConnect = false;
16031
16114
  } finally {
@@ -16044,7 +16127,7 @@ var RTCPeerConnectionController = class extends Destroyable {
16044
16127
  await this.handleOfferReceived();
16045
16128
  break;
16046
16129
  case "failed":
16047
- logger$16.error("[RTCPeerConnectionController] Offer failed to be processed by remote.");
16130
+ logger$17.error("[RTCPeerConnectionController] Offer failed to be processed by remote.");
16048
16131
  break;
16049
16132
  case "sent":
16050
16133
  default:
@@ -16057,13 +16140,14 @@ var RTCPeerConnectionController = class extends Destroyable {
16057
16140
  */
16058
16141
  async acceptInbound(mediaOverrides) {
16059
16142
  if (mediaOverrides) {
16060
- const { audio, video, receiveAudio, receiveVideo } = mediaOverrides;
16143
+ const { audio, video, receiveAudio, receiveVideo, fallbackToReceiveOnly } = mediaOverrides;
16061
16144
  this.options = {
16062
16145
  ...this.options,
16063
16146
  ...audio !== void 0 ? { audio } : {},
16064
16147
  ...video !== void 0 ? { video } : {},
16065
16148
  ...receiveAudio !== void 0 ? { receiveAudio } : {},
16066
- ...receiveVideo !== void 0 ? { receiveVideo } : {}
16149
+ ...receiveVideo !== void 0 ? { receiveVideo } : {},
16150
+ ...fallbackToReceiveOnly !== void 0 ? { fallbackToReceiveOnly } : {}
16067
16151
  };
16068
16152
  this.transceiverController?.updateOptions({
16069
16153
  receiveAudio: this.receiveAudio,
@@ -16076,7 +16160,7 @@ var RTCPeerConnectionController = class extends Destroyable {
16076
16160
  }
16077
16161
  await this.setupLocalTracks();
16078
16162
  const { answerOptions } = this;
16079
- logger$16.debug("[RTCPeerConnectionController] Creating inbound answer with options:", answerOptions);
16163
+ logger$17.debug("[RTCPeerConnectionController] Creating inbound answer with options:", answerOptions);
16080
16164
  await this.createAnswer(answerOptions);
16081
16165
  }
16082
16166
  async handleOfferReceived() {
@@ -16084,7 +16168,7 @@ var RTCPeerConnectionController = class extends Destroyable {
16084
16168
  this._isNegotiating$.next(true);
16085
16169
  await this._setRemoteDescription(this.sdpInit);
16086
16170
  const { answerOptions } = this;
16087
- logger$16.debug("[RTCPeerConnectionController] Creating answer with options:", answerOptions);
16171
+ logger$17.debug("[RTCPeerConnectionController] Creating answer with options:", answerOptions);
16088
16172
  await this.createAnswer(answerOptions);
16089
16173
  }
16090
16174
  readyToConnect() {
@@ -16092,7 +16176,7 @@ var RTCPeerConnectionController = class extends Destroyable {
16092
16176
  this.connectionTimer = setTimeout(() => {
16093
16177
  this.removeConnectionTimer();
16094
16178
  if (this.peerConnection?.connectionState !== "connected") {
16095
- logger$16.debug("[RTCPeerConnectionController] Connection timeout, restarting ICE gathering with relay only.");
16179
+ logger$17.debug("[RTCPeerConnectionController] Connection timeout, restarting ICE gathering with relay only.");
16096
16180
  this.iceGatheringController.restartICEGatheringWithRelayOnly();
16097
16181
  }
16098
16182
  }, this.connectionTimeout);
@@ -16114,14 +16198,14 @@ var RTCPeerConnectionController = class extends Destroyable {
16114
16198
  const stereo = this.options.stereo ?? PreferencesContainer.instance.stereoAudio;
16115
16199
  if (preferredAudioCodecs.length > 0 || preferredVideoCodecs.length > 0) {
16116
16200
  result = setCodecPreferences(result, preferredAudioCodecs, preferredVideoCodecs);
16117
- logger$16.debug("[RTCPeerConnectionController] Applied codec preferences to SDP", {
16201
+ logger$17.debug("[RTCPeerConnectionController] Applied codec preferences to SDP", {
16118
16202
  preferredAudioCodecs,
16119
16203
  preferredVideoCodecs
16120
16204
  });
16121
16205
  }
16122
16206
  if (stereo) {
16123
16207
  result = enableStereoOpus(result);
16124
- logger$16.debug("[RTCPeerConnectionController] Applied stereo Opus to SDP");
16208
+ logger$17.debug("[RTCPeerConnectionController] Applied stereo Opus to SDP");
16125
16209
  }
16126
16210
  return Promise.resolve(result);
16127
16211
  }
@@ -16177,25 +16261,25 @@ var RTCPeerConnectionController = class extends Destroyable {
16177
16261
  ...this.peerConnection.getConfiguration(),
16178
16262
  iceTransportPolicy: "relay"
16179
16263
  });
16180
- logger$16.debug("[RTCPeerConnectionController] ICE transport policy set to relay-only");
16264
+ logger$17.debug("[RTCPeerConnectionController] ICE transport policy set to relay-only");
16181
16265
  } catch (error) {
16182
- logger$16.warn("[RTCPeerConnectionController] Failed to set relay-only policy:", error);
16266
+ logger$17.warn("[RTCPeerConnectionController] Failed to set relay-only policy:", error);
16183
16267
  }
16184
16268
  this.setupEventListeners();
16185
16269
  this._isNegotiating$.next(true);
16186
- logger$16.debug(`[RTCPeerConnectionController] Triggering ICE restart${relayOnly ? " (relay-only)" : ""}.`);
16270
+ logger$17.debug(`[RTCPeerConnectionController] Triggering ICE restart${relayOnly ? " (relay-only)" : ""}.`);
16187
16271
  try {
16188
16272
  const offer = await this.peerConnection.createOffer({ iceRestart: true });
16189
16273
  await this.setLocalDescription(offer);
16190
16274
  } catch (error) {
16191
- logger$16.error("[RTCPeerConnectionController] ICE restart offer failed:", error);
16275
+ logger$17.error("[RTCPeerConnectionController] ICE restart offer failed:", error);
16192
16276
  this._errors$.next(toError(error));
16193
16277
  this.negotiationEnded();
16194
16278
  if (policyChanged) this.restoreIceTransportPolicy();
16195
16279
  throw error;
16196
16280
  }
16197
16281
  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) => {
16198
- logger$16.warn("[RTCPeerConnectionController] Error waiting for ICE gathering to complete:", error);
16282
+ logger$17.warn("[RTCPeerConnectionController] Error waiting for ICE gathering to complete:", error);
16199
16283
  this.restoreIceTransportPolicy();
16200
16284
  });
16201
16285
  }
@@ -16205,9 +16289,9 @@ var RTCPeerConnectionController = class extends Destroyable {
16205
16289
  ...this.peerConnection.getConfiguration(),
16206
16290
  iceTransportPolicy: this.options.relayOnly ? "relay" : "all"
16207
16291
  });
16208
- logger$16.debug("[RTCPeerConnectionController] ICE transport policy restored");
16292
+ logger$17.debug("[RTCPeerConnectionController] ICE transport policy restored");
16209
16293
  } catch (error) {
16210
- logger$16.warn("[RTCPeerConnectionController] Failed to restore ICE transport policy:", error);
16294
+ logger$17.warn("[RTCPeerConnectionController] Failed to restore ICE transport policy:", error);
16211
16295
  }
16212
16296
  }
16213
16297
  /**
@@ -16219,13 +16303,25 @@ var RTCPeerConnectionController = class extends Destroyable {
16219
16303
  await this.setupRemoteTracks();
16220
16304
  }
16221
16305
  async setupLocalTracks() {
16222
- logger$16.debug("[RTCPeerConnectionController] Setting up local tracks/transceivers.");
16223
- const localStream = this.localStream ?? await this.localStreamController.buildLocalStream();
16306
+ logger$17.debug("[RTCPeerConnectionController] Setting up local tracks/transceivers.");
16307
+ if (this.hasNoLocalMediaToSend()) {
16308
+ if (!this.receiveAudio && !this.receiveVideo) throw new InvalidParams("Call requests no media: enable audio/video or receiveAudio/receiveVideo");
16309
+ logger$17.debug("[RTCPeerConnectionController] No local media requested; negotiating receive-only.");
16310
+ this.setupReceiveOnlyTransceivers();
16311
+ return;
16312
+ }
16313
+ let localStream;
16314
+ try {
16315
+ localStream = this.localStream ?? await this.localStreamController.buildLocalStream();
16316
+ } catch (error) {
16317
+ this.handleLocalMediaFailure(error);
16318
+ return;
16319
+ }
16224
16320
  if (this.transceiverController?.useAddStream ?? false) {
16225
- logger$16.warn("[RTCPeerConnectionController] Using deprecated addStream API to add local stream.");
16321
+ logger$17.warn("[RTCPeerConnectionController] Using deprecated addStream API to add local stream.");
16226
16322
  this.peerConnection?.addStream(localStream);
16227
16323
  if (!this.isNegotiating) {
16228
- logger$16.debug("[RTCPeerConnectionController] Forcing negotiationneeded after local tracks setup.");
16324
+ logger$17.debug("[RTCPeerConnectionController] Forcing negotiationneeded after local tracks setup.");
16229
16325
  this.negotiationNeeded$.next();
16230
16326
  }
16231
16327
  return;
@@ -16241,12 +16337,54 @@ var RTCPeerConnectionController = class extends Destroyable {
16241
16337
  const transceivers = (kind === "audio" ? this.transceiverController?.audioTransceivers : this.transceiverController?.videoTransceivers) ?? [];
16242
16338
  await this.transceiverController?.setupTransceiverSender(track, localStream, transceivers[index]);
16243
16339
  } else {
16244
- logger$16.debug(`[RTCPeerConnectionController] Using addTrack for local ${kind} track:`, track.id);
16340
+ logger$17.debug(`[RTCPeerConnectionController] Using addTrack for local ${kind} track:`, track.id);
16245
16341
  this.peerConnection?.addTrack(track, localStream);
16246
16342
  }
16247
16343
  }
16248
16344
  }
16249
16345
  }
16346
+ /** True for a main connection with no local media to send. */
16347
+ hasNoLocalMediaToSend() {
16348
+ const hasInputStreams = Boolean(this.options.inputAudioStream ?? this.options.inputVideoStream);
16349
+ return this.propose === "main" && !this.localStream && !hasInputStreams && !this.inputAudioDeviceConstraints && !this.inputVideoDeviceConstraints;
16350
+ }
16351
+ /** The media kinds this connection wants to send: 'audiovideo' | 'video' | 'audio'. */
16352
+ get requestedMediaKinds() {
16353
+ const wantsAudio = Boolean(this.inputAudioDeviceConstraints);
16354
+ const wantsVideo = Boolean(this.inputVideoDeviceConstraints);
16355
+ if (wantsAudio && wantsVideo) return "audiovideo";
16356
+ return wantsVideo ? "video" : "audio";
16357
+ }
16358
+ /**
16359
+ * Handle a local media acquisition failure with a typed, semantically
16360
+ * accurate MediaAccessError created at the acquisition site:
16361
+ * - Auxiliary connections (screenshare / additional-device) throw a
16362
+ * non-fatal error — VertoManager surfaces it and the call is unaffected.
16363
+ * - The main connection degrades to receive-only when allowed (default),
16364
+ * otherwise fails with a fatal error.
16365
+ */
16366
+ handleLocalMediaFailure(error) {
16367
+ if (this.propose === "screenshare") throw new MediaAccessError("startScreenShare", "screen", error, false);
16368
+ if (this.propose === "additional-device") throw new MediaAccessError("addInputDevice", this.requestedMediaKinds, error, false);
16369
+ const canReceive = this.receiveAudio || this.receiveVideo;
16370
+ if (!((this.options.fallbackToReceiveOnly ?? true) && canReceive)) throw new MediaAccessError("acquireLocalMedia", this.requestedMediaKinds, error, true);
16371
+ logger$17.warn("[RTCPeerConnectionController] Local media unavailable; continuing receive-only:", error);
16372
+ this._errors$.next(new MediaAccessError("acquireLocalMedia", this.requestedMediaKinds, error, false));
16373
+ this.setupReceiveOnlyTransceivers();
16374
+ }
16375
+ /**
16376
+ * Negotiate receive-only m-lines when there are no local tracks to send.
16377
+ * Only offer-type connections add transceivers — answer-type connections
16378
+ * reuse the transceivers created from the remote offer.
16379
+ */
16380
+ setupReceiveOnlyTransceivers() {
16381
+ if (this.type !== "offer") return;
16382
+ if (this.transceiverController?.useAddTransceivers ?? false) {
16383
+ this.peerConnection?.addTransceiver("audio", { direction: this.receiveAudio ? "recvonly" : "inactive" });
16384
+ this.peerConnection?.addTransceiver("video", { direction: this.receiveVideo ? "recvonly" : "inactive" });
16385
+ }
16386
+ if (!this.isNegotiating) this.negotiationNeeded$.next();
16387
+ }
16250
16388
  async getUserMedia(constraints) {
16251
16389
  return (this.options.webRTCApiProvider?.mediaDevices ?? navigator.mediaDevices).getUserMedia(constraints);
16252
16390
  }
@@ -16258,7 +16396,7 @@ var RTCPeerConnectionController = class extends Destroyable {
16258
16396
  async setupRemoteTracks() {
16259
16397
  if (!this.peerConnection) throw new DependencyError("RTCPeerConnection is not initialized");
16260
16398
  this.peerConnection.ontrack = (event) => {
16261
- logger$16.debug("[RTCPeerConnectionController] Remote track received:", event.track.kind);
16399
+ logger$17.debug("[RTCPeerConnectionController] Remote track received:", event.track.kind);
16262
16400
  if (event.streams[0]) this._remoteStream$.next(event.streams[0]);
16263
16401
  else {
16264
16402
  const existingTracks = this._remoteStream$.value?.getTracks() ?? [];
@@ -16282,8 +16420,8 @@ var RTCPeerConnectionController = class extends Destroyable {
16282
16420
  try {
16283
16421
  stream = await this.getUserMedia({ audio: constraints });
16284
16422
  } catch (error) {
16285
- logger$16.error("[RTCPeerConnectionController] Failed to re-acquire mic for pipeline restore:", error);
16286
- this._errors$.next(toError(error));
16423
+ logger$17.error("[RTCPeerConnectionController] Failed to re-acquire mic for pipeline restore:", error);
16424
+ this._errors$.next(new MediaTrackError("restoreAudioPipelineInput", "audio", error));
16287
16425
  return;
16288
16426
  }
16289
16427
  const newTrack = stream.getAudioTracks().at(0);
@@ -16304,7 +16442,7 @@ var RTCPeerConnectionController = class extends Destroyable {
16304
16442
  try {
16305
16443
  this._localAudioPipeline = new LocalAudioPipeline();
16306
16444
  } catch (error) {
16307
- logger$16.warn("[RTCPeerConnectionController] Failed to create LocalAudioPipeline:", error);
16445
+ logger$17.warn("[RTCPeerConnectionController] Failed to create LocalAudioPipeline:", error);
16308
16446
  return null;
16309
16447
  }
16310
16448
  this.subscribeTo(this.localStreamController.localAudioTracks$, () => {
@@ -16326,7 +16464,7 @@ var RTCPeerConnectionController = class extends Destroyable {
16326
16464
  try {
16327
16465
  await sender.replaceTrack(this._localAudioPipeline.outputTrack);
16328
16466
  } catch (error) {
16329
- logger$16.warn("[RTCPeerConnectionController] Failed to route audio sender through pipeline:", error);
16467
+ logger$17.warn("[RTCPeerConnectionController] Failed to route audio sender through pipeline:", error);
16330
16468
  }
16331
16469
  }
16332
16470
  /**
@@ -16342,10 +16480,10 @@ var RTCPeerConnectionController = class extends Destroyable {
16342
16480
  try {
16343
16481
  const localStream = this.localStreamController.addTrack(track);
16344
16482
  this.peerConnection.addTrack(track, localStream);
16345
- logger$16.debug(`[RTCPeerConnectionController] ${track.kind} track added:`, track.id);
16483
+ logger$17.debug(`[RTCPeerConnectionController] ${track.kind} track added:`, track.id);
16346
16484
  } catch (error) {
16347
- logger$16.error(`[RTCPeerConnectionController] Failed to add ${track.kind} track:`, error);
16348
- this._errors$.next(toError(error));
16485
+ logger$17.error(`[RTCPeerConnectionController] Failed to add ${track.kind} track:`, error);
16486
+ this._errors$.next(new MediaTrackError("addLocalTrack", track.kind, error));
16349
16487
  throw error;
16350
16488
  }
16351
16489
  }
@@ -16361,16 +16499,16 @@ var RTCPeerConnectionController = class extends Destroyable {
16361
16499
  }
16362
16500
  const sender = this.peerConnection.getSenders().find((sender$1) => sender$1.track?.id === trackId);
16363
16501
  if (!sender) {
16364
- logger$16.debug(`[RTCPeerConnectionController] track not found: ${trackId}`);
16502
+ logger$17.debug(`[RTCPeerConnectionController] track not found: ${trackId}`);
16365
16503
  return;
16366
16504
  }
16367
16505
  try {
16368
16506
  this.peerConnection.removeTrack(sender);
16369
16507
  this.localStreamController.removeTrack(trackId);
16370
- logger$16.debug(`[RTCPeerConnectionController] ${sender.track?.kind} track removed:`, trackId);
16508
+ logger$17.debug(`[RTCPeerConnectionController] ${sender.track?.kind} track removed:`, trackId);
16371
16509
  } catch (error) {
16372
- logger$16.error(`[RTCPeerConnectionController] Failed to remove ${sender.track?.kind} track:`, error);
16373
- this._errors$.next(toError(error));
16510
+ logger$17.error(`[RTCPeerConnectionController] Failed to remove ${sender.track?.kind} track:`, error);
16511
+ this._errors$.next(new MediaTrackError("removeLocalTrack", sender.track?.kind ?? "unknown", error));
16374
16512
  throw error;
16375
16513
  }
16376
16514
  }
@@ -16396,7 +16534,7 @@ var RTCPeerConnectionController = class extends Destroyable {
16396
16534
  async replaceAudioTrackWithConstraints(constraints) {
16397
16535
  const senders = this.peerConnection?.getSenders().filter((s) => s.track?.kind === "audio" && s.track.readyState === "live");
16398
16536
  if (!senders || senders.length === 0) {
16399
- logger$16.warn("[RTCPeerConnectionController] No live audio sender to replace");
16537
+ logger$17.warn("[RTCPeerConnectionController] No live audio sender to replace");
16400
16538
  return;
16401
16539
  }
16402
16540
  for (const sender of senders) {
@@ -16414,7 +16552,7 @@ var RTCPeerConnectionController = class extends Destroyable {
16414
16552
  const newTrack = (await this.getUserMedia({ audio: mergedConstraints })).getAudioTracks()[0];
16415
16553
  await sender.replaceTrack(newTrack);
16416
16554
  this.localStreamController.addTrack(newTrack);
16417
- logger$16.debug(`[RTCPeerConnectionController] Audio track replaced for server-pushed params. New track: ${newTrack.id}`);
16555
+ logger$17.debug(`[RTCPeerConnectionController] Audio track replaced for server-pushed params. New track: ${newTrack.id}`);
16418
16556
  }
16419
16557
  }
16420
16558
  /**
@@ -16422,7 +16560,7 @@ var RTCPeerConnectionController = class extends Destroyable {
16422
16560
  * Completes all observables to prevent memory leaks.
16423
16561
  */
16424
16562
  destroy() {
16425
- logger$16.debug(`[RTCPeerConnectionController] Destroying RTCPeerConnectionController. ${this.propose}`);
16563
+ logger$17.debug(`[RTCPeerConnectionController] Destroying RTCPeerConnectionController. ${this.propose}`);
16426
16564
  this.removeConnectionTimer();
16427
16565
  this._iceGatheringController?.destroy();
16428
16566
  this._localAudioPipeline?.destroy();
@@ -16448,7 +16586,7 @@ var RTCPeerConnectionController = class extends Destroyable {
16448
16586
  }
16449
16587
  stopRemoteTracks() {
16450
16588
  this._remoteStream$.value?.getTracks().forEach((track) => {
16451
- logger$16.debug(`[RTCPeerConnectionController] Stopping remote track: ${track.kind}`);
16589
+ logger$17.debug(`[RTCPeerConnectionController] Stopping remote track: ${track.kind}`);
16452
16590
  track.stop();
16453
16591
  });
16454
16592
  }
@@ -16465,7 +16603,7 @@ var RTCPeerConnectionController = class extends Destroyable {
16465
16603
  ...params,
16466
16604
  sdp: finalRemote
16467
16605
  };
16468
- logger$16.debug("[RTCPeerConnectionController] Setting remote description:", answer);
16606
+ logger$17.debug("[RTCPeerConnectionController] Setting remote description:", answer);
16469
16607
  return this.peerConnection.setRemoteDescription(answer);
16470
16608
  }
16471
16609
  };
@@ -16480,7 +16618,11 @@ function isVertoInviteMessage(value) {
16480
16618
  const msg = value;
16481
16619
  return msg.method === "verto.invite" && isObject(msg.params) && hasProperty(msg.params, "sdp") && hasProperty(msg.params, "callID");
16482
16620
  }
16483
- function isVertoByeMessage(value) {
16621
+ /**
16622
+ * Guards inbound `verto.bye` frames (server → client). Outbound bye messages are
16623
+ * built locally and never type-guarded, so only the inbound shape is asserted.
16624
+ */
16625
+ function isVertoByeInboundMessage(value) {
16484
16626
  if (!isVertoMethodMessage(value)) return false;
16485
16627
  return value.method === "verto.bye";
16486
16628
  }
@@ -16500,11 +16642,19 @@ function isVertoMediaParamsInnerParams(value) {
16500
16642
  function isVertoPingInnerParams(value) {
16501
16643
  return isObject(value) && hasProperty(value, "jsonrpc") && value.jsonrpc === "2.0" && hasProperty(value, "method") && value.method === "verto.ping";
16502
16644
  }
16645
+ /**
16646
+ * Guards the params of an inbound `verto.bye` frame (top-level `callID`). Use
16647
+ * this to recognise the bye payload extracted by `vertoBye$`, e.g. when racing
16648
+ * it against a boolean answer/reject signal.
16649
+ */
16650
+ function isVertoByeInboundParamsGuard(value) {
16651
+ return isObject(value) && hasProperty(value, "callID") && typeof value.callID === "string";
16652
+ }
16503
16653
 
16504
16654
  //#endregion
16505
16655
  //#region src/managers/VertoManager.ts
16506
16656
  var import_cjs$15 = require_cjs();
16507
- const logger$15 = getLogger();
16657
+ const logger$16 = getLogger();
16508
16658
  /**
16509
16659
  * Decide what value goes on the `node_id` field of a `webrtc.verto` envelope.
16510
16660
  *
@@ -16560,7 +16710,7 @@ var WebRTCVertoManager = class extends VertoManager {
16560
16710
  try {
16561
16711
  await this.executeVerto(vertoModifyMessage);
16562
16712
  } catch (error) {
16563
- logger$15.warn("[WebRTCManager] Call might already be disconnected, error sending Verto hold:", error);
16713
+ logger$16.warn("[WebRTCManager] Call might already be disconnected, error sending Verto hold:", error);
16564
16714
  throw error;
16565
16715
  }
16566
16716
  }
@@ -16573,7 +16723,7 @@ var WebRTCVertoManager = class extends VertoManager {
16573
16723
  try {
16574
16724
  await this.executeVerto(vertoModifyMessage);
16575
16725
  } catch (error) {
16576
- logger$15.warn("[WebRTCManager] Call might already be disconnected, error sending Verto unhold:", error);
16726
+ logger$16.warn("[WebRTCManager] Call might already be disconnected, error sending Verto unhold:", error);
16577
16727
  throw error;
16578
16728
  }
16579
16729
  }
@@ -16626,7 +16776,7 @@ var WebRTCVertoManager = class extends VertoManager {
16626
16776
  if (event.member_id) this.setSelfIdIfNull(event.member_id);
16627
16777
  });
16628
16778
  this.subscribeTo(this.vertoMedia$, (event) => {
16629
- logger$15.debug("[WebRTCManager] Received Verto media event (early media SDP):", event);
16779
+ logger$16.debug("[WebRTCManager] Received Verto media event (early media SDP):", event);
16630
16780
  const { sdp, callID } = event;
16631
16781
  this.emitMainSignalingStatus(callID, "ringing");
16632
16782
  this._rtcPeerConnectionsMap.get(callID)?.updateAnswerStatus({
@@ -16635,7 +16785,7 @@ var WebRTCVertoManager = class extends VertoManager {
16635
16785
  });
16636
16786
  });
16637
16787
  this.subscribeTo(this.vertoAnswer$, (event) => {
16638
- logger$15.debug("[WebRTCManager] Received Verto answer event:", event);
16788
+ logger$16.debug("[WebRTCManager] Received Verto answer event:", event);
16639
16789
  const { sdp, callID } = event;
16640
16790
  this.emitMainSignalingStatus(callID, "connecting");
16641
16791
  this._rtcPeerConnectionsMap.get(callID)?.updateAnswerStatus({
@@ -16644,7 +16794,7 @@ var WebRTCVertoManager = class extends VertoManager {
16644
16794
  });
16645
16795
  });
16646
16796
  this.subscribeTo(this.vertoMediaParams$, (event) => {
16647
- logger$15.debug("[WebRTCManager] Received Verto mediaParams event:", event);
16797
+ logger$16.debug("[WebRTCManager] Received Verto mediaParams event:", event);
16648
16798
  const { mediaParams, callID } = event;
16649
16799
  const rtcPeerConnController = this._rtcPeerConnectionsMap.get(callID);
16650
16800
  const { audio, video } = mediaParams;
@@ -16658,7 +16808,7 @@ var WebRTCVertoManager = class extends VertoManager {
16658
16808
  timestamp: Date.now()
16659
16809
  });
16660
16810
  } catch (error) {
16661
- logger$15.warn("[WebRTCManager] Error applying server-pushed media params:", error);
16811
+ logger$16.warn("[WebRTCManager] Error applying server-pushed media params:", error);
16662
16812
  this.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
16663
16813
  }
16664
16814
  })();
@@ -16680,13 +16830,13 @@ var WebRTCVertoManager = class extends VertoManager {
16680
16830
  */
16681
16831
  setNodeIdIfNull(nodeId) {
16682
16832
  if (!this._nodeId$.value && nodeId) {
16683
- logger$15.debug(`[WebRTCManager] Early node_id set: ${nodeId}`);
16833
+ logger$16.debug(`[WebRTCManager] Early node_id set: ${nodeId}`);
16684
16834
  this._nodeId$.next(nodeId);
16685
16835
  }
16686
16836
  }
16687
16837
  setSelfIdIfNull(selfId) {
16688
16838
  if (!this._selfId$.value && selfId) {
16689
- logger$15.debug(`[WebRTCManager] Early selfId set: ${selfId}`);
16839
+ logger$16.debug(`[WebRTCManager] Early selfId set: ${selfId}`);
16690
16840
  this._selfId$.next(selfId);
16691
16841
  }
16692
16842
  }
@@ -16695,7 +16845,7 @@ var WebRTCVertoManager = class extends VertoManager {
16695
16845
  const vertoPongMessage = VertoPong({ ...vertoPing });
16696
16846
  await this.executeVerto(vertoPongMessage);
16697
16847
  } catch (error) {
16698
- logger$15.warn("[WebRTCManager] Call might disconnect, error sending Verto pong:", error);
16848
+ logger$16.warn("[WebRTCManager] Call might disconnect, error sending Verto pong:", error);
16699
16849
  this.onError?.(new VertoPongError(error));
16700
16850
  }
16701
16851
  }
@@ -16705,7 +16855,7 @@ var WebRTCVertoManager = class extends VertoManager {
16705
16855
  if (audio) await this.mainPeerConnection.updateSendersConstraints("audio", audio);
16706
16856
  if (video) await this.mainPeerConnection.updateSendersConstraints("video", video);
16707
16857
  } catch (error) {
16708
- logger$15.warn("[WebRTCManager] Error updating media constraints:", error);
16858
+ logger$16.warn("[WebRTCManager] Error updating media constraints:", error);
16709
16859
  this.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
16710
16860
  throw error;
16711
16861
  }
@@ -16735,20 +16885,20 @@ var WebRTCVertoManager = class extends VertoManager {
16735
16885
  try {
16736
16886
  const pc = this.mainPeerConnection.peerConnection;
16737
16887
  if (!pc) {
16738
- logger$15.warn("[WebRTCManager] No peer connection for keyframe request");
16888
+ logger$16.warn("[WebRTCManager] No peer connection for keyframe request");
16739
16889
  return;
16740
16890
  }
16741
16891
  const videoReceiver = pc.getReceivers().find((r) => r.track.kind === "video");
16742
16892
  if (!videoReceiver) {
16743
- logger$15.warn("[WebRTCManager] No video receiver for keyframe request");
16893
+ logger$16.warn("[WebRTCManager] No video receiver for keyframe request");
16744
16894
  return;
16745
16895
  }
16746
16896
  if (typeof videoReceiver.requestKeyFrame === "function") {
16747
16897
  videoReceiver.requestKeyFrame();
16748
- logger$15.debug("[WebRTCManager] Keyframe requested via RTCRtpReceiver.requestKeyFrame()");
16749
- } else logger$15.debug("[WebRTCManager] requestKeyFrame() not supported, skipping");
16898
+ logger$16.debug("[WebRTCManager] Keyframe requested via RTCRtpReceiver.requestKeyFrame()");
16899
+ } else logger$16.debug("[WebRTCManager] requestKeyFrame() not supported, skipping");
16750
16900
  } catch (error) {
16751
- logger$15.warn("[WebRTCManager] Keyframe request failed (non-fatal):", error);
16901
+ logger$16.warn("[WebRTCManager] Keyframe request failed (non-fatal):", error);
16752
16902
  }
16753
16903
  }
16754
16904
  /**
@@ -16766,13 +16916,13 @@ var WebRTCVertoManager = class extends VertoManager {
16766
16916
  try {
16767
16917
  const controller = this.mainPeerConnection;
16768
16918
  if (!controller.peerConnection) {
16769
- logger$15.warn("[WebRTCManager] No peer connection for ICE restart");
16919
+ logger$16.warn("[WebRTCManager] No peer connection for ICE restart");
16770
16920
  return;
16771
16921
  }
16772
16922
  await controller.triggerIceRestart(relayOnly);
16773
- logger$15.info(`[WebRTCManager] ICE restart initiated${relayOnly ? " (relay-only)" : ""}`);
16923
+ logger$16.info(`[WebRTCManager] ICE restart initiated${relayOnly ? " (relay-only)" : ""}`);
16774
16924
  } catch (error) {
16775
- logger$15.error("[WebRTCManager] ICE restart failed:", error);
16925
+ logger$16.error("[WebRTCManager] ICE restart failed:", error);
16776
16926
  throw error;
16777
16927
  }
16778
16928
  }
@@ -16790,13 +16940,13 @@ var WebRTCVertoManager = class extends VertoManager {
16790
16940
  const entries = Array.from(this._rtcPeerConnectionsMap.entries());
16791
16941
  for (const [id, controller] of entries) try {
16792
16942
  if (!controller.peerConnection) {
16793
- logger$15.debug(`[WebRTCManager] No peer connection for leg ${id}, skipping ICE restart`);
16943
+ logger$16.debug(`[WebRTCManager] No peer connection for leg ${id}, skipping ICE restart`);
16794
16944
  continue;
16795
16945
  }
16796
16946
  await controller.triggerIceRestart(relayOnly);
16797
- logger$15.info(`[WebRTCManager] ICE restart initiated for leg ${id}${relayOnly ? " (relay-only)" : ""}`);
16947
+ logger$16.info(`[WebRTCManager] ICE restart initiated for leg ${id}${relayOnly ? " (relay-only)" : ""}`);
16798
16948
  } catch (error) {
16799
- logger$15.warn(`[WebRTCManager] ICE restart failed for leg ${id}:`, error);
16949
+ logger$16.warn(`[WebRTCManager] ICE restart failed for leg ${id}:`, error);
16800
16950
  }
16801
16951
  }
16802
16952
  /**
@@ -16808,7 +16958,7 @@ var WebRTCVertoManager = class extends VertoManager {
16808
16958
  requestKeyframeAll() {
16809
16959
  for (const [id, controller] of this._rtcPeerConnectionsMap) {
16810
16960
  if (controller.isScreenShare) {
16811
- logger$15.debug(`[WebRTCManager] Skipping keyframe for send-only screen share leg ${id}`);
16961
+ logger$16.debug(`[WebRTCManager] Skipping keyframe for send-only screen share leg ${id}`);
16812
16962
  continue;
16813
16963
  }
16814
16964
  try {
@@ -16818,10 +16968,10 @@ var WebRTCVertoManager = class extends VertoManager {
16818
16968
  if (!videoReceiver) continue;
16819
16969
  if (typeof videoReceiver.requestKeyFrame === "function") {
16820
16970
  videoReceiver.requestKeyFrame();
16821
- logger$15.debug(`[WebRTCManager] Keyframe requested for leg ${id}`);
16971
+ logger$16.debug(`[WebRTCManager] Keyframe requested for leg ${id}`);
16822
16972
  }
16823
16973
  } catch (error) {
16824
- logger$15.warn(`[WebRTCManager] Keyframe request failed for leg ${id} (non-fatal):`, error);
16974
+ logger$16.warn(`[WebRTCManager] Keyframe request failed for leg ${id} (non-fatal):`, error);
16825
16975
  }
16826
16976
  }
16827
16977
  }
@@ -16838,7 +16988,7 @@ var WebRTCVertoManager = class extends VertoManager {
16838
16988
  return this.cachedObservable("vertoMediaParams$", () => this.webRtcCallSession.webrtcMessages$.pipe(filterAs(isVertoMediaParamsInnerParams, "params"), (0, import_cjs$15.takeUntil)(this.destroyed$)));
16839
16989
  }
16840
16990
  get vertoBye$() {
16841
- return this.cachedObservable("vertoBye$", () => this.webRtcCallSession.webrtcMessages$.pipe(filterAs(isVertoByeMessage, "params"), (0, import_cjs$15.takeUntil)(this.destroyed$)));
16991
+ return this.cachedObservable("vertoBye$", () => this.webRtcCallSession.webrtcMessages$.pipe(filterAs(isVertoByeInboundMessage, "params"), (0, import_cjs$15.takeUntil)(this.destroyed$)));
16842
16992
  }
16843
16993
  get vertoAttach$() {
16844
16994
  return this.cachedObservable("vertoAttach$", () => this.webRtcCallSession.webrtcMessages$.pipe(filterAs(isVertoAttachMessage, "params"), (0, import_cjs$15.takeUntil)(this.destroyed$)));
@@ -16882,7 +17032,7 @@ var WebRTCVertoManager = class extends VertoManager {
16882
17032
  default:
16883
17033
  }
16884
17034
  } catch (error) {
16885
- logger$15.error(`[WebRTCManager] Error sending Verto ${vertoMethod}:`, error);
17035
+ logger$16.error(`[WebRTCManager] Error sending Verto ${vertoMethod}:`, error);
16886
17036
  this.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
16887
17037
  if (vertoMethod === "verto.modify") this.onModifyFailed?.();
16888
17038
  }
@@ -16897,7 +17047,7 @@ var WebRTCVertoManager = class extends VertoManager {
16897
17047
  sdp
16898
17048
  });
16899
17049
  } catch (error) {
16900
- logger$15.warn("[WebRTCManager] Error processing modify response:", error);
17050
+ logger$16.warn("[WebRTCManager] Error processing modify response:", error);
16901
17051
  const modifyError = error instanceof Error ? error : new Error(String(error), { cause: error });
16902
17052
  this.onError?.(modifyError);
16903
17053
  }
@@ -16907,7 +17057,7 @@ var WebRTCVertoManager = class extends VertoManager {
16907
17057
  const rtcPeerConnController = this._rtcPeerConnectionsMap.get(callId);
16908
17058
  if (!rtcPeerConnController) {
16909
17059
  const signalingError = new DependencyError(`Cannot emit signaling status, RTCPeerConnectionController not found for callID: ${callId}`);
16910
- logger$15.error("[WebRTCManager] Failed to emit signaling status:", {
17060
+ logger$16.error("[WebRTCManager] Failed to emit signaling status:", {
16911
17061
  callId,
16912
17062
  status,
16913
17063
  signalingError
@@ -16923,7 +17073,7 @@ var WebRTCVertoManager = class extends VertoManager {
16923
17073
  this._nodeId$.next(getValueFrom(response, "result.node_id") ?? null);
16924
17074
  const memberId = getValueFrom(response, "result.result.result.memberID") ?? null;
16925
17075
  const callId = getValueFrom(response, "result.result.result.callID") ?? null;
16926
- logger$15.debug("[WebRTCManager] Verto invite response:", {
17076
+ logger$16.debug("[WebRTCManager] Verto invite response:", {
16927
17077
  callId,
16928
17078
  memberId,
16929
17079
  response
@@ -16933,14 +17083,14 @@ var WebRTCVertoManager = class extends VertoManager {
16933
17083
  if (callId) {
16934
17084
  this.webRtcCallSession.addCallId(callId);
16935
17085
  this.attachManager.attach(this.buildAttachableCall(callId));
16936
- } else logger$15.warn("[WebRTCManager] Cannot attach call, missing callId:", {
17086
+ } else logger$16.warn("[WebRTCManager] Cannot attach call, missing callId:", {
16937
17087
  nodeId: this.nodeId,
16938
17088
  callId
16939
17089
  });
16940
- logger$15.info("[WebRTCManager] Verto invite successful");
16941
- logger$15.debug(`[WebRTCManager] nodeid: ${this._nodeId$.value}, selfId: ${this._selfId$.value}`);
17090
+ logger$16.info("[WebRTCManager] Verto invite successful");
17091
+ logger$16.debug(`[WebRTCManager] nodeid: ${this._nodeId$.value}, selfId: ${this._selfId$.value}`);
16942
17092
  } else {
16943
- logger$15.error("[WebRTCManager] Verto invite failed:", response);
17093
+ logger$16.error("[WebRTCManager] Verto invite failed:", response);
16944
17094
  const inviteError = response.error ? new JSONRPCError(response.error.code, response.error.message, response.error.data) : /* @__PURE__ */ new Error("Verto invite failed: unexpected response");
16945
17095
  this.onError?.(inviteError);
16946
17096
  }
@@ -16967,6 +17117,7 @@ var WebRTCVertoManager = class extends VertoManager {
16967
17117
  inputVideoStream: options.inputVideoStream,
16968
17118
  receiveAudio: options.receiveAudio,
16969
17119
  receiveVideo: options.receiveVideo,
17120
+ fallbackToReceiveOnly: options.fallbackToReceiveOnly,
16970
17121
  webRTCApiProvider: this.webRTCApiProvider,
16971
17122
  preferredVideoCodecs: options.preferredVideoCodecs,
16972
17123
  preferredAudioCodecs: options.preferredAudioCodecs,
@@ -16985,17 +17136,17 @@ var WebRTCVertoManager = class extends VertoManager {
16985
17136
  if (options.initOffer) this.handleInboundAnswer(rtcPeerConnController);
16986
17137
  }
16987
17138
  async handleInboundAnswer(rtcPeerConnController) {
16988
- logger$15.debug("[WebRTCManager] Waiting for inbound call to be accepted or rejected");
17139
+ logger$16.debug("[WebRTCManager] Waiting for inbound call to be accepted or rejected");
16989
17140
  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);
16990
17141
  if (vertoByeOrAccepted === null) {
16991
- logger$15.debug("[WebRTCManager] Inbound answer handler aborted (destroyed).");
17142
+ logger$16.debug("[WebRTCManager] Inbound answer handler aborted (destroyed).");
16992
17143
  return;
16993
17144
  }
16994
- if (isVertoByeMessage(vertoByeOrAccepted)) {
16995
- logger$15.info("[WebRTCManager] Inbound call ended by remote before answer.");
17145
+ if (isVertoByeInboundParamsGuard(vertoByeOrAccepted)) {
17146
+ logger$16.info("[WebRTCManager] Inbound call ended by remote before answer.");
16996
17147
  this.callSession?.destroy();
16997
17148
  } else if (!vertoByeOrAccepted) {
16998
- logger$15.info("[WebRTCManager] Inbound call rejected by user.");
17149
+ logger$16.info("[WebRTCManager] Inbound call rejected by user.");
16999
17150
  try {
17000
17151
  await this.bye("USER_BUSY");
17001
17152
  } finally {
@@ -17003,19 +17154,19 @@ var WebRTCVertoManager = class extends VertoManager {
17003
17154
  this.callSession?.destroy();
17004
17155
  }
17005
17156
  } else {
17006
- logger$15.debug("[WebRTCManager] Inbound call accepted, creating SDP answer");
17157
+ logger$16.debug("[WebRTCManager] Inbound call accepted, creating SDP answer");
17007
17158
  const answerOptions = this.webRtcCallSession.answerMediaOptions;
17008
17159
  try {
17009
17160
  await rtcPeerConnController.acceptInbound(answerOptions);
17010
17161
  } catch (error) {
17011
- logger$15.error("[WebRTCManager] Error creating inbound answer:", error);
17162
+ logger$16.error("[WebRTCManager] Error creating inbound answer:", error);
17012
17163
  this.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
17013
17164
  }
17014
17165
  }
17015
17166
  }
17016
17167
  setupVertoAttachHandler() {
17017
17168
  this.subscribeTo(this.vertoAttach$, async (vertoAttach) => {
17018
- logger$15.debug("[WebRTCManager] Received Verto attach event for existing call:", vertoAttach);
17169
+ logger$16.debug("[WebRTCManager] Received Verto attach event for existing call:", vertoAttach);
17019
17170
  const { callID } = vertoAttach;
17020
17171
  await this.attachManager.attach({
17021
17172
  nodeId: this.nodeId ?? void 0,
@@ -17087,17 +17238,17 @@ var WebRTCVertoManager = class extends VertoManager {
17087
17238
  };
17088
17239
  }
17089
17240
  async sendLocalDescriptionOnceAccepted(vertoMessageRequest, rtcPeerConnectionController) {
17090
- logger$15.debug("[WebRTCManager] Waiting for call to be accepted or ended before sending answer");
17241
+ logger$16.debug("[WebRTCManager] Waiting for call to be accepted or ended before sending answer");
17091
17242
  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);
17092
17243
  if (vertoByeOrAccepted === null) {
17093
- logger$15.debug("[WebRTCManager] Destroyed while waiting for call acceptance");
17244
+ logger$16.debug("[WebRTCManager] Destroyed while waiting for call acceptance");
17094
17245
  return;
17095
17246
  }
17096
- if (isVertoByeMessage(vertoByeOrAccepted)) {
17097
- logger$15.info("[WebRTCManager] Call ended before answer was sent.");
17247
+ if (isVertoByeInboundParamsGuard(vertoByeOrAccepted)) {
17248
+ logger$16.info("[WebRTCManager] Call ended before answer was sent.");
17098
17249
  this.callSession?.destroy();
17099
17250
  } else if (!vertoByeOrAccepted) {
17100
- logger$15.info("[WebRTCManager] Call was not accepted, sending verto.bye.");
17251
+ logger$16.info("[WebRTCManager] Call was not accepted, sending verto.bye.");
17101
17252
  try {
17102
17253
  await this.bye("USER_BUSY");
17103
17254
  } finally {
@@ -17105,14 +17256,14 @@ var WebRTCVertoManager = class extends VertoManager {
17105
17256
  this.callSession?.destroy();
17106
17257
  }
17107
17258
  } else {
17108
- logger$15.debug("[WebRTCManager] Call accepted, sending answer");
17259
+ logger$16.debug("[WebRTCManager] Call accepted, sending answer");
17109
17260
  try {
17110
17261
  this.emitMainSignalingStatus(rtcPeerConnectionController.id, "connecting");
17111
17262
  await this.sendLocalDescription(vertoMessageRequest, rtcPeerConnectionController);
17112
17263
  await rtcPeerConnectionController.updateAnswerStatus({ status: "sent" });
17113
17264
  await this.attachManager.attach(this.buildAttachableCall());
17114
17265
  } catch (error) {
17115
- logger$15.error("[WebRTCManager] Error sending Verto answer:", error);
17266
+ logger$16.error("[WebRTCManager] Error sending Verto answer:", error);
17116
17267
  this.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
17117
17268
  await rtcPeerConnectionController.updateAnswerStatus({ status: "failed" });
17118
17269
  }
@@ -17193,9 +17344,11 @@ var WebRTCVertoManager = class extends VertoManager {
17193
17344
  await this.initAdditionalPeerConnection("screenshare", options);
17194
17345
  }
17195
17346
  async initAdditionalPeerConnection(propose, options) {
17347
+ const isScreenShare = propose === "screenshare";
17348
+ let firstPeerConnectionError;
17196
17349
  let rtcPeerConnController = null;
17197
17350
  try {
17198
- this._screenShareStatus$.next("starting");
17351
+ if (isScreenShare) this._screenShareStatus$.next("starting");
17199
17352
  rtcPeerConnController = new RTCPeerConnectionController({
17200
17353
  ...options,
17201
17354
  ...this.RTCPeerConnectionConfig,
@@ -17203,21 +17356,27 @@ var WebRTCVertoManager = class extends VertoManager {
17203
17356
  webRTCApiProvider: this.webRTCApiProvider
17204
17357
  }, void 0, this.deviceController);
17205
17358
  this.setupLocalDescriptionHandler(rtcPeerConnController);
17206
- if (propose === "screenshare") this._screenShareId = rtcPeerConnController.id;
17359
+ if (isScreenShare) this._screenShareId = rtcPeerConnController.id;
17207
17360
  this._rtcPeerConnectionsMap.set(rtcPeerConnController.id, rtcPeerConnController);
17208
17361
  this._rtcPeerConnections$.next(Array.from(this._rtcPeerConnectionsMap.values()));
17209
17362
  this.subscribeTo(rtcPeerConnController.errors$, (error) => {
17210
- this.onError?.(error);
17363
+ firstPeerConnectionError ??= error;
17364
+ this.onError?.(error, { fatal: false });
17211
17365
  });
17212
17366
  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$)));
17213
- this._screenShareStatus$.next("started");
17214
- logger$15.info("[WebRTCManager] Screen share started successfully.");
17367
+ if (isScreenShare) this._screenShareStatus$.next("started");
17368
+ logger$16.info(`[WebRTCManager] Additional peer connection connected (${propose}).`);
17215
17369
  return rtcPeerConnController.id;
17216
17370
  } catch (error) {
17217
- logger$15.warn("[WebRTCManager] Error initializing additional peer connection:", error);
17218
- this.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
17371
+ logger$16.warn("[WebRTCManager] Error initializing additional peer connection:", error);
17219
17372
  if (rtcPeerConnController) rtcPeerConnController.destroy();
17220
- this._screenShareStatus$.next("none");
17373
+ if (isScreenShare) this._screenShareStatus$.next("none");
17374
+ if (firstPeerConnectionError) throw firstPeerConnectionError instanceof MediaAccessError && firstPeerConnectionError.originalError instanceof Error ? firstPeerConnectionError.originalError : firstPeerConnectionError;
17375
+ if (error instanceof import_cjs$15.EmptyError) {
17376
+ logger$16.debug("[WebRTCManager] Additional peer connection aborted before connecting.");
17377
+ return;
17378
+ }
17379
+ throw error instanceof Error ? error : new Error(String(error), { cause: error });
17221
17380
  }
17222
17381
  }
17223
17382
  async removeInputDevices(id) {
@@ -17233,9 +17392,9 @@ var WebRTCVertoManager = class extends VertoManager {
17233
17392
  if (removeTrack) return this.mainPeerConnection.stopTrackSender(removeTrack, { updateTransceiverDirection: true });
17234
17393
  }
17235
17394
  async removeScreenMedia() {
17236
- if (!["starting", "started"].includes(this._screenShareStatus$.value)) logger$15.warn("[WebRTCManager] No active screen share to stop.");
17395
+ if (!["starting", "started"].includes(this._screenShareStatus$.value)) logger$16.warn("[WebRTCManager] No active screen share to stop.");
17237
17396
  if (!this._screenShareId) {
17238
- logger$15.debug("[WebRTCManager] No screen share peer connection found.");
17397
+ logger$16.debug("[WebRTCManager] No screen share peer connection found.");
17239
17398
  return;
17240
17399
  }
17241
17400
  this._screenShareStatus$.next("stopping");
@@ -17264,7 +17423,7 @@ var WebRTCVertoManager = class extends VertoManager {
17264
17423
  dialogParams: this.dialogParams(rtcPeerConnController)
17265
17424
  }));
17266
17425
  } catch (error) {
17267
- logger$15.warn("[WebRTCManager] Call might already be disconnected, error sending Verto bye:", error);
17426
+ logger$16.warn("[WebRTCManager] Call might already be disconnected, error sending Verto bye:", error);
17268
17427
  throw error;
17269
17428
  }
17270
17429
  }
@@ -17282,7 +17441,7 @@ var WebRTCVertoManager = class extends VertoManager {
17282
17441
  try {
17283
17442
  await this.executeVerto(vertoInfoMessage);
17284
17443
  } catch (error) {
17285
- logger$15.warn("[WebRTCManager] Error sending DTMF digits:", error);
17444
+ logger$16.warn("[WebRTCManager] Error sending DTMF digits:", error);
17286
17445
  throw error;
17287
17446
  }
17288
17447
  }
@@ -17293,10 +17452,10 @@ var WebRTCVertoManager = class extends VertoManager {
17293
17452
  action: "transfer"
17294
17453
  });
17295
17454
  try {
17296
- logger$15.debug("[WebRTCManager] Transferring call with options:", options);
17455
+ logger$16.debug("[WebRTCManager] Transferring call with options:", options);
17297
17456
  await this.executeVerto(message);
17298
17457
  } catch (error) {
17299
- logger$15.error("[WebRTCManager] Error transferring call:", error);
17458
+ logger$16.error("[WebRTCManager] Error transferring call:", error);
17300
17459
  throw error;
17301
17460
  }
17302
17461
  }
@@ -17313,7 +17472,7 @@ var WebRTCVertoManager = class extends VertoManager {
17313
17472
  //#endregion
17314
17473
  //#region src/controllers/RemoteAudioMeter.ts
17315
17474
  var import_cjs$14 = require_cjs();
17316
- const logger$14 = getLogger();
17475
+ const logger$15 = getLogger();
17317
17476
  /**
17318
17477
  * Read-only audio level meter for a remote MediaStream. Attaches an
17319
17478
  * AnalyserNode to a MediaStreamAudioSourceNode so it observes the stream
@@ -17348,7 +17507,7 @@ var RemoteAudioMeter = class extends Destroyable {
17348
17507
  try {
17349
17508
  this._source.disconnect();
17350
17509
  } catch (error) {
17351
- logger$14.debug("[RemoteAudioMeter] source disconnect warning:", error);
17510
+ logger$15.debug("[RemoteAudioMeter] source disconnect warning:", error);
17352
17511
  }
17353
17512
  this._source = null;
17354
17513
  this._stream = null;
@@ -17365,7 +17524,7 @@ var RemoteAudioMeter = class extends Destroyable {
17365
17524
  this._source = null;
17366
17525
  }
17367
17526
  this._audioContext.close().catch((error) => {
17368
- logger$14.debug("[RemoteAudioMeter] audio context close warning:", error);
17527
+ logger$15.debug("[RemoteAudioMeter] audio context close warning:", error);
17369
17528
  });
17370
17529
  super.destroy();
17371
17530
  }
@@ -17384,7 +17543,7 @@ var RemoteAudioMeter = class extends Destroyable {
17384
17543
  //#endregion
17385
17544
  //#region src/controllers/RTCStatsMonitor.ts
17386
17545
  var import_cjs$13 = require_cjs();
17387
- const logger$13 = getLogger();
17546
+ const logger$14 = getLogger();
17388
17547
  const DEFAULT_POLLING_INTERVAL_MS = 1e3;
17389
17548
  const DEFAULT_BASELINE_SAMPLES = 10;
17390
17549
  const DEFAULT_NO_AUDIO_PACKET_THRESHOLD_MS = 2e3;
@@ -17474,9 +17633,9 @@ var RTCStatsMonitor = class extends Destroyable {
17474
17633
  const now = Date.now();
17475
17634
  this.lastAudioPacketChangeTime = now;
17476
17635
  this.lastVideoPacketChangeTime = now;
17477
- logger$13.debug("[RTCStatsMonitor] Starting stats monitoring");
17636
+ logger$14.debug("[RTCStatsMonitor] Starting stats monitoring");
17478
17637
  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) => {
17479
- logger$13.warn("[RTCStatsMonitor] Failed to get stats:", err);
17638
+ logger$14.warn("[RTCStatsMonitor] Failed to get stats:", err);
17480
17639
  return import_cjs$13.EMPTY;
17481
17640
  }))), (0, import_cjs$13.filter)(() => this.running), (0, import_cjs$13.map)((report) => this.extractSample(report))), (sample$1) => this._sample$.next(sample$1));
17482
17641
  this.subscribeTo(this._sample$.pipe((0, import_cjs$13.take)(this.baselineSampleCount), (0, import_cjs$13.toArray)(), (0, import_cjs$13.map)((samples) => ({
@@ -17484,7 +17643,7 @@ var RTCStatsMonitor = class extends Destroyable {
17484
17643
  jitter: samples.reduce((s, b) => s + b.audioJitter, 0) / samples.length,
17485
17644
  ready: true
17486
17645
  }))), (baseline) => {
17487
- logger$13.debug(`[RTCStatsMonitor] Baseline established: rtt=${baseline.rtt.toFixed(1)}ms, jitter=${baseline.jitter.toFixed(1)}ms`);
17646
+ logger$14.debug(`[RTCStatsMonitor] Baseline established: rtt=${baseline.rtt.toFixed(1)}ms, jitter=${baseline.jitter.toFixed(1)}ms`);
17488
17647
  this._baseline$.next(baseline);
17489
17648
  });
17490
17649
  this.subscribeTo(this._sample$.pipe((0, import_cjs$13.scan)((acc, sample$1) => ({
@@ -17521,10 +17680,10 @@ var RTCStatsMonitor = class extends Destroyable {
17521
17680
  stop() {
17522
17681
  if (!this.running) return;
17523
17682
  this.running = false;
17524
- logger$13.debug("[RTCStatsMonitor] Stopping stats monitoring");
17683
+ logger$14.debug("[RTCStatsMonitor] Stopping stats monitoring");
17525
17684
  }
17526
17685
  destroy() {
17527
- logger$13.debug("[RTCStatsMonitor] Destroying RTCStatsMonitor");
17686
+ logger$14.debug("[RTCStatsMonitor] Destroying RTCStatsMonitor");
17528
17687
  this.stop();
17529
17688
  super.destroy();
17530
17689
  }
@@ -17653,7 +17812,7 @@ var RTCStatsMonitor = class extends Destroyable {
17653
17812
  //#endregion
17654
17813
  //#region src/managers/CallRecoveryManager.ts
17655
17814
  var import_cjs$12 = require_cjs();
17656
- const logger$12 = getLogger();
17815
+ const logger$13 = getLogger();
17657
17816
  const DEFAULT_DEBOUNCE_TIME_MS = 2e3;
17658
17817
  const DEFAULT_COOLDOWN_MS = 1e4;
17659
17818
  const DEFAULT_ICE_GRACE_PERIOD_MS = 3e3;
@@ -17750,10 +17909,10 @@ var CallRecoveryManager = class extends Destroyable {
17750
17909
  */
17751
17910
  async requestIceRestart() {
17752
17911
  if (this._recoveryState$.value === "recovering") {
17753
- logger$12.info("CallRecoveryManager: manual ICE restart skipped — recovery already in progress");
17912
+ logger$13.info("CallRecoveryManager: manual ICE restart skipped — recovery already in progress");
17754
17913
  return;
17755
17914
  }
17756
- logger$12.info("CallRecoveryManager: manual ICE restart requested");
17915
+ logger$13.info("CallRecoveryManager: manual ICE restart requested");
17757
17916
  this.transitionTo("recovering");
17758
17917
  await this.executeIceRestart(false);
17759
17918
  this.startCooldown();
@@ -17769,7 +17928,7 @@ var CallRecoveryManager = class extends Destroyable {
17769
17928
  * WebSocket reconnect or call state recovers to 'connected'.
17770
17929
  */
17771
17930
  reset() {
17772
- logger$12.info("CallRecoveryManager: resetting counters");
17931
+ logger$13.info("CallRecoveryManager: resetting counters");
17773
17932
  this._attemptCount = 0;
17774
17933
  this._keyframeBurstCount = 0;
17775
17934
  this._keyframeBurstStart = 0;
@@ -17784,7 +17943,7 @@ var CallRecoveryManager = class extends Destroyable {
17784
17943
  */
17785
17944
  notifyModifyFailed() {
17786
17945
  if (this._recoveryState$.value === "cooldown" || this._recoveryState$.value === "idle") {
17787
- logger$12.info("CallRecoveryManager: verto.modify failed — re-entering recovery");
17946
+ logger$13.info("CallRecoveryManager: verto.modify failed — re-entering recovery");
17788
17947
  this._cooldownUntil = 0;
17789
17948
  this.transitionTo("idle");
17790
17949
  this.pushTrigger({
@@ -17808,7 +17967,7 @@ var CallRecoveryManager = class extends Destroyable {
17808
17967
  reason: `bandwidth ${bitrateKbps}kbps below threshold ${this._config.degradationBitrateThreshold}kbps`,
17809
17968
  timestamp: Date.now()
17810
17969
  });
17811
- logger$12.warn(`CallRecoveryManager: disabling video — bandwidth ${bitrateKbps}kbps < ${this._config.degradationBitrateThreshold}kbps`);
17970
+ logger$13.warn(`CallRecoveryManager: disabling video — bandwidth ${bitrateKbps}kbps < ${this._config.degradationBitrateThreshold}kbps`);
17812
17971
  } else if (wasConstrained && bitrateKbps >= this._config.degradationRecoveryThreshold) {
17813
17972
  this._bandwidthConstrained$.next(false);
17814
17973
  this._callbacks.enableVideo();
@@ -17817,7 +17976,7 @@ var CallRecoveryManager = class extends Destroyable {
17817
17976
  reason: `bandwidth ${bitrateKbps}kbps recovered above ${this._config.degradationRecoveryThreshold}kbps`,
17818
17977
  timestamp: Date.now()
17819
17978
  });
17820
- logger$12.info(`CallRecoveryManager: restoring video — bandwidth ${bitrateKbps}kbps >= ${this._config.degradationRecoveryThreshold}kbps`);
17979
+ logger$13.info(`CallRecoveryManager: restoring video — bandwidth ${bitrateKbps}kbps >= ${this._config.degradationRecoveryThreshold}kbps`);
17821
17980
  }
17822
17981
  }
17823
17982
  /**
@@ -17836,14 +17995,14 @@ var CallRecoveryManager = class extends Destroyable {
17836
17995
  handleWebSocketReconnect() {
17837
17996
  const pcState = this._callbacks.getPeerConnectionState();
17838
17997
  if (pcState === "connected" || pcState === "completed") {
17839
- logger$12.info("CallRecoveryManager: signal-only reconnect — peer connection still alive");
17998
+ logger$13.info("CallRecoveryManager: signal-only reconnect — peer connection still alive");
17840
17999
  this.emitEvent({
17841
18000
  action: "signal_reconnect",
17842
18001
  reason: "WebSocket reconnected, peer connection still connected",
17843
18002
  timestamp: Date.now()
17844
18003
  });
17845
18004
  } else {
17846
- logger$12.info("CallRecoveryManager: full reconnect — peer connection also down");
18005
+ logger$13.info("CallRecoveryManager: full reconnect — peer connection also down");
17847
18006
  this.emitEvent({
17848
18007
  action: "full_reconnect",
17849
18008
  reason: "WebSocket reconnected, peer connection not connected — ICE restart needed",
@@ -17867,7 +18026,7 @@ var CallRecoveryManager = class extends Destroyable {
17867
18026
  }), (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$))), {
17868
18027
  next: () => {},
17869
18028
  error: (err) => {
17870
- logger$12.error("CallRecoveryManager: pipeline error", err);
18029
+ logger$13.error("CallRecoveryManager: pipeline error", err);
17871
18030
  this.transitionTo("idle");
17872
18031
  }
17873
18032
  });
@@ -17894,27 +18053,27 @@ var CallRecoveryManager = class extends Destroyable {
17894
18053
  reason: `no packet loss for ${this._config.packetLossRecoveryDelaySec}s — restoring video`,
17895
18054
  timestamp: Date.now()
17896
18055
  });
17897
- logger$12.info(`CallRecoveryManager: restoring video — no packet loss for ${this._config.packetLossRecoveryDelaySec}s`);
18056
+ logger$13.info(`CallRecoveryManager: restoring video — no packet loss for ${this._config.packetLossRecoveryDelaySec}s`);
17898
18057
  });
17899
18058
  }
17900
18059
  passGateChecks(signalingReady) {
17901
18060
  if (this._callbacks.isNegotiating()) {
17902
- logger$12.debug("CallRecoveryManager: gate blocked — negotiation in progress");
18061
+ logger$13.debug("CallRecoveryManager: gate blocked — negotiation in progress");
17903
18062
  this.transitionTo("idle");
17904
18063
  return false;
17905
18064
  }
17906
18065
  if (!signalingReady) {
17907
- logger$12.debug("CallRecoveryManager: gate blocked — signaling not ready");
18066
+ logger$13.debug("CallRecoveryManager: gate blocked — signaling not ready");
17908
18067
  this.transitionTo("idle");
17909
18068
  return false;
17910
18069
  }
17911
18070
  if (!this._callbacks.isCallConnected()) {
17912
- logger$12.debug("CallRecoveryManager: gate blocked — call not connected");
18071
+ logger$13.debug("CallRecoveryManager: gate blocked — call not connected");
17913
18072
  this.transitionTo("idle");
17914
18073
  return false;
17915
18074
  }
17916
18075
  if (this.isCooldownActive()) {
17917
- logger$12.debug("CallRecoveryManager: gate blocked — cooldown active");
18076
+ logger$13.debug("CallRecoveryManager: gate blocked — cooldown active");
17918
18077
  this.transitionTo("cooldown");
17919
18078
  return false;
17920
18079
  }
@@ -17925,9 +18084,9 @@ var CallRecoveryManager = class extends Destroyable {
17925
18084
  }
17926
18085
  executeTieredRecovery(trigger) {
17927
18086
  this.transitionTo("recovering");
17928
- logger$12.info(`CallRecoveryManager: starting tiered recovery — source=${trigger.source} detail=${trigger.detail}`);
18087
+ logger$13.info(`CallRecoveryManager: starting tiered recovery — source=${trigger.source} detail=${trigger.detail}`);
17929
18088
  return (0, import_cjs$12.from)(this.runTiers(trigger)).pipe((0, import_cjs$12.tap)(() => this.startCooldown()), (0, import_cjs$12.catchError)((err) => {
17930
- logger$12.error("CallRecoveryManager: tiered recovery failed", err);
18089
+ logger$13.error("CallRecoveryManager: tiered recovery failed", err);
17931
18090
  this.startCooldown();
17932
18091
  return import_cjs$12.EMPTY;
17933
18092
  }));
@@ -17935,7 +18094,7 @@ var CallRecoveryManager = class extends Destroyable {
17935
18094
  async runTiers(trigger) {
17936
18095
  this.executeKeyframe(trigger.detail);
17937
18096
  if (trigger.issueType && DEGRADATION_ONLY_ISSUES.has(trigger.issueType)) {
17938
- logger$12.debug(`CallRecoveryManager: degradation-only issue (${trigger.issueType}) — Tier 1 only, skipping ICE restart`);
18097
+ logger$13.debug(`CallRecoveryManager: degradation-only issue (${trigger.issueType}) — Tier 1 only, skipping ICE restart`);
17939
18098
  return;
17940
18099
  }
17941
18100
  if (this._attemptCount < this._config.maxAttempts) {
@@ -17952,13 +18111,13 @@ var CallRecoveryManager = class extends Destroyable {
17952
18111
  maxAttempts: this._config.maxAttempts,
17953
18112
  timestamp: Date.now()
17954
18113
  });
17955
- logger$12.warn("CallRecoveryManager: max recovery attempts reached");
18114
+ logger$13.warn("CallRecoveryManager: max recovery attempts reached");
17956
18115
  }
17957
18116
  }
17958
18117
  executeKeyframe(reason) {
17959
18118
  const now = Date.now();
17960
18119
  if (now < this._keyframeCooldownUntil) {
17961
- logger$12.debug("CallRecoveryManager: keyframe request skipped — cooldown active");
18120
+ logger$13.debug("CallRecoveryManager: keyframe request skipped — cooldown active");
17962
18121
  return;
17963
18122
  }
17964
18123
  if (now - this._keyframeBurstStart > this._config.keyframeBurstWindowMs) {
@@ -17967,7 +18126,7 @@ var CallRecoveryManager = class extends Destroyable {
17967
18126
  }
17968
18127
  if (this._keyframeBurstCount >= this._config.keyframeMaxBurst) {
17969
18128
  this._keyframeCooldownUntil = now + this._config.keyframeCooldownMs;
17970
- logger$12.debug(`CallRecoveryManager: keyframe burst limit reached (${this._config.keyframeMaxBurst}), cooldown until ${this._keyframeCooldownUntil}`);
18129
+ logger$13.debug(`CallRecoveryManager: keyframe burst limit reached (${this._config.keyframeMaxBurst}), cooldown until ${this._keyframeCooldownUntil}`);
17971
18130
  return;
17972
18131
  }
17973
18132
  this._keyframeBurstCount += 1;
@@ -17977,12 +18136,12 @@ var CallRecoveryManager = class extends Destroyable {
17977
18136
  reason,
17978
18137
  timestamp: now
17979
18138
  });
17980
- logger$12.debug(`CallRecoveryManager: keyframe requested (burst ${this._keyframeBurstCount}/${this._config.keyframeMaxBurst})`);
18139
+ logger$13.debug(`CallRecoveryManager: keyframe requested (burst ${this._keyframeBurstCount}/${this._config.keyframeMaxBurst})`);
17981
18140
  }
17982
18141
  async executeIceRestart(relayOnly) {
17983
18142
  this._attemptCount += 1;
17984
18143
  const tier = relayOnly ? "Tier 3 (relay-only)" : "Tier 2 (standard)";
17985
- logger$12.info(`CallRecoveryManager: ${tier} ICE restart — attempt ${this._attemptCount}/${this._config.maxAttempts}`);
18144
+ logger$13.info(`CallRecoveryManager: ${tier} ICE restart — attempt ${this._attemptCount}/${this._config.maxAttempts}`);
17986
18145
  this.emitEvent({
17987
18146
  action: "reinvite_started",
17988
18147
  reason: `${tier} ICE restart`,
@@ -17999,7 +18158,7 @@ var CallRecoveryManager = class extends Destroyable {
17999
18158
  maxAttempts: this._config.maxAttempts,
18000
18159
  timestamp: Date.now()
18001
18160
  });
18002
- logger$12.info(`CallRecoveryManager: ${tier} ICE restart succeeded`);
18161
+ logger$13.info(`CallRecoveryManager: ${tier} ICE restart succeeded`);
18003
18162
  this._attemptCount = 0;
18004
18163
  return true;
18005
18164
  }
@@ -18010,7 +18169,7 @@ var CallRecoveryManager = class extends Destroyable {
18010
18169
  maxAttempts: this._config.maxAttempts,
18011
18170
  timestamp: Date.now()
18012
18171
  });
18013
- logger$12.warn(`CallRecoveryManager: ${tier} ICE restart failed`);
18172
+ logger$13.warn(`CallRecoveryManager: ${tier} ICE restart failed`);
18014
18173
  return false;
18015
18174
  } catch {
18016
18175
  this.emitEvent({
@@ -18020,7 +18179,7 @@ var CallRecoveryManager = class extends Destroyable {
18020
18179
  maxAttempts: this._config.maxAttempts,
18021
18180
  timestamp: Date.now()
18022
18181
  });
18023
- logger$12.warn(`CallRecoveryManager: ${tier} ICE restart timed out`);
18182
+ logger$13.warn(`CallRecoveryManager: ${tier} ICE restart timed out`);
18024
18183
  return false;
18025
18184
  }
18026
18185
  }
@@ -18041,7 +18200,7 @@ var CallRecoveryManager = class extends Destroyable {
18041
18200
  transitionTo(state) {
18042
18201
  const prev = this._recoveryState$.value;
18043
18202
  if (prev !== state) {
18044
- logger$12.debug(`CallRecoveryManager: state ${prev} -> ${state}`);
18203
+ logger$13.debug(`CallRecoveryManager: state ${prev} -> ${state}`);
18045
18204
  this._recoveryState$.next(state);
18046
18205
  }
18047
18206
  }
@@ -18144,7 +18303,13 @@ function mosToQualityLevel(mos) {
18144
18303
  //#endregion
18145
18304
  //#region src/core/entities/Call.ts
18146
18305
  var import_cjs$11 = require_cjs();
18147
- const logger$11 = getLogger();
18306
+ const logger$12 = getLogger();
18307
+ /**
18308
+ * Verto method for setting member layout positions. Its gateway DTO requires a
18309
+ * `targets` array whose entries are `{ target, position }` (NOT bare targets),
18310
+ * so {@link WebRTCCall.buildMethodParams} special-cases it. See issue #19400.
18311
+ */
18312
+ const POSITION_SET_METHOD = "call.member.position.set";
18148
18313
  /**
18149
18314
  * Ratio between the critical and warning RTT spike multipliers.
18150
18315
  * Warning threshold = baseline * warningMultiplier (default 3x)
@@ -18162,7 +18327,7 @@ const fromDestinationParams = (destination) => {
18162
18327
  });
18163
18328
  return params;
18164
18329
  } catch (error) {
18165
- logger$11.warn(`Failed to parse destination URI: ${destination}`, error);
18330
+ logger$12.warn(`Failed to parse destination URI: ${destination}`, error);
18166
18331
  return {};
18167
18332
  }
18168
18333
  };
@@ -18352,7 +18517,7 @@ var WebRTCCall = class extends Destroyable {
18352
18517
  * @throws {JSONRPCError} If the RPC call returns an error.
18353
18518
  */
18354
18519
  async executeMethod(target, method, args) {
18355
- const params = this.buildMethodParams(target, args);
18520
+ const params = this.buildMethodParams(target, args, method);
18356
18521
  const request = buildRPCRequest({
18357
18522
  method,
18358
18523
  params
@@ -18362,16 +18527,20 @@ var WebRTCCall = class extends Destroyable {
18362
18527
  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);
18363
18528
  return response;
18364
18529
  } catch (error) {
18365
- logger$11.error(`[Call] Error executing method ${method} with params`, params, error);
18530
+ logger$12.error(`[Call] Error executing method ${method} with params`, params, error);
18366
18531
  throw error;
18367
18532
  }
18368
18533
  }
18369
- buildMethodParams(target, args) {
18534
+ buildMethodParams(target, args, method) {
18370
18535
  const self = {
18371
18536
  node_id: this.nodeId ?? "",
18372
18537
  call_id: this.id,
18373
18538
  member_id: this.vertoManager.selfId ?? ""
18374
18539
  };
18540
+ if (method === POSITION_SET_METHOD) return {
18541
+ ...args,
18542
+ self
18543
+ };
18375
18544
  if (typeof target === "object") return {
18376
18545
  ...args,
18377
18546
  self,
@@ -18565,9 +18734,9 @@ var WebRTCCall = class extends Destroyable {
18565
18734
  */
18566
18735
  initResilienceSubsystems() {
18567
18736
  const pc = this.rtcPeerConnection;
18568
- logger$11.debug(`[Call] initResilienceSubsystems: pc=${pc ? "exists" : "undefined"}, connectionState=${pc?.connectionState}`);
18737
+ logger$12.debug(`[Call] initResilienceSubsystems: pc=${pc ? "exists" : "undefined"}, connectionState=${pc?.connectionState}`);
18569
18738
  if (!pc) {
18570
- logger$11.warn("[Call] No peer connection available, skipping resilience init");
18739
+ logger$12.warn("[Call] No peer connection available, skipping resilience init");
18571
18740
  return;
18572
18741
  }
18573
18742
  try {
@@ -18602,14 +18771,14 @@ var WebRTCCall = class extends Destroyable {
18602
18771
  disableVideo: () => {
18603
18772
  try {
18604
18773
  this.vertoManager.muteMainVideoInputDevice();
18605
- logger$11.debug("[Call] Recovery manager disabled video");
18774
+ logger$12.debug("[Call] Recovery manager disabled video");
18606
18775
  } catch {
18607
- logger$11.debug("[Call] Recovery manager failed to disable video");
18776
+ logger$12.debug("[Call] Recovery manager failed to disable video");
18608
18777
  }
18609
18778
  },
18610
18779
  enableVideo: () => {
18611
18780
  this.vertoManager.unmuteMainVideoInputDevice().catch(() => {
18612
- logger$11.debug("[Call] Recovery manager failed to enable video");
18781
+ logger$12.debug("[Call] Recovery manager failed to enable video");
18613
18782
  });
18614
18783
  },
18615
18784
  isNegotiating: () => this.vertoManager.mainPeerConnection.isNegotiating,
@@ -18659,7 +18828,7 @@ var WebRTCCall = class extends Destroyable {
18659
18828
  this.subscribeTo(this._recoveryManager.recoveryEvent$, (event) => {
18660
18829
  this._recoveryEvent$.next(event);
18661
18830
  if (event.action === "max_attempts_reached") {
18662
- logger$11.warn("[Call] All recovery attempts exhausted, terminating call");
18831
+ logger$12.warn("[Call] All recovery attempts exhausted, terminating call");
18663
18832
  this.emitError({
18664
18833
  kind: "network",
18665
18834
  fatal: true,
@@ -18679,13 +18848,13 @@ var WebRTCCall = class extends Destroyable {
18679
18848
  else if (event.type === "online") this._recoveryManager?.handleWebSocketReconnect();
18680
18849
  });
18681
18850
  this.subscribeTo(this.clientSession.authenticated$.pipe((0, import_cjs$11.skip)(1), (0, import_cjs$11.filter)(Boolean)), () => {
18682
- logger$11.debug("[Call] WebSocket reconnected — notifying recovery manager");
18851
+ logger$12.debug("[Call] WebSocket reconnected — notifying recovery manager");
18683
18852
  this._recoveryManager?.handleWebSocketReconnect();
18684
18853
  });
18685
18854
  this._statsMonitor.start();
18686
- logger$11.debug("[Call] Resilience subsystems initialized for call", this.id);
18855
+ logger$12.debug("[Call] Resilience subsystems initialized for call", this.id);
18687
18856
  } catch (error) {
18688
- logger$11.warn("[Call] Failed to initialize resilience subsystems:", error);
18857
+ logger$12.warn("[Call] Failed to initialize resilience subsystems:", error);
18689
18858
  }
18690
18859
  }
18691
18860
  /**
@@ -18768,19 +18937,19 @@ var WebRTCCall = class extends Destroyable {
18768
18937
  }
18769
18938
  isCallSessionEvent(event) {
18770
18939
  try {
18771
- logger$11.debug("[Call] Checking if event is for this call session:", event);
18940
+ logger$12.debug("[Call] Checking if event is for this call session:", event);
18772
18941
  const callId = getValueFrom(event, "params.params.callID") ?? getValueFrom(event, "params.call_id");
18773
18942
  const roomSessionId = getValueFrom(event, "params.room_session_id");
18774
- logger$11.debug(`[Call] Extracted session identifiers callID: ${callId} and roomSessionID: ${roomSessionId} from event:`);
18943
+ logger$12.debug(`[Call] Extracted session identifiers callID: ${callId} and roomSessionID: ${roomSessionId} from event:`);
18775
18944
  return callId === this.id || !!callId && this.callEventsManager.isCallIdValid(callId) || !!roomSessionId && this.callEventsManager.isRoomSessionIdValid(roomSessionId);
18776
18945
  } catch (error) {
18777
- logger$11.error("[Call] Error checking if event is for this call session:", error);
18946
+ logger$12.error("[Call] Error checking if event is for this call session:", error);
18778
18947
  return false;
18779
18948
  }
18780
18949
  }
18781
18950
  get callSessionEvents$() {
18782
18951
  return this.cachedObservable("callSessionEvents$", () => this.clientSession.signalingEvent$.pipe((0, import_cjs$11.filter)((event) => this.isCallSessionEvent(event)), (0, import_cjs$11.tap)((event) => {
18783
- logger$11.debug("[Call] Received call session event:", event);
18952
+ logger$12.debug("[Call] Received call session event:", event);
18784
18953
  }), (0, import_cjs$11.takeUntil)(this.destroyed$), (0, import_cjs$11.share)()));
18785
18954
  }
18786
18955
  /** Observable of call-updated events. */
@@ -18850,16 +19019,16 @@ var WebRTCCall = class extends Destroyable {
18850
19019
  this._customSubscriptions.set(eventType, filtered$);
18851
19020
  }, (error) => {
18852
19021
  this._customSubscriptions.delete(eventType);
18853
- logger$11.warn(`[Call] verto.subscribe for '${eventType}' failed, not caching:`, error);
19022
+ logger$12.warn(`[Call] verto.subscribe for '${eventType}' failed, not caching:`, error);
18854
19023
  });
18855
19024
  this._customSubscriptions.set(eventType, filtered$);
18856
19025
  return filtered$;
18857
19026
  }
18858
19027
  get webrtcMessages$() {
18859
- 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)()));
19028
+ 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)()));
18860
19029
  }
18861
19030
  get callEvent$() {
18862
- 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)()));
19031
+ 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)()));
18863
19032
  }
18864
19033
  get layoutEvent$() {
18865
19034
  return this.cachedObservable("layoutEvent$", () => this.callEvent$.pipe(filterAs(isLayoutChangedMetadata, "params")));
@@ -18937,10 +19106,21 @@ var WebRTCCall = class extends Destroyable {
18937
19106
  return this.deferEmission(this._answered$.asObservable());
18938
19107
  }
18939
19108
  /**
18940
- * Sets the call layout and participant positions.
19109
+ * Sets the call layout and, optionally, individual participant positions.
19110
+ *
19111
+ * The gateway `call.layout.set` DTO has **no** `positions` member, so when
19112
+ * `positions` is provided this method issues a `call.member.position.set`
19113
+ * request per member (via {@link Participant.setPosition}, which keys each
19114
+ * position by that member's own call context) alongside `call.layout.set`
19115
+ * (issue #19400, Flag #6).
19116
+ *
19117
+ * **These operations are NOT atomic.** The layout is applied first, then each
19118
+ * member position sequentially, so members may briefly flash into their
19119
+ * default slots before being moved to the requested positions.
18941
19120
  *
18942
19121
  * @param layout - Layout name (must be one of {@link layouts}).
18943
- * @param positions - Map of member IDs to {@link VideoPosition} values.
19122
+ * @param positions - Optional map of member IDs to {@link VideoPosition} values.
19123
+ * When omitted or empty, only the layout is changed.
18944
19124
  * @throws {InvalidParams} If the layout is not in the available {@link layouts}.
18945
19125
  *
18946
19126
  * @example
@@ -18953,10 +19133,17 @@ var WebRTCCall = class extends Destroyable {
18953
19133
  async setLayout(layout, positions) {
18954
19134
  if (!this.layouts.includes(layout)) throw new InvalidParams(`Layout ${layout} is not available in the current call layouts: ${this.layouts.join(", ")}`);
18955
19135
  const selfId = await (0, import_cjs$11.firstValueFrom)(this.selfId$.pipe((0, import_cjs$11.filter)((id) => id !== null)));
18956
- await this.executeMethod(selfId, "call.layout.set", {
18957
- layout,
18958
- positions
18959
- });
19136
+ await this.executeMethod(selfId, "call.layout.set", { layout });
19137
+ const positionEntries = Object.entries(positions ?? {});
19138
+ if (positionEntries.length === 0) return;
19139
+ for (const [memberId, position] of positionEntries) {
19140
+ const participant = this.participants.find((p) => p.id === memberId);
19141
+ if (!participant) {
19142
+ logger$12.warn(`[Call] setLayout: member ${memberId} not found in participants; skipping position ${position}`);
19143
+ continue;
19144
+ }
19145
+ await participant.setPosition(position);
19146
+ }
18960
19147
  }
18961
19148
  /**
18962
19149
  * Transfers the call to another destination.
@@ -18988,7 +19175,7 @@ var WebRTCCall = class extends Destroyable {
18988
19175
  setLocalMicrophoneGain(value) {
18989
19176
  const pipeline = this.vertoManager.ensureLocalAudioPipeline();
18990
19177
  if (!pipeline) {
18991
- logger$11.warn("[Call] setLocalMicrophoneGain: audio pipeline unavailable");
19178
+ logger$12.warn("[Call] setLocalMicrophoneGain: audio pipeline unavailable");
18992
19179
  return;
18993
19180
  }
18994
19181
  const percent = Math.max(0, Math.min(200, value));
@@ -19033,7 +19220,7 @@ var WebRTCCall = class extends Destroyable {
19033
19220
  enablePushToTalk() {
19034
19221
  const pipeline = this.vertoManager.ensureLocalAudioPipeline();
19035
19222
  if (!pipeline) {
19036
- logger$11.warn("[Call] enablePushToTalk: audio pipeline unavailable");
19223
+ logger$12.warn("[Call] enablePushToTalk: audio pipeline unavailable");
19037
19224
  return;
19038
19225
  }
19039
19226
  pipeline.setPTTActive(false);
@@ -19130,6 +19317,7 @@ function inferCallErrorKind(error) {
19130
19317
  if (error instanceof RPCTimeoutError) return "timeout";
19131
19318
  if (error instanceof JSONRPCError) return "signaling";
19132
19319
  if (error instanceof MediaTrackError) return "media";
19320
+ if (error instanceof MediaAccessError) return "media";
19133
19321
  if (error instanceof WebSocketConnectionError || error instanceof TransportConnectionError) return "network";
19134
19322
  return "internal";
19135
19323
  }
@@ -19146,6 +19334,7 @@ const RECOVERABLE_RPC_CODES = new Set([
19146
19334
  function isFatalError(error) {
19147
19335
  if (error instanceof VertoPongError) return false;
19148
19336
  if (error instanceof MediaTrackError) return false;
19337
+ if (error instanceof MediaAccessError) return error.fatal;
19149
19338
  if (error instanceof RPCTimeoutError) return false;
19150
19339
  if (error instanceof JSONRPCError && RECOVERABLE_RPC_CODES.has(error.code)) return false;
19151
19340
  return true;
@@ -19171,10 +19360,10 @@ var CallFactory = class {
19171
19360
  return {
19172
19361
  vertoManager: new WebRTCVertoManager(callInstance, this.attachManager, this.deviceController, this.webRTCApiProvider, {
19173
19362
  nodeId: options.nodeId,
19174
- onError: (error) => {
19363
+ onError: (error, options$1) => {
19175
19364
  const callError = {
19176
19365
  kind: inferCallErrorKind(error),
19177
- fatal: isFatalError(error),
19366
+ fatal: options$1?.fatal ?? isFatalError(error),
19178
19367
  error,
19179
19368
  callId: callInstance.id
19180
19369
  };
@@ -19196,7 +19385,7 @@ var CallFactory = class {
19196
19385
  //#endregion
19197
19386
  //#region src/behaviors/Collection.ts
19198
19387
  var import_cjs$10 = require_cjs();
19199
- const logger$10 = getLogger();
19388
+ const logger$11 = getLogger();
19200
19389
  var Fetcher = class {
19201
19390
  constructor(endpoint, params, http) {
19202
19391
  this.endpoint = endpoint;
@@ -19220,7 +19409,7 @@ var Fetcher = class {
19220
19409
  this.hasMore = !!this.nextUrl;
19221
19410
  return result.data.filter(this.filter).map(this.mapper);
19222
19411
  }
19223
- logger$10.error("Failed to fetch entity");
19412
+ logger$11.error("Failed to fetch entity");
19224
19413
  return [];
19225
19414
  }
19226
19415
  async id(v) {
@@ -19296,7 +19485,7 @@ var EntityCollection = class extends Destroyable {
19296
19485
  this._hasMore$.next(this.fetchController.hasMore ?? false);
19297
19486
  this._loading$.next(false);
19298
19487
  } catch (error) {
19299
- logger$10.error(`Failed to fetch initial collection data`, error);
19488
+ logger$11.error(`Failed to fetch initial collection data`, error);
19300
19489
  this._hasMore$.next(this.fetchController.hasMore ?? false);
19301
19490
  this._loading$.next(false);
19302
19491
  this.onError?.(new CollectionFetchError("fetchMore", error));
@@ -19310,7 +19499,7 @@ var EntityCollection = class extends Destroyable {
19310
19499
  if (data) this.upsertData(data);
19311
19500
  return data;
19312
19501
  } catch (error) {
19313
- logger$10.error(`Failed to fetch data for (${String(key)}:${String(value)}) :`, error);
19502
+ logger$11.error(`Failed to fetch data for (${String(key)}:${String(value)}) :`, error);
19314
19503
  this._loading$.next(false);
19315
19504
  this.onError?.(new CollectionFetchError(`tryFetch(${String(key)})`, error));
19316
19505
  }
@@ -19559,13 +19748,13 @@ var Address = class extends Destroyable {
19559
19748
  //#endregion
19560
19749
  //#region src/core/utils.ts
19561
19750
  var import_cjs$8 = require_cjs();
19562
- const logger$9 = getLogger();
19751
+ const logger$10 = getLogger();
19563
19752
  const isRPCConnectResult = (e) => {
19564
- logger$9.debug("isRPCConnectResult check:", e);
19753
+ logger$10.debug("isRPCConnectResult check:", e);
19565
19754
  if (!e || typeof e !== "object") return false;
19566
19755
  const result = e;
19567
19756
  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";
19568
- logger$9.debug("isRPCConnectResult check result:", is);
19757
+ logger$10.debug("isRPCConnectResult check result:", is);
19569
19758
  return is;
19570
19759
  };
19571
19760
  var PendingRPC = class PendingRPC {
@@ -19574,7 +19763,7 @@ var PendingRPC = class PendingRPC {
19574
19763
  }
19575
19764
  constructor(request, responses$, options) {
19576
19765
  this.id = v4_default();
19577
- logger$9.debug(`[PendingRPC(${this.id}) request:${request.id}: method:${request.method}] Creating PendingRPC`);
19766
+ logger$10.debug(`[PendingRPC(${this.id}) request:${request.id}: method:${request.method}] Creating PendingRPC`);
19578
19767
  this.request = request;
19579
19768
  const timeoutMs = options?.timeoutMs ?? PendingRPC.defaultTimeoutMs;
19580
19769
  const signal = options?.signal;
@@ -19600,22 +19789,22 @@ var PendingRPC = class PendingRPC {
19600
19789
  isSettled = true;
19601
19790
  if (response.error) {
19602
19791
  const rpcError = new JSONRPCError(response.error.code, response.error.message, response.error.data, void 0, request.id);
19603
- logger$9.debug(`[PendingRPC(${this.id}) request:${request.id}] Rejecting promise with RPC error:`, rpcError);
19792
+ logger$10.debug(`[PendingRPC(${this.id}) request:${request.id}] Rejecting promise with RPC error:`, rpcError);
19604
19793
  reject(rpcError);
19605
19794
  } else {
19606
- logger$9.debug(`[PendingRPC(${this.id}) request:${request.id}] Resolving promise with response:`, response);
19795
+ logger$10.debug(`[PendingRPC(${this.id}) request:${request.id}] Resolving promise with response:`, response);
19607
19796
  resolve(response);
19608
19797
  }
19609
19798
  subscription.unsubscribe();
19610
19799
  },
19611
19800
  error: (error) => {
19612
- logger$9.debug(`[PendingRPC(${this.id}) request:${request.id}] Rejecting promise with error:`, error);
19801
+ logger$10.debug(`[PendingRPC(${this.id}) request:${request.id}] Rejecting promise with error:`, error);
19613
19802
  isSettled = true;
19614
19803
  reject(error);
19615
19804
  subscription.unsubscribe();
19616
19805
  },
19617
19806
  complete: () => {
19618
- logger$9.debug(`[PendingRPC(${this.id}) request:${request.id}] Observable completed`);
19807
+ logger$10.debug(`[PendingRPC(${this.id}) request:${request.id}] Observable completed`);
19619
19808
  if (!isSettled) reject(new RPCTimeoutError(request.id, timeoutMs));
19620
19809
  subscription.unsubscribe();
19621
19810
  }
@@ -19636,7 +19825,18 @@ var PendingRPC = class PendingRPC {
19636
19825
  //#endregion
19637
19826
  //#region src/managers/ClientSessionManager.ts
19638
19827
  var import_cjs$7 = require_cjs();
19639
- const logger$8 = getLogger();
19828
+ const logger$9 = getLogger();
19829
+ /**
19830
+ * Decide whether an error emitted on `call.errors$` during dial should
19831
+ * abort the dial. A non-fatal MediaAccessError means the call degraded to
19832
+ * receive-only and still connects — everything else rejects `dial()` with
19833
+ * the real cause.
19834
+ *
19835
+ * Pure function — exported for unit testing.
19836
+ */
19837
+ function shouldAbortDial(callError) {
19838
+ return callError.fatal || !(callError.error instanceof MediaAccessError);
19839
+ }
19640
19840
  const getAddressSearchURI = (options) => {
19641
19841
  const to = options.to?.split("?")[0];
19642
19842
  const from$9 = options.from?.startsWith("subscriber://") ? options.from.replace("subscriber://", "") : options.from;
@@ -19734,7 +19934,7 @@ var ClientSessionManager = class extends Destroyable {
19734
19934
  try {
19735
19935
  return await this.transport.execute(request, options);
19736
19936
  } catch (error) {
19737
- logger$8.debug("[Session] Execute Error", error);
19937
+ logger$9.debug("[Session] Execute Error", error);
19738
19938
  this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
19739
19939
  throw error;
19740
19940
  }
@@ -19748,13 +19948,13 @@ var ClientSessionManager = class extends Destroyable {
19748
19948
  return true;
19749
19949
  }
19750
19950
  setupMessageHandlers() {
19751
- logger$8.debug("[Session] Setting up message handlers");
19951
+ logger$9.debug("[Session] Setting up message handlers");
19752
19952
  this.subscribeTo(this.authStateEvent$, async (authStateEvent) => {
19753
- logger$8.debug("[Session] Authorization state event received:", authStateEvent);
19953
+ logger$9.debug("[Session] Authorization state event received:", authStateEvent);
19754
19954
  try {
19755
19955
  await this.updateAuthorizationStateInStorage(authStateEvent.authorization_state);
19756
19956
  } catch (error) {
19757
- logger$8.error("[Session] Failed to handle authorization state update:", error);
19957
+ logger$9.error("[Session] Failed to handle authorization state update:", error);
19758
19958
  this._errors$.next(new AuthStateHandlerError(error));
19759
19959
  }
19760
19960
  });
@@ -19762,29 +19962,29 @@ var ClientSessionManager = class extends Destroyable {
19762
19962
  if (this._authState$.value.kind === "authenticated") this._authState$.next({ kind: "unauthenticated" });
19763
19963
  });
19764
19964
  this.subscribeTo(this.transport.connectionStatus$.pipe((0, import_cjs$7.filter)((status) => status === "connected"), (0, import_cjs$7.exhaustMap)(() => {
19765
- logger$8.debug("[Session] Connection established, initiating authentication");
19965
+ logger$9.debug("[Session] Connection established, initiating authentication");
19766
19966
  return (0, import_cjs$7.from)(this.authenticate()).pipe((0, import_cjs$7.catchError)((error) => {
19767
19967
  this.handleAuthenticationError(error).catch((err) => {
19768
- logger$8.error("[Session] Error handling authentication failure:", err);
19968
+ logger$9.error("[Session] Error handling authentication failure:", err);
19769
19969
  });
19770
19970
  return import_cjs$7.EMPTY;
19771
19971
  }));
19772
19972
  })), void 0);
19773
19973
  this.subscribeTo(this.vertoInvite$, async (invite) => {
19774
- logger$8.debug("[Session] Verto invite received:", invite);
19974
+ logger$9.debug("[Session] Verto invite received:", invite);
19775
19975
  try {
19776
19976
  await this.createInboundCall(invite);
19777
19977
  } catch (error) {
19778
- logger$8.error("[Session] Error handling Verto invite:", error);
19978
+ logger$9.error("[Session] Error handling Verto invite:", error);
19779
19979
  this._errors$.next(new VertoInviteHandlerError(error));
19780
19980
  }
19781
19981
  });
19782
19982
  this.subscribeTo(this.vertoAttach$, async (attach) => {
19783
- logger$8.debug("[Session] Verto attach received:", attach);
19983
+ logger$9.debug("[Session] Verto attach received:", attach);
19784
19984
  try {
19785
19985
  await this.handleVertoAttach(attach);
19786
19986
  } catch (error) {
19787
- logger$8.error("[Session] Error handling Verto attach:", error);
19987
+ logger$9.error("[Session] Error handling Verto attach:", error);
19788
19988
  this._errors$.next(new VertoAttachHandlerError(error));
19789
19989
  }
19790
19990
  });
@@ -19794,36 +19994,36 @@ var ClientSessionManager = class extends Destroyable {
19794
19994
  const storedState = await this.storage.getItem(this.authorizationStateKey);
19795
19995
  this.authorizationState$.next(storedState ?? void 0);
19796
19996
  } catch (error) {
19797
- logger$8.error("Failed to retrieve authorization state from storage:", error);
19997
+ logger$9.error("Failed to retrieve authorization state from storage:", error);
19798
19998
  this.authorizationState$.next(void 0);
19799
19999
  }
19800
20000
  }
19801
20001
  async updateAuthorizationStateInStorage(authorizationState) {
19802
20002
  if (!authorizationState) {
19803
- logger$8.debug("[Session] Removing authorization state from storage");
20003
+ logger$9.debug("[Session] Removing authorization state from storage");
19804
20004
  try {
19805
20005
  await this.storage.removeItem(this.authorizationStateKey);
19806
20006
  this.authorizationState$.next(void 0);
19807
20007
  } catch (error) {
19808
- logger$8.error("Failed to remove authorization state from storage:", error);
20008
+ logger$9.error("Failed to remove authorization state from storage:", error);
19809
20009
  throw error;
19810
20010
  }
19811
20011
  return;
19812
20012
  }
19813
20013
  try {
19814
- logger$8.debug("[Session] Updating authorization state in storage");
20014
+ logger$9.debug("[Session] Updating authorization state in storage");
19815
20015
  await this.storage.setItem(this.authorizationStateKey, authorizationState);
19816
20016
  this.authorizationState$.next(authorizationState);
19817
20017
  } catch (error) {
19818
- logger$8.error("Failed to retrieve authorization state from storage:", error);
20018
+ logger$9.error("Failed to retrieve authorization state from storage:", error);
19819
20019
  throw error;
19820
20020
  }
19821
20021
  }
19822
20022
  get authStateEvent$() {
19823
20023
  return this.cachedObservable("authStateEvent$", () => this.signalingEvent$.pipe((0, import_cjs$7.tap)((msg) => {
19824
- logger$8.debug("[Session] Received incoming message:", msg);
20024
+ logger$9.debug("[Session] Received incoming message:", msg);
19825
20025
  }), filterAs(isSignalwireAuthorizationStateMetadata, "params"), (0, import_cjs$7.tap)((event) => {
19826
- logger$8.debug("[Session] Authorization state event received:", event.authorization_state);
20026
+ logger$9.debug("[Session] Authorization state event received:", event.authorization_state);
19827
20027
  })));
19828
20028
  }
19829
20029
  get signalingEvent$() {
@@ -19861,42 +20061,72 @@ var ClientSessionManager = class extends Destroyable {
19861
20061
  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 })));
19862
20062
  }
19863
20063
  async handleAuthenticationError(error) {
19864
- logger$8.error("Authentication error:", error);
20064
+ logger$9.error("Authentication error:", error);
19865
20065
  const isRecoverableAuthError = error instanceof JSONRPCError && (error.code === RPC_ERROR_REQUESTER_VALIDATION_FAILED || error.code === RPC_ERROR_INVALID_PARAMS || error.code === RPC_ERROR_AUTHENTICATION_FAILED);
19866
20066
  const hasStoredState = await (0, import_cjs$7.firstValueFrom)(this.authorizationState$.pipe((0, import_cjs$7.take)(1))) !== void 0;
19867
20067
  if (isRecoverableAuthError && hasStoredState) {
19868
- logger$8.debug("[Session] Recoverable auth error — cleaning up stored state and reconnecting fresh");
20068
+ logger$9.debug("[Session] Recoverable auth error — cleaning up stored state and reconnecting fresh");
19869
20069
  try {
19870
20070
  await this.cleanupStoredConnectionParams();
19871
20071
  } catch (cleanupError) {
19872
- logger$8.error("Failed to cleanup stored connection params:", cleanupError);
20072
+ logger$9.error("Failed to cleanup stored connection params:", cleanupError);
19873
20073
  } finally {
19874
20074
  this.transport.reconnect();
19875
20075
  }
19876
20076
  } else this._errors$.next(error);
19877
20077
  }
20078
+ /**
20079
+ * Clear the resume state (authorization_state + protocol) only.
20080
+ *
20081
+ * This is the stale-auth-state recovery helper used by handleAuthError:
20082
+ * the server rejected a reconnect, so the resume state is discarded and a
20083
+ * fresh connect follows. Attach records are deliberately preserved — the
20084
+ * session lives on through the reconnect and reattachCalls() needs the
20085
+ * stored call references afterwards. Do NOT add detachAll() here.
20086
+ *
20087
+ * For public teardown (disconnect/destroy), use {@link teardownSessionState}
20088
+ * instead, which clears the attach records as well.
20089
+ */
19878
20090
  async cleanupStoredConnectionParams() {
19879
20091
  await this.transport.setProtocol(void 0);
19880
20092
  await this.updateAuthorizationStateInStorage(void 0);
19881
20093
  this._authorization$.next(void 0);
19882
20094
  }
20095
+ /**
20096
+ * Public-teardown helper for disconnect()/destroy(). Clears the resume
20097
+ * state (authorization_state + protocol) AND the attach records as one
20098
+ * atomic unit.
20099
+ *
20100
+ * The two stores are coupled: the backend only honors attach records
20101
+ * within the session identified by the resume state, so ending the
20102
+ * session must clear both. Clearing one without the other strands records
20103
+ * no future session can honor (disconnect) or revives a session the
20104
+ * developer explicitly ended (destroy).
20105
+ *
20106
+ * Distinct from {@link cleanupStoredConnectionParams}, which keeps the
20107
+ * attach records for the stale-auth-state recovery path.
20108
+ */
20109
+ async teardownSessionState() {
20110
+ await this.cleanupStoredConnectionParams();
20111
+ await this.attachManager.detachAll();
20112
+ }
19883
20113
  async updateAuthState(authorization_state) {
19884
20114
  try {
19885
20115
  await this.storage.setItem(this.authorizationStateKey, authorization_state);
19886
20116
  } catch (error) {
19887
- logger$8.error("Failed to update authorization state in storage:", error);
20117
+ logger$9.error("Failed to update authorization state in storage:", error);
19888
20118
  this._errors$.next(new AuthStateHandlerError(error));
19889
20119
  }
19890
20120
  }
19891
20121
  async reauthenticate(token, dpopToken, options) {
19892
- logger$8.debug("[Session] Re-authenticating session");
20122
+ logger$9.debug("[Session] Re-authenticating session");
19893
20123
  try {
19894
20124
  let resolvedDpopToken = dpopToken;
19895
20125
  if (!resolvedDpopToken && this.dpopManager?.initialized) try {
19896
20126
  resolvedDpopToken = await this.dpopManager.createRpcProof({ method: "signalwire.reauthenticate" });
19897
20127
  } catch (error) {
19898
20128
  if (this.clientBound) throw error;
19899
- logger$8.warn("[Session] Failed to create DPoP proof for reauthenticate:", error);
20129
+ logger$9.warn("[Session] Failed to create DPoP proof for reauthenticate:", error);
19900
20130
  }
19901
20131
  const request = RPCReauthenticate({
19902
20132
  project: this._authorization$.value?.project_id ?? "",
@@ -19904,24 +20134,24 @@ var ClientSessionManager = class extends Destroyable {
19904
20134
  ...resolvedDpopToken ? { dpop_token: resolvedDpopToken } : {}
19905
20135
  });
19906
20136
  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) => {
19907
- logger$8.error("[Session] Re-authentication RPC failed:", err);
20137
+ logger$9.error("[Session] Re-authentication RPC failed:", err);
19908
20138
  throw err;
19909
20139
  })));
19910
20140
  if (options?.clientBound) this._wasClientBound = true;
19911
- logger$8.debug("[Session] Re-authentication successful, updating stored auth state");
20141
+ logger$9.debug("[Session] Re-authentication successful, updating stored auth state");
19912
20142
  } catch (error) {
19913
- logger$8.error("[Session] Re-authentication failed:", error);
20143
+ logger$9.error("[Session] Re-authentication failed:", error);
19914
20144
  this._errors$.next(new AuthStateHandlerError(error));
19915
20145
  throw error;
19916
20146
  }
19917
20147
  }
19918
20148
  async authenticate() {
19919
- logger$8.debug("[Session] Starting authentication process");
20149
+ logger$9.debug("[Session] Starting authentication process");
19920
20150
  const persistedParams = await (0, import_cjs$7.firstValueFrom)((0, import_cjs$7.combineLatest)({
19921
20151
  protocol: this.transport.protocol$,
19922
20152
  authorization_state: this.authorizationState$
19923
20153
  }).pipe((0, import_cjs$7.take)(1)));
19924
- logger$8.debug("[Session] Persisted params:\n", {
20154
+ logger$9.debug("[Session] Persisted params:\n", {
19925
20155
  protocol: persistedParams.protocol,
19926
20156
  authStateLength: persistedParams.authorization_state?.length
19927
20157
  });
@@ -19929,16 +20159,16 @@ var ClientSessionManager = class extends Destroyable {
19929
20159
  const storedToken = this.getCredential().token;
19930
20160
  const isReconnect = hasReconnectState && storedToken;
19931
20161
  let dpopToken;
19932
- if (isReconnect) logger$8.debug("[Session] Reconnecting with stored jwt_token + authorization_state");
20162
+ if (isReconnect) logger$9.debug("[Session] Reconnecting with stored jwt_token + authorization_state");
19933
20163
  else if (this.onBeforeReconnect && this.clientBound) {
19934
- logger$8.debug("[Session] Refreshing credentials before fresh connect");
20164
+ logger$9.debug("[Session] Refreshing credentials before fresh connect");
19935
20165
  await this.onBeforeReconnect();
19936
20166
  }
19937
20167
  if ((!isReconnect || this.clientBound) && this.dpopManager?.initialized) try {
19938
20168
  dpopToken = await this.dpopManager.createRpcProof({ method: "signalwire.connect" });
19939
20169
  } catch (error) {
19940
20170
  if (this.clientBound) throw error;
19941
- logger$8.warn("[Session] Failed to create DPoP proof for connect, proceeding without:", error);
20171
+ logger$9.warn("[Session] Failed to create DPoP proof for connect, proceeding without:", error);
19942
20172
  }
19943
20173
  const rpcConnectRequest = RPCConnect({
19944
20174
  authentication: isReconnect ? { jwt_token: storedToken } : this.authentication,
@@ -19955,12 +20185,12 @@ var ClientSessionManager = class extends Destroyable {
19955
20185
  } : {}
19956
20186
  });
19957
20187
  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)(() => {
19958
- logger$8.debug("[Session] Response passed filter, processing authentication result");
20188
+ logger$9.debug("[Session] Response passed filter, processing authentication result");
19959
20189
  }), (0, import_cjs$7.take)(1), (0, import_cjs$7.catchError)((err) => {
19960
- logger$8.error("[Session] Authentication RPC failed:", err);
20190
+ logger$9.error("[Session] Authentication RPC failed:", err);
19961
20191
  throw err;
19962
20192
  })));
19963
- logger$8.debug("[Session] Processing authentication result:", {
20193
+ logger$9.debug("[Session] Processing authentication result:", {
19964
20194
  hasProtocol: !!response.protocol,
19965
20195
  hasAuthorization: !!response.authorization,
19966
20196
  hasIceServers: !!response.ice_servers
@@ -19969,12 +20199,12 @@ var ClientSessionManager = class extends Destroyable {
19969
20199
  this._authorization$.next(response.authorization);
19970
20200
  this._iceServers$.next(response.ice_servers ?? []);
19971
20201
  this._authState$.next({ kind: "authenticated" });
19972
- logger$8.debug("[Session] Authentication completed successfully");
20202
+ logger$9.debug("[Session] Authentication completed successfully");
19973
20203
  }
19974
20204
  async disconnect() {
19975
20205
  this.transport.disconnect();
19976
20206
  this._authState$.next({ kind: "unauthenticated" });
19977
- await this.cleanupStoredConnectionParams();
20207
+ await this.teardownSessionState();
19978
20208
  }
19979
20209
  async createInboundCall(invite) {
19980
20210
  const callSession = await this.createCall({
@@ -20005,11 +20235,11 @@ var ClientSessionManager = class extends Destroyable {
20005
20235
  async handleVertoAttach(attach) {
20006
20236
  const { callID } = attach;
20007
20237
  if (callID in this._calls$.value) {
20008
- logger$8.debug(`[Session] Verto attach for existing call ${callID}, deferring to per-call handler`);
20238
+ logger$9.debug(`[Session] Verto attach for existing call ${callID}, deferring to per-call handler`);
20009
20239
  return;
20010
20240
  }
20011
20241
  const storedOptions = await this.attachManager.consumePendingAttachment(callID);
20012
- logger$8.debug(`[Session] Creating reattached call for callID: ${callID}`);
20242
+ logger$9.debug(`[Session] Creating reattached call for callID: ${callID}`);
20013
20243
  const callSession = await this.createCall({
20014
20244
  nodeId: attach.node_id,
20015
20245
  callId: callID,
@@ -20033,14 +20263,14 @@ var ClientSessionManager = class extends Destroyable {
20033
20263
  to: destinationURI,
20034
20264
  ...options
20035
20265
  });
20036
- 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)))));
20266
+ 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)))));
20037
20267
  this._calls$.next({
20038
20268
  [`${callSession.id}`]: callSession,
20039
20269
  ...this._calls$.value
20040
20270
  });
20041
20271
  return callSession;
20042
20272
  } catch (error) {
20043
- logger$8.error("[Session] Error creating outbound call:", error);
20273
+ logger$9.error("[Session] Error creating outbound call:", error);
20044
20274
  callSession?.destroy();
20045
20275
  const callError = new CallCreateError(error instanceof import_cjs$7.TimeoutError ? "Call create timeout" : "Call creation failed", error, "outbound");
20046
20276
  this._errors$.next(callError);
@@ -20058,7 +20288,7 @@ var ClientSessionManager = class extends Destroyable {
20058
20288
  address = this._directory.get(addressId);
20059
20289
  if (!address) throw new DependencyError(`Address ID: ${addressId} not found`);
20060
20290
  } catch {
20061
- logger$8.warn(`[Session] Directory lookup failed for ${addressURI}, proceeding with raw URI`);
20291
+ logger$9.warn(`[Session] Directory lookup failed for ${addressURI}, proceeding with raw URI`);
20062
20292
  }
20063
20293
  const callSession = this.callFactory.createCall(address, { ...options });
20064
20294
  this.subscribeTo(callSession.status$.pipe((0, import_cjs$7.filter)((status) => status === "destroyed"), (0, import_cjs$7.take)(1)), () => {
@@ -20067,7 +20297,7 @@ var ClientSessionManager = class extends Destroyable {
20067
20297
  });
20068
20298
  return callSession;
20069
20299
  } catch (error) {
20070
- logger$8.error("[Session] Error creating call session:", error);
20300
+ logger$9.error("[Session] Error creating call session:", error);
20071
20301
  throw new CallCreateError("Call create error", error, options.initOffer ? "inbound" : "outbound");
20072
20302
  }
20073
20303
  }
@@ -20116,7 +20346,7 @@ const isString = (obj) => typeof obj === "string";
20116
20346
  //#endregion
20117
20347
  //#region src/managers/ConversationsManager.ts
20118
20348
  var import_cjs$6 = require_cjs();
20119
- const logger$7 = getLogger();
20349
+ const logger$8 = getLogger();
20120
20350
  var ConversationMessagesFetcher = class extends Fetcher {
20121
20351
  constructor(groupId, http) {
20122
20352
  super(`/api/fabric/conversations/${groupId}/messages`, "page_size=100", http);
@@ -20156,13 +20386,13 @@ var ConversationsManager = class {
20156
20386
  }
20157
20387
  throw new ConversationError("Join Failed - Unexpected response");
20158
20388
  } catch (error) {
20159
- logger$7.error("[ConversationsManager] Failed to join conversation:", error);
20389
+ logger$8.error("[ConversationsManager] Failed to join conversation:", error);
20160
20390
  throw error;
20161
20391
  }
20162
20392
  }
20163
20393
  async getConversationMessageCollection(addressId) {
20164
20394
  const groupId = this.groupIds.get(addressId) ?? await this.join(addressId);
20165
- 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));
20395
+ 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));
20166
20396
  }
20167
20397
  async sendText(text, destinationAddressId) {
20168
20398
  const groupId = this.groupIds.get(destinationAddressId) ?? await this.join(destinationAddressId);
@@ -20179,7 +20409,7 @@ var ConversationsManager = class {
20179
20409
  })).ok) return;
20180
20410
  throw new ConversationError("Send Text Failed - Unexpected response");
20181
20411
  } catch (error) {
20182
- logger$7.error("[ConversationsManager] Failed to send text message:", error);
20412
+ logger$8.error("[ConversationsManager] Failed to send text message:", error);
20183
20413
  throw error;
20184
20414
  }
20185
20415
  }
@@ -20188,7 +20418,7 @@ var ConversationsManager = class {
20188
20418
  //#endregion
20189
20419
  //#region src/managers/DeviceTokenManager.ts
20190
20420
  var import_cjs$5 = require_cjs();
20191
- const logger$6 = getLogger();
20421
+ const logger$7 = getLogger();
20192
20422
  /**
20193
20423
  * Resolves the token expiry timestamp (epoch seconds) using a 3-tier priority chain:
20194
20424
  * 1. `data.expires_at` — server-provided absolute timestamp
@@ -20198,7 +20428,7 @@ const logger$6 = getLogger();
20198
20428
  function resolveExpiresAt(data) {
20199
20429
  if (data.expires_at) return data.expires_at;
20200
20430
  if (data.expires_in) return Math.floor(Date.now() / 1e3) + data.expires_in;
20201
- logger$6.warn("[DeviceToken] Could not determine token expiry, using default");
20431
+ logger$7.warn("[DeviceToken] Could not determine token expiry, using default");
20202
20432
  return Math.floor(Date.now() / 1e3) + DEVICE_TOKEN_DEFAULT_EXPIRE_IN;
20203
20433
  }
20204
20434
  /**
@@ -20231,11 +20461,12 @@ var DeviceTokenManager = class extends Destroyable {
20231
20461
  this.getCredential = getCredential;
20232
20462
  this._currentToken$ = this.createBehaviorSubject(null);
20233
20463
  this._refreshInProgress = false;
20464
+ this._paused = false;
20234
20465
  this._effectiveExpireIn = DEVICE_TOKEN_DEFAULT_EXPIRE_IN;
20235
20466
  this.subscribeTo(this._currentToken$.pipe((0, import_cjs$5.filter)(Boolean), (0, import_cjs$5.switchMap)((tokenData) => {
20236
20467
  const expiresAt = resolveExpiresAt(tokenData);
20237
20468
  const refreshIn = Math.max(expiresAt * 1e3 - Date.now() - DEVICE_TOKEN_REFRESH_BUFFER_MS, 1e3);
20238
- logger$6.debug(`[DeviceToken] Scheduling Client Bound SAT refresh in ${refreshIn}ms`);
20469
+ logger$7.debug(`[DeviceToken] Scheduling Client Bound SAT refresh in ${refreshIn}ms`);
20239
20470
  return (0, import_cjs$5.timer)(refreshIn);
20240
20471
  })), () => {
20241
20472
  this.executeRefresh();
@@ -20249,7 +20480,12 @@ var DeviceTokenManager = class extends Destroyable {
20249
20480
  * Activates the Client Bound SAT flow when the user's token has
20250
20481
  * `sat:refresh` scope.
20251
20482
  *
20252
- * Steps:
20483
+ * Returns an {@link ActivationResult} indicating whether the manager
20484
+ * took ownership of refresh duties. The caller must use the `activated`
20485
+ * boolean to decide whether to keep its own refresh path armed — when
20486
+ * `activated` is `false`, the caller is responsible for refresh.
20487
+ *
20488
+ * Steps on success:
20253
20489
  * 1. Check user's `sat_claims` for `sat:refresh` scope
20254
20490
  * 2. Call `/api/fabric/subscriber/devices/token` with a DPoP proof
20255
20491
  * 3. Reauthenticate the session with the Client Bound SAT + DPoP proof
@@ -20258,32 +20494,51 @@ var DeviceTokenManager = class extends Destroyable {
20258
20494
  async activate(user, session, updateCredential) {
20259
20495
  const { satClaims } = user;
20260
20496
  if (!satClaims?.scope?.includes(SAT_REFRESH_SCOPE)) {
20261
- logger$6.debug("[DeviceToken] No sat:refresh scope, skipping Client Bound SAT activation");
20262
- return;
20497
+ logger$7.debug("[DeviceToken] No sat:refresh scope, skipping Client Bound SAT activation");
20498
+ return {
20499
+ activated: false,
20500
+ reason: "no-scope"
20501
+ };
20263
20502
  }
20264
20503
  this._session = session;
20265
20504
  this._updateCredential = updateCredential;
20266
20505
  try {
20506
+ const cached = this._currentToken$.value;
20507
+ if (cached && this.isTokenFresh(cached)) {
20508
+ logger$7.debug("[DeviceToken] Reusing cached Client Bound SAT — skipping /devices/token");
20509
+ const rpcProof$1 = await this.dpopManager.createRpcProof({ method: "signalwire.reauthenticate" });
20510
+ await session.reauthenticate(cached.token, rpcProof$1, { clientBound: true });
20511
+ updateCredential({ token: cached.token });
20512
+ return { activated: true };
20513
+ }
20267
20514
  const tokenData = await this.obtainToken();
20268
20515
  if (!tokenData.expires_at && !tokenData.expires_in && satClaims.expires_at) tokenData.expires_at = satClaims.expires_at;
20269
20516
  this._effectiveExpireIn = resolveExpireIn(tokenData);
20270
20517
  const rpcProof = await this.dpopManager.createRpcProof({ method: "signalwire.reauthenticate" });
20271
20518
  await session.reauthenticate(tokenData.token, rpcProof, { clientBound: true });
20272
20519
  updateCredential({ token: tokenData.token });
20273
- logger$6.info("[DeviceToken] Client Bound SAT activated successfully");
20520
+ logger$7.info("[DeviceToken] Client Bound SAT activated successfully");
20274
20521
  this._currentToken$.next(tokenData);
20522
+ return { activated: true };
20275
20523
  } catch (error) {
20276
- logger$6.error("[DeviceToken] Failed to activate Client Bound SAT:", error);
20524
+ logger$7.error("[DeviceToken] Failed to activate Client Bound SAT:", error);
20277
20525
  this.errorHandler(new DPoPInitError(error, "Failed to activate Client Bound SAT"));
20278
- const credential = this.getCredential();
20279
- const expiresAt = satClaims.expires_at ?? (credential.expiry_at ? credential.expiry_at / 1e3 : Date.now() / 1e3 + DEVICE_TOKEN_DEFAULT_EXPIRE_IN / 1e3);
20280
- this._currentToken$.next({
20281
- token: credential.token ?? "",
20282
- expires_at: expiresAt
20283
- });
20526
+ return {
20527
+ activated: false,
20528
+ reason: "endpoint-failed"
20529
+ };
20284
20530
  }
20285
20531
  }
20286
20532
  /**
20533
+ * Returns true when the cached token has enough headroom before expiry to
20534
+ * be safely reused on reactivation. The headroom matches the refresh
20535
+ * buffer, so a token within the refresh window is treated as stale (the
20536
+ * reactive pipeline is about to refresh it anyway).
20537
+ */
20538
+ isTokenFresh(token) {
20539
+ return resolveExpiresAt(token) * 1e3 - Date.now() > DEVICE_TOKEN_REFRESH_BUFFER_MS;
20540
+ }
20541
+ /**
20287
20542
  * Obtains a Client Bound SAT from `/api/fabric/subscriber/devices/token`.
20288
20543
  * Returns the full {@link DeviceTokenResponse} including expiry metadata.
20289
20544
  */
@@ -20313,7 +20568,7 @@ var DeviceTokenManager = class extends Destroyable {
20313
20568
  * handled by the reactive pipeline).
20314
20569
  */
20315
20570
  async refreshToken(session, currentToken, updateCredential) {
20316
- logger$6.debug("[DeviceToken] Refreshing Client Bound SAT");
20571
+ logger$7.debug("[DeviceToken] Refreshing Client Bound SAT");
20317
20572
  const dpopProof = await this.dpopManager.createHttpProof({
20318
20573
  method: "POST",
20319
20574
  uri: DEVICE_REFRESH_ENDPOINT,
@@ -20335,7 +20590,7 @@ var DeviceTokenManager = class extends Destroyable {
20335
20590
  const rpcProof = await this.dpopManager.createRpcProof({ method: "signalwire.reauthenticate" });
20336
20591
  await session.reauthenticate(data.token, rpcProof);
20337
20592
  updateCredential({ token: data.token });
20338
- logger$6.info("[DeviceToken] Client Bound SAT refreshed successfully");
20593
+ logger$7.info("[DeviceToken] Client Bound SAT refreshed successfully");
20339
20594
  return data;
20340
20595
  }
20341
20596
  /**
@@ -20344,18 +20599,22 @@ var DeviceTokenManager = class extends Destroyable {
20344
20599
  * On all retries exhausted, emits to `errorHandler`.
20345
20600
  */
20346
20601
  async executeRefresh() {
20602
+ if (this._paused) {
20603
+ logger$7.debug("[DeviceToken] Manager paused, skipping refresh");
20604
+ return;
20605
+ }
20347
20606
  if (this._refreshInProgress) {
20348
- logger$6.debug("[DeviceToken] Refresh already in progress, skipping");
20607
+ logger$7.debug("[DeviceToken] Refresh already in progress, skipping");
20349
20608
  return;
20350
20609
  }
20351
20610
  const session = this._session;
20352
20611
  const updateCredential = this._updateCredential;
20353
20612
  if (!session || !updateCredential) {
20354
- logger$6.warn("[DeviceToken] Cannot refresh: session or updateCredential not set");
20613
+ logger$7.warn("[DeviceToken] Cannot refresh: session or updateCredential not set");
20355
20614
  return;
20356
20615
  }
20357
20616
  if (!session.authenticated) {
20358
- logger$6.debug("[DeviceToken] Session not authenticated, deferring refresh");
20617
+ logger$7.debug("[DeviceToken] Session not authenticated, deferring refresh");
20359
20618
  return;
20360
20619
  }
20361
20620
  this._refreshInProgress = true;
@@ -20365,7 +20624,7 @@ var DeviceTokenManager = class extends Destroyable {
20365
20624
  const newTokenData = await this.retryRefresh(session, currentToken, updateCredential);
20366
20625
  this._currentToken$.next(newTokenData);
20367
20626
  } catch (error) {
20368
- logger$6.error("[DeviceToken] Automatic Client Bound SAT refresh failed:", error);
20627
+ logger$7.error("[DeviceToken] Automatic Client Bound SAT refresh failed:", error);
20369
20628
  this.errorHandler(error instanceof TokenRefreshError ? error : new TokenRefreshError("Automatic token refresh failed", error));
20370
20629
  } finally {
20371
20630
  this._refreshInProgress = false;
@@ -20383,18 +20642,215 @@ var DeviceTokenManager = class extends Destroyable {
20383
20642
  lastError = error;
20384
20643
  if (attempt < DEVICE_TOKEN_REFRESH_MAX_RETRIES - 1) {
20385
20644
  const delay$1 = DEVICE_TOKEN_REFRESH_RETRY_BASE_MS * Math.pow(2, attempt);
20386
- logger$6.warn(`[DeviceToken] Refresh attempt ${attempt + 1} failed, retrying in ${delay$1}ms`);
20645
+ logger$7.warn(`[DeviceToken] Refresh attempt ${attempt + 1} failed, retrying in ${delay$1}ms`);
20387
20646
  await new Promise((resolve) => setTimeout(resolve, delay$1));
20388
20647
  }
20389
20648
  }
20390
20649
  throw lastError instanceof Error ? lastError : new TokenRefreshError("All refresh retries exhausted", lastError);
20391
20650
  }
20651
+ /**
20652
+ * Stops the reactive refresh pipeline from firing. Use when the underlying
20653
+ * session is being torn down (e.g., during {@link SignalWire.disconnect})
20654
+ * so a scheduled refresh cannot fire against a destroyed session.
20655
+ *
20656
+ * The manager's state (cached token, effective TTL, subscriptions) is
20657
+ * preserved — call {@link resume} to re-enable firing after reconnect.
20658
+ */
20659
+ pause() {
20660
+ this._paused = true;
20661
+ }
20662
+ /** Re-enables the reactive refresh pipeline after a previous {@link pause}. */
20663
+ resume() {
20664
+ this._paused = false;
20665
+ }
20392
20666
  /** Cleans up the manager, cancelling the reactive pipeline and all subscriptions. */
20393
20667
  destroy() {
20394
20668
  super.destroy();
20395
20669
  }
20396
20670
  };
20397
20671
 
20672
+ //#endregion
20673
+ //#region src/managers/CredentialRefreshCoordinator.ts
20674
+ const logger$6 = getLogger();
20675
+ const defaultDeviceTokenManagerFactory = (dpopManager, http, errorHandler, getCredential) => new DeviceTokenManager(dpopManager, http, errorHandler, getCredential);
20676
+ /**
20677
+ * Centralizes credential-refresh ownership across the two competing
20678
+ * mechanisms — developer-provided `CredentialProvider.refresh()` and the
20679
+ * Client Bound SAT path via {@link DeviceTokenManager}.
20680
+ *
20681
+ * Maintains the invariant: **at most one refresh mechanism is armed at a
20682
+ * time, and at least one is armed whenever the current credential has an
20683
+ * `expiry_at`**.
20684
+ *
20685
+ * Replaces the previous design where refresh state was distributed across
20686
+ * `SignalWire` (timer field, scheduler method, activation helper) and
20687
+ * `DeviceTokenManager` (reactive pipeline). Centralizing the invariant in
20688
+ * one component eliminates the bug class that produced issue #19074.
20689
+ *
20690
+ * Race-safety:
20691
+ * - `_activating` flag prevents overlapping `activate()` calls from racing.
20692
+ * - `_activationGeneration` lets late resolutions detect they've been
20693
+ * preempted by a newer activation (e.g., reconnect during in-flight
20694
+ * `obtainToken`).
20695
+ */
20696
+ var CredentialRefreshCoordinator = class extends Destroyable {
20697
+ constructor(dpopManager, deps) {
20698
+ super();
20699
+ this.deps = deps;
20700
+ this._activating = false;
20701
+ this._activationGeneration = 0;
20702
+ if (dpopManager?.initialized) this._deviceTokenManager = (deps.deviceTokenManagerFactory ?? defaultDeviceTokenManagerFactory)(dpopManager, deps.http, (error) => deps.notifier.onError(error), () => deps.store.read());
20703
+ }
20704
+ /** True when the Client Bound SAT path is available (DPoP initialized). */
20705
+ get clientBoundSATAvailable() {
20706
+ return this._deviceTokenManager !== void 0;
20707
+ }
20708
+ /** True when the developer-provided refresh timer is currently armed. */
20709
+ get developerRefreshArmed() {
20710
+ return this._developerTimerId !== void 0;
20711
+ }
20712
+ /**
20713
+ * Arms the developer-provided refresh timer to fire shortly before
20714
+ * `expiresAt`. Replaces any previously scheduled developer refresh.
20715
+ *
20716
+ * Idempotent — multiple calls just reschedule. On retry exhaustion,
20717
+ * invokes `deps.onRefreshExhausted` so the orchestrator can disconnect.
20718
+ */
20719
+ scheduleDeveloperRefresh(provider, expiresAt, attempt = 0) {
20720
+ if (this._developerTimerId !== void 0) clearTimeout(this._developerTimerId);
20721
+ 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);
20722
+ this._developerTimerId = setTimeout(async () => {
20723
+ try {
20724
+ if (!provider.refresh) throw new InvalidCredentialsError("Credential provider does not support refresh");
20725
+ const newCredentials = await provider.refresh();
20726
+ this.deps.store.write(newCredentials);
20727
+ this.deps.store.persist(newCredentials);
20728
+ logger$6.info("[Coordinator] Credentials refreshed successfully.");
20729
+ if (newCredentials.expiry_at) this.scheduleDeveloperRefresh(provider, newCredentials.expiry_at, 0);
20730
+ } catch (error) {
20731
+ const nextAttempt = attempt + 1;
20732
+ logger$6.error(`[Coordinator] Credential refresh failed (attempt ${nextAttempt}/${CREDENTIAL_REFRESH_MAX_RETRIES}):`, error);
20733
+ this.deps.notifier.onError(error instanceof Error ? error : new Error(String(error), { cause: error }));
20734
+ if (nextAttempt < CREDENTIAL_REFRESH_MAX_RETRIES) this.scheduleDeveloperRefresh(provider, expiresAt, nextAttempt);
20735
+ else {
20736
+ logger$6.error("[Coordinator] Credential refresh exhausted all retries. Disconnecting.");
20737
+ this.deps.notifier.onError(new TokenRefreshError("Credential refresh failed after max retries"));
20738
+ this.deps.notifier.onRefreshExhausted();
20739
+ }
20740
+ }
20741
+ }, refreshInterval);
20742
+ }
20743
+ /**
20744
+ * Cancels any scheduled developer-provided refresh. Idempotent.
20745
+ *
20746
+ * @internal Used by the coordinator's own activation flow. External
20747
+ * callers should use {@link suspend} for disconnect-time quiescence —
20748
+ * `suspend()` also pauses the internal Client Bound SAT pipeline.
20749
+ */
20750
+ cancelDeveloperRefresh() {
20751
+ if (this._developerTimerId !== void 0) {
20752
+ clearTimeout(this._developerTimerId);
20753
+ this._developerTimerId = void 0;
20754
+ }
20755
+ }
20756
+ /**
20757
+ * Suspends both refresh paths — cancels the developer timer and pauses
20758
+ * the internal reactive pipeline. Use when the underlying session is
20759
+ * being torn down (e.g., {@link SignalWire.disconnect}). The next
20760
+ * {@link activate} call re-enables the internal pipeline.
20761
+ *
20762
+ * The internal manager's cached token survives — see
20763
+ * {@link DeviceTokenManager.pause} — so a subsequent reconnect can
20764
+ * skip the `/devices/token` exchange entirely.
20765
+ */
20766
+ suspend() {
20767
+ this.cancelDeveloperRefresh();
20768
+ this._deviceTokenManager?.pause();
20769
+ }
20770
+ /**
20771
+ * Asks the Client Bound SAT path to take over refresh. If it accepts,
20772
+ * the developer-provided timer (if any) is cancelled. If it declines, the
20773
+ * developer timer remains armed and a `credential_refresh_fallback`
20774
+ * warning is emitted.
20775
+ *
20776
+ * **Idempotent** — re-entrant calls during an in-flight activation are
20777
+ * dropped. Use `_activationGeneration` to detect stale resolutions
20778
+ * (e.g., a reconnect-triggered activate() that races with an earlier one).
20779
+ */
20780
+ async activate(user, session) {
20781
+ if (this._activating) {
20782
+ logger$6.debug("[Coordinator] activate() in flight; ignoring re-entrant call");
20783
+ return;
20784
+ }
20785
+ if (!this._deviceTokenManager) return;
20786
+ this._deviceTokenManager.resume();
20787
+ const generation = ++this._activationGeneration;
20788
+ this._activating = true;
20789
+ try {
20790
+ const result = await this.withActivationTimeout(this._deviceTokenManager.activate(user, session, (cred) => this.deps.store.merge(cred)));
20791
+ if (generation !== this._activationGeneration) {
20792
+ logger$6.debug("[Coordinator] activate() result discarded (preempted by newer activation)");
20793
+ return;
20794
+ }
20795
+ if (result.activated) {
20796
+ this.cancelDeveloperRefresh();
20797
+ logger$6.debug("[Coordinator] Developer refresh disabled — Client Bound SAT owns refresh");
20798
+ return;
20799
+ }
20800
+ logger$6.warn(`[SignalWire] [SW-REFRESH-FALLBACK] Client Bound SAT declined (reason=${result.reason}); using developer-provided refresh handler.`);
20801
+ this.deps.notifier.onWarning({
20802
+ code: "credential_refresh_fallback",
20803
+ source: "CredentialProvider",
20804
+ reason: result.reason,
20805
+ message: `Client Bound SAT activation declined (${result.reason}); using developer-provided refresh.`
20806
+ });
20807
+ } finally {
20808
+ this._activating = false;
20809
+ }
20810
+ }
20811
+ destroy() {
20812
+ this.cancelDeveloperRefresh();
20813
+ this._deviceTokenManager?.destroy();
20814
+ this._deviceTokenManager = void 0;
20815
+ super.destroy();
20816
+ }
20817
+ /**
20818
+ * Races the manager's `activate()` against a hard timeout. A wedged HTTP
20819
+ * layer (e.g., proxy issues) could otherwise hang the activation
20820
+ * indefinitely, leaving the session with no refresh mechanism while the
20821
+ * `_activating` guard blocks subsequent retries.
20822
+ *
20823
+ * On timeout, the manager is paused immediately. The inner `activate()`
20824
+ * may still complete in the background and emit to `_currentToken$`,
20825
+ * which would normally arm the reactive refresh pipeline; pausing
20826
+ * prevents that pipeline from firing while the developer refresh path
20827
+ * is the active mechanism — preserving the "at most one mechanism
20828
+ * armed" invariant. The next `activate()` call resumes the manager.
20829
+ */
20830
+ async withActivationTimeout(inner) {
20831
+ return new Promise((resolve) => {
20832
+ const timer$4 = setTimeout(() => {
20833
+ this._deviceTokenManager?.pause();
20834
+ resolve({
20835
+ activated: false,
20836
+ reason: "activation-timeout"
20837
+ });
20838
+ }, CREDENTIAL_ACTIVATE_TIMEOUT_MS);
20839
+ inner.then((result) => {
20840
+ clearTimeout(timer$4);
20841
+ resolve(result);
20842
+ }, (error) => {
20843
+ clearTimeout(timer$4);
20844
+ this.deps.notifier.onError(error instanceof Error ? error : new Error(String(error)));
20845
+ resolve({
20846
+ activated: false,
20847
+ reason: "endpoint-failed"
20848
+ });
20849
+ });
20850
+ });
20851
+ }
20852
+ };
20853
+
20398
20854
  //#endregion
20399
20855
  //#region src/managers/DiagnosticsCollector.ts
20400
20856
  const logger$5 = getLogger();
@@ -20915,6 +21371,10 @@ var TransportManager = class extends Destroyable {
20915
21371
  if (!isSignalwireRequest(message)) return true;
20916
21372
  const eventChannel = message.params.event_channel;
20917
21373
  if (!eventChannel) return true;
21374
+ if (message.params.event_type.startsWith("conversation.")) {
21375
+ logger$2.debug(`[Transport] Received conversation event: ${message.params.event_type} (event_channel: ${eventChannel})`);
21376
+ return true;
21377
+ }
20918
21378
  const currentProtocol = this._currentProtocol;
20919
21379
  if (!currentProtocol) return true;
20920
21380
  if (!eventChannel.includes(currentProtocol)) {
@@ -21108,6 +21568,7 @@ var SignalWire = class extends Destroyable {
21108
21568
  this._isConnected$ = this.createBehaviorSubject(false);
21109
21569
  this._isRegistered$ = this.createBehaviorSubject(false);
21110
21570
  this._errors$ = this.createReplaySubject(1);
21571
+ this._warnings$ = this.createReplaySubject(10);
21111
21572
  this._options = {};
21112
21573
  this._deps = new DependencyContainer();
21113
21574
  this._credentialProvider = credentialProvider;
@@ -21167,6 +21628,27 @@ var SignalWire = class extends Destroyable {
21167
21628
  */
21168
21629
  async resolveCredentials() {
21169
21630
  const fingerprint = await this.initDPoP();
21631
+ this._refreshCoordinator = new CredentialRefreshCoordinator(this._dpopManager, {
21632
+ http: this._deps.http,
21633
+ notifier: {
21634
+ onError: (error) => this._errors$.next(error),
21635
+ onWarning: (warning) => this._warnings$.next(warning),
21636
+ onRefreshExhausted: () => void this.disconnect()
21637
+ },
21638
+ store: {
21639
+ read: () => this._deps.credential,
21640
+ write: (credential) => {
21641
+ this._deps.credential = credential;
21642
+ },
21643
+ merge: (partial) => {
21644
+ this._deps.credential = {
21645
+ ...this._deps.credential,
21646
+ ...partial
21647
+ };
21648
+ },
21649
+ persist: (credential) => this.persistCredential(credential)
21650
+ }
21651
+ });
21170
21652
  if (this._credentialProvider) return this.validateCredentials(this._credentialProvider, void 0, fingerprint);
21171
21653
  for (const scope of this._deps.persistSession ? ["local", "session"] : ["session"]) try {
21172
21654
  const cached = await this._deps.storage.getItem("sw:cached_credential", scope);
@@ -21196,7 +21678,16 @@ var SignalWire = class extends Destroyable {
21196
21678
  logger$1.error("[SignalWire] Provided credentials have expired.");
21197
21679
  throw new InvalidCredentialsError("Provided credentials have expired.");
21198
21680
  }
21199
- if (_credentials.expiry_at && credentialProvider?.refresh) this.scheduleCredentialRefresh(credentialProvider, _credentials.expiry_at);
21681
+ if (_credentials.expiry_at && credentialProvider?.refresh) this._refreshCoordinator?.scheduleDeveloperRefresh(credentialProvider, _credentials.expiry_at);
21682
+ else if (_credentials.expiry_at && !credentialProvider?.refresh) {
21683
+ 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.`);
21684
+ this._warnings$.next({
21685
+ code: "credential_no_refresh_handler",
21686
+ source: "CredentialProvider",
21687
+ message: "Credential has expiry_at but no refresh handler. Session will terminate at expiry unless the SAT carries 'sat:refresh' scope.",
21688
+ expiresAt: _credentials.expiry_at
21689
+ });
21690
+ }
21200
21691
  this._deps.credential = _credentials;
21201
21692
  this.persistCredential(_credentials);
21202
21693
  if (this.isConnected && this._clientSession.authenticated && _credentials.token) try {
@@ -21207,35 +21698,6 @@ var SignalWire = class extends Destroyable {
21207
21698
  this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
21208
21699
  }
21209
21700
  }
21210
- /**
21211
- * Schedules credential refresh with exponential backoff retry on failure.
21212
- * On success, resets attempt counter and schedules the next refresh.
21213
- * After exhausting retries, emits TokenRefreshError and disconnects.
21214
- */
21215
- scheduleCredentialRefresh(credentialProvider, expiresAt, attempt = 0) {
21216
- if (this._refreshTimerId !== void 0) clearTimeout(this._refreshTimerId);
21217
- 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);
21218
- this._refreshTimerId = setTimeout(async () => {
21219
- try {
21220
- if (!credentialProvider.refresh) throw new InvalidCredentialsError("Credential provider does not support refresh");
21221
- const newCredentials = await credentialProvider.refresh();
21222
- this._deps.credential = newCredentials;
21223
- this.persistCredential(newCredentials);
21224
- logger$1.info("[SignalWire] Credentials refreshed successfully.");
21225
- if (newCredentials.expiry_at) this.scheduleCredentialRefresh(credentialProvider, newCredentials.expiry_at, 0);
21226
- } catch (error) {
21227
- const nextAttempt = attempt + 1;
21228
- logger$1.error(`[SignalWire] Credential refresh failed (attempt ${nextAttempt}/${CREDENTIAL_REFRESH_MAX_RETRIES}):`, error);
21229
- this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
21230
- if (nextAttempt < CREDENTIAL_REFRESH_MAX_RETRIES) this.scheduleCredentialRefresh(credentialProvider, expiresAt, nextAttempt);
21231
- else {
21232
- logger$1.error("[SignalWire] Credential refresh exhausted all retries. Disconnecting.");
21233
- this._errors$.next(new TokenRefreshError("Credential refresh failed after max retries"));
21234
- this.disconnect();
21235
- }
21236
- }
21237
- }, refreshInterval);
21238
- }
21239
21701
  /** Persist credential to localStorage when persistSession is enabled. */
21240
21702
  persistCredential(credential) {
21241
21703
  if (!credential.token) return;
@@ -21329,6 +21791,7 @@ var SignalWire = class extends Destroyable {
21329
21791
  logger$1.debug("[SignalWire] Credential expired, refreshing before reconnect");
21330
21792
  const newCredentials = await this._credentialProvider.authenticate(fingerprint ? { fingerprint } : void 0);
21331
21793
  this._deps.credential = newCredentials;
21794
+ if (newCredentials.expiry_at && this._credentialProvider.refresh) this._refreshCoordinator?.scheduleDeveloperRefresh(this._credentialProvider, newCredentials.expiry_at);
21332
21795
  logger$1.debug("[SignalWire] Credential refreshed successfully for reconnect");
21333
21796
  } catch (error) {
21334
21797
  logger$1.error("[SignalWire] Failed to refresh credentials for reconnect:", error);
@@ -21340,33 +21803,12 @@ var SignalWire = class extends Destroyable {
21340
21803
  this._errors$.next(error);
21341
21804
  });
21342
21805
  await this._clientSession.connect();
21343
- if (this._dpopManager?.initialized) {
21344
- if (this._refreshTimerId) {
21345
- clearTimeout(this._refreshTimerId);
21346
- this._refreshTimerId = void 0;
21347
- logger$1.debug("[SignalWire] Developer refresh disabled — Client Bound SAT activation starting");
21348
- }
21349
- this._deviceTokenManager = new DeviceTokenManager(this._dpopManager, this._deps.http, (error) => this._errors$.next(error), () => this._deps.credential);
21350
- await this._deviceTokenManager.activate(this._deps.user, this._clientSession, (cred) => {
21351
- this._deps.credential = {
21352
- ...this._deps.credential,
21353
- ...cred
21354
- };
21355
- });
21356
- }
21806
+ await this._refreshCoordinator?.activate(this._deps.user, this._clientSession);
21357
21807
  this.subscribeTo(this._clientSession.authenticated$.pipe((0, import_cjs$1.skip)(1), (0, import_cjs$1.filter)(Boolean)), async () => {
21358
21808
  try {
21359
- if (this._deviceTokenManager) {
21360
- await this._deviceTokenManager.activate(this._deps.user, this._clientSession, (cred) => {
21361
- this._deps.credential = {
21362
- ...this._deps.credential,
21363
- ...cred
21364
- };
21365
- });
21366
- logger$1.debug("[SignalWire] Client Bound SAT re-activated after reconnect");
21367
- }
21809
+ await this._refreshCoordinator?.activate(this._deps.user, this._clientSession);
21368
21810
  } catch (error) {
21369
- logger$1.error("[SignalWire] Client Bound SAT re-activation failed (non-fatal):", error);
21811
+ logger$1.error("[SignalWire] Refresh re-arm after reconnect failed (non-fatal):", error);
21370
21812
  this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
21371
21813
  }
21372
21814
  try {
@@ -21452,6 +21894,19 @@ var SignalWire = class extends Destroyable {
21452
21894
  get errors$() {
21453
21895
  return this.deferEmission(this._errors$.asObservable());
21454
21896
  }
21897
+ /**
21898
+ * Observable stream of non-fatal SDK warnings.
21899
+ *
21900
+ * Subscribe to detect SDK behaviors that affect session liveness or developer-facing
21901
+ * contracts but do not warrant disconnection — e.g., a fallback from Client Bound SAT
21902
+ * refresh to the developer-provided `refresh()` because the SAT lacks `sat:refresh`
21903
+ * scope. Discriminated by `code`.
21904
+ *
21905
+ * Independent from {@link errors$}: existing error consumers are not notified.
21906
+ */
21907
+ get warnings$() {
21908
+ return this.deferEmission(this._warnings$.asObservable());
21909
+ }
21455
21910
  /** Platform WebRTC capabilities detected at construction time. */
21456
21911
  get platformCapabilities() {
21457
21912
  this._platformCapabilities ??= detectPlatformCapabilities(this._options.webRTCApiProvider);
@@ -21520,7 +21975,7 @@ var SignalWire = class extends Destroyable {
21520
21975
  logger$1.warn("[SignalWire] Failed to initialize VisibilityController:", error);
21521
21976
  }
21522
21977
  try {
21523
- this._diagnosticsCollector = new DiagnosticsCollector({ sdkVersion: "3.30.0" });
21978
+ this._diagnosticsCollector = new DiagnosticsCollector({ sdkVersion: "4.0.0-rc.1" });
21524
21979
  } catch (error) {
21525
21980
  logger$1.warn("[SignalWire] Failed to initialize DiagnosticsCollector:", error);
21526
21981
  }
@@ -21528,14 +21983,19 @@ var SignalWire = class extends Destroyable {
21528
21983
  /**
21529
21984
  * Disconnects the WebSocket and tears down the current session.
21530
21985
  *
21986
+ * Ends the session identified by the protocol and clears its persisted
21987
+ * resume state (`authorization_state` + protocol) and attach records
21988
+ * together — a later {@link connect} with the same credentials starts a
21989
+ * fresh session and cannot reattach to the ended session's calls.
21990
+ * Credentials and device preferences are preserved. To temporarily stop
21991
+ * receiving inbound calls while keeping the session alive, use
21992
+ * `unregister()` instead.
21993
+ *
21531
21994
  * The client can be reconnected by calling {@link connect} again,
21532
21995
  * which creates a fresh transport and session.
21533
21996
  */
21534
21997
  async disconnect() {
21535
- if (this._refreshTimerId) {
21536
- clearTimeout(this._refreshTimerId);
21537
- this._refreshTimerId = void 0;
21538
- }
21998
+ this._refreshCoordinator?.suspend();
21539
21999
  this._diagnosticsCollector?.record("connection", "disconnected");
21540
22000
  await this.teardownTransportAndSession();
21541
22001
  this._isConnected$.next(false);
@@ -21922,15 +22382,20 @@ var SignalWire = class extends Destroyable {
21922
22382
  prefs.preferredVideoInput = null;
21923
22383
  await this._deviceController.clearDeviceState();
21924
22384
  }
21925
- /** Destroys the client, clearing timers and releasing all resources. */
22385
+ /**
22386
+ * Destroys the client, clearing timers and releasing all resources.
22387
+ *
22388
+ * Intentionally destroying the client ends its session: the resume state
22389
+ * (`authorization_state` + protocol) and the attach records are both
22390
+ * cleared. Credentials and device preferences are preserved — use
22391
+ * {@link resetToDefaults} for a full wipe. To temporarily stop receiving
22392
+ * inbound calls while keeping the session alive, use `unregister()`.
22393
+ */
21926
22394
  destroy() {
21927
- if (this._refreshTimerId) {
21928
- clearTimeout(this._refreshTimerId);
21929
- this._refreshTimerId = void 0;
21930
- }
21931
- this._deviceTokenManager?.destroy();
22395
+ this._refreshCoordinator?.destroy();
22396
+ this._refreshCoordinator = void 0;
21932
22397
  this._dpopManager?.destroy();
21933
- if (this._attachManager) this._attachManager.detachAll();
22398
+ this._clientSession.teardownSessionState();
21934
22399
  this._transport.destroy();
21935
22400
  this._clientSession.destroy();
21936
22401
  try {
@@ -22045,7 +22510,7 @@ var StaticCredentialProvider = class {
22045
22510
  /**
22046
22511
  * Library version from package.json, injected at build time.
22047
22512
  */
22048
- const version = "3.30.0";
22513
+ const version = "4.0.0-rc.1";
22049
22514
  /**
22050
22515
  * Flag indicating the library has been loaded and is ready to use.
22051
22516
  * For UMD builds: `window.SignalWire.ready`
@@ -22067,7 +22532,7 @@ const ready = true;
22067
22532
  */
22068
22533
  const emitReadyEvent = () => {
22069
22534
  if (typeof window !== "undefined") {
22070
- const event = new CustomEvent("signalwire:js:ready", { detail: { version: "3.30.0" } });
22535
+ const event = new CustomEvent("signalwire:js:ready", { detail: { version: "4.0.0-rc.1" } });
22071
22536
  window.dispatchEvent(event);
22072
22537
  }
22073
22538
  };
@@ -22099,6 +22564,7 @@ exports.DPoPInitError = DPoPInitError;
22099
22564
  exports.DeviceTokenError = DeviceTokenError;
22100
22565
  exports.EmbedTokenCredentialProvider = EmbedTokenCredentialProvider;
22101
22566
  exports.InvalidCredentialsError = InvalidCredentialsError;
22567
+ exports.MediaAccessError = MediaAccessError;
22102
22568
  exports.MediaTrackError = MediaTrackError;
22103
22569
  exports.MessageParseError = MessageParseError;
22104
22570
  exports.OverconstrainedFallbackError = OverconstrainedFallbackError;