@signalwire/js 4.0.0-rc.0 → 4.0.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9021,6 +9021,143 @@ var Destroyable = class {
9021
9021
  }
9022
9022
  };
9023
9023
 
9024
+ //#endregion
9025
+ //#region src/core/constants.ts
9026
+ const INVITE_VERSION = 1e3;
9027
+ const DEFAULT_ICE_CANDIDATE_TIMEOUT_MS = 600;
9028
+ const DEFAULT_ICE_GATHERING_TIMEOUT_MS = 6e3;
9029
+ const DEFAULT_RECONNECT_CALLS_TIMEOUT_MS = 300 * 1e3;
9030
+ const DEFAULT_CONNECTION_TIMEOUT_MS = 1e4;
9031
+ const DEFAULT_RECONNECT_DELAY_MIN_MS = 100;
9032
+ const DEFAULT_RECONNECT_DELAY_MAX_MS = 3e3;
9033
+ const DEFAULT_DEVICE_DEBOUNCE_TIME_MS = 1500;
9034
+ const DEFAULT_DEVICE_POLLING_INTERVAL_MS = 0;
9035
+ const PREFERENCES_STORAGE_KEY = "sw:preferences";
9036
+ /** Scope value that enables automatic token refresh. */
9037
+ const SAT_REFRESH_SCOPE = "sat:refresh";
9038
+ /** API endpoints for device token operations. */
9039
+ const DEVICE_TOKEN_ENDPOINT = "/api/fabric/subscriber/devices/token";
9040
+ const DEVICE_REFRESH_ENDPOINT = "/api/fabric/subscriber/devices/refresh";
9041
+ /** Default device token TTL in seconds (15 minutes). */
9042
+ const DEVICE_TOKEN_DEFAULT_EXPIRE_IN = 900;
9043
+ /** Buffer time in milliseconds before expiry to trigger refresh. */
9044
+ const DEVICE_TOKEN_REFRESH_BUFFER_MS = 3e4;
9045
+ /** Maximum retry attempts for device token refresh on transient failure. */
9046
+ const DEVICE_TOKEN_REFRESH_MAX_RETRIES = 3;
9047
+ /** Base delay in milliseconds for exponential backoff on refresh retry. */
9048
+ const DEVICE_TOKEN_REFRESH_RETRY_BASE_MS = 1e3;
9049
+ /** Maximum retry attempts for developer credential refresh on transient failure. */
9050
+ const CREDENTIAL_REFRESH_MAX_RETRIES = 5;
9051
+ /** Base delay in milliseconds for exponential backoff on credential refresh retry. */
9052
+ const CREDENTIAL_REFRESH_RETRY_BASE_MS = 1e3;
9053
+ /** Maximum delay in milliseconds for credential refresh backoff. */
9054
+ const CREDENTIAL_REFRESH_MAX_DELAY_MS = 3e4;
9055
+ /** Buffer in milliseconds before token expiry to trigger refresh. */
9056
+ const CREDENTIAL_REFRESH_BUFFER_MS = 5e3;
9057
+ /**
9058
+ * Maximum time the coordinator will wait for `DeviceTokenManager.activate()`
9059
+ * to resolve before treating the activation as failed and falling back to
9060
+ * the developer-provided refresh path. Prevents a wedged HTTP layer from
9061
+ * leaving the session with no active refresh mechanism.
9062
+ */
9063
+ const CREDENTIAL_ACTIVATE_TIMEOUT_MS = 3e4;
9064
+ /** JSON-RPC error code for requester validation failure (corrupted auth state). */
9065
+ const RPC_ERROR_REQUESTER_VALIDATION_FAILED = -32003;
9066
+ /** JSON-RPC error code for invalid params (e.g., missing authentication block). */
9067
+ const RPC_ERROR_INVALID_PARAMS = -32602;
9068
+ /** JSON-RPC error code for authentication failure (invalid token, missing DPoP, etc.). */
9069
+ const RPC_ERROR_AUTHENTICATION_FAILED = -32002;
9070
+ /** Error names browsers use for a media permission denial (user or policy). */
9071
+ const MEDIA_ACCESS_DENIAL_NAMES = [
9072
+ "NotAllowedError",
9073
+ "SecurityError",
9074
+ "PermissionDeniedError"
9075
+ ];
9076
+ /** Default polling interval for RTCPeerConnection.getStats() in milliseconds. */
9077
+ const DEFAULT_STATS_POLLING_INTERVAL_MS = 1e3;
9078
+ /** Number of initial samples used to build a baseline for spike detection. */
9079
+ const DEFAULT_STATS_BASELINE_SAMPLES = 10;
9080
+ /** Duration in ms with no inbound audio packets before emitting a critical issue. */
9081
+ const DEFAULT_STATS_NO_PACKET_THRESHOLD_MS = 2e3;
9082
+ /** Multiplier applied to baseline RTT to detect a warning-level RTT spike. */
9083
+ const DEFAULT_STATS_RTT_SPIKE_MULTIPLIER = 3;
9084
+ /** Packet loss fraction (0-1) above which a warning is emitted. */
9085
+ const DEFAULT_STATS_PACKET_LOSS_THRESHOLD = .05;
9086
+ /** Multiplier applied to baseline jitter to detect a jitter spike. */
9087
+ const DEFAULT_STATS_JITTER_SPIKE_MULTIPLIER = 4;
9088
+ /** Number of seconds of metrics history to retain. */
9089
+ const DEFAULT_STATS_HISTORY_SIZE = 30;
9090
+ /** Maximum keyframe requests allowed within a single burst window. */
9091
+ const DEFAULT_KEYFRAME_MAX_BURST$1 = 3;
9092
+ /** Duration of the keyframe burst window in milliseconds. */
9093
+ const DEFAULT_KEYFRAME_BURST_WINDOW_MS$1 = 3e3;
9094
+ /** Cooldown period in ms after burst limit is reached before allowing more keyframes. */
9095
+ const DEFAULT_KEYFRAME_COOLDOWN_MS$1 = 1e4;
9096
+ /** Minimum time between re-INVITE attempts in milliseconds. */
9097
+ const DEFAULT_REINVITE_DEBOUNCE_TIME_MS = 1e4;
9098
+ /** Maximum number of re-INVITE attempts per call. */
9099
+ const DEFAULT_REINVITE_MAX_ATTEMPTS = 3;
9100
+ /** Timeout for a single re-INVITE attempt in milliseconds. */
9101
+ const DEFAULT_REINVITE_TIMEOUT_MS = 5e3;
9102
+ /** Debounce window in ms to collapse multiple detection signals into one trigger. */
9103
+ const DEFAULT_RECOVERY_DEBOUNCE_TIME_MS = 2e3;
9104
+ /** Cooldown period in ms between recovery attempts. */
9105
+ const DEFAULT_RECOVERY_COOLDOWN_MS = 1e4;
9106
+ /** Grace period in ms before treating ICE 'disconnected' as a failure. */
9107
+ const DEFAULT_ICE_DISCONNECTED_GRACE_PERIOD_MS = 3e3;
9108
+ /** Timeout for a single ICE restart attempt in milliseconds. */
9109
+ const DEFAULT_ICE_RESTART_TIMEOUT_MS$1 = 5e3;
9110
+ /** Maximum recovery attempts before emitting 'max_attempts_reached'. */
9111
+ const DEFAULT_MAX_RECOVERY_ATTEMPTS = 3;
9112
+ /** Upper bound in ms for waiting on iceGatheringState === 'complete' after an ICE restart. */
9113
+ const ICE_GATHERING_COMPLETE_TIMEOUT_MS = 1e4;
9114
+ /** Upper bound in ms for waiting on RTCPeerConnection.connectionState === 'connected' after a recovery ICE restart. */
9115
+ const PEER_CONNECTION_RECOVERY_WAIT_MS = 5e3;
9116
+ /** Polling interval in ms while waiting for RTCPeerConnection.connectionState to transition. */
9117
+ const PEER_CONNECTION_RECOVERY_POLL_MS = 100;
9118
+ /** Polling interval for LocalAudioPipeline.level$ (ms). ~30fps is smooth for meters. */
9119
+ const AUDIO_LEVEL_POLL_INTERVAL_MS = 33;
9120
+ /** RMS level threshold (0..1) above which the local participant is considered speaking. */
9121
+ const VAD_THRESHOLD = .03;
9122
+ /** Hold window in ms below the threshold before speaking$ flips back to false. */
9123
+ const VAD_HOLD_MS = 250;
9124
+ /** Whether to persist device selections to storage by default. */
9125
+ const DEFAULT_PERSIST_DEVICE_SELECTION = true;
9126
+ /** Whether to auto-apply device changes to active calls by default. */
9127
+ const DEFAULT_SYNC_DEVICES_TO_ACTIVE_CALLS = true;
9128
+ /** Storage keys for persisted device selections. */
9129
+ const DEVICE_STORAGE_KEY_AUDIO_INPUT = "sw:device:audioinput";
9130
+ const DEVICE_STORAGE_KEY_AUDIO_OUTPUT = "sw:device:audiooutput";
9131
+ const DEVICE_STORAGE_KEY_VIDEO_INPUT = "sw:device:videoinput";
9132
+ /** Whether to auto-mute video when the tab becomes hidden. */
9133
+ const DEFAULT_AUTO_MUTE_VIDEO_ON_HIDDEN = false;
9134
+ /** Whether to re-enumerate devices when the page becomes visible. */
9135
+ const DEFAULT_REFRESH_DEVICES_ON_VISIBLE = true;
9136
+ /** Whether to check peer connection health when the page becomes visible. */
9137
+ const DEFAULT_CHECK_CONNECTION_ON_VISIBLE = true;
9138
+ /** Whether automatic video degradation on low bandwidth is enabled. */
9139
+ const DEFAULT_ENABLE_AUTO_DEGRADATION = true;
9140
+ /** Bitrate in kbps below which video is automatically disabled. */
9141
+ const DEFAULT_DEGRADATION_BITRATE_THRESHOLD_KBPS = 150;
9142
+ /** Bitrate in kbps above which video is automatically re-enabled (hysteresis). */
9143
+ const DEFAULT_DEGRADATION_RECOVERY_THRESHOLD_KBPS = 300;
9144
+ /** Whether relay-only escalation is enabled as a last-resort recovery tier. */
9145
+ const DEFAULT_ENABLE_RELAY_FALLBACK = true;
9146
+ /** Whether to listen for browser online/offline/connection events. */
9147
+ const DEFAULT_ENABLE_NETWORK_CHANGE_DETECTION = true;
9148
+ /** Whether to intercept server-sent media-timeout hangups and attempt recovery. */
9149
+ const DEFAULT_ENABLE_SERVER_HANGUP_INTERCEPTION = true;
9150
+ /** Default video track constraints applied when video is enabled without explicit constraints. */
9151
+ const DEFAULT_VIDEO_CONSTRAINTS = {
9152
+ width: { ideal: 1280 },
9153
+ height: { ideal: 720 },
9154
+ aspectRatio: 16 / 9
9155
+ };
9156
+ /** Whether stereo Opus is enabled by default. */
9157
+ const DEFAULT_STEREO_AUDIO = false;
9158
+ /** Max average bitrate for stereo Opus in bits per second. */
9159
+ const DEFAULT_STEREO_MAX_AVERAGE_BITRATE = 51e4;
9160
+
9024
9161
  //#endregion
