@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.
package/dist/browser.mjs CHANGED
@@ -9015,6 +9015,143 @@ var Destroyable = class {
9015
9015
  }
9016
9016
  };
9017
9017
 
9018
+ //#endregion
9019
+ //#region src/core/constants.ts
9020
+ const INVITE_VERSION = 1e3;
9021
+ const DEFAULT_ICE_CANDIDATE_TIMEOUT_MS = 600;
9022
+ const DEFAULT_ICE_GATHERING_TIMEOUT_MS = 6e3;
9023
+ const DEFAULT_RECONNECT_CALLS_TIMEOUT_MS = 300 * 1e3;
9024
+ const DEFAULT_CONNECTION_TIMEOUT_MS = 1e4;
9025
+ const DEFAULT_RECONNECT_DELAY_MIN_MS = 100;
9026
+ const DEFAULT_RECONNECT_DELAY_MAX_MS = 3e3;
9027
+ const DEFAULT_DEVICE_DEBOUNCE_TIME_MS = 1500;
9028
+ const DEFAULT_DEVICE_POLLING_INTERVAL_MS = 0;
9029
+ const PREFERENCES_STORAGE_KEY = "sw:preferences";
9030
+ /** Scope value that enables automatic token refresh. */
9031
+ const SAT_REFRESH_SCOPE = "sat:refresh";
9032
+ /** API endpoints for device token operations. */
9033
+ const DEVICE_TOKEN_ENDPOINT = "/api/fabric/subscriber/devices/token";
9034
+ const DEVICE_REFRESH_ENDPOINT = "/api/fabric/subscriber/devices/refresh";
9035
+ /** Default device token TTL in seconds (15 minutes). */
9036
+ const DEVICE_TOKEN_DEFAULT_EXPIRE_IN = 900;
9037
+ /** Buffer time in milliseconds before expiry to trigger refresh. */
9038
+ const DEVICE_TOKEN_REFRESH_BUFFER_MS = 3e4;
9039
+ /** Maximum retry attempts for device token refresh on transient failure. */
9040
+ const DEVICE_TOKEN_REFRESH_MAX_RETRIES = 3;
9041
+ /** Base delay in milliseconds for exponential backoff on refresh retry. */
9042
+ const DEVICE_TOKEN_REFRESH_RETRY_BASE_MS = 1e3;
9043
+ /** Maximum retry attempts for developer credential refresh on transient failure. */
9044
+ const CREDENTIAL_REFRESH_MAX_RETRIES = 5;
9045
+ /** Base delay in milliseconds for exponential backoff on credential refresh retry. */
9046
+ const CREDENTIAL_REFRESH_RETRY_BASE_MS = 1e3;
9047
+ /** Maximum delay in milliseconds for credential refresh backoff. */
9048
+ const CREDENTIAL_REFRESH_MAX_DELAY_MS = 3e4;
9049
+ /** Buffer in milliseconds before token expiry to trigger refresh. */
9050
+ const CREDENTIAL_REFRESH_BUFFER_MS = 5e3;
9051
+ /**
9052
+ * Maximum time the coordinator will wait for `DeviceTokenManager.activate()`
9053
+ * to resolve before treating the activation as failed and falling back to
9054
+ * the developer-provided refresh path. Prevents a wedged HTTP layer from
9055
+ * leaving the session with no active refresh mechanism.
9056
+ */
9057
+ const CREDENTIAL_ACTIVATE_TIMEOUT_MS = 3e4;
9058
+ /** JSON-RPC error code for requester validation failure (corrupted auth state). */
9059
+ const RPC_ERROR_REQUESTER_VALIDATION_FAILED = -32003;
9060
+ /** JSON-RPC error code for invalid params (e.g., missing authentication block). */
9061
+ const RPC_ERROR_INVALID_PARAMS = -32602;
9062
+ /** JSON-RPC error code for authentication failure (invalid token, missing DPoP, etc.). */
9063
+ const RPC_ERROR_AUTHENTICATION_FAILED = -32002;
9064
+ /** Error names browsers use for a media permission denial (user or policy). */
9065
+ const MEDIA_ACCESS_DENIAL_NAMES = [
9066
+ "NotAllowedError",
9067
+ "SecurityError",
9068
+ "PermissionDeniedError"
9069
+ ];
9070
+ /** Default polling interval for RTCPeerConnection.getStats() in milliseconds. */
9071
+ const DEFAULT_STATS_POLLING_INTERVAL_MS = 1e3;
9072
+ /** Number of initial samples used to build a baseline for spike detection. */
9073
+ const DEFAULT_STATS_BASELINE_SAMPLES = 10;
9074
+ /** Duration in ms with no inbound audio packets before emitting a critical issue. */
9075
+ const DEFAULT_STATS_NO_PACKET_THRESHOLD_MS = 2e3;
9076
+ /** Multiplier applied to baseline RTT to detect a warning-level RTT spike. */
9077
+ const DEFAULT_STATS_RTT_SPIKE_MULTIPLIER = 3;
9078
+ /** Packet loss fraction (0-1) above which a warning is emitted. */
9079
+ const DEFAULT_STATS_PACKET_LOSS_THRESHOLD = .05;
9080
+ /** Multiplier applied to baseline jitter to detect a jitter spike. */
9081
+ const DEFAULT_STATS_JITTER_SPIKE_MULTIPLIER = 4;
9082
+ /** Number of seconds of metrics history to retain. */
9083
+ const DEFAULT_STATS_HISTORY_SIZE = 30;
9084
+ /** Maximum keyframe requests allowed within a single burst window. */
9085
+ const DEFAULT_KEYFRAME_MAX_BURST$1 = 3;
9086
+ /** Duration of the keyframe burst window in milliseconds. */
9087
+ const DEFAULT_KEYFRAME_BURST_WINDOW_MS$1 = 3e3;
9088
+ /** Cooldown period in ms after burst limit is reached before allowing more keyframes. */
9089
+ const DEFAULT_KEYFRAME_COOLDOWN_MS$1 = 1e4;
9090
+ /** Minimum time between re-INVITE attempts in milliseconds. */
9091
+ const DEFAULT_REINVITE_DEBOUNCE_TIME_MS = 1e4;
9092
+ /** Maximum number of re-INVITE attempts per call. */
9093
+ const DEFAULT_REINVITE_MAX_ATTEMPTS = 3;
9094
+ /** Timeout for a single re-INVITE attempt in milliseconds. */
9095
+ const DEFAULT_REINVITE_TIMEOUT_MS = 5e3;
9096
+ /** Debounce window in ms to collapse multiple detection signals into one trigger. */
9097
+ const DEFAULT_RECOVERY_DEBOUNCE_TIME_MS = 2e3;
9098
+ /** Cooldown period in ms between recovery attempts. */
9099
+ const DEFAULT_RECOVERY_COOLDOWN_MS = 1e4;
9100
+ /** Grace period in ms before treating ICE 'disconnected' as a failure. */
9101
+ const DEFAULT_ICE_DISCONNECTED_GRACE_PERIOD_MS = 3e3;
9102
+ /** Timeout for a single ICE restart attempt in milliseconds. */
9103
+ const DEFAULT_ICE_RESTART_TIMEOUT_MS$1 = 5e3;
9104
+ /** Maximum recovery attempts before emitting 'max_attempts_reached'. */
9105
+ const DEFAULT_MAX_RECOVERY_ATTEMPTS = 3;
9106
+ /** Upper bound in ms for waiting on iceGatheringState === 'complete' after an ICE restart. */
9107
+ const ICE_GATHERING_COMPLETE_TIMEOUT_MS = 1e4;
9108
+ /** Upper bound in ms for waiting on RTCPeerConnection.connectionState === 'connected' after a recovery ICE restart. */
9109
+ const PEER_CONNECTION_RECOVERY_WAIT_MS = 5e3;
9110
+ /** Polling interval in ms while waiting for RTCPeerConnection.connectionState to transition. */
9111
+ const PEER_CONNECTION_RECOVERY_POLL_MS = 100;
9112
+ /** Polling interval for LocalAudioPipeline.level$ (ms). ~30fps is smooth for meters. */
9113
+ const AUDIO_LEVEL_POLL_INTERVAL_MS = 33;
9114
+ /** RMS level threshold (0..1) above which the local participant is considered speaking. */
9115
+ const VAD_THRESHOLD = .03;
9116
+ /** Hold window in ms below the threshold before speaking$ flips back to false. */
9117
+ const VAD_HOLD_MS = 250;
9118
+ /** Whether to persist device selections to storage by default. */
9119
+ const DEFAULT_PERSIST_DEVICE_SELECTION = true;
9120
+ /** Whether to auto-apply device changes to active calls by default. */
9121
+ const DEFAULT_SYNC_DEVICES_TO_ACTIVE_CALLS = true;
9122
+ /** Storage keys for persisted device selections. */
9123
+ const DEVICE_STORAGE_KEY_AUDIO_INPUT = "sw:device:audioinput";
9124
+ const DEVICE_STORAGE_KEY_AUDIO_OUTPUT = "sw:device:audiooutput";
9125
+ const DEVICE_STORAGE_KEY_VIDEO_INPUT = "sw:device:videoinput";
9126
+ /** Whether to auto-mute video when the tab becomes hidden. */
9127
+ const DEFAULT_AUTO_MUTE_VIDEO_ON_HIDDEN = false;
9128
+ /** Whether to re-enumerate devices when the page becomes visible. */
9129
+ const DEFAULT_REFRESH_DEVICES_ON_VISIBLE = true;
9130
+ /** Whether to check peer connection health when the page becomes visible. */
9131
+ const DEFAULT_CHECK_CONNECTION_ON_VISIBLE = true;
9132
+ /** Whether automatic video degradation on low bandwidth is enabled. */
9133
+ const DEFAULT_ENABLE_AUTO_DEGRADATION = true;
9134
+ /** Bitrate in kbps below which video is automatically disabled. */
9135
+ const DEFAULT_DEGRADATION_BITRATE_THRESHOLD_KBPS = 150;
9136
+ /** Bitrate in kbps above which video is automatically re-enabled (hysteresis). */
9137
+ const DEFAULT_DEGRADATION_RECOVERY_THRESHOLD_KBPS = 300;
9138
+ /** Whether relay-only escalation is enabled as a last-resort recovery tier. */
9139
+ const DEFAULT_ENABLE_RELAY_FALLBACK = true;
9140
+ /** Whether to listen for browser online/offline/connection events. */
9141
+ const DEFAULT_ENABLE_NETWORK_CHANGE_DETECTION = true;
9142
+ /** Whether to intercept server-sent media-timeout hangups and attempt recovery. */
9143
+ const DEFAULT_ENABLE_SERVER_HANGUP_INTERCEPTION = true;
9144
+ /** Default video track constraints applied when video is enabled without explicit constraints. */
9145
+ const DEFAULT_VIDEO_CONSTRAINTS = {
9146
+ width: { ideal: 1280 },
9147
+ height: { ideal: 720 },
9148
+ aspectRatio: 16 / 9
9149
+ };
9150
+ /** Whether stereo Opus is enabled by default. */
9151
+ const DEFAULT_STEREO_AUDIO = false;
9152
+ /** Max average bitrate for stereo Opus in bits per second. */
9153
+ const DEFAULT_STEREO_MAX_AVERAGE_BITRATE = 51e4;
9154
+
9018
9155
  //#endregion
9019
9156
  //#region src/core/errors.ts
9020
9157
  var UnexpectedError = class extends Error {
@@ -9224,6 +9361,33 @@ var MediaTrackError = class extends Error {
9224
9361
  this.name = "MediaTrackError";
9225
9362
  }
9226
9363
  };
9364
+ /** True when a `getUserMedia`/`getDisplayMedia` rejection is a permission denial. */
9365
+ function isMediaAccessDenial(originalError) {
9366
+ return originalError instanceof Error && MEDIA_ACCESS_DENIAL_NAMES.includes(originalError.name);
9367
+ }
9368
+ /**
9369
+ * Failure to acquire local media (camera, microphone, or screen capture)
9370
+ * via `getUserMedia`/`getDisplayMedia`.
9371
+ *
9372
+ * Non-fatal by default: screenshare and additional-device failures never
9373
+ * end the call, and main-connection failures degrade to receive-only when
9374
+ * possible. The wrapping site sets `fatal` to `true` only when the call
9375
+ * cannot continue (receive-only fallback disabled or no receive intent).
9376
+ */
9377
+ var MediaAccessError = class extends Error {
9378
+ constructor(operation, media, originalError, fatal = false) {
9379
+ super(`Media access ${isMediaAccessDenial(originalError) ? "denied" : "failed"} for ${operation} (${media})`, { cause: originalError instanceof Error ? originalError : void 0 });
9380
+ this.operation = operation;
9381
+ this.media = media;
9382
+ this.originalError = originalError;
9383
+ this.fatal = fatal;
9384
+ this.name = "MediaAccessError";
9385
+ }
9386
+ /** True when the underlying failure is a permission denial (user or policy). */
9387
+ get denied() {
9388
+ return isMediaAccessDenial(this.originalError);
9389
+ }
9390
+ };
9227
9391
  var DPoPInitError = class extends Error {
9228
9392
  constructor(originalError, message = "Failed to initialize DPoP key pair") {
9229
9393
  super(message, { cause: originalError instanceof Error ? originalError : void 0 });
@@ -9464,9 +9628,9 @@ var require_loglevel = /* @__PURE__ */ __commonJSMin(((exports, module) => {
9464
9628
  defaultLogger$1 = new Logger();
9465
9629
  defaultLogger$1.getLogger = function getLogger$1(name) {
9466
9630
  if (typeof name !== "symbol" && typeof name !== "string" || name === "") throw new TypeError("You must supply a name when creating a logger.");
9467
- var logger$32 = _loggersByName[name];
9468
- if (!logger$32) logger$32 = _loggersByName[name] = new Logger(name, defaultLogger$1.methodFactory);
9469
- return logger$32;
9631
+ var logger$33 = _loggersByName[name];
9632
+ if (!logger$33) logger$33 = _loggersByName[name] = new Logger(name, defaultLogger$1.methodFactory);
9633
+ return logger$33;
9470
9634
  };
9471
9635
  var _log = typeof window !== undefinedType ? window.log : void 0;
9472
9636
  defaultLogger$1.noConflict = function() {
@@ -9502,8 +9666,8 @@ const defaultLoggerLevel = defaultLogger.levels.WARN;
9502
9666
  defaultLogger.setLevel(defaultLoggerLevel);
9503
9667
  let userLogger = null;
9504
9668
  /** Replace the built-in logger with a custom implementation. Pass `null` to restore defaults. */
9505
- const setLogger = (logger$32) => {
9506
- userLogger = logger$32;
9669
+ const setLogger = (logger$33) => {
9670
+ userLogger = logger$33;
9507
9671
  };
9508
9672
  let debugOptions = {};
9509
9673
  /** Configure debug options (e.g., `{ logWsTraffic: true }`). */
@@ -9547,8 +9711,8 @@ const wsTraffic = (options) => {
9547
9711
  loggerInstance.debug(`${options.type.toUpperCase()}: \n`, msg, "\n");
9548
9712
  };
9549
9713
  const getLogger = () => {
9550
- const logger$32 = getLoggerInstance();
9551
- return new Proxy(logger$32, { get(_target, prop, _receiver) {
9714
+ const logger$33 = getLoggerInstance();
9715
+ return new Proxy(logger$33, { get(_target, prop, _receiver) {
9552
9716
  if (prop === "wsTraffic") return wsTraffic;
9553
9717
  const instance = getLoggerInstance();
9554
9718
  const value = Reflect.get(instance, prop);
@@ -9600,7 +9764,7 @@ const asyncRetry = async ({ asyncCallable, maxRetries: retries = DEFAULT_MAX_RET
9600
9764
 
9601
9765
  //#endregion
9602
9766
  //#region src/controllers/HTTPRequestController.ts
9603
- const logger$31 = getLogger();
9767
+ const logger$32 = getLogger();
9604
9768
  const GET_PARAMS = {
9605
9769
  method: "GET",
9606
9770
  headers: { Accept: "application/json" }
@@ -9664,7 +9828,7 @@ var HTTPRequestController = class HTTPRequestController extends Destroyable {
9664
9828
  this._responses$.next(response);
9665
9829
  return response;
9666
9830
  } catch (error) {
9667
- logger$31.error("[HTTPRequestController] Request error:", error);
9831
+ logger$32.error("[HTTPRequestController] Request error:", error);
9668
9832
  this._status$.next("error");
9669
9833
  const err = error instanceof Error ? error : new Error("HTTP request failed", { cause: error });
9670
9834
  this._errors$.next(err);
@@ -9691,7 +9855,7 @@ var HTTPRequestController = class HTTPRequestController extends Destroyable {
9691
9855
  const url = this.buildURL(request.url);
9692
9856
  const headers = this.buildHeaders(request.headers);
9693
9857
  const timeout$5 = request.timeout ?? this.requestTimeout;
9694
- logger$31.debug("[HTTPRequestController] Executing request:", {
9858
+ logger$32.debug("[HTTPRequestController] Executing request:", {
9695
9859
  method: request.method,
9696
9860
  url,
9697
9861
  headers: Object.keys(headers).reduce((acc, key) => {
@@ -9711,7 +9875,7 @@ var HTTPRequestController = class HTTPRequestController extends Destroyable {
9711
9875
  });
9712
9876
  clearTimeout(timeoutId);
9713
9877
  const httpResponse = await this.convertResponse(response);
9714
- logger$31.debug("[HTTPRequestController] Response received:", {
9878
+ logger$32.debug("[HTTPRequestController] Response received:", {
9715
9879
  status: response.status,
9716
9880
  statusText: response.statusText,
9717
9881
  headers: [...response.headers.entries()],
@@ -9721,7 +9885,7 @@ var HTTPRequestController = class HTTPRequestController extends Destroyable {
9721
9885
  } catch (error) {
9722
9886
  clearTimeout(timeoutId);
9723
9887
  if (error instanceof Error && error.name === "AbortError") throw new RequestTimeoutError(`Request timeout after ${timeout$5}ms`, { cause: error });
9724
- logger$31.error("[HTTPRequestController] Request failed:", error);
9888
+ logger$32.error("[HTTPRequestController] Request failed:", error);
9725
9889
  throw error;
9726
9890
  }
9727
9891
  }
@@ -9735,8 +9899,8 @@ var HTTPRequestController = class HTTPRequestController extends Destroyable {
9735
9899
  const credential = this.getCredential();
9736
9900
  if (credential.token) {
9737
9901
  headers.Authorization = `Bearer ${credential.token}`;
9738
- logger$31.debug("[HTTPRequestController] Using Bearer token auth, token length:", credential.token.length);
9739
- } else logger$31.warn("[HTTPRequestController] No credentials available for authentication");
9902
+ logger$32.debug("[HTTPRequestController] Using Bearer token auth, token length:", credential.token.length);
9903
+ } else logger$32.warn("[HTTPRequestController] No credentials available for authentication");
9740
9904
  return headers;
9741
9905
  }
9742
9906
  /**
@@ -9873,130 +10037,6 @@ var DeviceHistoryManager = class {
9873
10037
  }
9874
10038
  };
9875
10039
 
9876
- //#endregion
9877
- //#region src/core/constants.ts
9878
- const INVITE_VERSION = 1e3;
9879
- const DEFAULT_ICE_CANDIDATE_TIMEOUT_MS = 600;
9880
- const DEFAULT_ICE_GATHERING_TIMEOUT_MS = 6e3;
9881
- const DEFAULT_RECONNECT_CALLS_TIMEOUT_MS = 300 * 1e3;
9882
- const DEFAULT_CONNECTION_TIMEOUT_MS = 1e4;
9883
- const DEFAULT_RECONNECT_DELAY_MIN_MS = 100;
9884
- const DEFAULT_RECONNECT_DELAY_MAX_MS = 3e3;
9885
- const DEFAULT_DEVICE_DEBOUNCE_TIME_MS = 1500;
9886
- const DEFAULT_DEVICE_POLLING_INTERVAL_MS = 0;
9887
- const PREFERENCES_STORAGE_KEY = "sw:preferences";
9888
- /** Scope value that enables automatic token refresh. */
9889
- const SAT_REFRESH_SCOPE = "sat:refresh";
9890
- /** API endpoints for device token operations. */
9891
- const DEVICE_TOKEN_ENDPOINT = "/api/fabric/subscriber/devices/token";
9892
- const DEVICE_REFRESH_ENDPOINT = "/api/fabric/subscriber/devices/refresh";
9893
- /** Default device token TTL in seconds (15 minutes). */
9894
- const DEVICE_TOKEN_DEFAULT_EXPIRE_IN = 900;
9895
- /** Buffer time in milliseconds before expiry to trigger refresh. */
9896
- const DEVICE_TOKEN_REFRESH_BUFFER_MS = 3e4;
9897
- /** Maximum retry attempts for device token refresh on transient failure. */
9898
- const DEVICE_TOKEN_REFRESH_MAX_RETRIES = 3;
9899
- /** Base delay in milliseconds for exponential backoff on refresh retry. */
9900
- const DEVICE_TOKEN_REFRESH_RETRY_BASE_MS = 1e3;
9901
- /** Maximum retry attempts for developer credential refresh on transient failure. */
9902
- const CREDENTIAL_REFRESH_MAX_RETRIES = 5;
9903
- /** Base delay in milliseconds for exponential backoff on credential refresh retry. */
9904
- const CREDENTIAL_REFRESH_RETRY_BASE_MS = 1e3;
9905
- /** Maximum delay in milliseconds for credential refresh backoff. */
9906
- const CREDENTIAL_REFRESH_MAX_DELAY_MS = 3e4;
9907
- /** Buffer in milliseconds before token expiry to trigger refresh. */
9908
- const CREDENTIAL_REFRESH_BUFFER_MS = 5e3;
9909
- /** JSON-RPC error code for requester validation failure (corrupted auth state). */
9910
- const RPC_ERROR_REQUESTER_VALIDATION_FAILED = -32003;
9911
- /** JSON-RPC error code for invalid params (e.g., missing authentication block). */
9912
- const RPC_ERROR_INVALID_PARAMS = -32602;
9913
- /** JSON-RPC error code for authentication failure (invalid token, missing DPoP, etc.). */
9914
- const RPC_ERROR_AUTHENTICATION_FAILED = -32002;
9915
- /** Default polling interval for RTCPeerConnection.getStats() in milliseconds. */
9916
- const DEFAULT_STATS_POLLING_INTERVAL_MS = 1e3;
9917
- /** Number of initial samples used to build a baseline for spike detection. */
9918
- const DEFAULT_STATS_BASELINE_SAMPLES = 10;
9919
- /** Duration in ms with no inbound audio packets before emitting a critical issue. */
9920
- const DEFAULT_STATS_NO_PACKET_THRESHOLD_MS = 2e3;
9921
- /** Multiplier applied to baseline RTT to detect a warning-level RTT spike. */
9922
- const DEFAULT_STATS_RTT_SPIKE_MULTIPLIER = 3;
9923
- /** Packet loss fraction (0-1) above which a warning is emitted. */
9924
- const DEFAULT_STATS_PACKET_LOSS_THRESHOLD = .05;
9925
- /** Multiplier applied to baseline jitter to detect a jitter spike. */
9926
- const DEFAULT_STATS_JITTER_SPIKE_MULTIPLIER = 4;
9927
- /** Number of seconds of metrics history to retain. */
9928
- const DEFAULT_STATS_HISTORY_SIZE = 30;
9929
- /** Maximum keyframe requests allowed within a single burst window. */
9930
- const DEFAULT_KEYFRAME_MAX_BURST$1 = 3;
9931
- /** Duration of the keyframe burst window in milliseconds. */
9932
- const DEFAULT_KEYFRAME_BURST_WINDOW_MS$1 = 3e3;
9933
- /** Cooldown period in ms after burst limit is reached before allowing more keyframes. */
9934
- const DEFAULT_KEYFRAME_COOLDOWN_MS$1 = 1e4;
9935
- /** Minimum time between re-INVITE attempts in milliseconds. */
9936
- const DEFAULT_REINVITE_DEBOUNCE_TIME_MS = 1e4;
9937
- /** Maximum number of re-INVITE attempts per call. */
9938
- const DEFAULT_REINVITE_MAX_ATTEMPTS = 3;
9939
- /** Timeout for a single re-INVITE attempt in milliseconds. */
9940
- const DEFAULT_REINVITE_TIMEOUT_MS = 5e3;
9941
- /** Debounce window in ms to collapse multiple detection signals into one trigger. */
9942
- const DEFAULT_RECOVERY_DEBOUNCE_TIME_MS = 2e3;
9943
- /** Cooldown period in ms between recovery attempts. */
9944
- const DEFAULT_RECOVERY_COOLDOWN_MS = 1e4;
9945
- /** Grace period in ms before treating ICE 'disconnected' as a failure. */
9946
- const DEFAULT_ICE_DISCONNECTED_GRACE_PERIOD_MS = 3e3;
9947
- /** Timeout for a single ICE restart attempt in milliseconds. */
9948
- const DEFAULT_ICE_RESTART_TIMEOUT_MS$1 = 5e3;
9949
- /** Maximum recovery attempts before emitting 'max_attempts_reached'. */
9950
- const DEFAULT_MAX_RECOVERY_ATTEMPTS = 3;
9951
- /** Upper bound in ms for waiting on iceGatheringState === 'complete' after an ICE restart. */
9952
- const ICE_GATHERING_COMPLETE_TIMEOUT_MS = 1e4;
9953
- /** Upper bound in ms for waiting on RTCPeerConnection.connectionState === 'connected' after a recovery ICE restart. */
9954
- const PEER_CONNECTION_RECOVERY_WAIT_MS = 5e3;
9955
- /** Polling interval in ms while waiting for RTCPeerConnection.connectionState to transition. */
9956
- const PEER_CONNECTION_RECOVERY_POLL_MS = 100;
9957
- /** Polling interval for LocalAudioPipeline.level$ (ms). ~30fps is smooth for meters. */
9958
- const AUDIO_LEVEL_POLL_INTERVAL_MS = 33;
9959
- /** RMS level threshold (0..1) above which the local participant is considered speaking. */
9960
- const VAD_THRESHOLD = .03;
9961
- /** Hold window in ms below the threshold before speaking$ flips back to false. */
9962
- const VAD_HOLD_MS = 250;
9963
- /** Whether to persist device selections to storage by default. */
9964
- const DEFAULT_PERSIST_DEVICE_SELECTION = true;
9965
- /** Whether to auto-apply device changes to active calls by default. */
9966
- const DEFAULT_SYNC_DEVICES_TO_ACTIVE_CALLS = true;
9967
- /** Storage keys for persisted device selections. */
9968
- const DEVICE_STORAGE_KEY_AUDIO_INPUT = "sw:device:audioinput";
9969
- const DEVICE_STORAGE_KEY_AUDIO_OUTPUT = "sw:device:audiooutput";
9970
- const DEVICE_STORAGE_KEY_VIDEO_INPUT = "sw:device:videoinput";
9971
- /** Whether to auto-mute video when the tab becomes hidden. */
9972
- const DEFAULT_AUTO_MUTE_VIDEO_ON_HIDDEN = false;
9973
- /** Whether to re-enumerate devices when the page becomes visible. */
9974
- const DEFAULT_REFRESH_DEVICES_ON_VISIBLE = true;
9975
- /** Whether to check peer connection health when the page becomes visible. */
9976
- const DEFAULT_CHECK_CONNECTION_ON_VISIBLE = true;
9977
- /** Whether automatic video degradation on low bandwidth is enabled. */
9978
- const DEFAULT_ENABLE_AUTO_DEGRADATION = true;
9979
- /** Bitrate in kbps below which video is automatically disabled. */
9980
- const DEFAULT_DEGRADATION_BITRATE_THRESHOLD_KBPS = 150;
9981
- /** Bitrate in kbps above which video is automatically re-enabled (hysteresis). */
9982
- const DEFAULT_DEGRADATION_RECOVERY_THRESHOLD_KBPS = 300;
9983
- /** Whether relay-only escalation is enabled as a last-resort recovery tier. */
9984
- const DEFAULT_ENABLE_RELAY_FALLBACK = true;
9985
- /** Whether to listen for browser online/offline/connection events. */
9986
- const DEFAULT_ENABLE_NETWORK_CHANGE_DETECTION = true;
9987
- /** Whether to intercept server-sent media-timeout hangups and attempt recovery. */
9988
- const DEFAULT_ENABLE_SERVER_HANGUP_INTERCEPTION = true;
9989
- /** Default video track constraints applied when video is enabled without explicit constraints. */
9990
- const DEFAULT_VIDEO_CONSTRAINTS = {
9991
- width: { ideal: 1280 },
9992
- height: { ideal: 720 },
9993
- aspectRatio: 16 / 9
9994
- };
9995
- /** Whether stereo Opus is enabled by default. */
9996
- const DEFAULT_STEREO_AUDIO = false;
9997
- /** Max average bitrate for stereo Opus in bits per second. */
9998
- const DEFAULT_STEREO_MAX_AVERAGE_BITRATE = 51e4;
9999
-
10000
10040
  //#endregion
10001
10041
  //#region src/utils/time.ts
10002
10042
  function fromSecToMs(seconds) {
@@ -10008,7 +10048,7 @@ function fromMsToSec(milliseconds) {
10008
10048
 
10009
10049
  //#endregion
10010
10050
  //#region src/containers/PreferencesContainer.ts
10011
- const logger$30 = getLogger();
10051
+ const logger$31 = getLogger();
10012
10052
  var PreferencesContainer = class PreferencesContainer {
10013
10053
  static get instance() {
10014
10054
  this._instance ??= new PreferencesContainer();
@@ -10670,7 +10710,7 @@ var ClientPreferences = class {
10670
10710
  if (!this._storage) return;
10671
10711
  const data = collectStoredPreferences();
10672
10712
  this._storage.setItem(PREFERENCES_STORAGE_KEY, data, "local").catch((error) => {
10673
- logger$30.error(`[ClientPreferences] Failed to save preferences: ${String(error)}`);
10713
+ logger$31.error(`[ClientPreferences] Failed to save preferences: ${String(error)}`);
10674
10714
  });
10675
10715
  }
10676
10716
  /** Loads preferences from storage and applies them to the container. */
@@ -10679,7 +10719,7 @@ var ClientPreferences = class {
10679
10719
  this._storage.getItem(PREFERENCES_STORAGE_KEY, "local").then((stored) => {
10680
10720
  if (stored) applyStoredPreferences(stored);
10681
10721
  }).catch((error) => {
10682
- logger$30.error(`[ClientPreferences] Failed to load preferences: ${String(error)}`);
10722
+ logger$31.error(`[ClientPreferences] Failed to load preferences: ${String(error)}`);
10683
10723
  });
10684
10724
  }
10685
10725
  };
@@ -10701,7 +10741,7 @@ function toError(value) {
10701
10741
  //#endregion
10702
10742
  //#region src/controllers/NavigatorDeviceController.ts
10703
10743
  var import_cjs$29 = require_cjs();
10704
- const logger$29 = getLogger();
10744
+ const logger$30 = getLogger();
10705
10745
  /** Maps a device kind to its storage key. */
10706
10746
  const DEVICE_STORAGE_KEYS = {
10707
10747
  audioinput: DEVICE_STORAGE_KEY_AUDIO_INPUT,
@@ -10723,7 +10763,7 @@ var NavigatorDeviceController = class extends Destroyable {
10723
10763
  super();
10724
10764
  this.webRTCApiProvider = webRTCApiProvider;
10725
10765
  this.deviceChangeHandler = () => {
10726
- logger$29.debug("[DeviceController] Device change detected");
10766
+ logger$30.debug("[DeviceController] Device change detected");
10727
10767
  this.enumerateDevices();
10728
10768
  };
10729
10769
  this._devicesState$ = this.createBehaviorSubject(initialDevicesState);
@@ -10788,13 +10828,13 @@ var NavigatorDeviceController = class extends Destroyable {
10788
10828
  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$)));
10789
10829
  }
10790
10830
  get selectedAudioInputDevice$() {
10791
- 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))));
10831
+ 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))));
10792
10832
  }
10793
10833
  get selectedAudioOutputDevice$() {
10794
- 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))));
10834
+ 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))));
10795
10835
  }
10796
10836
  get selectedVideoInputDevice$() {
10797
- 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))));
10837
+ 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))));
10798
10838
  }
10799
10839
  get selectedAudioInputDevice() {
10800
10840
  if (this._audioInputDisabled$.value) return null;
@@ -10869,7 +10909,7 @@ var NavigatorDeviceController = class extends Destroyable {
10869
10909
  if (device) this.persistDeviceSelection("audioinput", device);
10870
10910
  }
10871
10911
  selectVideoInputDevice(device) {
10872
- logger$29.debug("[DeviceController] Setting selected video input device:", device);
10912
+ logger$30.debug("[DeviceController] Setting selected video input device:", device);
10873
10913
  if (this._videoInputDisabled$.value && device) this._videoInputDisabled$.next(false);
10874
10914
  const previous = this._selectedDevicesState$.value.videoinput;
10875
10915
  if (previous && previous.deviceId !== device?.deviceId) this._deviceHistory.push("videoinput", previous);
@@ -10926,7 +10966,7 @@ var NavigatorDeviceController = class extends Destroyable {
10926
10966
  }
10927
10967
  const fromHistory = this._deviceHistory.findInHistory(kind, devices);
10928
10968
  if (fromHistory) {
10929
- logger$29.debug(`[DeviceController] Device disappeared, falling back to history: ${fromHistory.label}`);
10969
+ logger$30.debug(`[DeviceController] Device disappeared, falling back to history: ${fromHistory.label}`);
10930
10970
  this.emitDeviceRecovered(kind, selected, fromHistory, "device_disconnected");
10931
10971
  return fromHistory;
10932
10972
  }
@@ -10979,7 +11019,7 @@ var NavigatorDeviceController = class extends Destroyable {
10979
11019
  try {
10980
11020
  await this._storageManager.setItem(DEVICE_STORAGE_KEYS[kind], stored, "local");
10981
11021
  } catch (error) {
10982
- logger$29.error(`[DeviceController] Failed to persist device selection for ${kind}:`, error);
11022
+ logger$30.error(`[DeviceController] Failed to persist device selection for ${kind}:`, error);
10983
11023
  }
10984
11024
  }
10985
11025
  async loadPersistedDevices() {
@@ -10995,7 +11035,7 @@ var NavigatorDeviceController = class extends Destroyable {
10995
11035
  [kind]: stored
10996
11036
  };
10997
11037
  } catch (error) {
10998
- logger$29.error(`[DeviceController] Failed to load persisted device for ${kind}:`, error);
11038
+ logger$30.error(`[DeviceController] Failed to load persisted device for ${kind}:`, error);
10999
11039
  }
11000
11040
  }
11001
11041
  /** Clears device history, persisted selections, and re-enumerates devices. */
@@ -11013,7 +11053,7 @@ var NavigatorDeviceController = class extends Destroyable {
11013
11053
  this.disableDeviceMonitoring();
11014
11054
  this.webRTCApiProvider.mediaDevices.addEventListener("devicechange", this.deviceChangeHandler);
11015
11055
  if (PreferencesContainer.instance.devicePollingInterval > 0) this._devicesPoolingSubscription = (0, import_cjs$29.interval)(PreferencesContainer.instance.devicePollingInterval).subscribe(() => {
11016
- logger$29.debug("[DeviceController] Polling devices due to interval");
11056
+ logger$30.debug("[DeviceController] Polling devices due to interval");
11017
11057
  this.enumerateDevices();
11018
11058
  });
11019
11059
  this.enumerateDevices();
@@ -11039,13 +11079,13 @@ var NavigatorDeviceController = class extends Destroyable {
11039
11079
  videoinput: []
11040
11080
  });
11041
11081
  this._devicesState$.next(devicesByKind);
11042
- logger$29.debug("[DeviceController] Devices enumerated:", {
11082
+ logger$30.debug("[DeviceController] Devices enumerated:", {
11043
11083
  audioInputs: devicesByKind.audioinput.length,
11044
11084
  audioOutputs: devicesByKind.audiooutput.length,
11045
11085
  videoInputs: devicesByKind.videoinput.length
11046
11086
  });
11047
11087
  } catch (error) {
11048
- logger$29.error("[DeviceController] Failed to enumerate devices:", error);
11088
+ logger$30.error("[DeviceController] Failed to enumerate devices:", error);
11049
11089
  this._errors$.next(toError(error));
11050
11090
  }
11051
11091
  }
@@ -11061,7 +11101,7 @@ var NavigatorDeviceController = class extends Destroyable {
11061
11101
  stream.getTracks().forEach((t) => t.stop());
11062
11102
  return capabilities;
11063
11103
  } catch (error) {
11064
- logger$29.error("[DeviceController] Failed to get device capabilities:", error);
11104
+ logger$30.error("[DeviceController] Failed to get device capabilities:", error);
11065
11105
  this._errors$.next(toError(error));
11066
11106
  throw error;
11067
11107
  }
@@ -11312,7 +11352,7 @@ var DependencyContainer = class {
11312
11352
 
11313
11353
  //#endregion
11314
11354
  //#region src/controllers/CryptoController.ts
11315
- const logger$28 = getLogger();
11355
+ const logger$29 = getLogger();
11316
11356
  const DPOP_DB_NAME = "sw-dpop";
11317
11357
  const DPOP_DB_VERSION = 1;
11318
11358
  const DPOP_STORE_NAME = "keys";
@@ -11371,7 +11411,7 @@ async function loadKeyPairFromDB() {
11371
11411
  tx.oncomplete = () => db.close();
11372
11412
  });
11373
11413
  } catch (error) {
11374
- logger$28.warn("[DPoP] Failed to load key pair from IndexedDB:", error);
11414
+ logger$29.warn("[DPoP] Failed to load key pair from IndexedDB:", error);
11375
11415
  return null;
11376
11416
  }
11377
11417
  }
@@ -11391,7 +11431,7 @@ async function saveKeyPairToDB(keyPair) {
11391
11431
  };
11392
11432
  });
11393
11433
  } catch (error) {
11394
- logger$28.warn("[DPoP] Failed to save key pair to IndexedDB:", error);
11434
+ logger$29.warn("[DPoP] Failed to save key pair to IndexedDB:", error);
11395
11435
  }
11396
11436
  }