9025
9162
  //#region src/core/errors.ts
9026
9163
  var UnexpectedError = class extends Error {
@@ -9230,6 +9367,33 @@ var MediaTrackError = class extends Error {
9230
9367
  this.name = "MediaTrackError";
9231
9368
  }
9232
9369
  };
9370
+ /** True when a `getUserMedia`/`getDisplayMedia` rejection is a permission denial. */
9371
+ function isMediaAccessDenial(originalError) {
9372
+ return originalError instanceof Error && MEDIA_ACCESS_DENIAL_NAMES.includes(originalError.name);
9373
+ }
9374
+ /**
9375
+ * Failure to acquire local media (camera, microphone, or screen capture)
9376
+ * via `getUserMedia`/`getDisplayMedia`.
9377
+ *
9378
+ * Non-fatal by default: screenshare and additional-device failures never
9379
+ * end the call, and main-connection failures degrade to receive-only when
9380
+ * possible. The wrapping site sets `fatal` to `true` only when the call
9381
+ * cannot continue (receive-only fallback disabled or no receive intent).
9382
+ */
9383
+ var MediaAccessError = class extends Error {
9384
+ constructor(operation, media, originalError, fatal = false) {
9385
+ super(`Media access ${isMediaAccessDenial(originalError) ? "denied" : "failed"} for ${operation} (${media})`, { cause: originalError instanceof Error ? originalError : void 0 });
9386
+ this.operation = operation;
9387
+ this.media = media;
9388
+ this.originalError = originalError;
9389
+ this.fatal = fatal;
9390
+ this.name = "MediaAccessError";
9391
+ }
9392
+ /** True when the underlying failure is a permission denial (user or policy). */
9393
+ get denied() {
9394
+ return isMediaAccessDenial(this.originalError);
9395
+ }
9396
+ };
9233
9397
  var DPoPInitError = class extends Error {
9234
9398
  constructor(originalError, message = "Failed to initialize DPoP key pair") {
9235
9399
  super(message, { cause: originalError instanceof Error ? originalError : void 0 });
@@ -9879,137 +10043,6 @@ var DeviceHistoryManager = class {
9879
10043
  }
9880
10044
  };
9881
10045
 
9882
- //#endregion
9883
- //#region src/core/constants.ts
9884
- const INVITE_VERSION = 1e3;
9885
- const DEFAULT_ICE_CANDIDATE_TIMEOUT_MS = 600;
9886
- const DEFAULT_ICE_GATHERING_TIMEOUT_MS = 6e3;
9887
- const DEFAULT_RECONNECT_CALLS_TIMEOUT_MS = 300 * 1e3;
9888
- const DEFAULT_CONNECTION_TIMEOUT_MS = 1e4;
9889
- const DEFAULT_RECONNECT_DELAY_MIN_MS = 100;
9890
- const DEFAULT_RECONNECT_DELAY_MAX_MS = 3e3;
9891
- const DEFAULT_DEVICE_DEBOUNCE_TIME_MS = 1500;
9892
- const DEFAULT_DEVICE_POLLING_INTERVAL_MS = 0;
9893
- const PREFERENCES_STORAGE_KEY = "sw:preferences";
9894
- /** Scope value that enables automatic token refresh. */
9895
- const SAT_REFRESH_SCOPE = "sat:refresh";
9896
- /** API endpoints for device token operations. */
9897
- const DEVICE_TOKEN_ENDPOINT = "/api/fabric/subscriber/devices/token";
9898
- const DEVICE_REFRESH_ENDPOINT = "/api/fabric/subscriber/devices/refresh";
9899
- /** Default device token TTL in seconds (15 minutes). */
9900
- const DEVICE_TOKEN_DEFAULT_EXPIRE_IN = 900;
9901
- /** Buffer time in milliseconds before expiry to trigger refresh. */
9902
- const DEVICE_TOKEN_REFRESH_BUFFER_MS = 3e4;
9903
- /** Maximum retry attempts for device token refresh on transient failure. */
9904
- const DEVICE_TOKEN_REFRESH_MAX_RETRIES = 3;
9905
- /** Base delay in milliseconds for exponential backoff on refresh retry. */
9906
- const DEVICE_TOKEN_REFRESH_RETRY_BASE_MS = 1e3;
9907
- /** Maximum retry attempts for developer credential refresh on transient failure. */
9908
- const CREDENTIAL_REFRESH_MAX_RETRIES = 5;
9909
- /** Base delay in milliseconds for exponential backoff on credential refresh retry. */
9910
- const CREDENTIAL_REFRESH_RETRY_BASE_MS = 1e3;
9911
- /** Maximum delay in milliseconds for credential refresh backoff. */
9912
- const CREDENTIAL_REFRESH_MAX_DELAY_MS = 3e4;
9913
- /** Buffer in milliseconds before token expiry to trigger refresh. */
9914
- const CREDENTIAL_REFRESH_BUFFER_MS = 5e3;
9915
- /**
9916
- * Maximum time the coordinator will wait for `DeviceTokenManager.activate()`
9917
- * to resolve before treating the activation as failed and falling back to
9918
- * the developer-provided refresh path. Prevents a wedged HTTP layer from
9919
- * leaving the session with no active refresh mechanism.
9920
- */
9921
- const CREDENTIAL_ACTIVATE_TIMEOUT_MS = 3e4;
9922
- /** JSON-RPC error code for requester validation failure (corrupted auth state). */
9923
- const RPC_ERROR_REQUESTER_VALIDATION_FAILED = -32003;
9924
- /** JSON-RPC error code for invalid params (e.g., missing authentication block). */
9925
- const RPC_ERROR_INVALID_PARAMS = -32602;
9926
- /** JSON-RPC error code for authentication failure (invalid token, missing DPoP, etc.). */
9927
- const RPC_ERROR_AUTHENTICATION_FAILED = -32002;
9928
- /** Default polling interval for RTCPeerConnection.getStats() in milliseconds. */
9929
- const DEFAULT_STATS_POLLING_INTERVAL_MS = 1e3;
9930
- /** Number of initial samples used to build a baseline for spike detection. */
9931
- const DEFAULT_STATS_BASELINE_SAMPLES = 10;
9932
- /** Duration in ms with no inbound audio packets before emitting a critical issue. */
9933
- const DEFAULT_STATS_NO_PACKET_THRESHOLD_MS = 2e3;
9934
- /** Multiplier applied to baseline RTT to detect a warning-level RTT spike. */
9935
- const DEFAULT_STATS_RTT_SPIKE_MULTIPLIER = 3;
9936
- /** Packet loss fraction (0-1) above which a warning is emitted. */
9937
- const DEFAULT_STATS_PACKET_LOSS_THRESHOLD = .05;
9938
- /** Multiplier applied to baseline jitter to detect a jitter spike. */
9939
- const DEFAULT_STATS_JITTER_SPIKE_MULTIPLIER = 4;
9940
- /** Number of seconds of metrics history to retain. */
9941
- const DEFAULT_STATS_HISTORY_SIZE = 30;
9942
- /** Maximum keyframe requests allowed within a single burst window. */
9943
- const DEFAULT_KEYFRAME_MAX_BURST$1 = 3;
9944
- /** Duration of the keyframe burst window in milliseconds. */
9945
- const DEFAULT_KEYFRAME_BURST_WINDOW_MS$1 = 3e3;
9946
- /** Cooldown period in ms after burst limit is reached before allowing more keyframes. */
9947
- const DEFAULT_KEYFRAME_COOLDOWN_MS$1 = 1e4;
9948
- /** Minimum time between re-INVITE attempts in milliseconds. */
9949
- const DEFAULT_REINVITE_DEBOUNCE_TIME_MS = 1e4;
9950
- /** Maximum number of re-INVITE attempts per call. */
9951
- const DEFAULT_REINVITE_MAX_ATTEMPTS = 3;
9952
- /** Timeout for a single re-INVITE attempt in milliseconds. */
9953
- const DEFAULT_REINVITE_TIMEOUT_MS = 5e3;
9954
- /** Debounce window in ms to collapse multiple detection signals into one trigger. */
9955
- const DEFAULT_RECOVERY_DEBOUNCE_TIME_MS = 2e3;
9956
- /** Cooldown period in ms between recovery attempts. */
9957
- const DEFAULT_RECOVERY_COOLDOWN_MS = 1e4;
9958
- /** Grace period in ms before treating ICE 'disconnected' as a failure. */
9959
- const DEFAULT_ICE_DISCONNECTED_GRACE_PERIOD_MS = 3e3;
9960
- /** Timeout for a single ICE restart attempt in milliseconds. */
9961
- const DEFAULT_ICE_RESTART_TIMEOUT_MS$1 = 5e3;
9962
- /** Maximum recovery attempts before emitting 'max_attempts_reached'. */
9963
- const DEFAULT_MAX_RECOVERY_ATTEMPTS = 3;
9964
- /** Upper bound in ms for waiting on iceGatheringState === 'complete' after an ICE restart. */
9965
- const ICE_GATHERING_COMPLETE_TIMEOUT_MS = 1e4;
9966
- /** Upper bound in ms for waiting on RTCPeerConnection.connectionState === 'connected' after a recovery ICE restart. */
9967
- const PEER_CONNECTION_RECOVERY_WAIT_MS = 5e3;
9968
- /** Polling interval in ms while waiting for RTCPeerConnection.connectionState to transition. */
9969
- const PEER_CONNECTION_RECOVERY_POLL_MS = 100;
9970
- /** Polling interval for LocalAudioPipeline.level$ (ms). ~30fps is smooth for meters. */
9971
- const AUDIO_LEVEL_POLL_INTERVAL_MS = 33;
9972
- /** RMS level threshold (0..1) above which the local participant is considered speaking. */
9973
- const VAD_THRESHOLD = .03;
9974
- /** Hold window in ms below the threshold before speaking$ flips back to false. */
9975
- const VAD_HOLD_MS = 250;
9976
- /** Whether to persist device selections to storage by default. */
9977
- const DEFAULT_PERSIST_DEVICE_SELECTION = true;
9978
- /** Whether to auto-apply device changes to active calls by default. */
9979
- const DEFAULT_SYNC_DEVICES_TO_ACTIVE_CALLS = true;
9980
- /** Storage keys for persisted device selections. */
9981
- const DEVICE_STORAGE_KEY_AUDIO_INPUT = "sw:device:audioinput";
9982
- const DEVICE_STORAGE_KEY_AUDIO_OUTPUT = "sw:device:audiooutput";
9983
- const DEVICE_STORAGE_KEY_VIDEO_INPUT = "sw:device:videoinput";
9984
- /** Whether to auto-mute video when the tab becomes hidden. */
9985
- const DEFAULT_AUTO_MUTE_VIDEO_ON_HIDDEN = false;
9986
- /** Whether to re-enumerate devices when the page becomes visible. */
9987
- const DEFAULT_REFRESH_DEVICES_ON_VISIBLE = true;
9988
- /** Whether to check peer connection health when the page becomes visible. */
9989
- const DEFAULT_CHECK_CONNECTION_ON_VISIBLE = true;
9990
- /** Whether automatic video degradation on low bandwidth is enabled. */
9991
- const DEFAULT_ENABLE_AUTO_DEGRADATION = true;
9992
- /** Bitrate in kbps below which video is automatically disabled. */
9993
- const DEFAULT_DEGRADATION_BITRATE_THRESHOLD_KBPS = 150;
9994
- /** Bitrate in kbps above which video is automatically re-enabled (hysteresis). */
9995
- const DEFAULT_DEGRADATION_RECOVERY_THRESHOLD_KBPS = 300;
9996
- /** Whether relay-only escalation is enabled as a last-resort recovery tier. */
9997
- const DEFAULT_ENABLE_RELAY_FALLBACK = true;
9998
- /** Whether to listen for browser online/offline/connection events. */
9999
- const DEFAULT_ENABLE_NETWORK_CHANGE_DETECTION = true;
10000
- /** Whether to intercept server-sent media-timeout hangups and attempt recovery. */
10001
- const DEFAULT_ENABLE_SERVER_HANGUP_INTERCEPTION = true;
10002
- /** Default video track constraints applied when video is enabled without explicit constraints. */
10003
- const DEFAULT_VIDEO_CONSTRAINTS = {
10004
- width: { ideal: 1280 },
10005
- height: { ideal: 720 },
10006
- aspectRatio: 16 / 9
10007
- };
10008
- /** Whether stereo Opus is enabled by default. */
10009
- const DEFAULT_STEREO_AUDIO = false;
10010
- /** Max average bitrate for stereo Opus in bits per second. */
10011
- const DEFAULT_STEREO_MAX_AVERAGE_BITRATE = 51e4;
10012
-
10013
10046
  //#endregion
10014
10047
  //#region src/utils/time.ts
10015
10048
  function fromSecToMs(seconds) {
@@ -13373,7 +13406,9 @@ const DEFAULT_MEMBER_CAPABILITIES = {
13373
13406
  position: false,
13374
13407
  meta: false,
13375
13408
  remove: false,
13376
- audioFlags: false
13409
+ audioFlags: false,
13410
+ denoise: false,
13411
+ lowbitrate: false
13377
13412
  };
13378
13413
  /**
13379
13414
  * Default call capabilities with no permissions
@@ -13446,7 +13481,9 @@ function computeMemberCapabilities(flags, memberType) {
13446
13481
  position: hasBooleanCapability(flags, memberType, null, "position"),
13447
13482
  meta: hasBooleanCapability(flags, memberType, null, "meta"),
13448
13483
  remove: hasBooleanCapability(flags, memberType, null, "remove"),
13449
- audioFlags: hasBooleanCapability(flags, memberType, null, "audioflags")
13484
+ audioFlags: hasBooleanCapability(flags, memberType, null, "audioflags"),
13485
+ denoise: hasBooleanCapability(flags, memberType, null, "denoise"),
13486
+ lowbitrate: hasBooleanCapability(flags, memberType, null, "lowbitrate")
13450
13487
  };
13451
13488
  }
13452
13489
  /**
@@ -13829,6 +13866,10 @@ var Participant = class extends Destroyable {
13829
13866
  get nodeId() {
13830
13867
  return this._state$.value.node_id;
13831
13868
  }
13869
+ /** Call ID for this participant's leg, or `undefined` if not available. */
13870
+ get callId() {
13871
+ return this._state$.value.call_id;
13872
+ }
13832
13873
  /** @internal */
13833
13874
  get value() {
13834
13875
  return this._state$.value;
@@ -13890,8 +13931,9 @@ var Participant = class extends Destroyable {
13890
13931
  noise_suppression: !this.noiseSuppression
13891
13932
  });
13892
13933
  }
13934
+ /** Toggles low-bitrate mode for this participant's media. */
13893
13935
  async toggleLowbitrate() {
13894
- throw new UnimplementedError();
13936
+ await this.executeMethod(this.id, "call.lowbitrate.set", { lowbitrate: !this.lowbitrate });
13895
13937
  }
13896
13938
  /**
13897
13939
  * Adjusts the **conference-only** microphone energy gate / sensitivity level
@@ -13938,10 +13980,27 @@ var Participant = class extends Destroyable {
13938
13980
  }
13939
13981
  /**
13940
13982
  * Sets the participant's position in the video layout.
13983
+ *
13984
+ * Requires the `member.position` capability. The gateway keys positions by the
13985
+ * **target member's own** `call_id`/`node_id` (see issue #19400 and the legacy
13986
+ * `setPositions` implementation), so this sends the participant's own call
13987
+ * context — matching {@link Participant.remove}. A resolved promise does not
13988
+ * guarantee a visible change: the backend silently returns `200` (no-op) for
13989
+ * non-conference targets.
13990
+ *
13941
13991
  * @param value - The {@link VideoPosition} to assign (e.g. `'auto'`, `'reserved-0'`).
13942
13992
  */
13943
13993
  async setPosition(value) {
13944
- await this.executeMethod(this.id, "call.member.position.set", { position: value });
13994
+ const state = this._state$.value;
13995
+ const target = {
13996
+ member_id: this.id,
13997
+ call_id: state.call_id ?? "",
13998
+ node_id: state.node_id ?? ""
13999
+ };
14000
+ await this.executeMethod(target, "call.member.position.set", { targets: [{
14001
+ target,
14002
+ position: value
14003
+ }] });
13945
14004
  }
13946
14005
  /** Removes this participant from the call. */
13947
14006
  async remove() {
@@ -14031,12 +14090,21 @@ var SelfParticipant = class extends Participant {
14031
14090
  noise_suppression: true
14032
14091
  });
14033
14092
  }
14034
- /** Starts sharing the local screen. */
14093
+ /**
14094
+ * Starts sharing the local screen.
14095
+ *
14096
+ * The call is unaffected when acquisition fails.
14097
+ *
14098
+ * @throws The raw `getDisplayMedia` error. A dismissed picker or a
14099
+ * permission denial rejects with a `NotAllowedError` `DOMException` —
14100
+ * inspect `error.name` to tell benign cancels apart from real failures.
14101
+ */
14035
14102
  async startScreenShare() {
14036
14103
  try {
14037
14104
  await this.vertoManager.addScreenMedia();
14038
14105
  } catch (error) {
14039
14106
  logger$24.error("[Participant.startScreenShare] Screen share error:", error);
14107
+ throw error;
14040
14108
  }
14041
14109
  }
14042
14110
  /** Observable of the current screen share status. */
@@ -14051,12 +14119,20 @@ var SelfParticipant = class extends Participant {
14051
14119
  async stopScreenShare() {
14052
14120
  return this.vertoManager.removeScreenMedia();
14053
14121
  }
14054
- /** Adds an additional media input device to the call. */
14122
+ /**
14123
+ * Adds an additional media input device to the call.
14124
+ *
14125
+ * The call is unaffected when acquisition fails.
14126
+ *
14127
+ * @throws The raw `getUserMedia` error (e.g. `NotAllowedError` on
14128
+ * permission denial) — inspect `error.name` to decide how to react.
14129
+ */
14055
14130
  async addAdditionalDevice(options) {
14056
14131
  try {
14057
14132
  await this.vertoManager.addInputDevice(options);
14058
14133
  } catch (error) {
14059
- logger$24.error("[Participant.startScreenShare] Screen share error:", error);
14134
+ logger$24.error("[Participant.addAdditionalDevice] Additional device error:", error);
14135
+ throw error;
14060
14136
  }
14061
14137
  }
14062
14138
  /** Removes an additional media input device by ID. */
@@ -15688,7 +15764,7 @@ var RTCPeerConnectionController = class extends Destroyable {
15688
15764
  logger$17.debug(`[RTCPeerConnectionController] ${kind} input device selected:`, deviceInfo?.label);
15689
15765
  } catch (error) {
15690
15766
  logger$17.error(`[RTCPeerConnectionController] Failed to select ${kind} input device:`, error);
15691
- this._errors$.next(toError(error));
15767
+ this._errors$.next(new MediaTrackError("updateSelectedInputDevice", kind, error));
15692
15768
  throw error;
15693
15769
  }
15694
15770
  };