11397
11437
  async function deleteKeyPairFromDB() {
@@ -11410,7 +11450,7 @@ async function deleteKeyPairFromDB() {
11410
11450
  };
11411
11451
  });
11412
11452
  } catch (error) {
11413
- logger$28.warn("[DPoP] Failed to delete key pair from IndexedDB:", error);
11453
+ logger$29.warn("[DPoP] Failed to delete key pair from IndexedDB:", error);
11414
11454
  }
11415
11455
  }
11416
11456
  /**
@@ -11470,13 +11510,13 @@ var CryptoController = class {
11470
11510
  this._publicJwk = await crypto.subtle.exportKey("jwk", stored.publicKey);
11471
11511
  this._fingerprint = await computeJwkThumbprint(this._publicJwk);
11472
11512
  this._initialized = true;
11473
- logger$28.debug("[DPoP] Key pair restored from IndexedDB, fingerprint:", this._fingerprint);
11513
+ logger$29.debug("[DPoP] Key pair restored from IndexedDB, fingerprint:", this._fingerprint);
11474
11514
  return this._fingerprint;
11475
11515
  } catch (error) {
11476
- logger$28.warn("[DPoP] Stored key pair unusable, generating new one:", error);
11516
+ logger$29.warn("[DPoP] Stored key pair unusable, generating new one:", error);
11477
11517
  await deleteKeyPairFromDB();
11478
11518
  }
11479
- logger$28.debug("[DPoP] Generating RSA key pair");
11519
+ logger$29.debug("[DPoP] Generating RSA key pair");
11480
11520
  this._keyPair = await crypto.subtle.generateKey({
11481
11521
  name: "RSASSA-PKCS1-v1_5",
11482
11522
  modulusLength: 2048,
@@ -11491,7 +11531,7 @@ var CryptoController = class {
11491
11531
  this._fingerprint = await computeJwkThumbprint(this._publicJwk);
11492
11532
  this._initialized = true;
11493
11533
  await saveKeyPairToDB(this._keyPair);
11494
- logger$28.debug("[DPoP] Key pair generated and persisted, fingerprint:", this._fingerprint);
11534
+ logger$29.debug("[DPoP] Key pair generated and persisted, fingerprint:", this._fingerprint);
11495
11535
  return this._fingerprint;
11496
11536
  }
11497
11537
  /**
@@ -11557,7 +11597,7 @@ var CryptoController = class {
11557
11597
  this._fingerprint = null;
11558
11598
  this._initialized = false;
11559
11599
  deleteKeyPairFromDB();
11560
- logger$28.debug("[DPoP] Controller destroyed");
11600
+ logger$29.debug("[DPoP] Controller destroyed");
11561
11601
  }
11562
11602
  get publicJwk() {
11563
11603
  if (!this._publicJwk) throw new DPoPInitError("CryptoController not initialized. Call init() first.");
@@ -11581,7 +11621,7 @@ var CryptoController = class {
11581
11621
  //#endregion
11582
11622
  //#region src/controllers/NetworkMonitor.ts
11583
11623
  var import_cjs$28 = require_cjs();
11584
- const logger$27 = getLogger();
11624
+ const logger$28 = getLogger();
11585
11625
  /**
11586
11626
  * Safely check whether we are running in a browser environment
11587
11627
  * with `window` and the relevant event targets.
@@ -11638,7 +11678,7 @@ var NetworkMonitor = class extends Destroyable {
11638
11678
  }
11639
11679
  attachListeners() {
11640
11680
  if (!hasBrowserNetworkEvents()) {
11641
- logger$27.debug("NetworkMonitor: no browser environment detected, skipping event listeners");
11681
+ logger$28.debug("NetworkMonitor: no browser environment detected, skipping event listeners");
11642
11682
  return;
11643
11683
  }
11644
11684
  window.addEventListener("online", this._onOnline);
@@ -11646,7 +11686,7 @@ var NetworkMonitor = class extends Destroyable {
11646
11686
  const connection = getNetworkConnection();
11647
11687
  if (connection) connection.addEventListener("change", this._onConnectionChange);
11648
11688
  this._listenersAttached = true;
11649
- logger$27.debug("NetworkMonitor: event listeners attached");
11689
+ logger$28.debug("NetworkMonitor: event listeners attached");
11650
11690
  }
11651
11691
  removeListeners() {
11652
11692
  if (!this._listenersAttached) return;
@@ -11657,10 +11697,10 @@ var NetworkMonitor = class extends Destroyable {
11657
11697
  if (connection) connection.removeEventListener("change", this._onConnectionChange);
11658
11698
  }
11659
11699
  this._listenersAttached = false;
11660
- logger$27.debug("NetworkMonitor: event listeners removed");
11700
+ logger$28.debug("NetworkMonitor: event listeners removed");
11661
11701
  }
11662
11702
  handleOnline() {
11663
- logger$27.info("NetworkMonitor: browser went online");
11703
+ logger$28.info("NetworkMonitor: browser went online");
11664
11704
  this._isOnline$.next(true);
11665
11705
  this._networkChange$.next({
11666
11706
  type: "online",
@@ -11669,7 +11709,7 @@ var NetworkMonitor = class extends Destroyable {
11669
11709
  });
11670
11710
  }
11671
11711
  handleOffline() {
11672
- logger$27.info("NetworkMonitor: browser went offline");
11712
+ logger$28.info("NetworkMonitor: browser went offline");
11673
11713
  this._isOnline$.next(false);
11674
11714
  this._networkChange$.next({
11675
11715
  type: "offline",
@@ -11678,7 +11718,7 @@ var NetworkMonitor = class extends Destroyable {
11678
11718
  }
11679
11719
  handleConnectionChange() {
11680
11720
  const networkType = getNetworkType();
11681
- logger$27.info(`NetworkMonitor: connection changed — effectiveType=${networkType ?? "unknown"}`);
11721
+ logger$28.info(`NetworkMonitor: connection changed — effectiveType=${networkType ?? "unknown"}`);
11682
11722
  this._networkChange$.next({
11683
11723
  type: "connection_change",
11684
11724
  timestamp: Date.now(),
@@ -11794,7 +11834,7 @@ function getNavigatorMediaDevices() {
11794
11834
  //#endregion
11795
11835
  //#region src/controllers/PreflightRunner.ts
11796
11836
  var import_cjs$27 = require_cjs();
11797
- const logger$26 = getLogger();
11837
+ const logger$27 = getLogger();
11798
11838
  const DEFAULT_MEDIA_TEST_DURATION_S = 10;
11799
11839
  const ICE_GATHERING_TIMEOUT_MS = 1e4;
11800
11840
  const SIGNALING_RTT_TIMEOUT_MS = 5e3;
@@ -11843,7 +11883,7 @@ var PreflightRunner = class extends Destroyable {
11843
11883
  if (!this._options.skipMediaTest) try {
11844
11884
  bandwidth = await this.testMediaBandwidth(destination);
11845
11885
  } catch (error) {
11846
- logger$26.warn("[PreflightRunner] Media bandwidth test failed:", error);
11886
+ logger$27.warn("[PreflightRunner] Media bandwidth test failed:", error);
11847
11887
  warnings.push("Media bandwidth test failed");
11848
11888
  }
11849
11889
  return {
@@ -11855,7 +11895,7 @@ var PreflightRunner = class extends Destroyable {
11855
11895
  warnings
11856
11896
  };
11857
11897
  } catch (error) {
11858
- logger$26.error("[PreflightRunner] Preflight test failed:", error);
11898
+ logger$27.error("[PreflightRunner] Preflight test failed:", error);
11859
11899
  throw new PreflightError("preflight", error instanceof Error ? error : new Error(String(error)));
11860
11900
  } finally {
11861
11901
  this.destroy();
@@ -11886,7 +11926,7 @@ var PreflightRunner = class extends Destroyable {
11886
11926
  if (track.kind === "video" && track.readyState === "live") videoWorking = true;
11887
11927
  }
11888
11928
  } catch (error) {
11889
- logger$26.warn("[PreflightRunner] Device test failed:", error);
11929
+ logger$27.warn("[PreflightRunner] Device test failed:", error);
11890
11930
  } finally {
11891
11931
  if (audioStream) audioStream.getTracks().forEach((t) => t.stop());
11892
11932
  }
@@ -11944,7 +11984,7 @@ var PreflightRunner = class extends Destroyable {
11944
11984
  rttMs
11945
11985
  };
11946
11986
  } catch (error) {
11947
- logger$26.warn("[PreflightRunner] ICE connectivity test failed:", error);
11987
+ logger$27.warn("[PreflightRunner] ICE connectivity test failed:", error);
11948
11988
  return {
11949
11989
  type: "failed",
11950
11990
  turnReachable: false,
@@ -11992,7 +12032,7 @@ var PreflightRunner = class extends Destroyable {
11992
12032
  //#endregion
11993
12033
  //#region src/controllers/VisibilityController.ts
11994
12034
  var import_cjs$26 = require_cjs();
11995
- const logger$25 = getLogger();
12035
+ const logger$26 = getLogger();
11996
12036
  /**
11997
12037
  * Checks whether the document visibility API is available.
11998
12038
  */
@@ -12029,8 +12069,8 @@ var VisibilityController = class extends Destroyable {
12029
12069
  this._boundHandler = this._handleVisibilityChange.bind(this);
12030
12070
  if (this._hasVisibilityApi) {
12031
12071
  document.addEventListener("visibilitychange", this._boundHandler);
12032
- logger$25.debug("VisibilityController: listening for visibilitychange events");
12033
- } else logger$25.debug("VisibilityController: document visibility API not available, defaulting to visible");
12072
+ logger$26.debug("VisibilityController: listening for visibilitychange events");
12073
+ } else logger$26.debug("VisibilityController: document visibility API not available, defaulting to visible");
12034
12074
  }
12035
12075
  /**
12036
12076
  * Observable of the current visibility state.
@@ -12055,7 +12095,7 @@ var VisibilityController = class extends Destroyable {
12055
12095
  destroy() {
12056
12096
  if (this._hasVisibilityApi) {
12057
12097
  document.removeEventListener("visibilitychange", this._boundHandler);
12058
- logger$25.debug("VisibilityController: removed visibilitychange listener");
12098
+ logger$26.debug("VisibilityController: removed visibilitychange listener");
12059
12099
  }
12060
12100
  super.destroy();
12061
12101
  }
@@ -12073,7 +12113,7 @@ var VisibilityController = class extends Destroyable {
12073
12113
  timestamp: Date.now()
12074
12114
  };
12075
12115
  this._visibilityChange$.next(changeEvent);
12076
- logger$25.debug("VisibilityController: visibility changed", {
12116
+ logger$26.debug("VisibilityController: visibility changed", {
12077
12117
  from: previousState,
12078
12118
  to: newState
12079
12119
  });
@@ -12314,7 +12354,7 @@ const RPCEventAckResponse = (id) => makeRPCResponse({
12314
12354
 
12315
12355
  //#endregion
12316
12356
  //#region src/managers/AttachManager.ts
12317
- const logger$24 = getLogger();
12357
+ const logger$25 = getLogger();
12318
12358
  var AttachManager = class {
12319
12359
  constructor(storage, deviceController, reconnectCallsTimeout, attachKey) {
12320
12360
  this.storage = storage;
@@ -12335,7 +12375,7 @@ var AttachManager = class {
12335
12375
  try {
12336
12376
  return await this.storage.getItem(this.attachKey) ?? {};
12337
12377
  } catch (error) {
12338
- logger$24.warn("[AttachManager] Failed to retrieve attached calls from storage", error);
12378
+ logger$25.warn("[AttachManager] Failed to retrieve attached calls from storage", error);
12339
12379
  return {};
12340
12380
  }
12341
12381
  }
@@ -12343,7 +12383,7 @@ var AttachManager = class {
12343
12383
  try {
12344
12384
  await this.storage.setItem(this.attachKey, attached);
12345
12385
  } catch (error) {
12346
- logger$24.warn("[AttachManager] Failed to write attached calls to storage", error);
12386
+ logger$25.warn("[AttachManager] Failed to write attached calls to storage", error);
12347
12387
  }
12348
12388
  }
12349
12389
  /**
@@ -12362,7 +12402,7 @@ var AttachManager = class {
12362
12402
  }
12363
12403
  async attach(call) {
12364
12404
  if (!call.to) {
12365
- logger$24.warn("[AttachManager] Skip attach for calls with no destination");
12405
+ logger$25.warn("[AttachManager] Skip attach for calls with no destination");
12366
12406
  return;
12367
12407
  }
12368
12408
  const destination = call.to;
@@ -12415,15 +12455,15 @@ var AttachManager = class {
12415
12455
  callId,
12416
12456
  ...options
12417
12457
  });
12418
- logger$24.info(`[AttachManager] Reattached call ${callId} (attempt ${attempt})`);
12458
+ logger$25.info(`[AttachManager] Reattached call ${callId} (attempt ${attempt})`);
12419
12459
  succeeded = true;
12420
12460
  break;
12421
12461
  } catch (error) {
12422
- logger$24.warn(`[AttachManager] Reattach attempt ${attempt}/3 failed for call ${callId}:`, error);
12462
+ logger$25.warn(`[AttachManager] Reattach attempt ${attempt}/3 failed for call ${callId}:`, error);
12423
12463
  if (attempt < 3) await new Promise((r) => setTimeout(r, (attempt + 1) * 1e3));
12424
12464
  }
12425
12465
  if (!succeeded) {
12426
- logger$24.warn(`[AttachManager] Reattach failed after 3 attempts for call ${callId}, removing reference`);
12466
+ logger$25.warn(`[AttachManager] Reattach failed after 3 attempts for call ${callId}, removing reference`);
12427
12467
  await this.detach({
12428
12468
  id: callId,
12429
12469
  mediaDirections: attachment.mediaDirections
@@ -13360,7 +13400,9 @@ const DEFAULT_MEMBER_CAPABILITIES = {
13360
13400
  position: false,
13361
13401
  meta: false,
13362
13402
  remove: false,
13363
- audioFlags: false
13403
+ audioFlags: false,
13404
+ denoise: false,
13405
+ lowbitrate: false
13364
13406
  };
13365
13407
  /**
13366
13408
  * Default call capabilities with no permissions
@@ -13433,7 +13475,9 @@ function computeMemberCapabilities(flags, memberType) {
13433
13475
  position: hasBooleanCapability(flags, memberType, null, "position"),
13434
13476
  meta: hasBooleanCapability(flags, memberType, null, "meta"),
13435
13477
  remove: hasBooleanCapability(flags, memberType, null, "remove"),
13436
- audioFlags: hasBooleanCapability(flags, memberType, null, "audioflags")
13478
+ audioFlags: hasBooleanCapability(flags, memberType, null, "audioflags"),
13479
+ denoise: hasBooleanCapability(flags, memberType, null, "denoise"),
13480
+ lowbitrate: hasBooleanCapability(flags, memberType, null, "lowbitrate")
13437
13481
  };
13438
13482
  }
13439
13483
  /**
@@ -13590,7 +13634,7 @@ function toggleHandraiseMethod(is) {
13590
13634
 
13591
13635
  //#endregion
13592
13636
  //#region src/core/entities/Participant.ts
13593
- const logger$23 = getLogger();
13637
+ const logger$24 = getLogger();
13594
13638
  const initialState = {};
13595
13639
  /**
13596
13640
  * Represents a participant in a call.
@@ -13816,6 +13860,10 @@ var Participant = class extends Destroyable {
13816
13860
  get nodeId() {
13817
13861
  return this._state$.value.node_id;
13818
13862
  }
13863
+ /** Call ID for this participant's leg, or `undefined` if not available. */
13864
+ get callId() {
13865
+ return this._state$.value.call_id;
13866
+ }
13819
13867
  /** @internal */
13820
13868
  get value() {
13821
13869
  return this._state$.value;
@@ -13877,8 +13925,9 @@ var Participant = class extends Destroyable {
13877
13925
  noise_suppression: !this.noiseSuppression
13878
13926
  });
13879
13927
  }
13928
+ /** Toggles low-bitrate mode for this participant's media. */
13880
13929
  async toggleLowbitrate() {
13881
- throw new UnimplementedError();
13930
+ await this.executeMethod(this.id, "call.lowbitrate.set", { lowbitrate: !this.lowbitrate });
13882
13931
  }
13883
13932
  /**
13884
13933
  * Adjusts the **conference-only** microphone energy gate / sensitivity level
@@ -13925,10 +13974,27 @@ var Participant = class extends Destroyable {
13925
13974
  }
13926
13975
  /**
13927
13976
  * Sets the participant's position in the video layout.
13977
+ *
13978
+ * Requires the `member.position` capability. The gateway keys positions by the
13979
+ * **target member's own** `call_id`/`node_id` (see issue #19400 and the legacy
13980
+ * `setPositions` implementation), so this sends the participant's own call
13981
+ * context — matching {@link Participant.remove}. A resolved promise does not
13982
+ * guarantee a visible change: the backend silently returns `200` (no-op) for
13983
+ * non-conference targets.
13984
+ *
13928
13985
  * @param value - The {@link VideoPosition} to assign (e.g. `'auto'`, `'reserved-0'`).
13929
13986
  */
13930
13987
  async setPosition(value) {
13931
- await this.executeMethod(this.id, "call.member.position.set", { position: value });
13988
+ const state = this._state$.value;
13989
+ const target = {
13990
+ member_id: this.id,
13991
+ call_id: state.call_id ?? "",
13992
+ node_id: state.node_id ?? ""
13993
+ };
13994
+ await this.executeMethod(target, "call.member.position.set", { targets: [{
13995
+ target,
13996
+ position: value
13997
+ }] });
13932
13998
  }
13933
13999
  /** Removes this participant from the call. */
13934
14000
  async remove() {
@@ -14018,12 +14084,21 @@ var SelfParticipant = class extends Participant {
14018
14084
  noise_suppression: true
14019
14085
  });
14020
14086
  }
14021
- /** Starts sharing the local screen. */
14087
+ /**
14088
+ * Starts sharing the local screen.
14089
+ *
14090
+ * The call is unaffected when acquisition fails.
14091
+ *
14092
+ * @throws The raw `getDisplayMedia` error. A dismissed picker or a
14093
+ * permission denial rejects with a `NotAllowedError` `DOMException` —
14094
+ * inspect `error.name` to tell benign cancels apart from real failures.
14095
+ */
14022
14096
  async startScreenShare() {
14023
14097
  try {
14024
14098
  await this.vertoManager.addScreenMedia();
14025
14099
  } catch (error) {
14026
- logger$23.error("[Participant.startScreenShare] Screen share error:", error);
14100
+ logger$24.error("[Participant.startScreenShare] Screen share error:", error);
14101
+ throw error;
14027
14102
  }
14028
14103
  }
14029
14104
  /** Observable of the current screen share status. */
@@ -14038,12 +14113,20 @@ var SelfParticipant = class extends Participant {
14038
14113
  async stopScreenShare() {
14039
14114
  return this.vertoManager.removeScreenMedia();
14040
14115
  }
14041
- /** Adds an additional media input device to the call. */
14116
+ /**
14117
+ * Adds an additional media input device to the call.
14118
+ *
14119
+ * The call is unaffected when acquisition fails.
14120
+ *
14121
+ * @throws The raw `getUserMedia` error (e.g. `NotAllowedError` on
14122
+ * permission denial) — inspect `error.name` to decide how to react.
14123
+ */
14042
14124
  async addAdditionalDevice(options) {
14043
14125
  try {
14044
14126
  await this.vertoManager.addInputDevice(options);
14045
14127
  } catch (error) {
14046
- logger$23.error("[Participant.startScreenShare] Screen share error:", error);
14128
+ logger$24.error("[Participant.addAdditionalDevice] Additional device error:", error);
14129
+ throw error;
14047
14130
  }
14048
14131
  }
14049
14132
  /** Removes an additional media input device by ID. */
@@ -14105,7 +14188,7 @@ var SelfParticipant = class extends Participant {
14105
14188
  */
14106
14189
  exitStudioModeIfActive() {
14107
14190
  if (this._studioAudio$.value) {
14108
- logger$23.debug("[SelfParticipant] Exiting studio audio mode due to individual flag toggle");
14191
+ logger$24.debug("[SelfParticipant] Exiting studio audio mode due to individual flag toggle");
14109
14192
  this._studioAudio$.next(false);
14110
14193
  }
14111
14194
  }
@@ -14129,7 +14212,7 @@ var SelfParticipant = class extends Participant {
14129
14212
  try {
14130
14213
  await super.mute();
14131
14214
  } catch (error) {
14132
- logger$23.warn("[Participant.toggleAudioInput] Server Error while muting audio input, proceeding with local toggle anyway", error);
14215
+ logger$24.warn("[Participant.toggleAudioInput] Server Error while muting audio input, proceeding with local toggle anyway", error);
14133
14216
  } finally {
14134
14217
  this.vertoManager.muteMainAudioInputDevice();
14135
14218
  }
@@ -14139,7 +14222,7 @@ var SelfParticipant = class extends Participant {
14139
14222
  try {
14140
14223
  await super.unmute();
14141
14224
  } catch (error) {
14142
- logger$23.warn("[Participant.toggleAudioInput] Server Error while unmuting audio input, proceeding with local toggle anyway", error);
14225
+ logger$24.warn("[Participant.toggleAudioInput] Server Error while unmuting audio input, proceeding with local toggle anyway", error);
14143
14226
  } finally {
14144
14227
  await this.vertoManager.unmuteMainAudioInputDevice();
14145
14228
  }
@@ -14149,7 +14232,7 @@ var SelfParticipant = class extends Participant {
14149
14232
  try {
14150
14233
  await super.muteVideo();
14151
14234
  } catch (error) {
14152
- logger$23.warn("[Participant.toggleVideoInput] Server Error while muting video input, proceeding with local toggle anyway", error);
14235
+ logger$24.warn("[Participant.toggleVideoInput] Server Error while muting video input, proceeding with local toggle anyway", error);
14153
14236
  } finally {
14154
14237
  this.vertoManager.muteMainVideoInputDevice();
14155
14238
  }
@@ -14159,7 +14242,7 @@ var SelfParticipant = class extends Participant {
14159
14242
  try {
14160
14243
  await super.unmuteVideo();
14161
14244
  } catch (error) {
14162
- logger$23.warn("[Participant.toggleVideoInput] Server Error while unmuting video input, proceeding with local toggle anyway", error);
14245
+ logger$24.warn("[Participant.toggleVideoInput] Server Error while unmuting video input, proceeding with local toggle anyway", error);
14163
14246
  } finally {
14164
14247
  await this.vertoManager.unmuteMainVideoInputDevice();
14165
14248
  }
@@ -14364,7 +14447,7 @@ function filterAs(predicate, resultPath) {
14364
14447
  //#endregion
14365
14448
  //#region src/operators/throwOnRPCError.ts
14366
14449
  var import_cjs$21 = require_cjs();
14367
- const logger$22 = getLogger();
14450
+ const logger$23 = getLogger();
14368
14451
  /**
14369
14452
  * RxJS operator that throws a {@link JSONRPCError} when the RPC response contains an error.
14370
14453
  * Passes successful responses through unchanged.
@@ -14372,14 +14455,14 @@ const logger$22 = getLogger();
14372
14455
  function throwOnRPCError() {
14373
14456
  return (0, import_cjs$21.map)((response) => {
14374
14457
  if (response.error) {
14375
- logger$22.error("[throwOnRPCError] RPC error response:", {
14458
+ logger$23.error("[throwOnRPCError] RPC error response:", {
14376
14459
  code: response.error.code,
14377
14460
  message: response.error.message,
14378
14461
  data: response.error.data
14379
14462
  });
14380
14463
  throw new JSONRPCError(response.error.code, response.error.message, response.error.data);
14381
14464
  }
14382
- logger$22.debug("[throwOnRPCError] RPC successful response:", response);
14465
+ logger$23.debug("[throwOnRPCError] RPC successful response:", response);
14383
14466
  return response;
14384
14467
  });
14385
14468
  }
@@ -14387,7 +14470,7 @@ function throwOnRPCError() {
14387
14470
  //#endregion
14388
14471
  //#region src/managers/CallEventsManager.ts
14389
14472
  var import_cjs$20 = require_cjs();
14390
- const logger$21 = getLogger();
14473
+ const logger$22 = getLogger();
14391
14474
  const initialSessionState = {};
14392
14475
  /** @internal */
14393
14476
  var CallEventsManager = class extends Destroyable {
@@ -14491,7 +14574,7 @@ var CallEventsManager = class extends Destroyable {
14491
14574
  }
14492
14575
  initSubscriptions() {
14493
14576
  this.subscribeTo(this.callJoinedEvent$, (callJoinedEvent) => {
14494
- logger$21.debug("[CallEventsManager] Handling call.joined event for call/session IDs:", {
14577
+ logger$22.debug("[CallEventsManager] Handling call.joined event for call/session IDs:", {
14495
14578
  callId: callJoinedEvent.call_id,
14496
14579
  roomSessionId: callJoinedEvent.room_session_id
14497
14580
  });
@@ -14518,19 +14601,19 @@ var CallEventsManager = class extends Destroyable {
14518
14601
  if (this._self$.value?.capabilities.setLayout) this.updateLayouts();
14519
14602
  });
14520
14603
  this.subscribeTo(this.memberUpdates$, (member) => {
14521
- logger$21.debug("[CallEventsManager] Handling member update event for member ID:", member);
14604
+ logger$22.debug("[CallEventsManager] Handling member update event for member ID:", member);
14522
14605
  this.upsertParticipant(member);
14523
14606
  });
14524
14607
  this.subscribeTo(this.webRtcCallSession.memberLeft$, (memberLeftEvent) => {
14525
- logger$21.debug("[CallEventsManager] Handling member.left event for member ID:", memberLeftEvent.member.member_id);
14608
+ logger$22.debug("[CallEventsManager] Handling member.left event for member ID:", memberLeftEvent.member.member_id);
14526
14609
  const participants = { ...this._participants$.value };
14527
14610
  if (memberLeftEvent.member.member_id in participants) {
14528
14611
  delete participants[memberLeftEvent.member.member_id];
14529
14612
  this._participants$.next(participants);
14530
- } else logger$21.warn(`[CallEventsManager] Received member.left event for unknown member ID: ${memberLeftEvent.member.member_id}`);
14613
+ } else logger$22.warn(`[CallEventsManager] Received member.left event for unknown member ID: ${memberLeftEvent.member.member_id}`);
14531
14614
  });
14532
14615
  this.subscribeTo(this.webRtcCallSession.callUpdated$, (callUpdatedEvent) => {
14533
- logger$21.debug("[CallEventsManager] Handling call.updated event:", callUpdatedEvent);
14616
+ logger$22.debug("[CallEventsManager] Handling call.updated event:", callUpdatedEvent);
14534
14617
  const roomSession = callUpdatedEvent.room_session;
14535
14618
  this._sessionState$.next({
14536
14619
  ...this._sessionState$.value,
@@ -14545,7 +14628,7 @@ var CallEventsManager = class extends Destroyable {
14545
14628
  });
14546
14629
  });
14547
14630
  this.subscribeTo(this.layoutChangedEvent$, (layoutChangedEvent) => {
14548
- logger$21.debug("[CallEventsManager] Handling layout.changed event:", layoutChangedEvent);
14631
+ logger$22.debug("[CallEventsManager] Handling layout.changed event:", layoutChangedEvent);
14549
14632
  this._sessionState$.next({
14550
14633
  ...this._sessionState$.value,
14551
14634
  layout_name: layoutChangedEvent.id,
@@ -14555,10 +14638,10 @@ var CallEventsManager = class extends Destroyable {
14555
14638
  });
14556
14639
  }
14557
14640
  updateParticipantPositions(layoutChangedEvent) {
14558
- 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.");
14641
+ 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.");
14559
14642
  layoutChangedEvent.layers.filter((layer) => !!layer.member_id).filter((layer) => {
14560
14643
  if (!(layer.member_id in this._participants$.value)) {
14561
- logger$21.warn(`[CallEventsManager] Skipping layout layer for unknown member_id: ${layer.member_id}`);
14644
+ logger$22.warn(`[CallEventsManager] Skipping layout layer for unknown member_id: ${layer.member_id}`);
14562
14645
  return false;
14563
14646
  }
14564
14647
  return true;
@@ -14581,7 +14664,7 @@ var CallEventsManager = class extends Destroyable {
14581
14664
  layouts: response.result.layouts
14582
14665
  });
14583
14666
  }).catch((error) => {
14584
- logger$21.error("[CallEventsManager] Error fetching layouts:", error);
14667
+ logger$22.error("[CallEventsManager] Error fetching layouts:", error);
14585
14668
  });
14586
14669
  }
14587
14670
  updateParticipants(members) {
@@ -14597,7 +14680,7 @@ var CallEventsManager = class extends Destroyable {
14597
14680
  }
14598
14681
  const participant = this._participants$.value[member.member_id];
14599
14682
  const oldValue = participant.value;
14600
- logger$21.debug("[CallEventsManager] Updating participant:", member.member_id, {
14683
+ logger$22.debug("[CallEventsManager] Updating participant:", member.member_id, {
14601
14684
  oldValue,
14602
14685
  newValue: member
14603
14686
  });
@@ -14610,17 +14693,17 @@ var CallEventsManager = class extends Destroyable {
14610
14693
  }
14611
14694
  get callJoinedEvent$() {
14612
14695
  return this.cachedObservable("callJoinedEvent$", () => this.webRtcCallSession.callEvent$.pipe((0, import_cjs$20.filter)(isCallJoinedPayload), (0, import_cjs$20.tap)((event) => {
14613
- logger$21.debug("[CallEventsManager] Call joined event:", event);
14696
+ logger$22.debug("[CallEventsManager] Call joined event:", event);
14614
14697
  })));
14615
14698
  }
14616
14699
  get layoutChangedEvent$() {
14617
14700
  return this.cachedObservable("layoutChangedEvent$", () => this.webRtcCallSession.callEvent$.pipe(filterAs(isLayoutChangedPayload, "layout"), (0, import_cjs$20.tap)((event) => {
14618
- logger$21.debug("[CallEventsManager] Layout changed event:", event);
14701
+ logger$22.debug("[CallEventsManager] Layout changed event:", event);
14619
14702
  })));
14620
14703
  }
14621
14704
  get memberUpdates$() {
14622
14705
  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) => {
14623
- logger$21.debug("[CallEventsManager] Member update event:", event);
14706
+ logger$22.debug("[CallEventsManager] Member update event:", event);
14624
14707
  })));
14625
14708
  }
14626
14709
  destroy() {
@@ -14877,7 +14960,7 @@ function appendStereoParams(fmtpLine, maxBitrate) {
14877
14960
  //#endregion
14878
14961
  //#region src/controllers/ICEGatheringController.ts
14879
14962
  var import_cjs$19 = require_cjs();
14880
- const logger$20 = getLogger();
14963
+ const logger$21 = getLogger();
14881
14964
  var ICEGatheringController = class extends Destroyable {
14882
14965
  constructor(peerConnection, peerConnectionControllerNegotiating$, options = {}) {
14883
14966
  super();
@@ -14885,23 +14968,23 @@ var ICEGatheringController = class extends Destroyable {
14885
14968
  this.peerConnectionControllerNegotiating$ = peerConnectionControllerNegotiating$;
14886
14969
  this.onicegatheringstatechangeHandler = () => {
14887
14970
  const { iceGatheringState } = this.peerConnection;
14888
- logger$20.debug(`[ICEGatheringController] ICE gathering state changed to: ${iceGatheringState}`);
14971
+ logger$21.debug(`[ICEGatheringController] ICE gathering state changed to: ${iceGatheringState}`);
14889
14972
  if (iceGatheringState === "gathering") this._iceCandidatesState.next({
14890
14973
  state: "gathering",
14891
14974
  validSDP: false
14892
14975
  });
14893
14976
  };
14894
14977
  this.onicecandidateHandler = (event) => {
14895
- logger$20.debug("[ICEGatheringController] ICE candidate event received:", event.candidate);
14978
+ logger$21.debug("[ICEGatheringController] ICE candidate event received:", event.candidate);
14896
14979
  this.removeTimer("iceCandidateTimer");
14897
14980
  if (event.candidate) this.iceCandidateTimer = setTimeout(() => {
14898
14981
  if (this.peerConnection.iceGatheringState !== "complete") {
14899
- logger$20.warn("[ICEGatheringController] ICE candidate timeout, using current SDP");
14982
+ logger$21.warn("[ICEGatheringController] ICE candidate timeout, using current SDP");
14900
14983
  this.handleICECandidateTimeout();
14901
14984
  }
14902
14985
  }, this.iceCandidateTimeout);
14903
14986
  else {
14904
- logger$20.debug("[ICEGatheringController] ICE gathering completed: null candidate received");
14987
+ logger$21.debug("[ICEGatheringController] ICE gathering completed: null candidate received");
14905
14988
  this.removeTimer("iceGatheringTimer");
14906
14989
  this.handleICEGatheringComplete();
14907
14990
  }
@@ -14919,7 +15002,7 @@ var ICEGatheringController = class extends Destroyable {
14919
15002
  this.setupEventListeners();
14920
15003
  this.iceGatheringTimer = setTimeout(() => {
14921
15004
  if (this.peerConnection.iceGatheringState !== "complete") {
14922
- logger$20.warn("[ICEGatheringController] ICE gathering timeout, using current SDP");
15005
+ logger$21.warn("[ICEGatheringController] ICE gathering timeout, using current SDP");
14923
15006
  this.handleICEGatheringTimeout();
14924
15007
  }
14925
15008
  }, this.iceGatheringTimeout);
@@ -14946,9 +15029,9 @@ var ICEGatheringController = class extends Destroyable {
14946
15029
  this.relayOnly = value;
14947
15030
  }
14948
15031
  handleICEGatheringComplete() {
14949
- logger$20.debug("[ICEGatheringController] Handling ICE gathering complete");
14950
- logger$20.debug(`[ICEGatheringController] Checking ICE gathering state: ${this.peerConnection.iceGatheringState}`);
14951
- logger$20.debug("[ICEGatheringController] ICE gathering complete");
15032
+ logger$21.debug("[ICEGatheringController] Handling ICE gathering complete");
15033
+ logger$21.debug(`[ICEGatheringController] Checking ICE gathering state: ${this.peerConnection.iceGatheringState}`);
15034
+ logger$21.debug("[ICEGatheringController] ICE gathering complete");
14952
15035
  this._iceCandidatesState.next({
14953
15036
  state: "complete",
14954
15037
  validSDP: this.hasValidLocalDescriptionSDP
@@ -14964,21 +15047,21 @@ var ICEGatheringController = class extends Destroyable {
14964
15047
  this.removeTimer("iceGatheringTimer");
14965
15048
  const validSDP = this.hasValidLocalDescriptionSDP;
14966
15049
  if (validSDP) {
14967
- logger$20.debug("[ICEGatheringController] Local SDP is valid");
15050
+ logger$21.debug("[ICEGatheringController] Local SDP is valid");
14968
15051
  this._iceCandidatesState.next({
14969
15052
  state: "timeout",
14970
15053
  validSDP
14971
15054
  });
14972
15055
  this.stopGathering();
14973
- } else logger$20.debug("### ICE gathering timeout\n", this.peerConnection.localDescription?.sdp);
15056
+ } else logger$21.debug("### ICE gathering timeout\n", this.peerConnection.localDescription?.sdp);
14974
15057
  }
14975
15058
  handleICECandidateTimeout() {
14976
15059
  if (this.iceCandidateTimer) this.removeTimer("iceCandidateTimer");
14977
- logger$20.warn("[ICEGatheringController] ICE candidate timeout");
15060
+ logger$21.warn("[ICEGatheringController] ICE candidate timeout");
14978
15061
  const validSDP = this.hasValidLocalDescriptionSDP;
14979
15062
  if (!validSDP && !this.relayOnly) this.restartICEGatheringWithRelayOnly();
14980
15063
  else {
14981
- logger$20.debug("[ICEGatheringController] Using current SDP due to ICE candidate timeout");
15064
+ logger$21.debug("[ICEGatheringController] Using current SDP due to ICE candidate timeout");
14982
15065
  this._iceCandidatesState.next({
14983
15066
  state: "timeout",
14984
15067
  validSDP
@@ -14987,7 +15070,7 @@ var ICEGatheringController = class extends Destroyable {
14987
15070
  }
14988
15071
  }
14989
15072
  restartICEGatheringWithRelayOnly() {
14990
- logger$20.debug("[ICEGatheringController] Restarting ICE gathering with relay-only candidates");
15073
+ logger$21.debug("[ICEGatheringController] Restarting ICE gathering with relay-only candidates");
14991
15074
  this.relayOnly = true;
14992
15075
  this.peerConnection.setConfiguration({
14993
15076
  ...this.peerConnection.getConfiguration(),
@@ -15002,7 +15085,7 @@ var ICEGatheringController = class extends Destroyable {
15002
15085
  }
15003
15086
  }
15004
15087
  clearAllTimers() {
15005
- logger$20.debug("[ICEGatheringController] Clearing all timers");
15088
+ logger$21.debug("[ICEGatheringController] Clearing all timers");
15006
15089
  this.removeTimer("iceGatheringTimer");
15007
15090
  this.removeTimer("iceCandidateTimer");
15008
15091
  }
@@ -15011,7 +15094,7 @@ var ICEGatheringController = class extends Destroyable {
15011
15094
  this.peerConnection.removeEventListener("icecandidate", this.onicecandidateHandler);
15012
15095
  }
15013
15096
  destroy() {
15014
- logger$20.debug("[ICEGatheringController] Destroying ICEGatheringController");
15097
+ logger$21.debug("[ICEGatheringController] Destroying ICEGatheringController");
15015
15098
  this.clearAllTimers();
15016
15099
  this.removeEventListeners();
15017
15100
  super.destroy();
@@ -15021,7 +15104,7 @@ var ICEGatheringController = class extends Destroyable {
15021
15104
  //#endregion
15022
15105
  //#region src/controllers/LocalAudioPipeline.ts
15023
15106
  var import_cjs$18 = require_cjs();
15024
- const logger$19 = getLogger();
15107
+ const logger$20 = getLogger();
15025
15108
  /**
15026
15109
  * Web Audio pipeline for the local microphone stream.
15027
15110
  *
@@ -15133,7 +15216,7 @@ var LocalAudioPipeline = class extends Destroyable {
15133
15216
  try {
15134
15217
  this._inputSource.disconnect();
15135
15218
  } catch (error) {
15136
- logger$19.debug("[LocalAudioPipeline] input disconnect warning:", error);
15219
+ logger$20.debug("[LocalAudioPipeline] input disconnect warning:", error);
15137
15220
  }
15138
15221
  this._inputSource = null;
15139
15222
  }
@@ -15143,7 +15226,7 @@ var LocalAudioPipeline = class extends Destroyable {
15143
15226
  this._inputSource = this._audioContext.createMediaStreamSource(this._inputStream);
15144
15227
  this._inputSource.connect(this._gainNode);
15145
15228
  if (this._audioContext.state === "suspended") this._audioContext.resume().catch((error) => {
15146
- logger$19.warn("[LocalAudioPipeline] AudioContext resume failed:", error);
15229
+ logger$20.warn("[LocalAudioPipeline] AudioContext resume failed:", error);
15147
15230
  });
15148
15231
  }
15149
15232
  destroy() {
@@ -15158,7 +15241,7 @@ var LocalAudioPipeline = class extends Destroyable {
15158
15241
  this._analyser.disconnect();
15159
15242
  } catch {}
15160
15243
  this._audioContext.close().catch((error) => {
15161
- logger$19.debug("[LocalAudioPipeline] audio context close warning:", error);
15244
+ logger$20.debug("[LocalAudioPipeline] audio context close warning:", error);
15162
15245
  });
15163
15246
  super.destroy();
15164
15247
  }
@@ -15185,7 +15268,7 @@ var LocalAudioPipeline = class extends Destroyable {
15185
15268
  //#endregion
15186
15269
  //#region src/controllers/LocalStreamController.ts
15187
15270
  var import_cjs$17 = require_cjs();
15188
- const logger$18 = getLogger();
15271
+ const logger$19 = getLogger();
15189
15272
  var LocalStreamController = class extends Destroyable {
15190
15273
  constructor(options) {
15191
15274
  super();
@@ -15223,26 +15306,26 @@ var LocalStreamController = class extends Destroyable {
15223
15306
  * Build the local media stream based on the provided options.
15224
15307
  */
15225
15308
  async buildLocalStream() {
15226
- logger$18.debug("[LocalStreamController] Building local media stream.");
15309
+ logger$19.debug("[LocalStreamController] Building local media stream.");
15227
15310
  let stream;
15228
15311
  if (this.options.inputAudioStream ?? this.options.inputVideoStream) {
15229
15312
  const tracks = [...this.options.inputAudioStream?.getTracks() ?? [], ...this.options.inputVideoStream?.getTracks() ?? []];
15230
15313
  stream = new MediaStream(tracks);
15231
15314
  } else if (this.options.propose === "screenshare") {
15232
- logger$18.debug("[LocalStreamController] Requesting display media for screen sharing with audio:", Boolean(this.options.inputAudioDeviceConstraints));
15315
+ logger$19.debug("[LocalStreamController] Requesting display media for screen sharing with audio:", Boolean(this.options.inputAudioDeviceConstraints));
15233
15316
  stream = await this.options.getDisplayMedia({
15234
15317
  video: true,
15235
15318
  audio: Boolean(this.options.inputAudioDeviceConstraints)
15236
15319
  });
15237
- logger$18.debug("[LocalStreamController] Screen share media obtained:", stream);
15320
+ logger$19.debug("[LocalStreamController] Screen share media obtained:", stream);
15238
15321
  } else {
15239
15322
  const constraints = {
15240
15323
  audio: this.options.inputAudioDeviceConstraints,
15241
15324
  video: this.options.inputVideoDeviceConstraints
15242
15325
  };
15243
- logger$18.debug("[LocalStreamController] Requesting user media with constraints:", constraints);
15326
+ logger$19.debug("[LocalStreamController] Requesting user media with constraints:", constraints);
15244
15327
  stream = await this.options.getUserMedia(constraints);
15245
- logger$18.debug("[LocalStreamController] User media obtained:", stream);
15328
+ logger$19.debug("[LocalStreamController] User media obtained:", stream);
15246
15329
  }
15247
15330
  this._localStream$.next(stream);
15248
15331
  this._localAudioTracks$.next(stream.getAudioTracks());
@@ -15261,7 +15344,7 @@ var LocalStreamController = class extends Destroyable {
15261
15344
  this._localStream$.next(localStream);
15262
15345
  if (track.kind === "video") this._localVideoTracks$.next(localStream.getVideoTracks());
15263
15346
  else this._localAudioTracks$.next(localStream.getAudioTracks());
15264
- logger$18.debug(`[LocalStreamController] ${track.kind} track added:`, track.id);
15347
+ logger$19.debug(`[LocalStreamController] ${track.kind} track added:`, track.id);
15265
15348
  return localStream;
15266
15349
  }
15267
15350
  /**
@@ -15273,7 +15356,7 @@ var LocalStreamController = class extends Destroyable {
15273
15356
  const stream = this._localStream$.value;
15274
15357
  const track = stream?.getTracks().find((t) => t.id === trackId);
15275
15358
  if (!track) {
15276
- logger$18.debug(`[LocalStreamController] track not found: ${trackId}`);
15359
+ logger$19.debug(`[LocalStreamController] track not found: ${trackId}`);
15277
15360
  return;
15278
15361
  }
15279
15362
  track.removeEventListener("ended", this.mediaTrackEndedHandler);
@@ -15282,7 +15365,7 @@ var LocalStreamController = class extends Destroyable {
15282
15365
  this._localStream$.next(stream);
15283
15366
  if (track.kind === "video") this._localVideoTracks$.next(stream?.getVideoTracks() ?? []);
15284
15367
  else this._localAudioTracks$.next(stream?.getAudioTracks() ?? []);
15285
- logger$18.debug(`[LocalStreamController] ${track.kind} track removed:`, trackId);
15368
+ logger$19.debug(`[LocalStreamController] ${track.kind} track removed:`, trackId);
15286
15369
  return track;
15287
15370
  }
15288
15371
  /**
@@ -15317,7 +15400,7 @@ var LocalStreamController = class extends Destroyable {
15317
15400
  */
15318
15401
  stopAllTracks() {
15319
15402
  this._localStream$.value?.getTracks().forEach((track) => {
15320
- logger$18.debug(`[LocalStreamController] Stopping local track: ${track.kind}`);
15403
+ logger$19.debug(`[LocalStreamController] Stopping local track: ${track.kind}`);
15321
15404
  track.removeEventListener("ended", this.mediaTrackEndedHandler);
15322
15405
  track.stop();
15323
15406
  });
@@ -15333,7 +15416,7 @@ var LocalStreamController = class extends Destroyable {
15333
15416
 
15334
15417
  //#endregion
15335
15418
  //#region src/controllers/TransceiverController.ts
15336
- const logger$17 = getLogger();
15419
+ const logger$18 = getLogger();
15337
15420
  const getDirection = (send, recv) => {
15338
15421
  if (send && recv) return "sendrecv";
15339
15422
  else if (send && !recv) return "sendonly";
@@ -15441,7 +15524,7 @@ var TransceiverController = class extends Destroyable {
15441
15524
  sendEncodings: isAudio ? void 0 : this.sendEncodings,
15442
15525
  streams: direction === "recvonly" ? void 0 : [localStream]
15443
15526
  };
15444
- logger$17.debug(`[TransceiverController] Setting up transceiver sender for local ${track.kind} track:`, {
15527
+ logger$18.debug(`[TransceiverController] Setting up transceiver sender for local ${track.kind} track:`, {
15445
15528
  transceiver,
15446
15529
  transceiverParams
15447
15530
  });
@@ -15449,11 +15532,11 @@ var TransceiverController = class extends Destroyable {
15449
15532
  await transceiver.sender.replaceTrack(track);
15450
15533
  transceiver.direction = transceiverParams.direction;
15451
15534
  if (transceiverParams.streams?.some((stream) => Boolean(stream))) {
15452
- logger$17.debug(`[TransceiverController] Setting streams for transceiver sender for local ${track.kind} track:`, transceiverParams.streams);
15535
+ logger$18.debug(`[TransceiverController] Setting streams for transceiver sender for local ${track.kind} track:`, transceiverParams.streams);
15453
15536
  transceiver.sender.setStreams(...transceiverParams.streams);
15454
15537
  }
15455
15538
  } else {
15456
- logger$17.debug(`[TransceiverController] Adding new transceiver for local ${track.kind} track:`, track.id);
15539
+ logger$18.debug(`[TransceiverController] Adding new transceiver for local ${track.kind} track:`, track.id);
15457
15540
  this.peerConnection.addTransceiver(track, transceiverParams);
15458
15541
  }
15459
15542
  }
@@ -15467,13 +15550,13 @@ var TransceiverController = class extends Destroyable {
15467
15550
  if (options.updateTransceiverDirection) transceiver.direction = "inactive";
15468
15551
  }
15469
15552
  } catch (error) {
15470
- logger$17.error("[TransceiverController] stopTrackSender error", kind, error);
15553
+ logger$18.error("[TransceiverController] stopTrackSender error", kind, error);
15471
15554
  this.options.onError?.(new MediaTrackError("stopTrackSender", kind, error));
15472
15555
  }
15473
15556
  }
15474
15557
  async restoreTrackSender(kind) {
15475
15558
  try {
15476
- logger$17.debug("[TransceiverController] restoreTrackSender called", kind);
15559
+ logger$18.debug("[TransceiverController] restoreTrackSender called", kind);
15477
15560
  const constraints = {};
15478
15561
  const transceivers = this.transceiverByKind(kind);
15479
15562
  for (const transceiver of transceivers) {
@@ -15483,23 +15566,23 @@ var TransceiverController = class extends Destroyable {
15483
15566
  if (trackKind === "audio" || trackKind === "video") constraints[trackKind] = this.getConstraintsFor(trackKind);
15484
15567
  }
15485
15568
  }
15486
- logger$17.debug("[TransceiverController] restoreTrackSender constraints:", constraints);
15569
+ logger$18.debug("[TransceiverController] restoreTrackSender constraints:", constraints);
15487
15570
  if (Object.keys(constraints).length === 0) {
15488
- logger$17.warn("[TransceiverController] restoreTrackSender: no tracks need restoration", kind);
15571
+ logger$18.warn("[TransceiverController] restoreTrackSender: no tracks need restoration", kind);
15489
15572
  return;
15490
15573
  }
15491
15574
  const newTracks = (await this.options.getUserMedia(constraints)).getTracks();
15492
- logger$17.debug("[TransceiverController] restoreTrackSender new tracks:", newTracks);
15575
+ logger$18.debug("[TransceiverController] restoreTrackSender new tracks:", newTracks);
15493
15576
  for (const newTrack of newTracks) {
15494
15577
  this.options.localStreamController.addTrack(newTrack);
15495
15578
  const trackKind = newTrack.kind;
15496
15579
  const transceiverOfKind = this.transceiverByKind(trackKind)[0];
15497
15580
  transceiverOfKind.direction = trackKind === "audio" ? this.audioDirection : this.videoDirection;
15498
- logger$17.debug("[TransceiverController] restoreTrackSender setting direction for", trackKind, transceiverOfKind.direction);
15581
+ logger$18.debug("[TransceiverController] restoreTrackSender setting direction for", trackKind, transceiverOfKind.direction);
15499
15582
  await transceiverOfKind.sender.replaceTrack(newTrack);
15500
15583
  }
15501
15584
  } catch (error) {
15502
- logger$17.error("[TransceiverController] restoreTrackSender error", kind, error);
15585
+ logger$18.error("[TransceiverController] restoreTrackSender error", kind, error);
15503
15586
  this.options.onError?.(new MediaTrackError("restoreTrackSender", kind, error));
15504
15587
  }
15505
15588
  }
@@ -15540,14 +15623,14 @@ var TransceiverController = class extends Destroyable {
15540
15623
  };
15541
15624
  try {
15542
15625
  await track.applyConstraints(constraintsToApply);
15543
- logger$17.debug(`[TransceiverController] Updated ${kind} sender constraints:`, constraintsToApply);
15544
- logger$17.debug(`[TransceiverController] Updated ${kind} sender constraints:`, track.getConstraints());
15626
+ logger$18.debug(`[TransceiverController] Updated ${kind} sender constraints:`, constraintsToApply);
15627
+ logger$18.debug(`[TransceiverController] Updated ${kind} sender constraints:`, track.getConstraints());
15545
15628
  } catch (error) {
15546
- logger$17.warn(`[TransceiverController] applyConstraints failed for ${kind} track ${track.id}, attempting track replacement fallback:`, error);
15629
+ logger$18.warn(`[TransceiverController] applyConstraints failed for ${kind} track ${track.id}, attempting track replacement fallback:`, error);
15547
15630
  try {
15548
15631
  await this.replaceTrackFallback(sender, track, kind, constraintsToApply);
15549
15632
  } catch (fallbackError) {
15550
- logger$17.warn(`[TransceiverController] Track replacement fallback also failed for ${kind} track:`, fallbackError);
15633
+ logger$18.warn(`[TransceiverController] Track replacement fallback also failed for ${kind} track:`, fallbackError);
15551
15634
  this.options.onError?.(new MediaTrackError("updateSendersConstraints", kind, fallbackError));
15552
15635
  }
15553
15636
  }
@@ -15575,7 +15658,7 @@ var TransceiverController = class extends Destroyable {
15575
15658
  if (!newTrack) throw new MediaTrackError("replaceTrackFallback", kind, /* @__PURE__ */ new Error("getUserMedia returned no track of the requested kind"));
15576
15659
  await sender.replaceTrack(newTrack);
15577
15660
  this.options.localStreamController.addTrack(newTrack);
15578
- logger$17.debug(`[TransceiverController] Track replacement fallback succeeded for ${kind}. New track: ${newTrack.id}`);
15661
+ logger$18.debug(`[TransceiverController] Track replacement fallback succeeded for ${kind}. New track: ${newTrack.id}`);
15579
15662
  }
15580
15663
  getMediaDirections() {
15581
15664
  if (this.peerConnection.connectionState === "connected") return this.peerConnection.getTransceivers().reduce((acc, transceiver) => {
@@ -15606,7 +15689,7 @@ var TransceiverController = class extends Destroyable {
15606
15689
  //#endregion
15607
15690
  //#region src/controllers/RTCPeerConnectionController.ts
15608
15691
  var import_cjs$16 = require_cjs();
15609
- const logger$16 = getLogger();
15692
+ const logger$17 = getLogger();
15610
15693
  var RTCPeerConnectionController = class extends Destroyable {
15611
15694
  constructor(options = {}, remoteSessionDescription, deviceController) {
15612
15695
  super();
@@ -15622,43 +15705,43 @@ var RTCPeerConnectionController = class extends Destroyable {
15622
15705
  this.oniceconnectionstatechangeHandler = () => {
15623
15706
  if (this.peerConnection) {
15624
15707
  const { iceConnectionState } = this.peerConnection;
15625
- logger$16.debug(`[RTCPeerConnectionController] ICE connection state changed to: ${iceConnectionState}`);
15708
+ logger$17.debug(`[RTCPeerConnectionController] ICE connection state changed to: ${iceConnectionState}`);
15626
15709
  this._iceConnectionState$.next(this.peerConnection.iceConnectionState);
15627
15710
  }
15628
15711
  };
15629
15712
  this.onconnectionstatechangeHandler = () => {
15630
15713
  if (this.peerConnection) {
15631
15714
  const { connectionState } = this.peerConnection;
15632
- logger$16.debug(`[RTCPeerConnectionController] Connection state changed to: ${connectionState}`);
15715
+ logger$17.debug(`[RTCPeerConnectionController] Connection state changed to: ${connectionState}`);
15633
15716
  if (connectionState === "connected") this.removeConnectionTimer();
15634
15717
  this._connectionState$.next(this.peerConnection.connectionState);
15635
15718
  }
15636
15719
  };
15637
15720
  this.onsignalingstatechangeHandler = () => {
15638
- logger$16.debug(`[RTCPeerConnectionController] Signaling state changed to: ${this.peerConnection?.signalingState}`);
15721
+ logger$17.debug(`[RTCPeerConnectionController] Signaling state changed to: ${this.peerConnection?.signalingState}`);
15639
15722
  };
15640
15723
  this.onicegatheringstatechangeHandler = () => {
15641
15724
  if (this.peerConnection) this._iceGatheringState$.next(this.peerConnection.iceGatheringState);
15642
15725
  };
15643
15726
  this.onnegotiationneededHandler = (event) => {
15644
- logger$16.debug("[RTCPeerConnectionController] Negotiation needed event received.", event);
15727
+ logger$17.debug("[RTCPeerConnectionController] Negotiation needed event received.", event);
15645
15728
  this.negotiationNeeded$.next();
15646
15729
  };
15647
15730
  this.updateSelectedInputDevice = async (kind, deviceInfo) => {
15648
15731
  try {
15649
15732
  const { localStream } = this;
15650
15733
  if (!localStream) {
15651
- logger$16.warn("[RTCPeerConnectionController] No local stream available to update input device.");
15734
+ logger$17.warn("[RTCPeerConnectionController] No local stream available to update input device.");
15652
15735
  return;
15653
15736
  }
15654
- logger$16.debug(`[RTCPeerConnectionController] Updating selected ${kind} input device:`, localStream.getTracks());
15737
+ logger$17.debug(`[RTCPeerConnectionController] Updating selected ${kind} input device:`, localStream.getTracks());
15655
15738
  const track = localStream.getTracks().find((track$1) => track$1.kind === kind);
15656
15739
  if (track) {
15657
15740
  this.transceiverController?.stopTrackSender(kind);
15658
15741
  this.localStreamController.removeTrack(track.id);
15659
- logger$16.debug(`[RTCPeerConnectionController] Stopped existing ${kind} track: ${track.id}`, localStream.getTracks());
15742
+ logger$17.debug(`[RTCPeerConnectionController] Stopped existing ${kind} track: ${track.id}`, localStream.getTracks());
15660
15743
  if (!deviceInfo) {
15661
- logger$16.debug(`[RTCPeerConnectionController] ${kind} input device selected: none`);
15744
+ logger$17.debug(`[RTCPeerConnectionController] ${kind} input device selected: none`);
15662
15745
  return;
15663
15746
  }
15664
15747
  const streamTrack = (await this.getUserMedia({ [kind]: {
@@ -15666,16 +15749,16 @@ var RTCPeerConnectionController = class extends Destroyable {
15666
15749
  ...this.deviceController.deviceInfoToConstraints(deviceInfo)
15667
15750
  } })).getTracks().find((t) => t.kind === kind);
15668
15751
  if (streamTrack) {
15669
- logger$16.debug(`[RTCPeerConnectionController] Adding new ${kind} track: ${streamTrack.id}`);
15752
+ logger$17.debug(`[RTCPeerConnectionController] Adding new ${kind} track: ${streamTrack.id}`);
15670
15753
  this.localStreamController.addTrack(streamTrack);
15671
15754
  await this.transceiverController?.replaceSenderTrack(kind, streamTrack);
15672
- logger$16.debug(`[RTCPeerConnectionController] Added new ${kind} track: ${streamTrack.id}`, this.localStream?.getTracks());
15755
+ logger$17.debug(`[RTCPeerConnectionController] Added new ${kind} track: ${streamTrack.id}`, this.localStream?.getTracks());
15673
15756
  }
15674
15757
  }
15675
- logger$16.debug(`[RTCPeerConnectionController] ${kind} input device selected:`, deviceInfo?.label);
15758
+ logger$17.debug(`[RTCPeerConnectionController] ${kind} input device selected:`, deviceInfo?.label);
15676
15759
  } catch (error) {
15677
- logger$16.error(`[RTCPeerConnectionController] Failed to select ${kind} input device:`, error);
15678
- this._errors$.next(toError(error));
15760
+ logger$17.error(`[RTCPeerConnectionController] Failed to select ${kind} input device:`, error);
15761
+ this._errors$.next(new MediaTrackError("updateSelectedInputDevice", kind, error));
15679
15762
  throw error;
15680
15763
  }
15681
15764
  };
@@ -15897,7 +15980,7 @@ var RTCPeerConnectionController = class extends Destroyable {
15897
15980
  case "main":
15898
15981
  default: return {
15899
15982
  ...options,
15900
- offerToReceiveAudio: true,
15983
+ offerToReceiveAudio: this.options.receiveAudio ?? true,
15901
15984
  offerToReceiveVideo: this.options.receiveVideo ?? Boolean(this.inputVideoDeviceConstraints)
15902
15985
  };
15903
15986
  }
@@ -15924,15 +16007,15 @@ var RTCPeerConnectionController = class extends Destroyable {
15924
16007
  this.setupPeerConnection();
15925
16008
  this.subscribeTo(this.negotiationNeeded$.pipe((0, import_cjs$16.auditTime)(0), (0, import_cjs$16.exhaustMap)(async () => this.startNegotiation())), {
15926
16009
  next: () => {
15927
- logger$16.debug("[RTCPeerConnectionController] Start Negotiation completed successfully");
16010
+ logger$17.debug("[RTCPeerConnectionController] Start Negotiation completed successfully");
15928
16011
  },
15929
16012
  error: (error) => {
15930
- logger$16.error("[RTCPeerConnectionController] Start Negotiation error:", error);
16013
+ logger$17.error("[RTCPeerConnectionController] Start Negotiation error:", error);
15931
16014
  this._errors$.next(toError(error));
15932
16015
  }
15933
16016
  });
15934
16017
  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]) => {
15935
- logger$16.debug(`[RTCPeerConnectionController] Selected input device changed for:`, {
16018
+ logger$17.debug(`[RTCPeerConnectionController] Selected input device changed for:`, {
15936
16019
  kind,
15937
16020
  deviceInfo
15938
16021
  });
@@ -15949,7 +16032,7 @@ var RTCPeerConnectionController = class extends Destroyable {
15949
16032
  this._initialized$.next(true);
15950
16033
  }
15951
16034
  } catch (error) {
15952
- logger$16.error("[RTCPeerConnectionController] Initialization error:", error);
16035
+ logger$17.error("[RTCPeerConnectionController] Initialization error:", error);
15953
16036
  this._errors$.next(toError(error));
15954
16037
  this.destroy();
15955
16038
  }
@@ -15981,22 +16064,22 @@ var RTCPeerConnectionController = class extends Destroyable {
15981
16064
  }
15982
16065
  async startNegotiation() {
15983
16066
  if (this.isNegotiating) {
15984
- logger$16.debug("[RTCPeerConnectionController] Negotiation already in progress, skipping.");
16067
+ logger$17.debug("[RTCPeerConnectionController] Negotiation already in progress, skipping.");
15985
16068
  return;
15986
16069
  }
15987
16070
  this.setupEventListeners();
15988
16071
  if (this.type === "answer") {
15989
- logger$16.debug("[RTCPeerConnectionController] This is an answer type still, skipping offer creation.");
16072
+ logger$17.debug("[RTCPeerConnectionController] This is an answer type still, skipping offer creation.");
15990
16073
  return;
15991
16074
  }
15992
16075
  this._isNegotiating$.next(true);
15993
- logger$16.debug("[RTCPeerConnectionController] Starting negotiation.");
16076
+ logger$17.debug("[RTCPeerConnectionController] Starting negotiation.");
15994
16077
  try {
15995
16078
  const { offerOptions } = this;
15996
- logger$16.debug("[RTCPeerConnectionController] Creating offer with options:", offerOptions);
16079
+ logger$17.debug("[RTCPeerConnectionController] Creating offer with options:", offerOptions);
15997
16080
  await this.createOffer(offerOptions);
15998
16081
  } catch (error) {
15999
- logger$16.error("[RTCPeerConnectionController] Error during negotiation:", error);
16082
+ logger$17.error("[RTCPeerConnectionController] Error during negotiation:", error);
16000
16083
  this._errors$.next(toError(error));
16001
16084
  }
16002
16085
  }
@@ -16012,14 +16095,14 @@ var RTCPeerConnectionController = class extends Destroyable {
16012
16095
  let readyToConnect = status !== "failed";
16013
16096
  try {
16014
16097
  if (status === "received" && sdp) {
16015
- logger$16.debug("[RTCPeerConnectionController] Received answer SDP:", sdp);
16098
+ logger$17.debug("[RTCPeerConnectionController] Received answer SDP:", sdp);
16016
16099
  await this._setRemoteDescription({
16017
16100
  type: "answer",
16018
16101
  sdp
16019
16102
  });
16020
16103
  }
16021
16104
  } catch (error) {
16022
- logger$16.error("[RTCPeerConnectionController] Error updating answer status:", error);
16105
+ logger$17.error("[RTCPeerConnectionController] Error updating answer status:", error);
16023
16106
  this._errors$.next(toError(error));
16024
16107
  readyToConnect = false;
16025
16108
  } finally {
@@ -16038,7 +16121,7 @@ var RTCPeerConnectionController = class extends Destroyable {
16038
16121
  await this.handleOfferReceived();
16039
16122
  break;
16040
16123
  case "failed":
16041
- logger$16.error("[RTCPeerConnectionController] Offer failed to be processed by remote.");
16124
+ logger$17.error("[RTCPeerConnectionController] Offer failed to be processed by remote.");
16042
16125
  break;
16043
16126
  case "sent":
16044
16127
  default:
@@ -16051,13 +16134,14 @@ var RTCPeerConnectionController = class extends Destroyable {
16051
16134
  */
16052
16135
  async acceptInbound(mediaOverrides) {
16053
16136
  if (mediaOverrides) {
16054
- const { audio, video, receiveAudio, receiveVideo } = mediaOverrides;
16137
+ const { audio, video, receiveAudio, receiveVideo, fallbackToReceiveOnly } = mediaOverrides;
16055
16138
  this.options = {
16056
16139
  ...this.options,
16057
16140
  ...audio !== void 0 ? { audio } : {},
16058
16141
  ...video !== void 0 ? { video } : {},
16059
16142
  ...receiveAudio !== void 0 ? { receiveAudio } : {},
16060
- ...receiveVideo !== void 0 ? { receiveVideo } : {}
16143
+ ...receiveVideo !== void 0 ? { receiveVideo } : {},
16144
+ ...fallbackToReceiveOnly !== void 0 ? { fallbackToReceiveOnly } : {}
16061
16145
  };
16062
16146
  this.transceiverController?.updateOptions({
16063
16147
  receiveAudio: this.receiveAudio,
@@ -16070,7 +16154,7 @@ var RTCPeerConnectionController = class extends Destroyable {
16070
16154
  }
16071
16155
  await this.setupLocalTracks();
16072
16156
  const { answerOptions } = this;
16073
- logger$16.debug("[RTCPeerConnectionController] Creating inbound answer with options:", answerOptions);
16157
+ logger$17.debug("[RTCPeerConnectionController] Creating inbound answer with options:", answerOptions);
16074
16158
  await this.createAnswer(answerOptions);
16075
16159
  }
16076
16160
  async handleOfferReceived() {
@@ -16078,7 +16162,7 @@ var RTCPeerConnectionController = class extends Destroyable {
16078
16162
  this._isNegotiating$.next(true);
16079
16163
  await this._setRemoteDescription(this.sdpInit);
16080
16164
  const { answerOptions } = this;
16081
- logger$16.debug("[RTCPeerConnectionController] Creating answer with options:", answerOptions);
16165
+ logger$17.debug("[RTCPeerConnectionController] Creating answer with options:", answerOptions);
16082
16166
  await this.createAnswer(answerOptions);
16083
16167
  }
16084
16168
  readyToConnect() {
@@ -16086,7 +16170,7 @@ var RTCPeerConnectionController = class extends Destroyable {
16086
16170
  this.connectionTimer = setTimeout(() => {
16087
16171
  this.removeConnectionTimer();
16088
16172
  if (this.peerConnection?.connectionState !== "connected") {
16089
- logger$16.debug("[RTCPeerConnectionController] Connection timeout, restarting ICE gathering with relay only.");
16173
+ logger$17.debug("[RTCPeerConnectionController] Connection timeout, restarting ICE gathering with relay only.");
16090
16174
  this.iceGatheringController.restartICEGatheringWithRelayOnly();
16091
16175
  }
16092
16176
  }, this.connectionTimeout);
@@ -16108,14 +16192,14 @@ var RTCPeerConnectionController = class extends Destroyable {
16108
16192
  const stereo = this.options.stereo ?? PreferencesContainer.instance.stereoAudio;
16109
16193
  if (preferredAudioCodecs.length > 0 || preferredVideoCodecs.length > 0) {
16110
16194
  result = setCodecPreferences(result, preferredAudioCodecs, preferredVideoCodecs);
16111
- logger$16.debug("[RTCPeerConnectionController] Applied codec preferences to SDP", {
16195
+ logger$17.debug("[RTCPeerConnectionController] Applied codec preferences to SDP", {
16112
16196
  preferredAudioCodecs,
16113
16197
  preferredVideoCodecs
16114
16198
  });
16115
16199
  }
16116
16200
  if (stereo) {
16117
16201
  result = enableStereoOpus(result);
16118
- logger$16.debug("[RTCPeerConnectionController] Applied stereo Opus to SDP");
16202
+ logger$17.debug("[RTCPeerConnectionController] Applied stereo Opus to SDP");
16119
16203
  }
16120
16204
  return Promise.resolve(result);
16121
16205
  }
@@ -16171,25 +16255,25 @@ var RTCPeerConnectionController = class extends Destroyable {
16171
16255
  ...this.peerConnection.getConfiguration(),
16172
16256
  iceTransportPolicy: "relay"
16173
16257
  });
16174
- logger$16.debug("[RTCPeerConnectionController] ICE transport policy set to relay-only");
16258
+ logger$17.debug("[RTCPeerConnectionController] ICE transport policy set to relay-only");
16175
16259
  } catch (error) {
16176
- logger$16.warn("[RTCPeerConnectionController] Failed to set relay-only policy:", error);
16260
+ logger$17.warn("[RTCPeerConnectionController] Failed to set relay-only policy:", error);
16177
16261
  }
16178
16262
  this.setupEventListeners();
16179
16263
  this._isNegotiating$.next(true);
16180
- logger$16.debug(`[RTCPeerConnectionController] Triggering ICE restart${relayOnly ? " (relay-only)" : ""}.`);
16264
+ logger$17.debug(`[RTCPeerConnectionController] Triggering ICE restart${relayOnly ? " (relay-only)" : ""}.`);
16181
16265
  try {
16182
16266
  const offer = await this.peerConnection.createOffer({ iceRestart: true });
16183
16267
  await this.setLocalDescription(offer);
16184
16268
  } catch (error) {
16185
- logger$16.error("[RTCPeerConnectionController] ICE restart offer failed:", error);
16269
+ logger$17.error("[RTCPeerConnectionController] ICE restart offer failed:", error);
16186
16270
  this._errors$.next(toError(error));
16187
16271
  this.negotiationEnded();
16188
16272
  if (policyChanged) this.restoreIceTransportPolicy();
16189
16273
  throw error;
16190
16274
  }
16191
16275
  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) => {
16192
- logger$16.warn("[RTCPeerConnectionController] Error waiting for ICE gathering to complete:", error);
16276
+ logger$17.warn("[RTCPeerConnectionController] Error waiting for ICE gathering to complete:", error);
16193
16277
  this.restoreIceTransportPolicy();
16194
16278
  });
16195
16279
  }
@@ -16199,9 +16283,9 @@ var RTCPeerConnectionController = class extends Destroyable {
16199
16283
  ...this.peerConnection.getConfiguration(),
16200
16284
  iceTransportPolicy: this.options.relayOnly ? "relay" : "all"
16201
16285
  });
16202
- logger$16.debug("[RTCPeerConnectionController] ICE transport policy restored");
16286
+ logger$17.debug("[RTCPeerConnectionController] ICE transport policy restored");
16203
16287
  } catch (error) {
16204
- logger$16.warn("[RTCPeerConnectionController] Failed to restore ICE transport policy:", error);
16288
+ logger$17.warn("[RTCPeerConnectionController] Failed to restore ICE transport policy:", error);
16205
16289
  }
16206
16290
  }
16207
16291
  /**
@@ -16213,13 +16297,25 @@ var RTCPeerConnectionController = class extends Destroyable {
16213
16297
  await this.setupRemoteTracks();
16214
16298
  }
16215
16299
  async setupLocalTracks() {
16216
- logger$16.debug("[RTCPeerConnectionController] Setting up local tracks/transceivers.");
16217
- const localStream = this.localStream ?? await this.localStreamController.buildLocalStream();
16300
+ logger$17.debug("[RTCPeerConnectionController] Setting up local tracks/transceivers.");
16301
+ if (this.hasNoLocalMediaToSend()) {
16302
+ if (!this.receiveAudio && !this.receiveVideo) throw new InvalidParams("Call requests no media: enable audio/video or receiveAudio/receiveVideo");
16303
+ logger$17.debug("[RTCPeerConnectionController] No local media requested; negotiating receive-only.");
16304
+ this.setupReceiveOnlyTransceivers();
16305
+ return;
16306
+ }
16307
+ let localStream;
16308
+ try {
16309
+ localStream = this.localStream ?? await this.localStreamController.buildLocalStream();
16310
+ } catch (error) {
16311
+ this.handleLocalMediaFailure(error);
16312
+ return;
16313
+ }
16218
16314
  if (this.transceiverController?.useAddStream ?? false) {
16219
- logger$16.warn("[RTCPeerConnectionController] Using deprecated addStream API to add local stream.");
16315
+ logger$17.warn("[RTCPeerConnectionController] Using deprecated addStream API to add local stream.");
16220
16316
  this.peerConnection?.addStream(localStream);
16221
16317
  if (!this.isNegotiating) {
16222
- logger$16.debug("[RTCPeerConnectionController] Forcing negotiationneeded after local tracks setup.");
16318
+ logger$17.debug("[RTCPeerConnectionController] Forcing negotiationneeded after local tracks setup.");
16223
16319
  this.negotiationNeeded$.next();
16224
16320
  }
16225
16321
  return;
@@ -16235,12 +16331,54 @@ var RTCPeerConnectionController = class extends Destroyable {
16235
16331
  const transceivers = (kind === "audio" ? this.transceiverController?.audioTransceivers : this.transceiverController?.videoTransceivers) ?? [];
16236
16332
  await this.transceiverController?.setupTransceiverSender(track, localStream, transceivers[index]);
16237
16333
  } else {
16238
- logger$16.debug(`[RTCPeerConnectionController] Using addTrack for local ${kind} track:`, track.id);
16334
+ logger$17.debug(`[RTCPeerConnectionController] Using addTrack for local ${kind} track:`, track.id);
16239
16335
  this.peerConnection?.addTrack(track, localStream);
16240
16336
  }
16241
16337
  }
16242
16338
  }
16243
16339
  }
16340
+ /** True for a main connection with no local media to send. */
16341
+ hasNoLocalMediaToSend() {
16342
+ const hasInputStreams = Boolean(this.options.inputAudioStream ?? this.options.inputVideoStream);
16343
+ return this.propose === "main" && !this.localStream && !hasInputStreams && !this.inputAudioDeviceConstraints && !this.inputVideoDeviceConstraints;
16344
+ }
16345
+ /** The media kinds this connection wants to send: 'audiovideo' | 'video' | 'audio'. */
16346
+ get requestedMediaKinds() {
16347
+ const wantsAudio = Boolean(this.inputAudioDeviceConstraints);
16348
+ const wantsVideo = Boolean(this.inputVideoDeviceConstraints);
16349
+ if (wantsAudio && wantsVideo) return "audiovideo";
16350
+ return wantsVideo ? "video" : "audio";
16351
+ }
16352
+ /**
16353
+ * Handle a local media acquisition failure with a typed, semantically
16354
+ * accurate MediaAccessError created at the acquisition site:
16355
+ * - Auxiliary connections (screenshare / additional-device) throw a
16356
+ * non-fatal error — VertoManager surfaces it and the call is unaffected.
16357
+ * - The main connection degrades to receive-only when allowed (default),
16358
+ * otherwise fails with a fatal error.
16359
+ */
16360
+ handleLocalMediaFailure(error) {
16361
+ if (this.propose === "screenshare") throw new MediaAccessError("startScreenShare", "screen", error, false);
16362
+ if (this.propose === "additional-device") throw new MediaAccessError("addInputDevice", this.requestedMediaKinds, error, false);
16363
+ const canReceive = this.receiveAudio || this.receiveVideo;
16364
+ if (!((this.options.fallbackToReceiveOnly ?? true) && canReceive)) throw new MediaAccessError("acquireLocalMedia", this.requestedMediaKinds, error, true);
16365
+ logger$17.warn("[RTCPeerConnectionController] Local media unavailable; continuing receive-only:", error);
16366
+ this._errors$.next(new MediaAccessError("acquireLocalMedia", this.requestedMediaKinds, error, false));
16367
+ this.setupReceiveOnlyTransceivers();
16368
+ }
16369
+ /**
16370
+ * Negotiate receive-only m-lines when there are no local tracks to send.
16371
+ * Only offer-type connections add transceivers — answer-type connections
16372
+ * reuse the transceivers created from the remote offer.
16373
+ */
16374
+ setupReceiveOnlyTransceivers() {
16375
+ if (this.type !== "offer") return;
16376
+ if (this.transceiverController?.useAddTransceivers ?? false) {
16377
+ this.peerConnection?.addTransceiver("audio", { direction: this.receiveAudio ? "recvonly" : "inactive" });
16378
+ this.peerConnection?.addTransceiver("video", { direction: this.receiveVideo ? "recvonly" : "inactive" });
16379
+ }
16380
+ if (!this.isNegotiating) this.negotiationNeeded$.next();
16381
+ }
16244
16382
  async getUserMedia(constraints) {
16245
16383
  return (this.options.webRTCApiProvider?.mediaDevices ?? navigator.mediaDevices).getUserMedia(constraints);
16246
16384
  }
@@ -16252,7 +16390,7 @@ var RTCPeerConnectionController = class extends Destroyable {
16252
16390
  async setupRemoteTracks() {
16253
16391
  if (!this.peerConnection) throw new DependencyError("RTCPeerConnection is not initialized");
16254
16392
  this.peerConnection.ontrack = (event) => {
16255
- logger$16.debug("[RTCPeerConnectionController] Remote track received:", event.track.kind);
16393
+ logger$17.debug("[RTCPeerConnectionController] Remote track received:", event.track.kind);
16256
16394
  if (event.streams[0]) this._remoteStream$.next(event.streams[0]);
16257
16395
  else {
16258
16396
  const existingTracks = this._remoteStream$.value?.getTracks() ?? [];
@@ -16276,8 +16414,8 @@ var RTCPeerConnectionController = class extends Destroyable {
16276
16414
  try {
16277
16415
  stream = await this.getUserMedia({ audio: constraints });
16278
16416
  } catch (error) {
16279
- logger$16.error("[RTCPeerConnectionController] Failed to re-acquire mic for pipeline restore:", error);
16280
- this._errors$.next(toError(error));
16417
+ logger$17.error("[RTCPeerConnectionController] Failed to re-acquire mic for pipeline restore:", error);
16418
+ this._errors$.next(new MediaTrackError("restoreAudioPipelineInput", "audio", error));
16281
16419
  return;
16282
16420
  }
16283
16421
  const newTrack = stream.getAudioTracks().at(0);
@@ -16298,7 +16436,7 @@ var RTCPeerConnectionController = class extends Destroyable {
16298
16436
  try {
16299
16437
  this._localAudioPipeline = new LocalAudioPipeline();
16300
16438
  } catch (error) {
16301
- logger$16.warn("[RTCPeerConnectionController] Failed to create LocalAudioPipeline:", error);
16439
+ logger$17.warn("[RTCPeerConnectionController] Failed to create LocalAudioPipeline:", error);
16302
16440
  return null;
16303
16441
  }
16304
16442
  this.subscribeTo(this.localStreamController.localAudioTracks$, () => {
@@ -16320,7 +16458,7 @@ var RTCPeerConnectionController = class extends Destroyable {
16320
16458
  try {
16321
16459
  await sender.replaceTrack(this._localAudioPipeline.outputTrack);
16322
16460
  } catch (error) {
16323
- logger$16.warn("[RTCPeerConnectionController] Failed to route audio sender through pipeline:", error);
16461
+ logger$17.warn("[RTCPeerConnectionController] Failed to route audio sender through pipeline:", error);
16324
16462
  }
16325
16463
  }
16326
16464
  /**
@@ -16336,10 +16474,10 @@ var RTCPeerConnectionController = class extends Destroyable {
16336
16474
  try {
16337
16475
  const localStream = this.localStreamController.addTrack(track);
16338
16476
  this.peerConnection.addTrack(track, localStream);
16339
- logger$16.debug(`[RTCPeerConnectionController] ${track.kind} track added:`, track.id);
16477
+ logger$17.debug(`[RTCPeerConnectionController] ${track.kind} track added:`, track.id);
16340
16478
  } catch (error) {
16341
- logger$16.error(`[RTCPeerConnectionController] Failed to add ${track.kind} track:`, error);
16342
- this._errors$.next(toError(error));
16479
+ logger$17.error(`[RTCPeerConnectionController] Failed to add ${track.kind} track:`, error);
16480
+ this._errors$.next(new MediaTrackError("addLocalTrack", track.kind, error));
16343
16481
  throw error;
16344
16482
  }
16345
16483
  }
@@ -16355,16 +16493,16 @@ var RTCPeerConnectionController = class extends Destroyable {
16355
16493
  }
16356
16494
  const sender = this.peerConnection.getSenders().find((sender$1) => sender$1.track?.id === trackId);
16357
16495
  if (!sender) {
16358
- logger$16.debug(`[RTCPeerConnectionController] track not found: ${trackId}`);
16496
+ logger$17.debug(`[RTCPeerConnectionController] track not found: ${trackId}`);
16359
16497
  return;
16360
16498
  }
16361
16499
  try {
16362
16500
  this.peerConnection.removeTrack(sender);
16363
16501
  this.localStreamController.removeTrack(trackId);
16364
- logger$16.debug(`[RTCPeerConnectionController] ${sender.track?.kind} track removed:`, trackId);
16502
+ logger$17.debug(`[RTCPeerConnectionController] ${sender.track?.kind} track removed:`, trackId);
16365
16503
  } catch (error) {
16366
- logger$16.error(`[RTCPeerConnectionController] Failed to remove ${sender.track?.kind} track:`, error);
16367
- this._errors$.next(toError(error));
16504
+ logger$17.error(`[RTCPeerConnectionController] Failed to remove ${sender.track?.kind} track:`, error);
16505
+ this._errors$.next(new MediaTrackError("removeLocalTrack", sender.track?.kind ?? "unknown", error));
16368
16506
  throw error;
16369
16507
  }
16370
16508
  }
@@ -16390,7 +16528,7 @@ var RTCPeerConnectionController = class extends Destroyable {
16390
16528
  async replaceAudioTrackWithConstraints(constraints) {
16391
16529
  const senders = this.peerConnection?.getSenders().filter((s) => s.track?.kind === "audio" && s.track.readyState === "live");
16392
16530
  if (!senders || senders.length === 0) {
16393
- logger$16.warn("[RTCPeerConnectionController] No live audio sender to replace");
16531
+ logger$17.warn("[RTCPeerConnectionController] No live audio sender to replace");
16394
16532
  return;
16395
16533
  }
16396
16534
  for (const sender of senders) {
@@ -16408,7 +16546,7 @@ var RTCPeerConnectionController = class extends Destroyable {
16408
16546
  const newTrack = (await this.getUserMedia({ audio: mergedConstraints })).getAudioTracks()[0];
16409
16547
  await sender.replaceTrack(newTrack);
16410
16548
  this.localStreamController.addTrack(newTrack);
16411
- logger$16.debug(`[RTCPeerConnectionController] Audio track replaced for server-pushed params. New track: ${newTrack.id}`);
16549
+ logger$17.debug(`[RTCPeerConnectionController] Audio track replaced for server-pushed params. New track: ${newTrack.id}`);
16412
16550
  }
16413
16551
  }
16414
16552
  /**
@@ -16416,7 +16554,7 @@ var RTCPeerConnectionController = class extends Destroyable {
16416
16554
  * Completes all observables to prevent memory leaks.
16417
16555
  */
16418
16556
  destroy() {
16419
- logger$16.debug(`[RTCPeerConnectionController] Destroying RTCPeerConnectionController. ${this.propose}`);
16557
+ logger$17.debug(`[RTCPeerConnectionController] Destroying RTCPeerConnectionController. ${this.propose}`);
16420
16558
  this.removeConnectionTimer();
16421
16559
  this._iceGatheringController?.destroy();
16422
16560
  this._localAudioPipeline?.destroy();
@@ -16442,7 +16580,7 @@ var RTCPeerConnectionController = class extends Destroyable {
16442
16580
  }
16443
16581
  stopRemoteTracks() {
16444
16582
  this._remoteStream$.value?.getTracks().forEach((track) => {
16445
- logger$16.debug(`[RTCPeerConnectionController] Stopping remote track: ${track.kind}`);
16583
+ logger$17.debug(`[RTCPeerConnectionController] Stopping remote track: ${track.kind}`);
16446
16584
  track.stop();
16447
16585
  });
16448
16586
  }
@@ -16459,7 +16597,7 @@ var RTCPeerConnectionController = class extends Destroyable {
16459
16597
  ...params,
16460
16598
  sdp: finalRemote
16461
16599
  };
16462
- logger$16.debug("[RTCPeerConnectionController] Setting remote description:", answer);
16600
+ logger$17.debug("[RTCPeerConnectionController] Setting remote description:", answer);
16463
16601
  return this.peerConnection.setRemoteDescription(answer);
16464
16602
  }
16465
16603
  };
@@ -16474,7 +16612,11 @@ function isVertoInviteMessage(value) {
16474
16612
  const msg = value;
16475
16613
  return msg.method === "verto.invite" && isObject(msg.params) && hasProperty(msg.params, "sdp") && hasProperty(msg.params, "callID");
16476
16614
  }
16477
- function isVertoByeMessage(value) {
16615
+ /**
16616
+ * Guards inbound `verto.bye` frames (server → client). Outbound bye messages are
16617
+ * built locally and never type-guarded, so only the inbound shape is asserted.
16618
+ */
16619
+ function isVertoByeInboundMessage(value) {
16478
16620
  if (!isVertoMethodMessage(value)) return false;
16479
16621
  return value.method === "verto.bye";
16480
16622
  }
@@ -16494,11 +16636,19 @@ function isVertoMediaParamsInnerParams(value) {
16494
16636
  function isVertoPingInnerParams(value) {
16495
16637
  return isObject(value) && hasProperty(value, "jsonrpc") && value.jsonrpc === "2.0" && hasProperty(value, "method") && value.method === "verto.ping";
16496
16638
  }
16639
+ /**
16640
+ * Guards the params of an inbound `verto.bye` frame (top-level `callID`). Use
16641
+ * this to recognise the bye payload extracted by `vertoBye$`, e.g. when racing
16642
+ * it against a boolean answer/reject signal.
16643
+ */
16644
+ function isVertoByeInboundParamsGuard(value) {
16645
+ return isObject(value) && hasProperty(value, "callID") && typeof value.callID === "string";
16646
+ }
16497
16647
 
16498
16648
  //#endregion
16499
16649
  //#region src/managers/VertoManager.ts
16500
16650
  var import_cjs$15 = require_cjs();
16501
- const logger$15 = getLogger();
16651
+ const logger$16 = getLogger();
16502
16652
  /**
16503
16653
  * Decide what value goes on the `node_id` field of a `webrtc.verto` envelope.
16504
16654
  *
@@ -16554,7 +16704,7 @@ var WebRTCVertoManager = class extends VertoManager {
16554
16704
  try {
16555
16705
  await this.executeVerto(vertoModifyMessage);
16556
16706
  } catch (error) {
16557
- logger$15.warn("[WebRTCManager] Call might already be disconnected, error sending Verto hold:", error);
16707
+ logger$16.warn("[WebRTCManager] Call might already be disconnected, error sending Verto hold:", error);
16558
16708
  throw error;
16559
16709
  }
16560
16710
  }
@@ -16567,7 +16717,7 @@ var WebRTCVertoManager = class extends VertoManager {
16567
16717
  try {
16568
16718
  await this.executeVerto(vertoModifyMessage);
16569
16719
  } catch (error) {
16570
- logger$15.warn("[WebRTCManager] Call might already be disconnected, error sending Verto unhold:", error);
16720
+ logger$16.warn("[WebRTCManager] Call might already be disconnected, error sending Verto unhold:", error);
16571
16721
  throw error;
16572
16722
  }
16573
16723
  }
@@ -16620,7 +16770,7 @@ var WebRTCVertoManager = class extends VertoManager {
16620
16770
  if (event.member_id) this.setSelfIdIfNull(event.member_id);
16621
16771
  });
16622
16772
  this.subscribeTo(this.vertoMedia$, (event) => {
16623
- logger$15.debug("[WebRTCManager] Received Verto media event (early media SDP):", event);
16773
+ logger$16.debug("[WebRTCManager] Received Verto media event (early media SDP):", event);
16624
16774
  const { sdp, callID } = event;
16625
16775
  this.emitMainSignalingStatus(callID, "ringing");
16626
16776
  this._rtcPeerConnectionsMap.get(callID)?.updateAnswerStatus({
@@ -16629,7 +16779,7 @@ var WebRTCVertoManager = class extends VertoManager {
16629
16779
  });
16630
16780
  });
16631
16781
  this.subscribeTo(this.vertoAnswer$, (event) => {
16632
- logger$15.debug("[WebRTCManager] Received Verto answer event:", event);
16782
+ logger$16.debug("[WebRTCManager] Received Verto answer event:", event);
16633
16783
  const { sdp, callID } = event;
16634
16784
  this.emitMainSignalingStatus(callID, "connecting");
16635
16785
  this._rtcPeerConnectionsMap.get(callID)?.updateAnswerStatus({
@@ -16638,7 +16788,7 @@ var WebRTCVertoManager = class extends VertoManager {
16638
16788
  });
16639
16789
  });
16640
16790
  this.subscribeTo(this.vertoMediaParams$, (event) => {
16641
- logger$15.debug("[WebRTCManager] Received Verto mediaParams event:", event);
16791
+ logger$16.debug("[WebRTCManager] Received Verto mediaParams event:", event);
16642
16792
  const { mediaParams, callID } = event;
16643
16793
  const rtcPeerConnController = this._rtcPeerConnectionsMap.get(callID);
16644
16794
  const { audio, video } = mediaParams;
@@ -16652,7 +16802,7 @@ var WebRTCVertoManager = class extends VertoManager {
16652
16802
  timestamp: Date.now()
16653
16803
  });
16654
16804
  } catch (error) {
16655
- logger$15.warn("[WebRTCManager] Error applying server-pushed media params:", error);
16805
+ logger$16.warn("[WebRTCManager] Error applying server-pushed media params:", error);
16656
16806
  this.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
16657
16807
  }
16658
16808
  })();
@@ -16674,13 +16824,13 @@ var WebRTCVertoManager = class extends VertoManager {
16674
16824
  */
16675
16825
  setNodeIdIfNull(nodeId) {
16676
16826
  if (!this._nodeId$.value && nodeId) {
16677
- logger$15.debug(`[WebRTCManager] Early node_id set: ${nodeId}`);
16827
+ logger$16.debug(`[WebRTCManager] Early node_id set: ${nodeId}`);
16678
16828
  this._nodeId$.next(nodeId);
16679
16829
  }
16680
16830
  }
16681
16831
  setSelfIdIfNull(selfId) {
16682
16832
  if (!this._selfId$.value && selfId) {
16683
- logger$15.debug(`[WebRTCManager] Early selfId set: ${selfId}`);
16833
+ logger$16.debug(`[WebRTCManager] Early selfId set: ${selfId}`);
16684
16834
  this._selfId$.next(selfId);
16685
16835
  }
16686
16836
  }
@@ -16689,7 +16839,7 @@ var WebRTCVertoManager = class extends VertoManager {
16689
16839
  const vertoPongMessage = VertoPong({ ...vertoPing });
16690
16840
  await this.executeVerto(vertoPongMessage);
16691
16841
  } catch (error) {
16692
- logger$15.warn("[WebRTCManager] Call might disconnect, error sending Verto pong:", error);
16842
+ logger$16.warn("[WebRTCManager] Call might disconnect, error sending Verto pong:", error);
16693
16843
  this.onError?.(new VertoPongError(error));
16694
16844
  }
16695
16845
  }
@@ -16699,7 +16849,7 @@ var WebRTCVertoManager = class extends VertoManager {
16699
16849
  if (audio) await this.mainPeerConnection.updateSendersConstraints("audio", audio);
16700
16850
  if (video) await this.mainPeerConnection.updateSendersConstraints("video", video);
16701
16851
  } catch (error) {
16702
- logger$15.warn("[WebRTCManager] Error updating media constraints:", error);
16852
+ logger$16.warn("[WebRTCManager] Error updating media constraints:", error);
16703
16853
  this.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
16704
16854
  throw error;
16705
16855
  }
@@ -16729,20 +16879,20 @@ var WebRTCVertoManager = class extends VertoManager {
16729
16879
  try {
16730
16880
  const pc = this.mainPeerConnection.peerConnection;
16731
16881
  if (!pc) {
16732
- logger$15.warn("[WebRTCManager] No peer connection for keyframe request");
16882
+ logger$16.warn("[WebRTCManager] No peer connection for keyframe request");
16733
16883
  return;
16734
16884
  }
16735
16885
  const videoReceiver = pc.getReceivers().find((r) => r.track.kind === "video");
16736
16886
  if (!videoReceiver) {
16737
- logger$15.warn("[WebRTCManager] No video receiver for keyframe request");
16887
+ logger$16.warn("[WebRTCManager] No video receiver for keyframe request");
16738
16888
  return;
16739
16889
  }
16740
16890
  if (typeof videoReceiver.requestKeyFrame === "function") {
16741
16891
  videoReceiver.requestKeyFrame();
16742
- logger$15.debug("[WebRTCManager] Keyframe requested via RTCRtpReceiver.requestKeyFrame()");
16743
- } else logger$15.debug("[WebRTCManager] requestKeyFrame() not supported, skipping");
16892
+ logger$16.debug("[WebRTCManager] Keyframe requested via RTCRtpReceiver.requestKeyFrame()");
16893
+ } else logger$16.debug("[WebRTCManager] requestKeyFrame() not supported, skipping");
16744
16894
  } catch (error) {
16745
- logger$15.warn("[WebRTCManager] Keyframe request failed (non-fatal):", error);
16895
+ logger$16.warn("[WebRTCManager] Keyframe request failed (non-fatal):", error);
16746
16896
  }
16747
16897
  }
16748
16898
  /**
@@ -16760,13 +16910,13 @@ var WebRTCVertoManager = class extends VertoManager {
16760
16910
  try {
16761
16911
  const controller = this.mainPeerConnection;
16762
16912
  if (!controller.peerConnection) {
16763
- logger$15.warn("[WebRTCManager] No peer connection for ICE restart");
16913
+ logger$16.warn("[WebRTCManager] No peer connection for ICE restart");
16764
16914
  return;
16765
16915
  }
16766
16916
  await controller.triggerIceRestart(relayOnly);
16767
- logger$15.info(`[WebRTCManager] ICE restart initiated${relayOnly ? " (relay-only)" : ""}`);
16917
+ logger$16.info(`[WebRTCManager] ICE restart initiated${relayOnly ? " (relay-only)" : ""}`);
16768
16918
  } catch (error) {
16769
- logger$15.error("[WebRTCManager] ICE restart failed:", error);
16919
+ logger$16.error("[WebRTCManager] ICE restart failed:", error);
16770
16920
  throw error;
16771
16921
  }
16772
16922
  }
@@ -16784,13 +16934,13 @@ var WebRTCVertoManager = class extends VertoManager {
16784
16934
  const entries = Array.from(this._rtcPeerConnectionsMap.entries());
16785
16935
  for (const [id, controller] of entries) try {
16786
16936
  if (!controller.peerConnection) {
16787
- logger$15.debug(`[WebRTCManager] No peer connection for leg ${id}, skipping ICE restart`);
16937
+ logger$16.debug(`[WebRTCManager] No peer connection for leg ${id}, skipping ICE restart`);
16788
16938
  continue;
16789
16939
  }
16790
16940
  await controller.triggerIceRestart(relayOnly);
16791
- logger$15.info(`[WebRTCManager] ICE restart initiated for leg ${id}${relayOnly ? " (relay-only)" : ""}`);
16941
+ logger$16.info(`[WebRTCManager] ICE restart initiated for leg ${id}${relayOnly ? " (relay-only)" : ""}`);
16792
16942
  } catch (error) {
16793
- logger$15.warn(`[WebRTCManager] ICE restart failed for leg ${id}:`, error);
16943
+ logger$16.warn(`[WebRTCManager] ICE restart failed for leg ${id}:`, error);
16794
16944
  }
16795
16945
  }
16796
16946
  /**
@@ -16802,7 +16952,7 @@ var WebRTCVertoManager = class extends VertoManager {
16802
16952
  requestKeyframeAll() {
16803
16953
  for (const [id, controller] of this._rtcPeerConnectionsMap) {
16804
16954
  if (controller.isScreenShare) {
16805
- logger$15.debug(`[WebRTCManager] Skipping keyframe for send-only screen share leg ${id}`);
16955
+ logger$16.debug(`[WebRTCManager] Skipping keyframe for send-only screen share leg ${id}`);
16806
16956
  continue;
16807
16957
  }
16808
16958
  try {
@@ -16812,10 +16962,10 @@ var WebRTCVertoManager = class extends VertoManager {
16812
16962
  if (!videoReceiver) continue;
16813
16963
  if (typeof videoReceiver.requestKeyFrame === "function") {
16814
16964
  videoReceiver.requestKeyFrame();
16815
- logger$15.debug(`[WebRTCManager] Keyframe requested for leg ${id}`);
16965
+ logger$16.debug(`[WebRTCManager] Keyframe requested for leg ${id}`);
16816
16966
  }
16817
16967
  } catch (error) {
16818
- logger$15.warn(`[WebRTCManager] Keyframe request failed for leg ${id} (non-fatal):`, error);
16968
+ logger$16.warn(`[WebRTCManager] Keyframe request failed for leg ${id} (non-fatal):`, error);
16819
16969
  }
16820
16970
  }
16821
16971
  }
@@ -16832,7 +16982,7 @@ var WebRTCVertoManager = class extends VertoManager {
16832
16982
  return this.cachedObservable("vertoMediaParams$", () => this.webRtcCallSession.webrtcMessages$.pipe(filterAs(isVertoMediaParamsInnerParams, "params"), (0, import_cjs$15.takeUntil)(this.destroyed$)));
16833
16983
  }
16834
16984
  get vertoBye$() {
16835
- return this.cachedObservable("vertoBye$", () => this.webRtcCallSession.webrtcMessages$.pipe(filterAs(isVertoByeMessage, "params"), (0, import_cjs$15.takeUntil)(this.destroyed$)));
16985
+ return this.cachedObservable("vertoBye$", () => this.webRtcCallSession.webrtcMessages$.pipe(filterAs(isVertoByeInboundMessage, "params"), (0, import_cjs$15.takeUntil)(this.destroyed$)));
16836
16986
  }
16837
16987
  get vertoAttach$() {
16838
16988
  return this.cachedObservable("vertoAttach$", () => this.webRtcCallSession.webrtcMessages$.pipe(filterAs(isVertoAttachMessage, "params"), (0, import_cjs$15.takeUntil)(this.destroyed$)));
@@ -16876,7 +17026,7 @@ var WebRTCVertoManager = class extends VertoManager {
16876
17026
  default:
16877
17027
  }
16878
17028
  } catch (error) {
16879
- logger$15.error(`[WebRTCManager] Error sending Verto ${vertoMethod}:`, error);
17029
+ logger$16.error(`[WebRTCManager] Error sending Verto ${vertoMethod}:`, error);
16880
17030
  this.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
16881
17031
  if (vertoMethod === "verto.modify") this.onModifyFailed?.();
16882
17032
  }
@@ -16891,7 +17041,7 @@ var WebRTCVertoManager = class extends VertoManager {
16891
17041
  sdp
16892
17042
  });
16893
17043
  } catch (error) {
16894
- logger$15.warn("[WebRTCManager] Error processing modify response:", error);
17044
+ logger$16.warn("[WebRTCManager] Error processing modify response:", error);
16895
17045
  const modifyError = error instanceof Error ? error : new Error(String(error), { cause: error });
16896
17046
  this.onError?.(modifyError);
16897
17047
  }
@@ -16901,7 +17051,7 @@ var WebRTCVertoManager = class extends VertoManager {
16901
17051
  const rtcPeerConnController = this._rtcPeerConnectionsMap.get(callId);
16902
17052
  if (!rtcPeerConnController) {
16903
17053
  const signalingError = new DependencyError(`Cannot emit signaling status, RTCPeerConnectionController not found for callID: ${callId}`);
16904
- logger$15.error("[WebRTCManager] Failed to emit signaling status:", {
17054
+ logger$16.error("[WebRTCManager] Failed to emit signaling status:", {
16905
17055
  callId,
16906
17056
  status,
16907
17057
  signalingError
@@ -16917,7 +17067,7 @@ var WebRTCVertoManager = class extends VertoManager {
16917
17067
  this._nodeId$.next(getValueFrom(response, "result.node_id") ?? null);
16918
17068
  const memberId = getValueFrom(response, "result.result.result.memberID") ?? null;
16919
17069
  const callId = getValueFrom(response, "result.result.result.callID") ?? null;
16920
- logger$15.debug("[WebRTCManager] Verto invite response:", {
17070
+ logger$16.debug("[WebRTCManager] Verto invite response:", {
16921
17071
  callId,
16922
17072
  memberId,
16923
17073
  response
@@ -16927,14 +17077,14 @@ var WebRTCVertoManager = class extends VertoManager {
16927
17077
  if (callId) {
16928
17078
  this.webRtcCallSession.addCallId(callId);
16929
17079
  this.attachManager.attach(this.buildAttachableCall(callId));
16930
- } else logger$15.warn("[WebRTCManager] Cannot attach call, missing callId:", {
17080
+ } else logger$16.warn("[WebRTCManager] Cannot attach call, missing callId:", {
16931
17081
  nodeId: this.nodeId,
16932
17082
  callId
16933
17083
  });
16934
- logger$15.info("[WebRTCManager] Verto invite successful");
16935
- logger$15.debug(`[WebRTCManager] nodeid: ${this._nodeId$.value}, selfId: ${this._selfId$.value}`);
17084
+ logger$16.info("[WebRTCManager] Verto invite successful");
17085
+ logger$16.debug(`[WebRTCManager] nodeid: ${this._nodeId$.value}, selfId: ${this._selfId$.value}`);
16936
17086
  } else {
16937
- logger$15.error("[WebRTCManager] Verto invite failed:", response);
17087
+ logger$16.error("[WebRTCManager] Verto invite failed:", response);
16938
17088
  const inviteError = response.error ? new JSONRPCError(response.error.code, response.error.message, response.error.data) : /* @__PURE__ */ new Error("Verto invite failed: unexpected response");
16939
17089
  this.onError?.(inviteError);
16940
17090
  }
@@ -16961,6 +17111,7 @@ var WebRTCVertoManager = class extends VertoManager {
16961
17111
  inputVideoStream: options.inputVideoStream,
16962
17112
  receiveAudio: options.receiveAudio,
16963
17113
  receiveVideo: options.receiveVideo,
17114
+ fallbackToReceiveOnly: options.fallbackToReceiveOnly,
16964
17115
  webRTCApiProvider: this.webRTCApiProvider,
16965
17116
  preferredVideoCodecs: options.preferredVideoCodecs,
16966
17117
  preferredAudioCodecs: options.preferredAudioCodecs,
@@ -16979,17 +17130,17 @@ var WebRTCVertoManager = class extends VertoManager {
16979
17130
  if (options.initOffer) this.handleInboundAnswer(rtcPeerConnController);
16980
17131
  }
16981
17132
  async handleInboundAnswer(rtcPeerConnController) {
16982
- logger$15.debug("[WebRTCManager] Waiting for inbound call to be accepted or rejected");
17133
+ logger$16.debug("[WebRTCManager] Waiting for inbound call to be accepted or rejected");
16983
17134
  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);
16984
17135
  if (vertoByeOrAccepted === null) {
16985
- logger$15.debug("[WebRTCManager] Inbound answer handler aborted (destroyed).");
17136
+ logger$16.debug("[WebRTCManager] Inbound answer handler aborted (destroyed).");
16986
17137
  return;
16987
17138
  }
16988
- if (isVertoByeMessage(vertoByeOrAccepted)) {
16989
- logger$15.info("[WebRTCManager] Inbound call ended by remote before answer.");
17139
+ if (isVertoByeInboundParamsGuard(vertoByeOrAccepted)) {
17140
+ logger$16.info("[WebRTCManager] Inbound call ended by remote before answer.");
16990
17141
  this.callSession?.destroy();
16991
17142
  } else if (!vertoByeOrAccepted) {
16992
- logger$15.info("[WebRTCManager] Inbound call rejected by user.");
17143
+ logger$16.info("[WebRTCManager] Inbound call rejected by user.");
16993
17144
  try {
16994
17145
  await this.bye("USER_BUSY");
16995
17146
  } finally {
@@ -16997,19 +17148,19 @@ var WebRTCVertoManager = class extends VertoManager {
16997
17148
  this.callSession?.destroy();
16998
17149
  }
16999
17150
  } else {
17000
- logger$15.debug("[WebRTCManager] Inbound call accepted, creating SDP answer");
17151
+ logger$16.debug("[WebRTCManager] Inbound call accepted, creating SDP answer");
17001
17152
  const answerOptions = this.webRtcCallSession.answerMediaOptions;
17002
17153
  try {
17003
17154
  await rtcPeerConnController.acceptInbound(answerOptions);
17004
17155
  } catch (error) {
17005
- logger$15.error("[WebRTCManager] Error creating inbound answer:", error);
17156
+ logger$16.error("[WebRTCManager] Error creating inbound answer:", error);
17006
17157
  this.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
17007
17158
  }
17008
17159
  }
17009
17160
  }
17010
17161
  setupVertoAttachHandler() {
17011
17162
  this.subscribeTo(this.vertoAttach$, async (vertoAttach) => {
17012
- logger$15.debug("[WebRTCManager] Received Verto attach event for existing call:", vertoAttach);
17163
+ logger$16.debug("[WebRTCManager] Received Verto attach event for existing call:", vertoAttach);
17013
17164
  const { callID } = vertoAttach;
17014
17165
  await this.attachManager.attach({
17015
17166
  nodeId: this.nodeId ?? void 0,
@@ -17081,17 +17232,17 @@ var WebRTCVertoManager = class extends VertoManager {
17081
17232
  };
17082
17233
  }
17083
17234
  async sendLocalDescriptionOnceAccepted(vertoMessageRequest, rtcPeerConnectionController) {
17084
- logger$15.debug("[WebRTCManager] Waiting for call to be accepted or ended before sending answer");
17235
+ logger$16.debug("[WebRTCManager] Waiting for call to be accepted or ended before sending answer");
17085
17236
  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);
17086
17237
  if (vertoByeOrAccepted === null) {
17087
- logger$15.debug("[WebRTCManager] Destroyed while waiting for call acceptance");
17238
+ logger$16.debug("[WebRTCManager] Destroyed while waiting for call acceptance");
17088
17239
  return;
17089
17240
  }
17090
- if (isVertoByeMessage(vertoByeOrAccepted)) {
17091
- logger$15.info("[WebRTCManager] Call ended before answer was sent.");
17241
+ if (isVertoByeInboundParamsGuard(vertoByeOrAccepted)) {
17242
+ logger$16.info("[WebRTCManager] Call ended before answer was sent.");
17092
17243
  this.callSession?.destroy();
17093
17244
  } else if (!vertoByeOrAccepted) {
17094
- logger$15.info("[WebRTCManager] Call was not accepted, sending verto.bye.");
17245
+ logger$16.info("[WebRTCManager] Call was not accepted, sending verto.bye.");
17095
17246
  try {
17096
17247
  await this.bye("USER_BUSY");
17097
17248
  } finally {
@@ -17099,14 +17250,14 @@ var WebRTCVertoManager = class extends VertoManager {
17099
17250
  this.callSession?.destroy();
17100
17251
  }
17101
17252
  } else {
17102
- logger$15.debug("[WebRTCManager] Call accepted, sending answer");
17253
+ logger$16.debug("[WebRTCManager] Call accepted, sending answer");
17103
17254
  try {
17104
17255
  this.emitMainSignalingStatus(rtcPeerConnectionController.id, "connecting");
17105
17256
  await this.sendLocalDescription(vertoMessageRequest, rtcPeerConnectionController);
17106
17257
  await rtcPeerConnectionController.updateAnswerStatus({ status: "sent" });
17107
17258
  await this.attachManager.attach(this.buildAttachableCall());
17108
17259
  } catch (error) {
17109
- logger$15.error("[WebRTCManager] Error sending Verto answer:", error);
17260
+ logger$16.error("[WebRTCManager] Error sending Verto answer:", error);
17110
17261
  this.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
17111
17262
  await rtcPeerConnectionController.updateAnswerStatus({ status: "failed" });
17112
17263
  }
@@ -17187,9 +17338,11 @@ var WebRTCVertoManager = class extends VertoManager {
17187
17338
  await this.initAdditionalPeerConnection("screenshare", options);
17188
17339
  }
17189
17340
  async initAdditionalPeerConnection(propose, options) {
17341
+ const isScreenShare = propose === "screenshare";
17342
+ let firstPeerConnectionError;
17190
17343
  let rtcPeerConnController = null;
17191
17344
  try {
17192
- this._screenShareStatus$.next("starting");
17345
+ if (isScreenShare) this._screenShareStatus$.next("starting");
17193
17346
  rtcPeerConnController = new RTCPeerConnectionController({
17194
17347
  ...options,
17195
17348
  ...this.RTCPeerConnectionConfig,
@@ -17197,21 +17350,27 @@ var WebRTCVertoManager = class extends VertoManager {
17197
17350
  webRTCApiProvider: this.webRTCApiProvider
17198
17351
  }, void 0, this.deviceController);
17199
17352
  this.setupLocalDescriptionHandler(rtcPeerConnController);
17200
- if (propose === "screenshare") this._screenShareId = rtcPeerConnController.id;
17353
+ if (isScreenShare) this._screenShareId = rtcPeerConnController.id;
17201
17354
  this._rtcPeerConnectionsMap.set(rtcPeerConnController.id, rtcPeerConnController);
17202
17355
  this._rtcPeerConnections$.next(Array.from(this._rtcPeerConnectionsMap.values()));
17203
17356
  this.subscribeTo(rtcPeerConnController.errors$, (error) => {
17204
- this.onError?.(error);
17357
+ firstPeerConnectionError ??= error;
17358
+ this.onError?.(error, { fatal: false });
17205
17359
  });
17206
17360
  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$)));
17207
- this._screenShareStatus$.next("started");
17208
- logger$15.info("[WebRTCManager] Screen share started successfully.");
17361
+ if (isScreenShare) this._screenShareStatus$.next("started");
17362
+ logger$16.info(`[WebRTCManager] Additional peer connection connected (${propose}).`);
17209
17363
  return rtcPeerConnController.id;
17210
17364
  } catch (error) {
17211
- logger$15.warn("[WebRTCManager] Error initializing additional peer connection:", error);
17212
- this.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
17365
+ logger$16.warn("[WebRTCManager] Error initializing additional peer connection:", error);
17213
17366
  if (rtcPeerConnController) rtcPeerConnController.destroy();
17214
- this._screenShareStatus$.next("none");
17367
+ if (isScreenShare) this._screenShareStatus$.next("none");
17368
+ if (firstPeerConnectionError) throw firstPeerConnectionError instanceof MediaAccessError && firstPeerConnectionError.originalError instanceof Error ? firstPeerConnectionError.originalError : firstPeerConnectionError;
17369
+ if (error instanceof import_cjs$15.EmptyError) {
17370
+ logger$16.debug("[WebRTCManager] Additional peer connection aborted before connecting.");
17371
+ return;
17372
+ }
17373
+ throw error instanceof Error ? error : new Error(String(error), { cause: error });
17215
17374
  }
17216
17375
  }
17217
17376
  async removeInputDevices(id) {
@@ -17227,9 +17386,9 @@ var WebRTCVertoManager = class extends VertoManager {
17227
17386
  if (removeTrack) return this.mainPeerConnection.stopTrackSender(removeTrack, { updateTransceiverDirection: true });
17228
17387
  }
17229
17388
  async removeScreenMedia() {
17230
- if (!["starting", "started"].includes(this._screenShareStatus$.value)) logger$15.warn("[WebRTCManager] No active screen share to stop.");
17389
+ if (!["starting", "started"].includes(this._screenShareStatus$.value)) logger$16.warn("[WebRTCManager] No active screen share to stop.");
17231
17390
  if (!this._screenShareId) {
17232
- logger$15.debug("[WebRTCManager] No screen share peer connection found.");
17391
+ logger$16.debug("[WebRTCManager] No screen share peer connection found.");
17233
17392
  return;
17234
17393
  }
17235
17394
  this._screenShareStatus$.next("stopping");
@@ -17258,7 +17417,7 @@ var WebRTCVertoManager = class extends VertoManager {
17258
17417
  dialogParams: this.dialogParams(rtcPeerConnController)
17259
17418
  }));
17260
17419
  } catch (error) {
17261
- logger$15.warn("[WebRTCManager] Call might already be disconnected, error sending Verto bye:", error);
17420
+ logger$16.warn("[WebRTCManager] Call might already be disconnected, error sending Verto bye:", error);
17262
17421
  throw error;
17263
17422
  }
17264
17423
  }
@@ -17276,7 +17435,7 @@ var WebRTCVertoManager = class extends VertoManager {
17276
17435
  try {
17277
17436
  await this.executeVerto(vertoInfoMessage);
17278
17437
  } catch (error) {
17279
- logger$15.warn("[WebRTCManager] Error sending DTMF digits:", error);
17438
+ logger$16.warn("[WebRTCManager] Error sending DTMF digits:", error);
17280
17439
  throw error;
17281
17440
  }
17282
17441
  }
@@ -17287,10 +17446,10 @@ var WebRTCVertoManager = class extends VertoManager {
17287
17446
  action: "transfer"
17288
17447
  });
17289
17448
  try {
17290
- logger$15.debug("[WebRTCManager] Transferring call with options:", options);
17449
+ logger$16.debug("[WebRTCManager] Transferring call with options:", options);
17291
17450
  await this.executeVerto(message);
17292
17451
  } catch (error) {
17293
- logger$15.error("[WebRTCManager] Error transferring call:", error);
17452
+ logger$16.error("[WebRTCManager] Error transferring call:", error);
17294
17453
  throw error;
17295
17454
  }
17296
17455
  }
@@ -17307,7 +17466,7 @@ var WebRTCVertoManager = class extends VertoManager {
17307
17466
  //#endregion
17308
17467
  //#region src/controllers/RemoteAudioMeter.ts
17309
17468
  var import_cjs$14 = require_cjs();
17310
- const logger$14 = getLogger();
17469
+ const logger$15 = getLogger();
17311
17470
  /**
17312
17471
  * Read-only audio level meter for a remote MediaStream. Attaches an
17313
17472
  * AnalyserNode to a MediaStreamAudioSourceNode so it observes the stream
@@ -17342,7 +17501,7 @@ var RemoteAudioMeter = class extends Destroyable {
17342
17501
  try {
17343
17502
  this._source.disconnect();
17344
17503
  } catch (error) {
17345
- logger$14.debug("[RemoteAudioMeter] source disconnect warning:", error);
17504
+ logger$15.debug("[RemoteAudioMeter] source disconnect warning:", error);
17346
17505
  }
17347
17506
  this._source = null;
17348
17507
  this._stream = null;
@@ -17359,7 +17518,7 @@ var RemoteAudioMeter = class extends Destroyable {
17359
17518
  this._source = null;
17360
17519
  }
17361
17520
  this._audioContext.close().catch((error) => {
17362
- logger$14.debug("[RemoteAudioMeter] audio context close warning:", error);
17521
+ logger$15.debug("[RemoteAudioMeter] audio context close warning:", error);
17363
17522
  });
17364
17523
  super.destroy();
17365
17524
  }
@@ -17378,7 +17537,7 @@ var RemoteAudioMeter = class extends Destroyable {
17378
17537
  //#endregion
17379
17538
  //#region src/controllers/RTCStatsMonitor.ts
17380
17539
  var import_cjs$13 = require_cjs();
17381
- const logger$13 = getLogger();
17540
+ const logger$14 = getLogger();
17382
17541
  const DEFAULT_POLLING_INTERVAL_MS = 1e3;
17383
17542
  const DEFAULT_BASELINE_SAMPLES = 10;
17384
17543
  const DEFAULT_NO_AUDIO_PACKET_THRESHOLD_MS = 2e3;
@@ -17468,9 +17627,9 @@ var RTCStatsMonitor = class extends Destroyable {
17468
17627
  const now = Date.now();
17469
17628
  this.lastAudioPacketChangeTime = now;
17470
17629
  this.lastVideoPacketChangeTime = now;
17471
- logger$13.debug("[RTCStatsMonitor] Starting stats monitoring");
17630
+ logger$14.debug("[RTCStatsMonitor] Starting stats monitoring");
17472
17631
  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) => {
17473
- logger$13.warn("[RTCStatsMonitor] Failed to get stats:", err);
17632
+ logger$14.warn("[RTCStatsMonitor] Failed to get stats:", err);
17474
17633
  return import_cjs$13.EMPTY;
17475
17634
  }))), (0, import_cjs$13.filter)(() => this.running), (0, import_cjs$13.map)((report) => this.extractSample(report))), (sample$1) => this._sample$.next(sample$1));
17476
17635
  this.subscribeTo(this._sample$.pipe((0, import_cjs$13.take)(this.baselineSampleCount), (0, import_cjs$13.toArray)(), (0, import_cjs$13.map)((samples) => ({
@@ -17478,7 +17637,7 @@ var RTCStatsMonitor = class extends Destroyable {
17478
17637
  jitter: samples.reduce((s, b) => s + b.audioJitter, 0) / samples.length,
17479
17638
  ready: true
17480
17639
  }))), (baseline) => {
17481
- logger$13.debug(`[RTCStatsMonitor] Baseline established: rtt=${baseline.rtt.toFixed(1)}ms, jitter=${baseline.jitter.toFixed(1)}ms`);
17640
+ logger$14.debug(`[RTCStatsMonitor] Baseline established: rtt=${baseline.rtt.toFixed(1)}ms, jitter=${baseline.jitter.toFixed(1)}ms`);
17482
17641
  this._baseline$.next(baseline);
17483
17642
  });
17484
17643
  this.subscribeTo(this._sample$.pipe((0, import_cjs$13.scan)((acc, sample$1) => ({
@@ -17515,10 +17674,10 @@ var RTCStatsMonitor = class extends Destroyable {
17515
17674
  stop() {
17516
17675
  if (!this.running) return;
17517
17676
  this.running = false;
17518
- logger$13.debug("[RTCStatsMonitor] Stopping stats monitoring");
17677
+ logger$14.debug("[RTCStatsMonitor] Stopping stats monitoring");
17519
17678
  }
17520
17679
  destroy() {
17521
- logger$13.debug("[RTCStatsMonitor] Destroying RTCStatsMonitor");
17680
+ logger$14.debug("[RTCStatsMonitor] Destroying RTCStatsMonitor");
17522
17681
  this.stop();
17523
17682
  super.destroy();
17524
17683
  }
@@ -17647,7 +17806,7 @@ var RTCStatsMonitor = class extends Destroyable {
17647
17806
  //#endregion
17648
17807
  //#region src/managers/CallRecoveryManager.ts
17649
17808
  var import_cjs$12 = require_cjs();
17650
- const logger$12 = getLogger();
17809
+ const logger$13 = getLogger();
17651
17810
  const DEFAULT_DEBOUNCE_TIME_MS = 2e3;
17652
17811
  const DEFAULT_COOLDOWN_MS = 1e4;
17653
17812
  const DEFAULT_ICE_GRACE_PERIOD_MS = 3e3;
@@ -17744,10 +17903,10 @@ var CallRecoveryManager = class extends Destroyable {
17744
17903
  */
17745
17904
  async requestIceRestart() {
17746
17905
  if (this._recoveryState$.value === "recovering") {
17747
- logger$12.info("CallRecoveryManager: manual ICE restart skipped — recovery already in progress");
17906
+ logger$13.info("CallRecoveryManager: manual ICE restart skipped — recovery already in progress");
17748
17907
  return;
17749
17908
  }
17750
- logger$12.info("CallRecoveryManager: manual ICE restart requested");
17909
+ logger$13.info("CallRecoveryManager: manual ICE restart requested");
17751
17910
  this.transitionTo("recovering");
17752
17911
  await this.executeIceRestart(false);
17753
17912
  this.startCooldown();
@@ -17763,7 +17922,7 @@ var CallRecoveryManager = class extends Destroyable {
17763
17922
  * WebSocket reconnect or call state recovers to 'connected'.
17764
17923
  */
17765
17924
  reset() {
17766
- logger$12.info("CallRecoveryManager: resetting counters");
17925
+ logger$13.info("CallRecoveryManager: resetting counters");
17767
17926
  this._attemptCount = 0;
17768
17927
  this._keyframeBurstCount = 0;
17769
17928
  this._keyframeBurstStart = 0;
@@ -17778,7 +17937,7 @@ var CallRecoveryManager = class extends Destroyable {
17778
17937
  */
17779
17938
  notifyModifyFailed() {
17780
17939
  if (this._recoveryState$.value === "cooldown" || this._recoveryState$.value === "idle") {
17781
- logger$12.info("CallRecoveryManager: verto.modify failed — re-entering recovery");
17940
+ logger$13.info("CallRecoveryManager: verto.modify failed — re-entering recovery");
17782
17941
  this._cooldownUntil = 0;
17783
17942
  this.transitionTo("idle");
17784
17943
  this.pushTrigger({
@@ -17802,7 +17961,7 @@ var CallRecoveryManager = class extends Destroyable {
17802
17961
  reason: `bandwidth ${bitrateKbps}kbps below threshold ${this._config.degradationBitrateThreshold}kbps`,
17803
17962
  timestamp: Date.now()
17804
17963
  });
17805
- logger$12.warn(`CallRecoveryManager: disabling video — bandwidth ${bitrateKbps}kbps < ${this._config.degradationBitrateThreshold}kbps`);
17964
+ logger$13.warn(`CallRecoveryManager: disabling video — bandwidth ${bitrateKbps}kbps < ${this._config.degradationBitrateThreshold}kbps`);
17806
17965
  } else if (wasConstrained && bitrateKbps >= this._config.degradationRecoveryThreshold) {
17807
17966
  this._bandwidthConstrained$.next(false);
17808
17967
  this._callbacks.enableVideo();
@@ -17811,7 +17970,7 @@ var CallRecoveryManager = class extends Destroyable {
17811
17970
  reason: `bandwidth ${bitrateKbps}kbps recovered above ${this._config.degradationRecoveryThreshold}kbps`,
17812
17971
  timestamp: Date.now()
17813
17972
  });
17814
- logger$12.info(`CallRecoveryManager: restoring video — bandwidth ${bitrateKbps}kbps >= ${this._config.degradationRecoveryThreshold}kbps`);
17973
+ logger$13.info(`CallRecoveryManager: restoring video — bandwidth ${bitrateKbps}kbps >= ${this._config.degradationRecoveryThreshold}kbps`);
17815
17974
  }
17816
17975
  }
17817
17976
  /**
@@ -17830,14 +17989,14 @@ var CallRecoveryManager = class extends Destroyable {
17830
17989
  handleWebSocketReconnect() {
17831
17990
  const pcState = this._callbacks.getPeerConnectionState();
17832
17991
  if (pcState === "connected" || pcState === "completed") {
17833
- logger$12.info("CallRecoveryManager: signal-only reconnect — peer connection still alive");
17992
+ logger$13.info("CallRecoveryManager: signal-only reconnect — peer connection still alive");
17834
17993
  this.emitEvent({
17835
17994
  action: "signal_reconnect",
17836
17995
  reason: "WebSocket reconnected, peer connection still connected",
17837
17996
  timestamp: Date.now()
17838
17997
  });
17839
17998
  } else {
17840
- logger$12.info("CallRecoveryManager: full reconnect — peer connection also down");
17999
+ logger$13.info("CallRecoveryManager: full reconnect — peer connection also down");
17841
18000
  this.emitEvent({
17842
18001
  action: "full_reconnect",
17843
18002
  reason: "WebSocket reconnected, peer connection not connected — ICE restart needed",
@@ -17861,7 +18020,7 @@ var CallRecoveryManager = class extends Destroyable {
17861
18020
  }), (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$))), {
17862
18021
  next: () => {},
17863
18022
  error: (err) => {
17864
- logger$12.error("CallRecoveryManager: pipeline error", err);
18023
+ logger$13.error("CallRecoveryManager: pipeline error", err);
17865
18024
  this.transitionTo("idle");
17866
18025
  }
17867
18026
  });
@@ -17888,27 +18047,27 @@ var CallRecoveryManager = class extends Destroyable {
17888
18047
  reason: `no packet loss for ${this._config.packetLossRecoveryDelaySec}s — restoring video`,
17889
18048
  timestamp: Date.now()
17890
18049
  });
17891
- logger$12.info(`CallRecoveryManager: restoring video — no packet loss for ${this._config.packetLossRecoveryDelaySec}s`);
18050
+ logger$13.info(`CallRecoveryManager: restoring video — no packet loss for ${this._config.packetLossRecoveryDelaySec}s`);
17892
18051
  });
17893
18052
  }
17894
18053
  passGateChecks(signalingReady) {
17895
18054
  if (this._callbacks.isNegotiating()) {
17896
- logger$12.debug("CallRecoveryManager: gate blocked — negotiation in progress");
18055
+ logger$13.debug("CallRecoveryManager: gate blocked — negotiation in progress");
17897
18056
  this.transitionTo("idle");
17898
18057
  return false;
17899
18058
  }
17900
18059
  if (!signalingReady) {
17901
- logger$12.debug("CallRecoveryManager: gate blocked — signaling not ready");
18060
+ logger$13.debug("CallRecoveryManager: gate blocked — signaling not ready");
17902
18061
  this.transitionTo("idle");
17903
18062
  return false;
17904
18063
  }
17905
18064
  if (!this._callbacks.isCallConnected()) {
17906
- logger$12.debug("CallRecoveryManager: gate blocked — call not connected");
18065
+ logger$13.debug("CallRecoveryManager: gate blocked — call not connected");
17907
18066
  this.transitionTo("idle");
17908
18067
  return false;
17909
18068
  }
17910
18069
  if (this.isCooldownActive()) {
17911
- logger$12.debug("CallRecoveryManager: gate blocked — cooldown active");
18070
+ logger$13.debug("CallRecoveryManager: gate blocked — cooldown active");
17912
18071
  this.transitionTo("cooldown");
17913
18072
  return false;
17914
18073
  }
@@ -17919,9 +18078,9 @@ var CallRecoveryManager = class extends Destroyable {
17919
18078
  }
17920
18079
  executeTieredRecovery(trigger) {
17921
18080
  this.transitionTo("recovering");
17922
- logger$12.info(`CallRecoveryManager: starting tiered recovery — source=${trigger.source} detail=${trigger.detail}`);
18081
+ logger$13.info(`CallRecoveryManager: starting tiered recovery — source=${trigger.source} detail=${trigger.detail}`);
17923
18082
  return (0, import_cjs$12.from)(this.runTiers(trigger)).pipe((0, import_cjs$12.tap)(() => this.startCooldown()), (0, import_cjs$12.catchError)((err) => {
17924
- logger$12.error("CallRecoveryManager: tiered recovery failed", err);
18083
+ logger$13.error("CallRecoveryManager: tiered recovery failed", err);
17925
18084
  this.startCooldown();
17926
18085
  return import_cjs$12.EMPTY;
17927
18086
  }));
@@ -17929,7 +18088,7 @@ var CallRecoveryManager = class extends Destroyable {
17929
18088
  async runTiers(trigger) {
17930
18089
  this.executeKeyframe(trigger.detail);
17931
18090
  if (trigger.issueType && DEGRADATION_ONLY_ISSUES.has(trigger.issueType)) {
17932
- logger$12.debug(`CallRecoveryManager: degradation-only issue (${trigger.issueType}) — Tier 1 only, skipping ICE restart`);
18091
+ logger$13.debug(`CallRecoveryManager: degradation-only issue (${trigger.issueType}) — Tier 1 only, skipping ICE restart`);
17933
18092
  return;
17934
18093
  }
17935
18094
  if (this._attemptCount < this._config.maxAttempts) {
@@ -17946,13 +18105,13 @@ var CallRecoveryManager = class extends Destroyable {
17946
18105
  maxAttempts: this._config.maxAttempts,
17947
18106
  timestamp: Date.now()
17948
18107
  });
17949
- logger$12.warn("CallRecoveryManager: max recovery attempts reached");
18108
+ logger$13.warn("CallRecoveryManager: max recovery attempts reached");
17950
18109
  }
17951
18110
  }
17952
18111
  executeKeyframe(reason) {
17953
18112
  const now = Date.now();
17954
18113
  if (now < this._keyframeCooldownUntil) {
17955
- logger$12.debug("CallRecoveryManager: keyframe request skipped — cooldown active");
18114
+ logger$13.debug("CallRecoveryManager: keyframe request skipped — cooldown active");
17956
18115
  return;
17957
18116
  }
17958
18117
  if (now - this._keyframeBurstStart > this._config.keyframeBurstWindowMs) {
@@ -17961,7 +18120,7 @@ var CallRecoveryManager = class extends Destroyable {
17961
18120
  }
17962
18121
  if (this._keyframeBurstCount >= this._config.keyframeMaxBurst) {
17963
18122
  this._keyframeCooldownUntil = now + this._config.keyframeCooldownMs;
17964
- logger$12.debug(`CallRecoveryManager: keyframe burst limit reached (${this._config.keyframeMaxBurst}), cooldown until ${this._keyframeCooldownUntil}`);
18123
+ logger$13.debug(`CallRecoveryManager: keyframe burst limit reached (${this._config.keyframeMaxBurst}), cooldown until ${this._keyframeCooldownUntil}`);
17965
18124
  return;
17966
18125
  }
17967
18126
  this._keyframeBurstCount += 1;
@@ -17971,12 +18130,12 @@ var CallRecoveryManager = class extends Destroyable {
17971
18130
  reason,
17972
18131
  timestamp: now
17973
18132
  });
17974
- logger$12.debug(`CallRecoveryManager: keyframe requested (burst ${this._keyframeBurstCount}/${this._config.keyframeMaxBurst})`);
18133
+ logger$13.debug(`CallRecoveryManager: keyframe requested (burst ${this._keyframeBurstCount}/${this._config.keyframeMaxBurst})`);
17975
18134
  }
17976
18135
  async executeIceRestart(relayOnly) {
17977
18136
  this._attemptCount += 1;
17978
18137
  const tier = relayOnly ? "Tier 3 (relay-only)" : "Tier 2 (standard)";
17979
- logger$12.info(`CallRecoveryManager: ${tier} ICE restart — attempt ${this._attemptCount}/${this._config.maxAttempts}`);
18138
+ logger$13.info(`CallRecoveryManager: ${tier} ICE restart — attempt ${this._attemptCount}/${this._config.maxAttempts}`);
17980
18139
  this.emitEvent({
17981
18140
  action: "reinvite_started",
17982
18141
  reason: `${tier} ICE restart`,
@@ -17993,7 +18152,7 @@ var CallRecoveryManager = class extends Destroyable {
17993
18152
  maxAttempts: this._config.maxAttempts,
17994
18153
  timestamp: Date.now()
17995
18154
  });
17996
- logger$12.info(`CallRecoveryManager: ${tier} ICE restart succeeded`);
18155
+ logger$13.info(`CallRecoveryManager: ${tier} ICE restart succeeded`);
17997
18156
  this._attemptCount = 0;
17998
18157
  return true;
17999
18158
  }
@@ -18004,7 +18163,7 @@ var CallRecoveryManager = class extends Destroyable {
18004
18163
  maxAttempts: this._config.maxAttempts,
18005
18164
  timestamp: Date.now()
18006
18165
  });
18007
- logger$12.warn(`CallRecoveryManager: ${tier} ICE restart failed`);
18166
+ logger$13.warn(`CallRecoveryManager: ${tier} ICE restart failed`);
18008
18167
  return false;
18009
18168
  } catch {
18010
18169
  this.emitEvent({
@@ -18014,7 +18173,7 @@ var CallRecoveryManager = class extends Destroyable {
18014
18173
  maxAttempts: this._config.maxAttempts,
18015
18174
  timestamp: Date.now()
18016
18175
  });
18017
- logger$12.warn(`CallRecoveryManager: ${tier} ICE restart timed out`);
18176
+ logger$13.warn(`CallRecoveryManager: ${tier} ICE restart timed out`);
18018
18177
  return false;
18019
18178
  }
18020
18179
  }
@@ -18035,7 +18194,7 @@ var CallRecoveryManager = class extends Destroyable {
18035
18194
  transitionTo(state) {
18036
18195
  const prev = this._recoveryState$.value;
18037
18196
  if (prev !== state) {
18038
- logger$12.debug(`CallRecoveryManager: state ${prev} -> ${state}`);
18197
+ logger$13.debug(`CallRecoveryManager: state ${prev} -> ${state}`);
18039
18198
  this._recoveryState$.next(state);
18040
18199
  }
18041
18200
  }
@@ -18138,7 +18297,13 @@ function mosToQualityLevel(mos) {
18138
18297
  //#endregion
18139
18298
  //#region src/core/entities/Call.ts
18140
18299
  var import_cjs$11 = require_cjs();
18141
- const logger$11 = getLogger();
18300
+ const logger$12 = getLogger();
18301
+ /**
18302
+ * Verto method for setting member layout positions. Its gateway DTO requires a
18303
+ * `targets` array whose entries are `{ target, position }` (NOT bare targets),
18304
+ * so {@link WebRTCCall.buildMethodParams} special-cases it. See issue #19400.
18305
+ */
18306
+ const POSITION_SET_METHOD = "call.member.position.set";
18142
18307
  /**
18143
18308
  * Ratio between the critical and warning RTT spike multipliers.
18144
18309
  * Warning threshold = baseline * warningMultiplier (default 3x)
@@ -18156,7 +18321,7 @@ const fromDestinationParams = (destination) => {
18156
18321
  });
18157
18322
  return params;
18158
18323
  } catch (error) {
18159
- logger$11.warn(`Failed to parse destination URI: ${destination}`, error);
18324
+ logger$12.warn(`Failed to parse destination URI: ${destination}`, error);
18160
18325
  return {};
18161
18326
  }
18162
18327
  };
@@ -18346,7 +18511,7 @@ var WebRTCCall = class extends Destroyable {
18346
18511
  * @throws {JSONRPCError} If the RPC call returns an error.
18347
18512
  */
18348
18513
  async executeMethod(target, method, args) {
18349
- const params = this.buildMethodParams(target, args);
18514
+ const params = this.buildMethodParams(target, args, method);
18350
18515
  const request = buildRPCRequest({
18351
18516
  method,
18352
18517
  params
@@ -18356,16 +18521,20 @@ var WebRTCCall = class extends Destroyable {
18356
18521
  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);
18357
18522
  return response;
18358
18523
  } catch (error) {
18359
- logger$11.error(`[Call] Error executing method ${method} with params`, params, error);
18524
+ logger$12.error(`[Call] Error executing method ${method} with params`, params, error);
18360
18525
  throw error;
18361
18526
  }
18362
18527
  }
18363
- buildMethodParams(target, args) {
18528
+ buildMethodParams(target, args, method) {
18364
18529
  const self = {
18365
18530
  node_id: this.nodeId ?? "",
18366
18531
  call_id: this.id,
18367
18532
  member_id: this.vertoManager.selfId ?? ""
18368
18533
  };
18534
+ if (method === POSITION_SET_METHOD) return {
18535
+ ...args,
18536
+ self
18537
+ };
18369
18538
  if (typeof target === "object") return {
18370
18539
  ...args,
18371
18540
  self,
@@ -18559,9 +18728,9 @@ var WebRTCCall = class extends Destroyable {
18559
18728
  */
18560
18729
  initResilienceSubsystems() {
18561
18730
  const pc = this.rtcPeerConnection;
18562
- logger$11.debug(`[Call] initResilienceSubsystems: pc=${pc ? "exists" : "undefined"}, connectionState=${pc?.connectionState}`);
18731
+ logger$12.debug(`[Call] initResilienceSubsystems: pc=${pc ? "exists" : "undefined"}, connectionState=${pc?.connectionState}`);
18563
18732
  if (!pc) {
18564
- logger$11.warn("[Call] No peer connection available, skipping resilience init");
18733
+ logger$12.warn("[Call] No peer connection available, skipping resilience init");
18565
18734
  return;
18566
18735
  }
18567
18736
  try {
@@ -18596,14 +18765,14 @@ var WebRTCCall = class extends Destroyable {
18596
18765
  disableVideo: () => {
18597
18766
  try {
18598
18767
  this.vertoManager.muteMainVideoInputDevice();
18599
- logger$11.debug("[Call] Recovery manager disabled video");
18768
+ logger$12.debug("[Call] Recovery manager disabled video");
18600
18769
  } catch {
18601
- logger$11.debug("[Call] Recovery manager failed to disable video");
18770
+ logger$12.debug("[Call] Recovery manager failed to disable video");
18602
18771
  }
18603
18772
  },
18604
18773
  enableVideo: () => {
18605
18774
  this.vertoManager.unmuteMainVideoInputDevice().catch(() => {
18606
- logger$11.debug("[Call] Recovery manager failed to enable video");
18775
+ logger$12.debug("[Call] Recovery manager failed to enable video");
18607
18776
  });
18608
18777
  },
18609
18778
  isNegotiating: () => this.vertoManager.mainPeerConnection.isNegotiating,
@@ -18653,7 +18822,7 @@ var WebRTCCall = class extends Destroyable {
18653
18822
  this.subscribeTo(this._recoveryManager.recoveryEvent$, (event) => {
18654
18823
  this._recoveryEvent$.next(event);
18655
18824
  if (event.action === "max_attempts_reached") {
18656
- logger$11.warn("[Call] All recovery attempts exhausted, terminating call");
18825
+ logger$12.warn("[Call] All recovery attempts exhausted, terminating call");
18657
18826
  this.emitError({
18658
18827
  kind: "network",
18659
18828
  fatal: true,
@@ -18673,13 +18842,13 @@ var WebRTCCall = class extends Destroyable {
18673
18842
  else if (event.type === "online") this._recoveryManager?.handleWebSocketReconnect();
18674
18843
  });
18675
18844
  this.subscribeTo(this.clientSession.authenticated$.pipe((0, import_cjs$11.skip)(1), (0, import_cjs$11.filter)(Boolean)), () => {
18676
- logger$11.debug("[Call] WebSocket reconnected — notifying recovery manager");
18845
+ logger$12.debug("[Call] WebSocket reconnected — notifying recovery manager");
18677
18846
  this._recoveryManager?.handleWebSocketReconnect();
18678
18847
  });
18679
18848
  this._statsMonitor.start();
18680
- logger$11.debug("[Call] Resilience subsystems initialized for call", this.id);
18849
+ logger$12.debug("[Call] Resilience subsystems initialized for call", this.id);
18681
18850
  } catch (error) {
18682
- logger$11.warn("[Call] Failed to initialize resilience subsystems:", error);
18851
+ logger$12.warn("[Call] Failed to initialize resilience subsystems:", error);
18683
18852
  }
18684
18853
  }
18685
18854
  /**
@@ -18762,19 +18931,19 @@ var WebRTCCall = class extends Destroyable {
18762
18931
  }
18763
18932
  isCallSessionEvent(event) {
18764
18933
  try {
18765
- logger$11.debug("[Call] Checking if event is for this call session:", event);
18934
+ logger$12.debug("[Call] Checking if event is for this call session:", event);
18766
18935
  const callId = getValueFrom(event, "params.params.callID") ?? getValueFrom(event, "params.call_id");
18767
18936
  const roomSessionId = getValueFrom(event, "params.room_session_id");
18768
- logger$11.debug(`[Call] Extracted session identifiers callID: ${callId} and roomSessionID: ${roomSessionId} from event:`);
18937
+ logger$12.debug(`[Call] Extracted session identifiers callID: ${callId} and roomSessionID: ${roomSessionId} from event:`);
18769
18938
  return callId === this.id || !!callId && this.callEventsManager.isCallIdValid(callId) || !!roomSessionId && this.callEventsManager.isRoomSessionIdValid(roomSessionId);
18770
18939
  } catch (error) {
18771
- logger$11.error("[Call] Error checking if event is for this call session:", error);
18940
+ logger$12.error("[Call] Error checking if event is for this call session:", error);
18772
18941
  return false;
18773
18942
  }
18774
18943
  }
18775
18944
  get callSessionEvents$() {
18776
18945
  return this.cachedObservable("callSessionEvents$", () => this.clientSession.signalingEvent$.pipe((0, import_cjs$11.filter)((event) => this.isCallSessionEvent(event)), (0, import_cjs$11.tap)((event) => {
18777
- logger$11.debug("[Call] Received call session event:", event);
18946
+ logger$12.debug("[Call] Received call session event:", event);
18778
18947
  }), (0, import_cjs$11.takeUntil)(this.destroyed$), (0, import_cjs$11.share)()));
18779
18948
  }
18780
18949
  /** Observable of call-updated events. */
@@ -18844,16 +19013,16 @@ var WebRTCCall = class extends Destroyable {
18844
19013
  this._customSubscriptions.set(eventType, filtered$);
18845
19014
  }, (error) => {
18846
19015
  this._customSubscriptions.delete(eventType);
18847
- logger$11.warn(`[Call] verto.subscribe for '${eventType}' failed, not caching:`, error);
19016
+ logger$12.warn(`[Call] verto.subscribe for '${eventType}' failed, not caching:`, error);
18848
19017
  });
18849
19018
  this._customSubscriptions.set(eventType, filtered$);
18850
19019
  return filtered$;
18851
19020
  }
18852
19021
  get webrtcMessages$() {
18853
- 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)()));
19022
+ 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)()));
18854
19023
  }
18855
19024
  get callEvent$() {
18856
- 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)()));
19025
+ 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)()));
18857
19026
  }
18858
19027
  get layoutEvent$() {
18859
19028
  return this.cachedObservable("layoutEvent$", () => this.callEvent$.pipe(filterAs(isLayoutChangedMetadata, "params")));
@@ -18931,10 +19100,21 @@ var WebRTCCall = class extends Destroyable {
18931
19100
  return this.deferEmission(this._answered$.asObservable());
18932
19101
  }
18933
19102
  /**
18934
- * Sets the call layout and participant positions.
19103
+ * Sets the call layout and, optionally, individual participant positions.
19104
+ *
19105
+ * The gateway `call.layout.set` DTO has **no** `positions` member, so when
19106
+ * `positions` is provided this method issues a `call.member.position.set`
19107
+ * request per member (via {@link Participant.setPosition}, which keys each
19108
+ * position by that member's own call context) alongside `call.layout.set`
19109
+ * (issue #19400, Flag #6).
19110
+ *
19111
+ * **These operations are NOT atomic.** The layout is applied first, then each
19112
+ * member position sequentially, so members may briefly flash into their
19113
+ * default slots before being moved to the requested positions.
18935
19114
  *
18936
19115
  * @param layout - Layout name (must be one of {@link layouts}).
18937
- * @param positions - Map of member IDs to {@link VideoPosition} values.
19116
+ * @param positions - Optional map of member IDs to {@link VideoPosition} values.
19117
+ * When omitted or empty, only the layout is changed.
18938
19118
  * @throws {InvalidParams} If the layout is not in the available {@link layouts}.
18939
19119
  *
18940
19120
  * @example
@@ -18947,10 +19127,17 @@ var WebRTCCall = class extends Destroyable {
18947
19127
  async setLayout(layout, positions) {
18948
19128
  if (!this.layouts.includes(layout)) throw new InvalidParams(`Layout ${layout} is not available in the current call layouts: ${this.layouts.join(", ")}`);
18949
19129
  const selfId = await (0, import_cjs$11.firstValueFrom)(this.selfId$.pipe((0, import_cjs$11.filter)((id) => id !== null)));
18950
- await this.executeMethod(selfId, "call.layout.set", {
18951
- layout,
18952
- positions
18953
- });
19130
+ await this.executeMethod(selfId, "call.layout.set", { layout });
19131
+ const positionEntries = Object.entries(positions ?? {});
19132
+ if (positionEntries.length === 0) return;
19133
+ for (const [memberId, position] of positionEntries) {
19134
+ const participant = this.participants.find((p) => p.id === memberId);
19135
+ if (!participant) {
19136
+ logger$12.warn(`[Call] setLayout: member ${memberId} not found in participants; skipping position ${position}`);
19137
+ continue;
19138
+ }
19139
+ await participant.setPosition(position);
19140
+ }
18954
19141
  }
18955
19142
  /**
18956
19143
  * Transfers the call to another destination.
@@ -18982,7 +19169,7 @@ var WebRTCCall = class extends Destroyable {
18982
19169
  setLocalMicrophoneGain(value) {
18983
19170
  const pipeline = this.vertoManager.ensureLocalAudioPipeline();
18984
19171
  if (!pipeline) {
18985
- logger$11.warn("[Call] setLocalMicrophoneGain: audio pipeline unavailable");
19172
+ logger$12.warn("[Call] setLocalMicrophoneGain: audio pipeline unavailable");
18986
19173
  return;
18987
19174
  }
18988
19175
  const percent = Math.max(0, Math.min(200, value));
@@ -19027,7 +19214,7 @@ var WebRTCCall = class extends Destroyable {
19027
19214
  enablePushToTalk() {
19028
19215
  const pipeline = this.vertoManager.ensureLocalAudioPipeline();
19029
19216
  if (!pipeline) {
19030
- logger$11.warn("[Call] enablePushToTalk: audio pipeline unavailable");
19217
+ logger$12.warn("[Call] enablePushToTalk: audio pipeline unavailable");
19031
19218
  return;
19032
19219
  }
19033
19220
  pipeline.setPTTActive(false);
@@ -19124,6 +19311,7 @@ function inferCallErrorKind(error) {
19124
19311
  if (error instanceof RPCTimeoutError) return "timeout";
19125
19312
  if (error instanceof JSONRPCError) return "signaling";
19126
19313
  if (error instanceof MediaTrackError) return "media";
19314
+ if (error instanceof MediaAccessError) return "media";
19127
19315
  if (error instanceof WebSocketConnectionError || error instanceof TransportConnectionError) return "network";
19128
19316
  return "internal";
19129
19317
  }
@@ -19140,6 +19328,7 @@ const RECOVERABLE_RPC_CODES = new Set([
19140
19328
  function isFatalError(error) {
19141
19329
  if (error instanceof VertoPongError) return false;
19142
19330
  if (error instanceof MediaTrackError) return false;
19331
+ if (error instanceof MediaAccessError) return error.fatal;
19143
19332
  if (error instanceof RPCTimeoutError) return false;
19144
19333
  if (error instanceof JSONRPCError && RECOVERABLE_RPC_CODES.has(error.code)) return false;
19145
19334
  return true;
@@ -19165,10 +19354,10 @@ var CallFactory = class {
19165
19354
  return {
19166
19355
  vertoManager: new WebRTCVertoManager(callInstance, this.attachManager, this.deviceController, this.webRTCApiProvider, {
19167
19356
  nodeId: options.nodeId,
19168
- onError: (error) => {
19357
+ onError: (error, options$1) => {
19169
19358
  const callError = {
19170
19359
  kind: inferCallErrorKind(error),
19171
- fatal: isFatalError(error),
19360
+ fatal: options$1?.fatal ?? isFatalError(error),
19172
19361
  error,
19173
19362
  callId: callInstance.id
19174
19363
  };
@@ -19190,7 +19379,7 @@ var CallFactory = class {
19190
19379
  //#endregion
19191
19380
  //#region src/behaviors/Collection.ts
19192
19381
  var import_cjs$10 = require_cjs();
19193
- const logger$10 = getLogger();
19382
+ const logger$11 = getLogger();
19194
19383
  var Fetcher = class {
19195
19384
  constructor(endpoint, params, http) {
19196
19385
  this.endpoint = endpoint;
@@ -19214,7 +19403,7 @@ var Fetcher = class {
19214
19403
  this.hasMore = !!this.nextUrl;
19215
19404
  return result.data.filter(this.filter).map(this.mapper);
19216
19405
  }
19217
- logger$10.error("Failed to fetch entity");
19406
+ logger$11.error("Failed to fetch entity");
19218
19407
  return [];
19219
19408
  }
19220
19409
  async id(v) {
@@ -19290,7 +19479,7 @@ var EntityCollection = class extends Destroyable {
19290
19479
  this._hasMore$.next(this.fetchController.hasMore ?? false);
19291
19480
  this._loading$.next(false);
19292
19481
  } catch (error) {
19293
- logger$10.error(`Failed to fetch initial collection data`, error);
19482
+ logger$11.error(`Failed to fetch initial collection data`, error);
19294
19483
  this._hasMore$.next(this.fetchController.hasMore ?? false);
19295
19484
  this._loading$.next(false);
19296
19485
  this.onError?.(new CollectionFetchError("fetchMore", error));
@@ -19304,7 +19493,7 @@ var EntityCollection = class extends Destroyable {
19304
19493
  if (data) this.upsertData(data);
19305
19494
  return data;
19306
19495
  } catch (error) {
19307
- logger$10.error(`Failed to fetch data for (${String(key)}:${String(value)}) :`, error);
19496
+ logger$11.error(`Failed to fetch data for (${String(key)}:${String(value)}) :`, error);
19308
19497
  this._loading$.next(false);
19309
19498
  this.onError?.(new CollectionFetchError(`tryFetch(${String(key)})`, error));
19310
19499
  }
@@ -19553,13 +19742,13 @@ var Address = class extends Destroyable {
19553
19742
  //#endregion
19554
19743
  //#region src/core/utils.ts
19555
19744
  var import_cjs$8 = require_cjs();
19556
- const logger$9 = getLogger();
19745
+ const logger$10 = getLogger();
19557
19746
  const isRPCConnectResult = (e) => {
19558
- logger$9.debug("isRPCConnectResult check:", e);
19747
+ logger$10.debug("isRPCConnectResult check:", e);
19559
19748
  if (!e || typeof e !== "object") return false;
19560
19749
  const result = e;
19561
19750
  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";
19562
- logger$9.debug("isRPCConnectResult check result:", is);
19751
+ logger$10.debug("isRPCConnectResult check result:", is);
19563
19752
  return is;
19564
19753
  };
19565
19754
  var PendingRPC = class PendingRPC {
@@ -19568,7 +19757,7 @@ var PendingRPC = class PendingRPC {
19568
19757
  }
19569
19758
  constructor(request, responses$, options) {
19570
19759
  this.id = v4_default();
19571
- logger$9.debug(`[PendingRPC(${this.id}) request:${request.id}: method:${request.method}] Creating PendingRPC`);
19760
+ logger$10.debug(`[PendingRPC(${this.id}) request:${request.id}: method:${request.method}] Creating PendingRPC`);
19572
19761
  this.request = request;
19573
19762
  const timeoutMs = options?.timeoutMs ?? PendingRPC.defaultTimeoutMs;
19574
19763
  const signal = options?.signal;
@@ -19594,22 +19783,22 @@ var PendingRPC = class PendingRPC {
19594
19783
  isSettled = true;
19595
19784
  if (response.error) {
19596
19785
  const rpcError = new JSONRPCError(response.error.code, response.error.message, response.error.data, void 0, request.id);
19597
- logger$9.debug(`[PendingRPC(${this.id}) request:${request.id}] Rejecting promise with RPC error:`, rpcError);
19786
+ logger$10.debug(`[PendingRPC(${this.id}) request:${request.id}] Rejecting promise with RPC error:`, rpcError);
19598
19787
  reject(rpcError);
19599
19788
  } else {
19600
- logger$9.debug(`[PendingRPC(${this.id}) request:${request.id}] Resolving promise with response:`, response);
19789
+ logger$10.debug(`[PendingRPC(${this.id}) request:${request.id}] Resolving promise with response:`, response);
19601
19790
  resolve(response);
19602
19791
  }
19603
19792
  subscription.unsubscribe();
19604
19793
  },
19605
19794
  error: (error) => {
19606
- logger$9.debug(`[PendingRPC(${this.id}) request:${request.id}] Rejecting promise with error:`, error);
19795
+ logger$10.debug(`[PendingRPC(${this.id}) request:${request.id}] Rejecting promise with error:`, error);
19607
19796
  isSettled = true;
19608
19797
  reject(error);
19609
19798
  subscription.unsubscribe();
19610
19799
  },
19611
19800
  complete: () => {
19612
- logger$9.debug(`[PendingRPC(${this.id}) request:${request.id}] Observable completed`);
19801
+ logger$10.debug(`[PendingRPC(${this.id}) request:${request.id}] Observable completed`);
19613
19802
  if (!isSettled) reject(new RPCTimeoutError(request.id, timeoutMs));
19614
19803
  subscription.unsubscribe();
19615
19804
  }
@@ -19630,7 +19819,18 @@ var PendingRPC = class PendingRPC {
19630
19819
  //#endregion
19631
19820
  //#region src/managers/ClientSessionManager.ts
19632
19821
  var import_cjs$7 = require_cjs();
19633
- const logger$8 = getLogger();
19822
+ const logger$9 = getLogger();
19823
+ /**
19824
+ * Decide whether an error emitted on `call.errors$` during dial should
19825
+ * abort the dial. A non-fatal MediaAccessError means the call degraded to
19826
+ * receive-only and still connects — everything else rejects `dial()` with
19827
+ * the real cause.
19828
+ *
19829
+ * Pure function — exported for unit testing.
19830
+ */
19831
+ function shouldAbortDial(callError) {
19832
+ return callError.fatal || !(callError.error instanceof MediaAccessError);
19833
+ }
19634
19834
  const getAddressSearchURI = (options) => {
19635
19835
  const to = options.to?.split("?")[0];
19636
19836
  const from$9 = options.from?.startsWith("subscriber://") ? options.from.replace("subscriber://", "") : options.from;
@@ -19728,7 +19928,7 @@ var ClientSessionManager = class extends Destroyable {
19728
19928
  try {
19729
19929
  return await this.transport.execute(request, options);
19730
19930
  } catch (error) {
19731
- logger$8.debug("[Session] Execute Error", error);
19931
+ logger$9.debug("[Session] Execute Error", error);
19732
19932
  this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
19733
19933
  throw error;
19734
19934
  }
@@ -19742,13 +19942,13 @@ var ClientSessionManager = class extends Destroyable {
19742
19942
  return true;
19743
19943
  }
19744
19944
  setupMessageHandlers() {
19745
- logger$8.debug("[Session] Setting up message handlers");
19945
+ logger$9.debug("[Session] Setting up message handlers");
19746
19946
  this.subscribeTo(this.authStateEvent$, async (authStateEvent) => {
19747
- logger$8.debug("[Session] Authorization state event received:", authStateEvent);
19947
+ logger$9.debug("[Session] Authorization state event received:", authStateEvent);
19748
19948
  try {
19749
19949
  await this.updateAuthorizationStateInStorage(authStateEvent.authorization_state);
19750
19950
  } catch (error) {
19751
- logger$8.error("[Session] Failed to handle authorization state update:", error);
19951
+ logger$9.error("[Session] Failed to handle authorization state update:", error);
19752
19952
  this._errors$.next(new AuthStateHandlerError(error));
19753
19953
  }
19754
19954
  });
@@ -19756,29 +19956,29 @@ var ClientSessionManager = class extends Destroyable {
19756
19956
  if (this._authState$.value.kind === "authenticated") this._authState$.next({ kind: "unauthenticated" });
19757
19957
  });
19758
19958
  this.subscribeTo(this.transport.connectionStatus$.pipe((0, import_cjs$7.filter)((status) => status === "connected"), (0, import_cjs$7.exhaustMap)(() => {
19759
- logger$8.debug("[Session] Connection established, initiating authentication");
19959
+ logger$9.debug("[Session] Connection established, initiating authentication");
19760
19960
  return (0, import_cjs$7.from)(this.authenticate()).pipe((0, import_cjs$7.catchError)((error) => {
19761
19961
  this.handleAuthenticationError(error).catch((err) => {
19762
- logger$8.error("[Session] Error handling authentication failure:", err);
19962
+ logger$9.error("[Session] Error handling authentication failure:", err);
19763
19963
  });
19764
19964
  return import_cjs$7.EMPTY;
19765
19965
  }));
19766
19966
  })), void 0);
19767
19967
  this.subscribeTo(this.vertoInvite$, async (invite) => {
19768
- logger$8.debug("[Session] Verto invite received:", invite);
19968
+ logger$9.debug("[Session] Verto invite received:", invite);
19769
19969
  try {
19770
19970
  await this.createInboundCall(invite);
19771
19971
  } catch (error) {
19772
- logger$8.error("[Session] Error handling Verto invite:", error);
19972
+ logger$9.error("[Session] Error handling Verto invite:", error);
19773
19973
  this._errors$.next(new VertoInviteHandlerError(error));
19774
19974
  }
19775
19975
  });
19776
19976
  this.subscribeTo(this.vertoAttach$, async (attach) => {
19777
- logger$8.debug("[Session] Verto attach received:", attach);
19977
+ logger$9.debug("[Session] Verto attach received:", attach);
19778
19978
  try {
19779
19979
  await this.handleVertoAttach(attach);
19780
19980
  } catch (error) {
19781
- logger$8.error("[Session] Error handling Verto attach:", error);
19981
+ logger$9.error("[Session] Error handling Verto attach:", error);
19782
19982
  this._errors$.next(new VertoAttachHandlerError(error));
19783
19983
  }
19784
19984
  });
@@ -19788,36 +19988,36 @@ var ClientSessionManager = class extends Destroyable {
19788
19988
  const storedState = await this.storage.getItem(this.authorizationStateKey);
19789
19989
  this.authorizationState$.next(storedState ?? void 0);
19790
19990
  } catch (error) {
19791
- logger$8.error("Failed to retrieve authorization state from storage:", error);
19991
+ logger$9.error("Failed to retrieve authorization state from storage:", error);
19792
19992
  this.authorizationState$.next(void 0);
19793
19993
  }
19794
19994
  }
19795
19995
  async updateAuthorizationStateInStorage(authorizationState) {
19796
19996
  if (!authorizationState) {
19797
- logger$8.debug("[Session] Removing authorization state from storage");
19997
+ logger$9.debug("[Session] Removing authorization state from storage");
19798
19998
  try {
19799
19999
  await this.storage.removeItem(this.authorizationStateKey);
19800
20000
  this.authorizationState$.next(void 0);
19801
20001
  } catch (error) {
19802
- logger$8.error("Failed to remove authorization state from storage:", error);
20002
+ logger$9.error("Failed to remove authorization state from storage:", error);
19803
20003
  throw error;
19804
20004
  }
19805
20005
  return;
19806
20006
  }
19807
20007
  try {
19808
- logger$8.debug("[Session] Updating authorization state in storage");
20008
+ logger$9.debug("[Session] Updating authorization state in storage");
19809
20009
  await this.storage.setItem(this.authorizationStateKey, authorizationState);
19810
20010
  this.authorizationState$.next(authorizationState);
19811
20011
  } catch (error) {
19812
- logger$8.error("Failed to retrieve authorization state from storage:", error);
20012
+ logger$9.error("Failed to retrieve authorization state from storage:", error);
19813
20013
  throw error;
19814
20014
  }
19815
20015
  }
19816
20016
  get authStateEvent$() {
19817
20017
  return this.cachedObservable("authStateEvent$", () => this.signalingEvent$.pipe((0, import_cjs$7.tap)((msg) => {
19818
- logger$8.debug("[Session] Received incoming message:", msg);
20018
+ logger$9.debug("[Session] Received incoming message:", msg);
19819
20019
  }), filterAs(isSignalwireAuthorizationStateMetadata, "params"), (0, import_cjs$7.tap)((event) => {
19820
- logger$8.debug("[Session] Authorization state event received:", event.authorization_state);
20020
+ logger$9.debug("[Session] Authorization state event received:", event.authorization_state);
19821
20021
  })));
19822
20022
  }
19823
20023
  get signalingEvent$() {
@@ -19855,42 +20055,72 @@ var ClientSessionManager = class extends Destroyable {
19855
20055
  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 })));
19856
20056
  }
19857
20057
  async handleAuthenticationError(error) {
19858
- logger$8.error("Authentication error:", error);
20058
+ logger$9.error("Authentication error:", error);
19859
20059
  const isRecoverableAuthError = error instanceof JSONRPCError && (error.code === RPC_ERROR_REQUESTER_VALIDATION_FAILED || error.code === RPC_ERROR_INVALID_PARAMS || error.code === RPC_ERROR_AUTHENTICATION_FAILED);
19860
20060
  const hasStoredState = await (0, import_cjs$7.firstValueFrom)(this.authorizationState$.pipe((0, import_cjs$7.take)(1))) !== void 0;
19861
20061
  if (isRecoverableAuthError && hasStoredState) {
19862
- logger$8.debug("[Session] Recoverable auth error — cleaning up stored state and reconnecting fresh");
20062
+ logger$9.debug("[Session] Recoverable auth error — cleaning up stored state and reconnecting fresh");
19863
20063
  try {
19864
20064
  await this.cleanupStoredConnectionParams();
19865
20065
  } catch (cleanupError) {
19866
- logger$8.error("Failed to cleanup stored connection params:", cleanupError);
20066
+ logger$9.error("Failed to cleanup stored connection params:", cleanupError);
19867
20067
  } finally {
19868
20068
  this.transport.reconnect();
19869
20069
  }
19870
20070
  } else this._errors$.next(error);
19871
20071
  }
20072
+ /**
20073
+ * Clear the resume state (authorization_state + protocol) only.
20074
+ *
20075
+ * This is the stale-auth-state recovery helper used by handleAuthError:
20076
+ * the server rejected a reconnect, so the resume state is discarded and a
20077
+ * fresh connect follows. Attach records are deliberately preserved — the
20078
+ * session lives on through the reconnect and reattachCalls() needs the
20079
+ * stored call references afterwards. Do NOT add detachAll() here.
20080
+ *
20081
+ * For public teardown (disconnect/destroy), use {@link teardownSessionState}
20082
+ * instead, which clears the attach records as well.
20083
+ */
19872
20084
  async cleanupStoredConnectionParams() {
19873
20085
  await this.transport.setProtocol(void 0);
19874
20086
  await this.updateAuthorizationStateInStorage(void 0);
19875
20087
  this._authorization$.next(void 0);
19876
20088
  }
20089
+ /**
20090
+ * Public-teardown helper for disconnect()/destroy(). Clears the resume
20091
+ * state (authorization_state + protocol) AND the attach records as one
20092
+ * atomic unit.
20093
+ *
20094
+ * The two stores are coupled: the backend only honors attach records
20095
+ * within the session identified by the resume state, so ending the
20096
+ * session must clear both. Clearing one without the other strands records
20097
+ * no future session can honor (disconnect) or revives a session the
20098
+ * developer explicitly ended (destroy).
20099
+ *
20100
+ * Distinct from {@link cleanupStoredConnectionParams}, which keeps the
20101
+ * attach records for the stale-auth-state recovery path.
20102
+ */
20103
+ async teardownSessionState() {
20104
+ await this.cleanupStoredConnectionParams();
20105
+ await this.attachManager.detachAll();
20106
+ }
19877
20107
  async updateAuthState(authorization_state) {
19878
20108
  try {
19879
20109
  await this.storage.setItem(this.authorizationStateKey, authorization_state);
19880
20110
  } catch (error) {
19881
- logger$8.error("Failed to update authorization state in storage:", error);
20111
+ logger$9.error("Failed to update authorization state in storage:", error);
19882
20112
  this._errors$.next(new AuthStateHandlerError(error));
19883
20113
  }
19884
20114
  }
19885
20115
  async reauthenticate(token, dpopToken, options) {
19886
- logger$8.debug("[Session] Re-authenticating session");
20116
+ logger$9.debug("[Session] Re-authenticating session");
19887
20117
  try {
19888
20118
  let resolvedDpopToken = dpopToken;
19889
20119
  if (!resolvedDpopToken && this.dpopManager?.initialized) try {
19890
20120
  resolvedDpopToken = await this.dpopManager.createRpcProof({ method: "signalwire.reauthenticate" });
19891
20121
  } catch (error) {
19892
20122
  if (this.clientBound) throw error;
19893
- logger$8.warn("[Session] Failed to create DPoP proof for reauthenticate:", error);
20123
+ logger$9.warn("[Session] Failed to create DPoP proof for reauthenticate:", error);
19894
20124
  }
19895
20125
  const request = RPCReauthenticate({
19896
20126
  project: this._authorization$.value?.project_id ?? "",
@@ -19898,24 +20128,24 @@ var ClientSessionManager = class extends Destroyable {
19898
20128
  ...resolvedDpopToken ? { dpop_token: resolvedDpopToken } : {}
19899
20129
  });
19900
20130
  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) => {
19901
- logger$8.error("[Session] Re-authentication RPC failed:", err);
20131
+ logger$9.error("[Session] Re-authentication RPC failed:", err);
19902
20132
  throw err;
19903
20133
  })));
19904
20134
  if (options?.clientBound) this._wasClientBound = true;
19905
- logger$8.debug("[Session] Re-authentication successful, updating stored auth state");
20135
+ logger$9.debug("[Session] Re-authentication successful, updating stored auth state");
19906
20136
  } catch (error) {
19907
- logger$8.error("[Session] Re-authentication failed:", error);
20137
+ logger$9.error("[Session] Re-authentication failed:", error);
19908
20138
  this._errors$.next(new AuthStateHandlerError(error));
19909
20139
  throw error;
19910
20140
  }
19911
20141
  }
19912
20142
  async authenticate() {
19913
- logger$8.debug("[Session] Starting authentication process");
20143
+ logger$9.debug("[Session] Starting authentication process");
19914
20144
  const persistedParams = await (0, import_cjs$7.firstValueFrom)((0, import_cjs$7.combineLatest)({
19915
20145
  protocol: this.transport.protocol$,
19916
20146
  authorization_state: this.authorizationState$
19917
20147
  }).pipe((0, import_cjs$7.take)(1)));
19918
- logger$8.debug("[Session] Persisted params:\n", {
20148
+ logger$9.debug("[Session] Persisted params:\n", {
19919
20149
  protocol: persistedParams.protocol,
19920
20150
  authStateLength: persistedParams.authorization_state?.length
19921
20151
  });
@@ -19923,16 +20153,16 @@ var ClientSessionManager = class extends Destroyable {
19923
20153
  const storedToken = this.getCredential().token;
19924
20154
  const isReconnect = hasReconnectState && storedToken;
19925
20155
  let dpopToken;
19926
- if (isReconnect) logger$8.debug("[Session] Reconnecting with stored jwt_token + authorization_state");
20156
+ if (isReconnect) logger$9.debug("[Session] Reconnecting with stored jwt_token + authorization_state");
19927
20157
  else if (this.onBeforeReconnect && this.clientBound) {
19928
- logger$8.debug("[Session] Refreshing credentials before fresh connect");
20158
+ logger$9.debug("[Session] Refreshing credentials before fresh connect");
19929
20159
  await this.onBeforeReconnect();
19930
20160
  }
19931
20161
  if ((!isReconnect || this.clientBound) && this.dpopManager?.initialized) try {
19932
20162
  dpopToken = await this.dpopManager.createRpcProof({ method: "signalwire.connect" });
19933
20163
  } catch (error) {
19934
20164
  if (this.clientBound) throw error;
19935
- logger$8.warn("[Session] Failed to create DPoP proof for connect, proceeding without:", error);
20165
+ logger$9.warn("[Session] Failed to create DPoP proof for connect, proceeding without:", error);
19936
20166
  }
19937
20167
  const rpcConnectRequest = RPCConnect({
19938
20168
  authentication: isReconnect ? { jwt_token: storedToken } : this.authentication,
@@ -19949,12 +20179,12 @@ var ClientSessionManager = class extends Destroyable {
19949
20179
  } : {}
19950
20180
  });
19951
20181
  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)(() => {
19952
- logger$8.debug("[Session] Response passed filter, processing authentication result");
20182
+ logger$9.debug("[Session] Response passed filter, processing authentication result");
19953
20183
  }), (0, import_cjs$7.take)(1), (0, import_cjs$7.catchError)((err) => {
19954
- logger$8.error("[Session] Authentication RPC failed:", err);
20184
+ logger$9.error("[Session] Authentication RPC failed:", err);
19955
20185
  throw err;
19956
20186
  })));
19957
- logger$8.debug("[Session] Processing authentication result:", {
20187
+ logger$9.debug("[Session] Processing authentication result:", {
19958
20188
  hasProtocol: !!response.protocol,
19959
20189
  hasAuthorization: !!response.authorization,
19960
20190
  hasIceServers: !!response.ice_servers
@@ -19963,12 +20193,12 @@ var ClientSessionManager = class extends Destroyable {
19963
20193
  this._authorization$.next(response.authorization);
19964
20194
  this._iceServers$.next(response.ice_servers ?? []);
19965
20195
  this._authState$.next({ kind: "authenticated" });
19966
- logger$8.debug("[Session] Authentication completed successfully");
20196
+ logger$9.debug("[Session] Authentication completed successfully");
19967
20197
  }
19968
20198
  async disconnect() {
19969
20199
  this.transport.disconnect();
19970
20200
  this._authState$.next({ kind: "unauthenticated" });
19971
- await this.cleanupStoredConnectionParams();
20201
+ await this.teardownSessionState();
19972
20202
  }
19973
20203
  async createInboundCall(invite) {
19974
20204
  const callSession = await this.createCall({
@@ -19999,11 +20229,11 @@ var ClientSessionManager = class extends Destroyable {
19999
20229
  async handleVertoAttach(attach) {
20000
20230
  const { callID } = attach;
20001
20231
  if (callID in this._calls$.value) {
20002
- logger$8.debug(`[Session] Verto attach for existing call ${callID}, deferring to per-call handler`);
20232
+ logger$9.debug(`[Session] Verto attach for existing call ${callID}, deferring to per-call handler`);
20003
20233
  return;
20004
20234
  }
20005
20235
  const storedOptions = await this.attachManager.consumePendingAttachment(callID);
20006
- logger$8.debug(`[Session] Creating reattached call for callID: ${callID}`);
20236
+ logger$9.debug(`[Session] Creating reattached call for callID: ${callID}`);
20007
20237
  const callSession = await this.createCall({
20008
20238
  nodeId: attach.node_id,
20009
20239
  callId: callID,
@@ -20027,14 +20257,14 @@ var ClientSessionManager = class extends Destroyable {
20027
20257
  to: destinationURI,
20028
20258
  ...options
20029
20259
  });
20030
- 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)))));
20260
+ 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)))));
20031
20261
  this._calls$.next({
20032
20262
  [`${callSession.id}`]: callSession,
20033
20263
  ...this._calls$.value
20034
20264
  });
20035
20265
  return callSession;
20036
20266
  } catch (error) {
20037
- logger$8.error("[Session] Error creating outbound call:", error);
20267
+ logger$9.error("[Session] Error creating outbound call:", error);
20038
20268
  callSession?.destroy();
20039
20269
  const callError = new CallCreateError(error instanceof import_cjs$7.TimeoutError ? "Call create timeout" : "Call creation failed", error, "outbound");
20040
20270
  this._errors$.next(callError);
@@ -20052,7 +20282,7 @@ var ClientSessionManager = class extends Destroyable {
20052
20282
  address = this._directory.get(addressId);
20053
20283
  if (!address) throw new DependencyError(`Address ID: ${addressId} not found`);
20054
20284
  } catch {
20055
- logger$8.warn(`[Session] Directory lookup failed for ${addressURI}, proceeding with raw URI`);
20285
+ logger$9.warn(`[Session] Directory lookup failed for ${addressURI}, proceeding with raw URI`);
20056
20286
  }
20057
20287
  const callSession = this.callFactory.createCall(address, { ...options });
20058
20288
  this.subscribeTo(callSession.status$.pipe((0, import_cjs$7.filter)((status) => status === "destroyed"), (0, import_cjs$7.take)(1)), () => {
@@ -20061,7 +20291,7 @@ var ClientSessionManager = class extends Destroyable {
20061
20291
  });
20062
20292
  return callSession;
20063
20293
  } catch (error) {
20064
- logger$8.error("[Session] Error creating call session:", error);
20294
+ logger$9.error("[Session] Error creating call session:", error);
20065
20295
  throw new CallCreateError("Call create error", error, options.initOffer ? "inbound" : "outbound");
20066
20296
  }
20067
20297
  }
@@ -20110,7 +20340,7 @@ const isString = (obj) => typeof obj === "string";
20110
20340
  //#endregion
20111
20341
  //#region src/managers/ConversationsManager.ts
20112
20342
  var import_cjs$6 = require_cjs();
20113
- const logger$7 = getLogger();
20343
+ const logger$8 = getLogger();
20114
20344
  var ConversationMessagesFetcher = class extends Fetcher {
20115
20345
  constructor(groupId, http) {
20116
20346
  super(`/api/fabric/conversations/${groupId}/messages`, "page_size=100", http);
@@ -20150,13 +20380,13 @@ var ConversationsManager = class {
20150
20380
  }
20151
20381
  throw new ConversationError("Join Failed - Unexpected response");
20152
20382
  } catch (error) {
20153
- logger$7.error("[ConversationsManager] Failed to join conversation:", error);
20383
+ logger$8.error("[ConversationsManager] Failed to join conversation:", error);
20154
20384
  throw error;
20155
20385
  }
20156
20386
  }
20157
20387
  async getConversationMessageCollection(addressId) {
20158
20388
  const groupId = this.groupIds.get(addressId) ?? await this.join(addressId);
20159
- 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));
20389
+ 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));
20160
20390
  }
20161
20391
  async sendText(text, destinationAddressId) {
20162
20392
  const groupId = this.groupIds.get(destinationAddressId) ?? await this.join(destinationAddressId);
@@ -20173,7 +20403,7 @@ var ConversationsManager = class {
20173
20403
  })).ok) return;
20174
20404
  throw new ConversationError("Send Text Failed - Unexpected response");
20175
20405
  } catch (error) {
20176
- logger$7.error("[ConversationsManager] Failed to send text message:", error);
20406
+ logger$8.error("[ConversationsManager] Failed to send text message:", error);
20177
20407
  throw error;
20178
20408
  }
20179
20409
  }
@@ -20182,7 +20412,7 @@ var ConversationsManager = class {
20182
20412
  //#endregion
20183
20413
  //#region src/managers/DeviceTokenManager.ts
20184
20414
  var import_cjs$5 = require_cjs();
20185
- const logger$6 = getLogger();
20415
+ const logger$7 = getLogger();
20186
20416
  /**
20187
20417
  * Resolves the token expiry timestamp (epoch seconds) using a 3-tier priority chain:
20188
20418
  * 1. `data.expires_at` — server-provided absolute timestamp
@@ -20192,7 +20422,7 @@ const logger$6 = getLogger();
20192
20422
  function resolveExpiresAt(data) {
20193
20423
  if (data.expires_at) return data.expires_at;
20194
20424
  if (data.expires_in) return Math.floor(Date.now() / 1e3) + data.expires_in;
20195
- logger$6.warn("[DeviceToken] Could not determine token expiry, using default");
20425
+ logger$7.warn("[DeviceToken] Could not determine token expiry, using default");
20196
20426
  return Math.floor(Date.now() / 1e3) + DEVICE_TOKEN_DEFAULT_EXPIRE_IN;
20197
20427
  }
20198
20428
  /**
@@ -20225,11 +20455,12 @@ var DeviceTokenManager = class extends Destroyable {
20225
20455
  this.getCredential = getCredential;
20226
20456
  this._currentToken$ = this.createBehaviorSubject(null);
20227
20457
  this._refreshInProgress = false;
20458
+ this._paused = false;
20228
20459
  this._effectiveExpireIn = DEVICE_TOKEN_DEFAULT_EXPIRE_IN;
20229
20460
  this.subscribeTo(this._currentToken$.pipe((0, import_cjs$5.filter)(Boolean), (0, import_cjs$5.switchMap)((tokenData) => {
20230
20461
  const expiresAt = resolveExpiresAt(tokenData);
20231
20462
  const refreshIn = Math.max(expiresAt * 1e3 - Date.now() - DEVICE_TOKEN_REFRESH_BUFFER_MS, 1e3);
20232
- logger$6.debug(`[DeviceToken] Scheduling Client Bound SAT refresh in ${refreshIn}ms`);
20463
+ logger$7.debug(`[DeviceToken] Scheduling Client Bound SAT refresh in ${refreshIn}ms`);
20233
20464
  return (0, import_cjs$5.timer)(refreshIn);
20234
20465
  })), () => {
20235
20466
  this.executeRefresh();
@@ -20243,7 +20474,12 @@ var DeviceTokenManager = class extends Destroyable {
20243
20474
  * Activates the Client Bound SAT flow when the user's token has
20244
20475
  * `sat:refresh` scope.
20245
20476
  *
20246
- * Steps:
20477
+ * Returns an {@link ActivationResult} indicating whether the manager
20478
+ * took ownership of refresh duties. The caller must use the `activated`
20479
+ * boolean to decide whether to keep its own refresh path armed — when
20480
+ * `activated` is `false`, the caller is responsible for refresh.
20481
+ *
20482
+ * Steps on success:
20247
20483
  * 1. Check user's `sat_claims` for `sat:refresh` scope
20248
20484
  * 2. Call `/api/fabric/subscriber/devices/token` with a DPoP proof
20249
20485
  * 3. Reauthenticate the session with the Client Bound SAT + DPoP proof
@@ -20252,32 +20488,51 @@ var DeviceTokenManager = class extends Destroyable {
20252
20488
  async activate(user, session, updateCredential) {
20253
20489
  const { satClaims } = user;
20254
20490
  if (!satClaims?.scope?.includes(SAT_REFRESH_SCOPE)) {
20255
- logger$6.debug("[DeviceToken] No sat:refresh scope, skipping Client Bound SAT activation");
20256
- return;
20491
+ logger$7.debug("[DeviceToken] No sat:refresh scope, skipping Client Bound SAT activation");
20492
+ return {
20493
+ activated: false,
20494
+ reason: "no-scope"
20495
+ };
20257
20496
  }
20258
20497
  this._session = session;
20259
20498
  this._updateCredential = updateCredential;
20260
20499
  try {
20500
+ const cached = this._currentToken$.value;
20501
+ if (cached && this.isTokenFresh(cached)) {
20502
+ logger$7.debug("[DeviceToken] Reusing cached Client Bound SAT — skipping /devices/token");
20503
+ const rpcProof$1 = await this.dpopManager.createRpcProof({ method: "signalwire.reauthenticate" });
20504
+ await session.reauthenticate(cached.token, rpcProof$1, { clientBound: true });
20505
+ updateCredential({ token: cached.token });
20506
+ return { activated: true };
20507
+ }
20261
20508
  const tokenData = await this.obtainToken();
20262
20509
  if (!tokenData.expires_at && !tokenData.expires_in && satClaims.expires_at) tokenData.expires_at = satClaims.expires_at;
20263
20510
  this._effectiveExpireIn = resolveExpireIn(tokenData);
20264
20511
  const rpcProof = await this.dpopManager.createRpcProof({ method: "signalwire.reauthenticate" });
20265
20512
  await session.reauthenticate(tokenData.token, rpcProof, { clientBound: true });
20266
20513
  updateCredential({ token: tokenData.token });
20267
- logger$6.info("[DeviceToken] Client Bound SAT activated successfully");
20514
+ logger$7.info("[DeviceToken] Client Bound SAT activated successfully");
20268
20515
  this._currentToken$.next(tokenData);
20516
+ return { activated: true };
20269
20517
  } catch (error) {
20270
- logger$6.error("[DeviceToken] Failed to activate Client Bound SAT:", error);
20518
+ logger$7.error("[DeviceToken] Failed to activate Client Bound SAT:", error);
20271
20519
  this.errorHandler(new DPoPInitError(error, "Failed to activate Client Bound SAT"));
20272
- const credential = this.getCredential();
20273
- const expiresAt = satClaims.expires_at ?? (credential.expiry_at ? credential.expiry_at / 1e3 : Date.now() / 1e3 + DEVICE_TOKEN_DEFAULT_EXPIRE_IN / 1e3);
20274
- this._currentToken$.next({
20275
- token: credential.token ?? "",
20276
- expires_at: expiresAt
20277
- });
20520
+ return {
20521
+ activated: false,
20522
+ reason: "endpoint-failed"
20523
+ };
20278
20524
  }
20279
20525
  }
20280
20526
  /**
20527
+ * Returns true when the cached token has enough headroom before expiry to
20528
+ * be safely reused on reactivation. The headroom matches the refresh
20529
+ * buffer, so a token within the refresh window is treated as stale (the
20530
+ * reactive pipeline is about to refresh it anyway).
20531
+ */
20532
+ isTokenFresh(token) {
20533
+ return resolveExpiresAt(token) * 1e3 - Date.now() > DEVICE_TOKEN_REFRESH_BUFFER_MS;
20534
+ }
20535
+ /**
20281
20536
  * Obtains a Client Bound SAT from `/api/fabric/subscriber/devices/token`.
20282
20537
  * Returns the full {@link DeviceTokenResponse} including expiry metadata.
20283
20538
  */
@@ -20307,7 +20562,7 @@ var DeviceTokenManager = class extends Destroyable {
20307
20562
  * handled by the reactive pipeline).
20308
20563
  */
20309
20564
  async refreshToken(session, currentToken, updateCredential) {
20310
- logger$6.debug("[DeviceToken] Refreshing Client Bound SAT");
20565
+ logger$7.debug("[DeviceToken] Refreshing Client Bound SAT");
20311
20566
  const dpopProof = await this.dpopManager.createHttpProof({
20312
20567
  method: "POST",
20313
20568
  uri: DEVICE_REFRESH_ENDPOINT,
@@ -20329,7 +20584,7 @@ var DeviceTokenManager = class extends Destroyable {
20329
20584
  const rpcProof = await this.dpopManager.createRpcProof({ method: "signalwire.reauthenticate" });
20330
20585
  await session.reauthenticate(data.token, rpcProof);
20331
20586
  updateCredential({ token: data.token });
20332
- logger$6.info("[DeviceToken] Client Bound SAT refreshed successfully");
20587
+ logger$7.info("[DeviceToken] Client Bound SAT refreshed successfully");
20333
20588
  return data;
20334
20589
  }
20335
20590
  /**
@@ -20338,18 +20593,22 @@ var DeviceTokenManager = class extends Destroyable {
20338
20593
  * On all retries exhausted, emits to `errorHandler`.
20339
20594
  */
20340
20595
  async executeRefresh() {
20596
+ if (this._paused) {
20597
+ logger$7.debug("[DeviceToken] Manager paused, skipping refresh");
20598
+ return;
20599
+ }
20341
20600
  if (this._refreshInProgress) {
20342
- logger$6.debug("[DeviceToken] Refresh already in progress, skipping");
20601
+ logger$7.debug("[DeviceToken] Refresh already in progress, skipping");
20343
20602
  return;
20344
20603
  }
20345
20604
  const session = this._session;
20346
20605
  const updateCredential = this._updateCredential;
20347
20606
  if (!session || !updateCredential) {
20348
- logger$6.warn("[DeviceToken] Cannot refresh: session or updateCredential not set");
20607
+ logger$7.warn("[DeviceToken] Cannot refresh: session or updateCredential not set");
20349
20608
  return;
20350
20609
  }
20351
20610
  if (!session.authenticated) {
20352
- logger$6.debug("[DeviceToken] Session not authenticated, deferring refresh");
20611
+ logger$7.debug("[DeviceToken] Session not authenticated, deferring refresh");
20353
20612
  return;
20354
20613
  }
20355
20614
  this._refreshInProgress = true;
@@ -20359,7 +20618,7 @@ var DeviceTokenManager = class extends Destroyable {
20359
20618
  const newTokenData = await this.retryRefresh(session, currentToken, updateCredential);
20360
20619
  this._currentToken$.next(newTokenData);
20361
20620
  } catch (error) {
20362
- logger$6.error("[DeviceToken] Automatic Client Bound SAT refresh failed:", error);
20621
+ logger$7.error("[DeviceToken] Automatic Client Bound SAT refresh failed:", error);
20363
20622
  this.errorHandler(error instanceof TokenRefreshError ? error : new TokenRefreshError("Automatic token refresh failed", error));
20364
20623
  } finally {
20365
20624
  this._refreshInProgress = false;
@@ -20377,18 +20636,215 @@ var DeviceTokenManager = class extends Destroyable {
20377
20636
  lastError = error;
20378
20637
  if (attempt < DEVICE_TOKEN_REFRESH_MAX_RETRIES - 1) {
20379
20638
  const delay$1 = DEVICE_TOKEN_REFRESH_RETRY_BASE_MS * Math.pow(2, attempt);
20380
- logger$6.warn(`[DeviceToken] Refresh attempt ${attempt + 1} failed, retrying in ${delay$1}ms`);
20639
+ logger$7.warn(`[DeviceToken] Refresh attempt ${attempt + 1} failed, retrying in ${delay$1}ms`);
20381
20640
  await new Promise((resolve) => setTimeout(resolve, delay$1));
20382
20641
  }
20383
20642
  }
20384
20643
  throw lastError instanceof Error ? lastError : new TokenRefreshError("All refresh retries exhausted", lastError);
20385
20644
  }
20645
+ /**
20646
+ * Stops the reactive refresh pipeline from firing. Use when the underlying
20647
+ * session is being torn down (e.g., during {@link SignalWire.disconnect})
20648
+ * so a scheduled refresh cannot fire against a destroyed session.
20649
+ *
20650
+ * The manager's state (cached token, effective TTL, subscriptions) is
20651
+ * preserved — call {@link resume} to re-enable firing after reconnect.
20652
+ */
20653
+ pause() {
20654
+ this._paused = true;
20655
+ }
20656
+ /** Re-enables the reactive refresh pipeline after a previous {@link pause}. */
20657
+ resume() {
20658
+ this._paused = false;
20659
+ }
20386
20660
  /** Cleans up the manager, cancelling the reactive pipeline and all subscriptions. */
20387
20661
  destroy() {
20388
20662
  super.destroy();
20389
20663
  }
20390
20664
  };
20391
20665
 
20666
+ //#endregion
20667
+ //#region src/managers/CredentialRefreshCoordinator.ts
20668
+ const logger$6 = getLogger();
20669
+ const defaultDeviceTokenManagerFactory = (dpopManager, http, errorHandler, getCredential) => new DeviceTokenManager(dpopManager, http, errorHandler, getCredential);
20670
+ /**
20671
+ * Centralizes credential-refresh ownership across the two competing
20672
+ * mechanisms — developer-provided `CredentialProvider.refresh()` and the
20673
+ * Client Bound SAT path via {@link DeviceTokenManager}.
20674
+ *
20675
+ * Maintains the invariant: **at most one refresh mechanism is armed at a
20676
+ * time, and at least one is armed whenever the current credential has an
20677
+ * `expiry_at`**.
20678
+ *
20679
+ * Replaces the previous design where refresh state was distributed across
20680
+ * `SignalWire` (timer field, scheduler method, activation helper) and
20681
+ * `DeviceTokenManager` (reactive pipeline). Centralizing the invariant in
20682
+ * one component eliminates the bug class that produced issue #19074.
20683
+ *
20684
+ * Race-safety:
20685
+ * - `_activating` flag prevents overlapping `activate()` calls from racing.
20686
+ * - `_activationGeneration` lets late resolutions detect they've been
20687
+ * preempted by a newer activation (e.g., reconnect during in-flight
20688
+ * `obtainToken`).
20689
+ */
20690
+ var CredentialRefreshCoordinator = class extends Destroyable {
20691
+ constructor(dpopManager, deps) {
20692
+ super();
20693
+ this.deps = deps;
20694
+ this._activating = false;
20695
+ this._activationGeneration = 0;
20696
+ if (dpopManager?.initialized) this._deviceTokenManager = (deps.deviceTokenManagerFactory ?? defaultDeviceTokenManagerFactory)(dpopManager, deps.http, (error) => deps.notifier.onError(error), () => deps.store.read());
20697
+ }
20698
+ /** True when the Client Bound SAT path is available (DPoP initialized). */
20699
+ get clientBoundSATAvailable() {
20700
+ return this._deviceTokenManager !== void 0;
20701
+ }
20702
+ /** True when the developer-provided refresh timer is currently armed. */
20703
+ get developerRefreshArmed() {
20704
+ return this._developerTimerId !== void 0;
20705
+ }
20706
+ /**
20707
+ * Arms the developer-provided refresh timer to fire shortly before
20708
+ * `expiresAt`. Replaces any previously scheduled developer refresh.
20709
+ *
20710
+ * Idempotent — multiple calls just reschedule. On retry exhaustion,
20711
+ * invokes `deps.onRefreshExhausted` so the orchestrator can disconnect.
20712
+ */
20713
+ scheduleDeveloperRefresh(provider, expiresAt, attempt = 0) {
20714
+ if (this._developerTimerId !== void 0) clearTimeout(this._developerTimerId);
20715
+ 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);
20716
+ this._developerTimerId = setTimeout(async () => {
20717
+ try {
20718
+ if (!provider.refresh) throw new InvalidCredentialsError("Credential provider does not support refresh");
20719
+ const newCredentials = await provider.refresh();
20720
+ this.deps.store.write(newCredentials);
20721
+ this.deps.store.persist(newCredentials);
20722
+ logger$6.info("[Coordinator] Credentials refreshed successfully.");
20723
+ if (newCredentials.expiry_at) this.scheduleDeveloperRefresh(provider, newCredentials.expiry_at, 0);
20724
+ } catch (error) {
20725
+ const nextAttempt = attempt + 1;
20726
+ logger$6.error(`[Coordinator] Credential refresh failed (attempt ${nextAttempt}/${CREDENTIAL_REFRESH_MAX_RETRIES}):`, error);
20727
+ this.deps.notifier.onError(error instanceof Error ? error : new Error(String(error), { cause: error }));
20728
+ if (nextAttempt < CREDENTIAL_REFRESH_MAX_RETRIES) this.scheduleDeveloperRefresh(provider, expiresAt, nextAttempt);
20729
+ else {
20730
+ logger$6.error("[Coordinator] Credential refresh exhausted all retries. Disconnecting.");
20731
+ this.deps.notifier.onError(new TokenRefreshError("Credential refresh failed after max retries"));
20732
+ this.deps.notifier.onRefreshExhausted();
20733
+ }
20734
+ }
20735
+ }, refreshInterval);
20736
+ }
20737
+ /**
20738
+ * Cancels any scheduled developer-provided refresh. Idempotent.
20739
+ *
20740
+ * @internal Used by the coordinator's own activation flow. External
20741
+ * callers should use {@link suspend} for disconnect-time quiescence —
20742
+ * `suspend()` also pauses the internal Client Bound SAT pipeline.
20743
+ */
20744
+ cancelDeveloperRefresh() {
20745
+ if (this._developerTimerId !== void 0) {
20746
+ clearTimeout(this._developerTimerId);
20747
+ this._developerTimerId = void 0;
20748
+ }
20749
+ }
20750
+ /**
20751
+ * Suspends both refresh paths — cancels the developer timer and pauses
20752
+ * the internal reactive pipeline. Use when the underlying session is
20753
+ * being torn down (e.g., {@link SignalWire.disconnect}). The next
20754
+ * {@link activate} call re-enables the internal pipeline.
20755
+ *
20756
+ * The internal manager's cached token survives — see
20757
+ * {@link DeviceTokenManager.pause} — so a subsequent reconnect can
20758
+ * skip the `/devices/token` exchange entirely.
20759
+ */
20760
+ suspend() {
20761
+ this.cancelDeveloperRefresh();
20762
+ this._deviceTokenManager?.pause();
20763
+ }
20764
+ /**
20765
+ * Asks the Client Bound SAT path to take over refresh. If it accepts,
20766
+ * the developer-provided timer (if any) is cancelled. If it declines, the
20767
+ * developer timer remains armed and a `credential_refresh_fallback`
20768
+ * warning is emitted.
20769
+ *
20770
+ * **Idempotent** — re-entrant calls during an in-flight activation are
20771
+ * dropped. Use `_activationGeneration` to detect stale resolutions
20772
+ * (e.g., a reconnect-triggered activate() that races with an earlier one).
20773
+ */
20774
+ async activate(user, session) {
20775
+ if (this._activating) {
20776
+ logger$6.debug("[Coordinator] activate() in flight; ignoring re-entrant call");
20777
+ return;
20778
+ }
20779
+ if (!this._deviceTokenManager) return;
20780
+ this._deviceTokenManager.resume();
20781
+ const generation = ++this._activationGeneration;
20782
+ this._activating = true;
20783
+ try {
20784
+ const result = await this.withActivationTimeout(this._deviceTokenManager.activate(user, session, (cred) => this.deps.store.merge(cred)));
20785
+ if (generation !== this._activationGeneration) {
20786
+ logger$6.debug("[Coordinator] activate() result discarded (preempted by newer activation)");
20787
+ return;
20788
+ }
20789
+ if (result.activated) {
20790
+ this.cancelDeveloperRefresh();
20791
+ logger$6.debug("[Coordinator] Developer refresh disabled — Client Bound SAT owns refresh");
20792
+ return;
20793
+ }
20794
+ logger$6.warn(`[SignalWire] [SW-REFRESH-FALLBACK] Client Bound SAT declined (reason=${result.reason}); using developer-provided refresh handler.`);
20795
+ this.deps.notifier.onWarning({
20796
+ code: "credential_refresh_fallback",
20797
+ source: "CredentialProvider",
20798
+ reason: result.reason,
20799
+ message: `Client Bound SAT activation declined (${result.reason}); using developer-provided refresh.`
20800
+ });
20801
+ } finally {
20802
+ this._activating = false;
20803
+ }
20804
+ }
20805
+ destroy() {
20806
+ this.cancelDeveloperRefresh();
20807
+ this._deviceTokenManager?.destroy();
20808
+ this._deviceTokenManager = void 0;
20809
+ super.destroy();
20810
+ }
20811
+ /**
20812
+ * Races the manager's `activate()` against a hard timeout. A wedged HTTP
20813
+ * layer (e.g., proxy issues) could otherwise hang the activation
20814
+ * indefinitely, leaving the session with no refresh mechanism while the
20815
+ * `_activating` guard blocks subsequent retries.
20816
+ *
20817
+ * On timeout, the manager is paused immediately. The inner `activate()`
20818
+ * may still complete in the background and emit to `_currentToken$`,
20819
+ * which would normally arm the reactive refresh pipeline; pausing
20820
+ * prevents that pipeline from firing while the developer refresh path
20821
+ * is the active mechanism — preserving the "at most one mechanism
20822
+ * armed" invariant. The next `activate()` call resumes the manager.
20823
+ */
20824
+ async withActivationTimeout(inner) {
20825
+ return new Promise((resolve) => {
20826
+ const timer$4 = setTimeout(() => {
20827
+ this._deviceTokenManager?.pause();
20828
+ resolve({
20829
+ activated: false,
20830
+ reason: "activation-timeout"
20831
+ });
20832
+ }, CREDENTIAL_ACTIVATE_TIMEOUT_MS);
20833
+ inner.then((result) => {
20834
+ clearTimeout(timer$4);
20835
+ resolve(result);
20836
+ }, (error) => {
20837
+ clearTimeout(timer$4);
20838
+ this.deps.notifier.onError(error instanceof Error ? error : new Error(String(error)));
20839
+ resolve({
20840
+ activated: false,
20841
+ reason: "endpoint-failed"
20842
+ });
20843
+ });
20844
+ });
20845
+ }
20846
+ };
20847
+
20392
20848
  //#endregion
20393
20849
  //#region src/managers/DiagnosticsCollector.ts
20394
20850
  const logger$5 = getLogger();
@@ -20909,6 +21365,10 @@ var TransportManager = class extends Destroyable {
20909
21365
  if (!isSignalwireRequest(message)) return true;
20910
21366
  const eventChannel = message.params.event_channel;
20911
21367
  if (!eventChannel) return true;
21368
+ if (message.params.event_type.startsWith("conversation.")) {
21369
+ logger$2.debug(`[Transport] Received conversation event: ${message.params.event_type} (event_channel: ${eventChannel})`);
21370
+ return true;
21371
+ }
20912
21372
  const currentProtocol = this._currentProtocol;
20913
21373
  if (!currentProtocol) return true;
20914
21374
  if (!eventChannel.includes(currentProtocol)) {
@@ -21102,6 +21562,7 @@ var SignalWire = class extends Destroyable {
21102
21562
  this._isConnected$ = this.createBehaviorSubject(false);
21103
21563
  this._isRegistered$ = this.createBehaviorSubject(false);
21104
21564
  this._errors$ = this.createReplaySubject(1);
21565
+ this._warnings$ = this.createReplaySubject(10);
21105
21566
  this._options = {};
21106
21567
  this._deps = new DependencyContainer();
21107
21568
  this._credentialProvider = credentialProvider;
@@ -21161,6 +21622,27 @@ var SignalWire = class extends Destroyable {
21161
21622
  */
21162
21623
  async resolveCredentials() {
21163
21624
  const fingerprint = await this.initDPoP();
21625
+ this._refreshCoordinator = new CredentialRefreshCoordinator(this._dpopManager, {
21626
+ http: this._deps.http,
21627
+ notifier: {
21628
+ onError: (error) => this._errors$.next(error),
21629
+ onWarning: (warning) => this._warnings$.next(warning),
21630
+ onRefreshExhausted: () => void this.disconnect()
21631
+ },
21632
+ store: {
21633
+ read: () => this._deps.credential,
21634
+ write: (credential) => {
21635
+ this._deps.credential = credential;
21636
+ },
21637
+ merge: (partial) => {
21638
+ this._deps.credential = {
21639
+ ...this._deps.credential,
21640
+ ...partial
21641
+ };
21642
+ },
21643
+ persist: (credential) => this.persistCredential(credential)
21644
+ }
21645
+ });
21164
21646
  if (this._credentialProvider) return this.validateCredentials(this._credentialProvider, void 0, fingerprint);
21165
21647
  for (const scope of this._deps.persistSession ? ["local", "session"] : ["session"]) try {
21166
21648
  const cached = await this._deps.storage.getItem("sw:cached_credential", scope);
@@ -21190,7 +21672,16 @@ var SignalWire = class extends Destroyable {
21190
21672
  logger$1.error("[SignalWire] Provided credentials have expired.");
21191
21673
  throw new InvalidCredentialsError("Provided credentials have expired.");
21192
21674
  }
21193
- if (_credentials.expiry_at && credentialProvider?.refresh) this.scheduleCredentialRefresh(credentialProvider, _credentials.expiry_at);
21675
+ if (_credentials.expiry_at && credentialProvider?.refresh) this._refreshCoordinator?.scheduleDeveloperRefresh(credentialProvider, _credentials.expiry_at);
21676
+ else if (_credentials.expiry_at && !credentialProvider?.refresh) {
21677
+ 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.`);
21678
+ this._warnings$.next({
21679
+ code: "credential_no_refresh_handler",
21680
+ source: "CredentialProvider",
21681
+ message: "Credential has expiry_at but no refresh handler. Session will terminate at expiry unless the SAT carries 'sat:refresh' scope.",
21682
+ expiresAt: _credentials.expiry_at
21683
+ });
21684
+ }
21194
21685
  this._deps.credential = _credentials;
21195
21686
  this.persistCredential(_credentials);
21196
21687
  if (this.isConnected && this._clientSession.authenticated && _credentials.token) try {
@@ -21201,35 +21692,6 @@ var SignalWire = class extends Destroyable {
21201
21692
  this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
21202
21693
  }
21203
21694
  }
21204
- /**
21205
- * Schedules credential refresh with exponential backoff retry on failure.
21206
- * On success, resets attempt counter and schedules the next refresh.
21207
- * After exhausting retries, emits TokenRefreshError and disconnects.
21208
- */
21209
- scheduleCredentialRefresh(credentialProvider, expiresAt, attempt = 0) {
21210
- if (this._refreshTimerId !== void 0) clearTimeout(this._refreshTimerId);
21211
- 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);
21212
- this._refreshTimerId = setTimeout(async () => {
21213
- try {
21214
- if (!credentialProvider.refresh) throw new InvalidCredentialsError("Credential provider does not support refresh");
21215
- const newCredentials = await credentialProvider.refresh();
21216
- this._deps.credential = newCredentials;
21217
- this.persistCredential(newCredentials);
21218
- logger$1.info("[SignalWire] Credentials refreshed successfully.");
21219
- if (newCredentials.expiry_at) this.scheduleCredentialRefresh(credentialProvider, newCredentials.expiry_at, 0);
21220
- } catch (error) {
21221
- const nextAttempt = attempt + 1;
21222
- logger$1.error(`[SignalWire] Credential refresh failed (attempt ${nextAttempt}/${CREDENTIAL_REFRESH_MAX_RETRIES}):`, error);
21223
- this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
21224
- if (nextAttempt < CREDENTIAL_REFRESH_MAX_RETRIES) this.scheduleCredentialRefresh(credentialProvider, expiresAt, nextAttempt);
21225
- else {
21226
- logger$1.error("[SignalWire] Credential refresh exhausted all retries. Disconnecting.");
21227
- this._errors$.next(new TokenRefreshError("Credential refresh failed after max retries"));
21228
- this.disconnect();
21229
- }
21230
- }
21231
- }, refreshInterval);
21232
- }
21233
21695
  /** Persist credential to localStorage when persistSession is enabled. */
21234
21696
  persistCredential(credential) {
21235
21697
  if (!credential.token) return;
@@ -21323,6 +21785,7 @@ var SignalWire = class extends Destroyable {
21323
21785
  logger$1.debug("[SignalWire] Credential expired, refreshing before reconnect");
21324
21786
  const newCredentials = await this._credentialProvider.authenticate(fingerprint ? { fingerprint } : void 0);
21325
21787
  this._deps.credential = newCredentials;
21788
+ if (newCredentials.expiry_at && this._credentialProvider.refresh) this._refreshCoordinator?.scheduleDeveloperRefresh(this._credentialProvider, newCredentials.expiry_at);
21326
21789
  logger$1.debug("[SignalWire] Credential refreshed successfully for reconnect");
21327
21790
  } catch (error) {
21328
21791
  logger$1.error("[SignalWire] Failed to refresh credentials for reconnect:", error);
@@ -21334,33 +21797,12 @@ var SignalWire = class extends Destroyable {
21334
21797
  this._errors$.next(error);
21335
21798
  });
21336
21799
  await this._clientSession.connect();
21337
- if (this._dpopManager?.initialized) {
21338
- if (this._refreshTimerId) {
21339
- clearTimeout(this._refreshTimerId);
21340
- this._refreshTimerId = void 0;
21341
- logger$1.debug("[SignalWire] Developer refresh disabled — Client Bound SAT activation starting");
21342
- }
21343
- this._deviceTokenManager = new DeviceTokenManager(this._dpopManager, this._deps.http, (error) => this._errors$.next(error), () => this._deps.credential);
21344
- await this._deviceTokenManager.activate(this._deps.user, this._clientSession, (cred) => {
21345
- this._deps.credential = {
21346
- ...this._deps.credential,
21347
- ...cred
21348
- };
21349
- });
21350
- }
21800
+ await this._refreshCoordinator?.activate(this._deps.user, this._clientSession);
21351
21801
  this.subscribeTo(this._clientSession.authenticated$.pipe((0, import_cjs$1.skip)(1), (0, import_cjs$1.filter)(Boolean)), async () => {
21352
21802
  try {
21353
- if (this._deviceTokenManager) {
21354
- await this._deviceTokenManager.activate(this._deps.user, this._clientSession, (cred) => {
21355
- this._deps.credential = {
21356
- ...this._deps.credential,
21357
- ...cred
21358
- };
21359
- });
21360
- logger$1.debug("[SignalWire] Client Bound SAT re-activated after reconnect");
21361
- }
21803
+ await this._refreshCoordinator?.activate(this._deps.user, this._clientSession);
21362
21804
  } catch (error) {
21363
- logger$1.error("[SignalWire] Client Bound SAT re-activation failed (non-fatal):", error);
21805
+ logger$1.error("[SignalWire] Refresh re-arm after reconnect failed (non-fatal):", error);
21364
21806
  this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
21365
21807
  }
21366
21808
  try {
@@ -21446,6 +21888,19 @@ var SignalWire = class extends Destroyable {
21446
21888
  get errors$() {
21447
21889
  return this.deferEmission(this._errors$.asObservable());
21448
21890
  }
21891
+ /**
21892
+ * Observable stream of non-fatal SDK warnings.
21893
+ *
21894
+ * Subscribe to detect SDK behaviors that affect session liveness or developer-facing
21895
+ * contracts but do not warrant disconnection — e.g., a fallback from Client Bound SAT
21896
+ * refresh to the developer-provided `refresh()` because the SAT lacks `sat:refresh`
21897
+ * scope. Discriminated by `code`.
21898
+ *
21899
+ * Independent from {@link errors$}: existing error consumers are not notified.
21900
+ */
21901
+ get warnings$() {
21902
+ return this.deferEmission(this._warnings$.asObservable());
21903
+ }
21449
21904
  /** Platform WebRTC capabilities detected at construction time. */
21450
21905
  get platformCapabilities() {
21451
21906
  this._platformCapabilities ??= detectPlatformCapabilities(this._options.webRTCApiProvider);
@@ -21514,7 +21969,7 @@ var SignalWire = class extends Destroyable {
21514
21969
  logger$1.warn("[SignalWire] Failed to initialize VisibilityController:", error);
21515
21970
  }
21516
21971
  try {
21517
- this._diagnosticsCollector = new DiagnosticsCollector({ sdkVersion: "3.30.0" });
21972
+ this._diagnosticsCollector = new DiagnosticsCollector({ sdkVersion: "4.0.0-rc.1" });
21518
21973
  } catch (error) {
21519
21974
  logger$1.warn("[SignalWire] Failed to initialize DiagnosticsCollector:", error);
21520
21975
  }
@@ -21522,14 +21977,19 @@ var SignalWire = class extends Destroyable {
21522
21977
  /**
21523
21978
  * Disconnects the WebSocket and tears down the current session.
21524
21979
  *
21980
+ * Ends the session identified by the protocol and clears its persisted
21981
+ * resume state (`authorization_state` + protocol) and attach records
21982
+ * together — a later {@link connect} with the same credentials starts a
21983
+ * fresh session and cannot reattach to the ended session's calls.
21984
+ * Credentials and device preferences are preserved. To temporarily stop
21985
+ * receiving inbound calls while keeping the session alive, use
21986
+ * `unregister()` instead.
21987
+ *
21525
21988
  * The client can be reconnected by calling {@link connect} again,
21526
21989
  * which creates a fresh transport and session.
21527
21990
  */
21528
21991
  async disconnect() {
21529
- if (this._refreshTimerId) {
21530
- clearTimeout(this._refreshTimerId);
21531
- this._refreshTimerId = void 0;
21532
- }
21992
+ this._refreshCoordinator?.suspend();
21533
21993
  this._diagnosticsCollector?.record("connection", "disconnected");
21534
21994
  await this.teardownTransportAndSession();
21535
21995
  this._isConnected$.next(false);
@@ -21916,15 +22376,20 @@ var SignalWire = class extends Destroyable {
21916
22376
  prefs.preferredVideoInput = null;
21917
22377
  await this._deviceController.clearDeviceState();
21918
22378
  }
21919
- /** Destroys the client, clearing timers and releasing all resources. */
22379
+ /**
22380
+ * Destroys the client, clearing timers and releasing all resources.
22381
+ *
22382
+ * Intentionally destroying the client ends its session: the resume state
22383
+ * (`authorization_state` + protocol) and the attach records are both
22384
+ * cleared. Credentials and device preferences are preserved — use
22385
+ * {@link resetToDefaults} for a full wipe. To temporarily stop receiving
22386
+ * inbound calls while keeping the session alive, use `unregister()`.
22387
+ */
21920
22388
  destroy() {
21921
- if (this._refreshTimerId) {
21922
- clearTimeout(this._refreshTimerId);
21923
- this._refreshTimerId = void 0;
21924
- }
21925
- this._deviceTokenManager?.destroy();
22389
+ this._refreshCoordinator?.destroy();
22390
+ this._refreshCoordinator = void 0;
21926
22391
  this._dpopManager?.destroy();
21927
- if (this._attachManager) this._attachManager.detachAll();
22392
+ this._clientSession.teardownSessionState();
21928
22393
  this._transport.destroy();
21929
22394
  this._clientSession.destroy();
21930
22395
  try {
@@ -22039,7 +22504,7 @@ var StaticCredentialProvider = class {
22039
22504
  /**
22040
22505
  * Library version from package.json, injected at build time.
22041
22506
  */
22042
- const version = "3.30.0";
22507
+ const version = "4.0.0-rc.1";
22043
22508
  /**
22044
22509
  * Flag indicating the library has been loaded and is ready to use.
22045
22510
  * For UMD builds: `window.SignalWire.ready`
@@ -22061,7 +22526,7 @@ const ready = true;
22061
22526
  */
22062
22527
  const emitReadyEvent = () => {
22063
22528
  if (typeof window !== "undefined") {
22064
- const event = new CustomEvent("signalwire:js:ready", { detail: { version: "3.30.0" } });
22529
+ const event = new CustomEvent("signalwire:js:ready", { detail: { version: "4.0.0-rc.1" } });
22065
22530
  window.dispatchEvent(event);
22066
22531
  }
22067
22532
  };
@@ -22085,5 +22550,5 @@ emitReadyEvent();
22085
22550
  if (typeof process === "undefined") globalThis.process = { env: { NODE_ENV: "production" } };
22086
22551
 
22087
22552
  //#endregion
22088
- export { Address, CallCreateError, ClientPreferences, CollectionFetchError, DPoPInitError, DeviceTokenError, EmbedTokenCredentialProvider, InvalidCredentialsError, MediaTrackError, MessageParseError, OverconstrainedFallbackError, Participant, PreflightError, RecoveryError, SelfCapabilities, SelfParticipant, SignalWire, StaticCredentialProvider, TokenRefreshError, UnexpectedError, User, VertoPongError, WebRTCCall, embeddableCall, getLogger, isSelfParticipant, ready, setDebugOptions, setLogLevel, setLogger, version };
22553
+ export { Address, CallCreateError, ClientPreferences, CollectionFetchError, DPoPInitError, DeviceTokenError, EmbedTokenCredentialProvider, InvalidCredentialsError, MediaAccessError, MediaTrackError, MessageParseError, OverconstrainedFallbackError, Participant, PreflightError, RecoveryError, SelfCapabilities, SelfParticipant, SignalWire, StaticCredentialProvider, TokenRefreshError, UnexpectedError, User, VertoPongError, WebRTCCall, embeddableCall, getLogger, isSelfParticipant, ready, setDebugOptions, setLogLevel, setLogger, version };
22089
22554
  //# sourceMappingURL=browser.mjs.map