@@ -15910,7 +15986,7 @@ var RTCPeerConnectionController = class extends Destroyable {
15910
15986
  case "main":
15911
15987
  default: return {
15912
15988
  ...options,
15913
- offerToReceiveAudio: true,
15989
+ offerToReceiveAudio: this.options.receiveAudio ?? true,
15914
15990
  offerToReceiveVideo: this.options.receiveVideo ?? Boolean(this.inputVideoDeviceConstraints)
15915
15991
  };
15916
15992
  }
@@ -16064,13 +16140,14 @@ var RTCPeerConnectionController = class extends Destroyable {
16064
16140
  */
16065
16141
  async acceptInbound(mediaOverrides) {
16066
16142
  if (mediaOverrides) {
16067
- const { audio, video, receiveAudio, receiveVideo } = mediaOverrides;
16143
+ const { audio, video, receiveAudio, receiveVideo, fallbackToReceiveOnly } = mediaOverrides;
16068
16144
  this.options = {
16069
16145
  ...this.options,
16070
16146
  ...audio !== void 0 ? { audio } : {},
16071
16147
  ...video !== void 0 ? { video } : {},
16072
16148
  ...receiveAudio !== void 0 ? { receiveAudio } : {},
16073
- ...receiveVideo !== void 0 ? { receiveVideo } : {}
16149
+ ...receiveVideo !== void 0 ? { receiveVideo } : {},
16150
+ ...fallbackToReceiveOnly !== void 0 ? { fallbackToReceiveOnly } : {}
16074
16151
  };
16075
16152
  this.transceiverController?.updateOptions({
16076
16153
  receiveAudio: this.receiveAudio,
@@ -16227,7 +16304,19 @@ var RTCPeerConnectionController = class extends Destroyable {
16227
16304
  }
16228
16305
  async setupLocalTracks() {
16229
16306
  logger$17.debug("[RTCPeerConnectionController] Setting up local tracks/transceivers.");
16230
- const localStream = this.localStream ?? await this.localStreamController.buildLocalStream();
16307
+ if (this.hasNoLocalMediaToSend()) {
16308
+ if (!this.receiveAudio && !this.receiveVideo) throw new InvalidParams("Call requests no media: enable audio/video or receiveAudio/receiveVideo");
16309
+ logger$17.debug("[RTCPeerConnectionController] No local media requested; negotiating receive-only.");
16310
+ this.setupReceiveOnlyTransceivers();
16311
+ return;
16312
+ }
16313
+ let localStream;
16314
+ try {
16315
+ localStream = this.localStream ?? await this.localStreamController.buildLocalStream();
16316
+ } catch (error) {
16317
+ this.handleLocalMediaFailure(error);
16318
+ return;
16319
+ }
16231
16320
  if (this.transceiverController?.useAddStream ?? false) {
16232
16321
  logger$17.warn("[RTCPeerConnectionController] Using deprecated addStream API to add local stream.");
16233
16322
  this.peerConnection?.addStream(localStream);
@@ -16254,6 +16343,48 @@ var RTCPeerConnectionController = class extends Destroyable {
16254
16343
  }
16255
16344
  }
16256
16345
  }
16346
+ /** True for a main connection with no local media to send. */
16347
+ hasNoLocalMediaToSend() {
16348
+ const hasInputStreams = Boolean(this.options.inputAudioStream ?? this.options.inputVideoStream);
16349
+ return this.propose === "main" && !this.localStream && !hasInputStreams && !this.inputAudioDeviceConstraints && !this.inputVideoDeviceConstraints;
16350
+ }
16351
+ /** The media kinds this connection wants to send: 'audiovideo' | 'video' | 'audio'. */
16352
+ get requestedMediaKinds() {
16353
+ const wantsAudio = Boolean(this.inputAudioDeviceConstraints);
16354
+ const wantsVideo = Boolean(this.inputVideoDeviceConstraints);
16355
+ if (wantsAudio && wantsVideo) return "audiovideo";
16356
+ return wantsVideo ? "video" : "audio";
16357
+ }
16358
+ /**
16359
+ * Handle a local media acquisition failure with a typed, semantically
16360
+ * accurate MediaAccessError created at the acquisition site:
16361
+ * - Auxiliary connections (screenshare / additional-device) throw a
16362
+ * non-fatal error — VertoManager surfaces it and the call is unaffected.
16363
+ * - The main connection degrades to receive-only when allowed (default),
16364
+ * otherwise fails with a fatal error.
16365
+ */
16366
+ handleLocalMediaFailure(error) {
16367
+ if (this.propose === "screenshare") throw new MediaAccessError("startScreenShare", "screen", error, false);
16368
+ if (this.propose === "additional-device") throw new MediaAccessError("addInputDevice", this.requestedMediaKinds, error, false);
16369
+ const canReceive = this.receiveAudio || this.receiveVideo;
16370
+ if (!((this.options.fallbackToReceiveOnly ?? true) && canReceive)) throw new MediaAccessError("acquireLocalMedia", this.requestedMediaKinds, error, true);
16371
+ logger$17.warn("[RTCPeerConnectionController] Local media unavailable; continuing receive-only:", error);
16372
+ this._errors$.next(new MediaAccessError("acquireLocalMedia", this.requestedMediaKinds, error, false));
16373
+ this.setupReceiveOnlyTransceivers();
16374
+ }
16375
+ /**
16376
+ * Negotiate receive-only m-lines when there are no local tracks to send.
16377
+ * Only offer-type connections add transceivers — answer-type connections
16378
+ * reuse the transceivers created from the remote offer.
16379
+ */
16380
+ setupReceiveOnlyTransceivers() {
16381
+ if (this.type !== "offer") return;
16382
+ if (this.transceiverController?.useAddTransceivers ?? false) {
16383
+ this.peerConnection?.addTransceiver("audio", { direction: this.receiveAudio ? "recvonly" : "inactive" });
16384
+ this.peerConnection?.addTransceiver("video", { direction: this.receiveVideo ? "recvonly" : "inactive" });
16385
+ }
16386
+ if (!this.isNegotiating) this.negotiationNeeded$.next();
16387
+ }
16257
16388
  async getUserMedia(constraints) {
16258
16389
  return (this.options.webRTCApiProvider?.mediaDevices ?? navigator.mediaDevices).getUserMedia(constraints);
16259
16390
  }
@@ -16290,7 +16421,7 @@ var RTCPeerConnectionController = class extends Destroyable {
16290
16421
  stream = await this.getUserMedia({ audio: constraints });
16291
16422
  } catch (error) {
16292
16423
  logger$17.error("[RTCPeerConnectionController] Failed to re-acquire mic for pipeline restore:", error);
16293
- this._errors$.next(toError(error));
16424
+ this._errors$.next(new MediaTrackError("restoreAudioPipelineInput", "audio", error));
16294
16425
  return;
16295
16426
  }
16296
16427
  const newTrack = stream.getAudioTracks().at(0);
@@ -16352,7 +16483,7 @@ var RTCPeerConnectionController = class extends Destroyable {
16352
16483
  logger$17.debug(`[RTCPeerConnectionController] ${track.kind} track added:`, track.id);
16353
16484
  } catch (error) {
16354
16485
  logger$17.error(`[RTCPeerConnectionController] Failed to add ${track.kind} track:`, error);
16355
- this._errors$.next(toError(error));
16486
+ this._errors$.next(new MediaTrackError("addLocalTrack", track.kind, error));
16356
16487
  throw error;
16357
16488
  }
16358
16489
  }
@@ -16377,7 +16508,7 @@ var RTCPeerConnectionController = class extends Destroyable {
16377
16508
  logger$17.debug(`[RTCPeerConnectionController] ${sender.track?.kind} track removed:`, trackId);
16378
16509
  } catch (error) {
16379
16510
  logger$17.error(`[RTCPeerConnectionController] Failed to remove ${sender.track?.kind} track:`, error);
16380
- this._errors$.next(toError(error));
16511
+ this._errors$.next(new MediaTrackError("removeLocalTrack", sender.track?.kind ?? "unknown", error));
16381
16512
  throw error;
16382
16513
  }
16383
16514
  }
@@ -16487,7 +16618,11 @@ function isVertoInviteMessage(value) {
16487
16618
  const msg = value;
16488
16619
  return msg.method === "verto.invite" && isObject(msg.params) && hasProperty(msg.params, "sdp") && hasProperty(msg.params, "callID");
16489
16620
  }
16490
- function isVertoByeMessage(value) {
16621
+ /**
16622
+ * Guards inbound `verto.bye` frames (server → client). Outbound bye messages are
16623
+ * built locally and never type-guarded, so only the inbound shape is asserted.
16624
+ */
16625
+ function isVertoByeInboundMessage(value) {
16491
16626
  if (!isVertoMethodMessage(value)) return false;
16492
16627
  return value.method === "verto.bye";
16493
16628
  }
@@ -16507,6 +16642,14 @@ function isVertoMediaParamsInnerParams(value) {
16507
16642
  function isVertoPingInnerParams(value) {
16508
16643
  return isObject(value) && hasProperty(value, "jsonrpc") && value.jsonrpc === "2.0" && hasProperty(value, "method") && value.method === "verto.ping";
16509
16644
  }
16645
+ /**
16646
+ * Guards the params of an inbound `verto.bye` frame (top-level `callID`). Use
16647
+ * this to recognise the bye payload extracted by `vertoBye$`, e.g. when racing
16648
+ * it against a boolean answer/reject signal.
16649
+ */
16650
+ function isVertoByeInboundParamsGuard(value) {
16651
+ return isObject(value) && hasProperty(value, "callID") && typeof value.callID === "string";
16652
+ }
16510
16653
 
16511
16654
  //#endregion
16512
16655
  //#region src/managers/VertoManager.ts
@@ -16845,7 +16988,7 @@ var WebRTCVertoManager = class extends VertoManager {
16845
16988
  return this.cachedObservable("vertoMediaParams$", () => this.webRtcCallSession.webrtcMessages$.pipe(filterAs(isVertoMediaParamsInnerParams, "params"), (0, import_cjs$15.takeUntil)(this.destroyed$)));
16846
16989
  }
16847
16990
  get vertoBye$() {
16848
- return this.cachedObservable("vertoBye$", () => this.webRtcCallSession.webrtcMessages$.pipe(filterAs(isVertoByeMessage, "params"), (0, import_cjs$15.takeUntil)(this.destroyed$)));
16991
+ return this.cachedObservable("vertoBye$", () => this.webRtcCallSession.webrtcMessages$.pipe(filterAs(isVertoByeInboundMessage, "params"), (0, import_cjs$15.takeUntil)(this.destroyed$)));
16849
16992
  }
16850
16993
  get vertoAttach$() {
16851
16994
  return this.cachedObservable("vertoAttach$", () => this.webRtcCallSession.webrtcMessages$.pipe(filterAs(isVertoAttachMessage, "params"), (0, import_cjs$15.takeUntil)(this.destroyed$)));
@@ -16974,6 +17117,7 @@ var WebRTCVertoManager = class extends VertoManager {
16974
17117
  inputVideoStream: options.inputVideoStream,
16975
17118
  receiveAudio: options.receiveAudio,
16976
17119
  receiveVideo: options.receiveVideo,
17120
+ fallbackToReceiveOnly: options.fallbackToReceiveOnly,
16977
17121
  webRTCApiProvider: this.webRTCApiProvider,
16978
17122
  preferredVideoCodecs: options.preferredVideoCodecs,
16979
17123
  preferredAudioCodecs: options.preferredAudioCodecs,
@@ -16998,7 +17142,7 @@ var WebRTCVertoManager = class extends VertoManager {
16998
17142
  logger$16.debug("[WebRTCManager] Inbound answer handler aborted (destroyed).");
16999
17143
  return;
17000
17144
  }
17001
- if (isVertoByeMessage(vertoByeOrAccepted)) {
17145
+ if (isVertoByeInboundParamsGuard(vertoByeOrAccepted)) {
17002
17146
  logger$16.info("[WebRTCManager] Inbound call ended by remote before answer.");
17003
17147
  this.callSession?.destroy();
17004
17148
  } else if (!vertoByeOrAccepted) {
@@ -17100,7 +17244,7 @@ var WebRTCVertoManager = class extends VertoManager {
17100
17244
  logger$16.debug("[WebRTCManager] Destroyed while waiting for call acceptance");
17101
17245
  return;
17102
17246
  }
17103
- if (isVertoByeMessage(vertoByeOrAccepted)) {
17247
+ if (isVertoByeInboundParamsGuard(vertoByeOrAccepted)) {
17104
17248
  logger$16.info("[WebRTCManager] Call ended before answer was sent.");
17105
17249
  this.callSession?.destroy();
17106
17250
  } else if (!vertoByeOrAccepted) {
@@ -17200,9 +17344,11 @@ var WebRTCVertoManager = class extends VertoManager {
17200
17344
  await this.initAdditionalPeerConnection("screenshare", options);
17201
17345
  }
17202
17346
  async initAdditionalPeerConnection(propose, options) {
17347
+ const isScreenShare = propose === "screenshare";
17348
+ let firstPeerConnectionError;
17203
17349
  let rtcPeerConnController = null;
17204
17350
  try {
17205
- this._screenShareStatus$.next("starting");
17351
+ if (isScreenShare) this._screenShareStatus$.next("starting");
17206
17352
  rtcPeerConnController = new RTCPeerConnectionController({
17207
17353
  ...options,
17208
17354
  ...this.RTCPeerConnectionConfig,
@@ -17210,21 +17356,27 @@ var WebRTCVertoManager = class extends VertoManager {
17210
17356
  webRTCApiProvider: this.webRTCApiProvider
17211
17357
  }, void 0, this.deviceController);
17212
17358
  this.setupLocalDescriptionHandler(rtcPeerConnController);
17213
- if (propose === "screenshare") this._screenShareId = rtcPeerConnController.id;
17359
+ if (isScreenShare) this._screenShareId = rtcPeerConnController.id;
17214
17360
  this._rtcPeerConnectionsMap.set(rtcPeerConnController.id, rtcPeerConnController);
17215
17361
  this._rtcPeerConnections$.next(Array.from(this._rtcPeerConnectionsMap.values()));
17216
17362
  this.subscribeTo(rtcPeerConnController.errors$, (error) => {
17217
- this.onError?.(error);
17363
+ firstPeerConnectionError ??= error;
17364
+ this.onError?.(error, { fatal: false });
17218
17365
  });
17219
17366
  await (0, import_cjs$15.firstValueFrom)(rtcPeerConnController.connectionState$.pipe((0, import_cjs$15.filter)((state) => state === "connected"), (0, import_cjs$15.take)(1), (0, import_cjs$15.timeout)(this._screenShareTimeoutMs), (0, import_cjs$15.takeUntil)(this.destroyed$)));
17220
- this._screenShareStatus$.next("started");
17221
- logger$16.info("[WebRTCManager] Screen share started successfully.");
17367
+ if (isScreenShare) this._screenShareStatus$.next("started");
17368
+ logger$16.info(`[WebRTCManager] Additional peer connection connected (${propose}).`);
17222
17369
  return rtcPeerConnController.id;
17223
17370
  } catch (error) {
17224
17371
  logger$16.warn("[WebRTCManager] Error initializing additional peer connection:", error);
17225
- this.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
17226
17372
  if (rtcPeerConnController) rtcPeerConnController.destroy();
17227
- this._screenShareStatus$.next("none");
17373
+ if (isScreenShare) this._screenShareStatus$.next("none");
17374
+ if (firstPeerConnectionError) throw firstPeerConnectionError instanceof MediaAccessError && firstPeerConnectionError.originalError instanceof Error ? firstPeerConnectionError.originalError : firstPeerConnectionError;
17375
+ if (error instanceof import_cjs$15.EmptyError) {
17376
+ logger$16.debug("[WebRTCManager] Additional peer connection aborted before connecting.");
17377
+ return;
17378
+ }
17379
+ throw error instanceof Error ? error : new Error(String(error), { cause: error });
17228
17380
  }
17229
17381
  }
17230
17382
  async removeInputDevices(id) {
@@ -18153,6 +18305,12 @@ function mosToQualityLevel(mos) {
18153
18305
  var import_cjs$11 = require_cjs();
18154
18306
  const logger$12 = getLogger();
18155
18307
  /**
18308
+ * Verto method for setting member layout positions. Its gateway DTO requires a
18309
+ * `targets` array whose entries are `{ target, position }` (NOT bare targets),
18310
+ * so {@link WebRTCCall.buildMethodParams} special-cases it. See issue #19400.
18311
+ */
18312
+ const POSITION_SET_METHOD = "call.member.position.set";
18313
+ /**
18156
18314
  * Ratio between the critical and warning RTT spike multipliers.
18157
18315
  * Warning threshold = baseline * warningMultiplier (default 3x)
18158
18316
  * Critical threshold = baseline * warningMultiplier * RTT_CRITICAL_TO_WARNING_RATIO
@@ -18359,7 +18517,7 @@ var WebRTCCall = class extends Destroyable {
18359
18517
  * @throws {JSONRPCError} If the RPC call returns an error.
18360
18518
  */
18361
18519
  async executeMethod(target, method, args) {
18362
- const params = this.buildMethodParams(target, args);
18520
+ const params = this.buildMethodParams(target, args, method);
18363
18521
  const request = buildRPCRequest({
18364
18522
  method,
18365
18523
  params
@@ -18373,12 +18531,16 @@ var WebRTCCall = class extends Destroyable {
18373
18531
  throw error;
18374
18532
  }
18375
18533
  }
18376
- buildMethodParams(target, args) {
18534
+ buildMethodParams(target, args, method) {
18377
18535
  const self = {
18378
18536
  node_id: this.nodeId ?? "",
18379
18537
  call_id: this.id,
18380
18538
  member_id: this.vertoManager.selfId ?? ""
18381
18539
  };
18540
+ if (method === POSITION_SET_METHOD) return {
18541
+ ...args,
18542
+ self
18543
+ };
18382
18544
  if (typeof target === "object") return {
18383
18545
  ...args,
18384
18546
  self,
@@ -18944,10 +19106,21 @@ var WebRTCCall = class extends Destroyable {
18944
19106
  return this.deferEmission(this._answered$.asObservable());
18945
19107
  }
18946
19108
  /**
18947
- * Sets the call layout and participant positions.
19109
+ * Sets the call layout and, optionally, individual participant positions.
19110
+ *
19111
+ * The gateway `call.layout.set` DTO has **no** `positions` member, so when
19112
+ * `positions` is provided this method issues a `call.member.position.set`
19113
+ * request per member (via {@link Participant.setPosition}, which keys each
19114
+ * position by that member's own call context) alongside `call.layout.set`
19115
+ * (issue #19400, Flag #6).
19116
+ *
19117
+ * **These operations are NOT atomic.** The layout is applied first, then each
19118
+ * member position sequentially, so members may briefly flash into their
19119
+ * default slots before being moved to the requested positions.
18948
19120
  *
18949
19121
  * @param layout - Layout name (must be one of {@link layouts}).
18950
- * @param positions - Map of member IDs to {@link VideoPosition} values.
19122
+ * @param positions - Optional map of member IDs to {@link VideoPosition} values.
19123
+ * When omitted or empty, only the layout is changed.
18951
19124
  * @throws {InvalidParams} If the layout is not in the available {@link layouts}.
18952
19125
  *
18953
19126
  * @example
@@ -18960,10 +19133,17 @@ var WebRTCCall = class extends Destroyable {
18960
19133
  async setLayout(layout, positions) {
18961
19134
  if (!this.layouts.includes(layout)) throw new InvalidParams(`Layout ${layout} is not available in the current call layouts: ${this.layouts.join(", ")}`);
18962
19135
  const selfId = await (0, import_cjs$11.firstValueFrom)(this.selfId$.pipe((0, import_cjs$11.filter)((id) => id !== null)));
18963
- await this.executeMethod(selfId, "call.layout.set", {
18964
- layout,
18965
- positions
18966
- });
19136
+ await this.executeMethod(selfId, "call.layout.set", { layout });
19137
+ const positionEntries = Object.entries(positions ?? {});
19138
+ if (positionEntries.length === 0) return;
19139
+ for (const [memberId, position] of positionEntries) {
19140
+ const participant = this.participants.find((p) => p.id === memberId);
19141
+ if (!participant) {
19142
+ logger$12.warn(`[Call] setLayout: member ${memberId} not found in participants; skipping position ${position}`);
19143
+ continue;
19144
+ }
19145
+ await participant.setPosition(position);
19146
+ }
18967
19147
  }
18968
19148
  /**
18969
19149
  * Transfers the call to another destination.
@@ -19137,6 +19317,7 @@ function inferCallErrorKind(error) {
19137
19317
  if (error instanceof RPCTimeoutError) return "timeout";
19138
19318
  if (error instanceof JSONRPCError) return "signaling";
19139
19319
  if (error instanceof MediaTrackError) return "media";
19320
+ if (error instanceof MediaAccessError) return "media";
19140
19321
  if (error instanceof WebSocketConnectionError || error instanceof TransportConnectionError) return "network";
19141
19322
  return "internal";
19142
19323
  }
@@ -19153,6 +19334,7 @@ const RECOVERABLE_RPC_CODES = new Set([
19153
19334
  function isFatalError(error) {
19154
19335
  if (error instanceof VertoPongError) return false;
19155
19336
  if (error instanceof MediaTrackError) return false;
19337
+ if (error instanceof MediaAccessError) return error.fatal;
19156
19338
  if (error instanceof RPCTimeoutError) return false;
19157
19339
  if (error instanceof JSONRPCError && RECOVERABLE_RPC_CODES.has(error.code)) return false;
19158
19340
  return true;
@@ -19178,10 +19360,10 @@ var CallFactory = class {
19178
19360
  return {
19179
19361
  vertoManager: new WebRTCVertoManager(callInstance, this.attachManager, this.deviceController, this.webRTCApiProvider, {
19180
19362
  nodeId: options.nodeId,
19181
- onError: (error) => {
19363
+ onError: (error, options$1) => {
19182
19364
  const callError = {
19183
19365
  kind: inferCallErrorKind(error),
19184
- fatal: isFatalError(error),
19366
+ fatal: options$1?.fatal ?? isFatalError(error),
19185
19367
  error,
19186
19368
  callId: callInstance.id
19187
19369
  };
@@ -19644,6 +19826,17 @@ var PendingRPC = class PendingRPC {
19644
19826
  //#region src/managers/ClientSessionManager.ts
19645
19827
  var import_cjs$7 = require_cjs();
19646
19828
  const logger$9 = getLogger();
19829
+ /**
19830
+ * Decide whether an error emitted on `call.errors$` during dial should
19831
+ * abort the dial. A non-fatal MediaAccessError means the call degraded to
19832
+ * receive-only and still connects — everything else rejects `dial()` with
19833
+ * the real cause.
19834
+ *
19835
+ * Pure function — exported for unit testing.
19836
+ */
19837
+ function shouldAbortDial(callError) {
19838
+ return callError.fatal || !(callError.error instanceof MediaAccessError);
19839
+ }
19647
19840
  const getAddressSearchURI = (options) => {
19648
19841
  const to = options.to?.split("?")[0];
19649
19842
  const from$9 = options.from?.startsWith("subscriber://") ? options.from.replace("subscriber://", "") : options.from;
@@ -19882,11 +20075,41 @@ var ClientSessionManager = class extends Destroyable {
19882
20075
  }
19883
20076
  } else this._errors$.next(error);
19884
20077
  }
20078
+ /**
20079
+ * Clear the resume state (authorization_state + protocol) only.
20080
+ *
20081
+ * This is the stale-auth-state recovery helper used by handleAuthError:
20082
+ * the server rejected a reconnect, so the resume state is discarded and a
20083
+ * fresh connect follows. Attach records are deliberately preserved — the
20084
+ * session lives on through the reconnect and reattachCalls() needs the
20085
+ * stored call references afterwards. Do NOT add detachAll() here.
20086
+ *
20087
+ * For public teardown (disconnect/destroy), use {@link teardownSessionState}
20088
+ * instead, which clears the attach records as well.
20089
+ */
19885
20090
  async cleanupStoredConnectionParams() {
19886
20091
  await this.transport.setProtocol(void 0);
19887
20092
  await this.updateAuthorizationStateInStorage(void 0);
19888
20093
  this._authorization$.next(void 0);
19889
20094
  }
20095
+ /**
20096
+ * Public-teardown helper for disconnect()/destroy(). Clears the resume
20097
+ * state (authorization_state + protocol) AND the attach records as one
20098
+ * atomic unit.
20099
+ *
20100
+ * The two stores are coupled: the backend only honors attach records
20101
+ * within the session identified by the resume state, so ending the
20102
+ * session must clear both. Clearing one without the other strands records
20103
+ * no future session can honor (disconnect) or revives a session the
20104
+ * developer explicitly ended (destroy).
20105
+ *
20106
+ * Distinct from {@link cleanupStoredConnectionParams}, which keeps the
20107
+ * attach records for the stale-auth-state recovery path.
20108
+ */
20109
+ async teardownSessionState() {
20110
+ await this.cleanupStoredConnectionParams();
20111
+ await this.attachManager.detachAll();
20112
+ }
19890
20113
  async updateAuthState(authorization_state) {
19891
20114
  try {
19892
20115
  await this.storage.setItem(this.authorizationStateKey, authorization_state);
@@ -19981,7 +20204,7 @@ var ClientSessionManager = class extends Destroyable {
19981
20204
  async disconnect() {
19982
20205
  this.transport.disconnect();
19983
20206
  this._authState$.next({ kind: "unauthenticated" });
19984
- await this.cleanupStoredConnectionParams();
20207
+ await this.teardownSessionState();
19985
20208
  }
19986
20209
  async createInboundCall(invite) {
19987
20210
  const callSession = await this.createCall({
@@ -20040,7 +20263,7 @@ var ClientSessionManager = class extends Destroyable {
20040
20263
  to: destinationURI,
20041
20264
  ...options
20042
20265
  });
20043
- await (0, import_cjs$7.firstValueFrom)((0, import_cjs$7.race)(callSession.selfId$.pipe((0, import_cjs$7.filter)((id) => Boolean(id)), (0, import_cjs$7.take)(1), (0, import_cjs$7.timeout)(this.callCreateTimeout)), callSession.errors$.pipe((0, import_cjs$7.take)(1), (0, import_cjs$7.switchMap)((callError) => (0, import_cjs$7.throwError)(() => callError.error)))));
20266
+ await (0, import_cjs$7.firstValueFrom)((0, import_cjs$7.race)(callSession.selfId$.pipe((0, import_cjs$7.filter)((id) => Boolean(id)), (0, import_cjs$7.take)(1), (0, import_cjs$7.timeout)(this.callCreateTimeout)), callSession.errors$.pipe((0, import_cjs$7.filter)(shouldAbortDial), (0, import_cjs$7.take)(1), (0, import_cjs$7.switchMap)((callError) => (0, import_cjs$7.throwError)(() => callError.error)))));
20044
20267
  this._calls$.next({
20045
20268
  [`${callSession.id}`]: callSession,
20046
20269
  ...this._calls$.value
@@ -21148,6 +21371,10 @@ var TransportManager = class extends Destroyable {
21148
21371
  if (!isSignalwireRequest(message)) return true;
21149
21372
  const eventChannel = message.params.event_channel;
21150
21373
  if (!eventChannel) return true;
21374
+ if (message.params.event_type.startsWith("conversation.")) {
21375
+ logger$2.debug(`[Transport] Received conversation event: ${message.params.event_type} (event_channel: ${eventChannel})`);
21376
+ return true;
21377
+ }
21151
21378
  const currentProtocol = this._currentProtocol;
21152
21379
  if (!currentProtocol) return true;
21153
21380
  if (!eventChannel.includes(currentProtocol)) {
@@ -21748,7 +21975,7 @@ var SignalWire = class extends Destroyable {
21748
21975
  logger$1.warn("[SignalWire] Failed to initialize VisibilityController:", error);
21749
21976
  }
21750
21977
  try {
21751
- this._diagnosticsCollector = new DiagnosticsCollector({ sdkVersion: "4.0.0-rc.0" });
21978
+ this._diagnosticsCollector = new DiagnosticsCollector({ sdkVersion: "4.0.0-rc.2" });
21752
21979
  } catch (error) {
21753
21980
  logger$1.warn("[SignalWire] Failed to initialize DiagnosticsCollector:", error);
21754
21981
  }
@@ -21756,6 +21983,14 @@ var SignalWire = class extends Destroyable {
21756
21983
  /**
21757
21984
  * Disconnects the WebSocket and tears down the current session.
21758
21985
  *
21986
+ * Ends the session identified by the protocol and clears its persisted
21987
+ * resume state (`authorization_state` + protocol) and attach records
21988
+ * together — a later {@link connect} with the same credentials starts a
21989
+ * fresh session and cannot reattach to the ended session's calls.
21990
+ * Credentials and device preferences are preserved. To temporarily stop
21991
+ * receiving inbound calls while keeping the session alive, use
21992
+ * `unregister()` instead.
21993
+ *
21759
21994
  * The client can be reconnected by calling {@link connect} again,
21760
21995
  * which creates a fresh transport and session.
21761
21996
  */
@@ -22147,12 +22382,20 @@ var SignalWire = class extends Destroyable {
22147
22382
  prefs.preferredVideoInput = null;
22148
22383
  await this._deviceController.clearDeviceState();
22149
22384
  }
22150
- /** Destroys the client, clearing timers and releasing all resources. */
22385
+ /**
22386
+ * Destroys the client, clearing timers and releasing all resources.
22387
+ *
22388
+ * Intentionally destroying the client ends its session: the resume state
22389
+ * (`authorization_state` + protocol) and the attach records are both
22390
+ * cleared. Credentials and device preferences are preserved — use
22391
+ * {@link resetToDefaults} for a full wipe. To temporarily stop receiving
22392
+ * inbound calls while keeping the session alive, use `unregister()`.
22393
+ */
22151
22394
  destroy() {
22152
22395
  this._refreshCoordinator?.destroy();
22153
22396
  this._refreshCoordinator = void 0;
22154
22397
  this._dpopManager?.destroy();
22155
- if (this._attachManager) this._attachManager.detachAll();
22398
+ this._clientSession.teardownSessionState();
22156
22399
  this._transport.destroy();
22157
22400
  this._clientSession.destroy();
22158
22401
  try {
@@ -22267,7 +22510,7 @@ var StaticCredentialProvider = class {
22267
22510
  /**
22268
22511
  * Library version from package.json, injected at build time.
22269
22512
  */
22270
- const version = "4.0.0-rc.0";
22513
+ const version = "4.0.0-rc.2";
22271
22514
  /**
22272
22515
  * Flag indicating the library has been loaded and is ready to use.
22273
22516
  * For UMD builds: `window.SignalWire.ready`
@@ -22289,7 +22532,7 @@ const ready = true;
22289
22532
  */
22290
22533
  const emitReadyEvent = () => {
22291
22534
  if (typeof window !== "undefined") {
22292
- const event = new CustomEvent("signalwire:js:ready", { detail: { version: "4.0.0-rc.0" } });
22535
+ const event = new CustomEvent("signalwire:js:ready", { detail: { version: "4.0.0-rc.2" } });
22293
22536
  window.dispatchEvent(event);
22294
22537
  }
22295
22538
  };
@@ -22321,6 +22564,7 @@ exports.DPoPInitError = DPoPInitError;
22321
22564
  exports.DeviceTokenError = DeviceTokenError;
22322
22565
  exports.EmbedTokenCredentialProvider = EmbedTokenCredentialProvider;
22323
22566
  exports.InvalidCredentialsError = InvalidCredentialsError;
22567
+ exports.MediaAccessError = MediaAccessError;
22324
22568
  exports.MediaTrackError = MediaTrackError;
22325
22569
  exports.MessageParseError = MessageParseError;
22326
22570
  exports.OverconstrainedFallbackError = OverconstrainedFallbackError;