@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.
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 });
@@ -9873,137 +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
- /**
9910
- * Maximum time the coordinator will wait for `DeviceTokenManager.activate()`
9911
- * to resolve before treating the activation as failed and falling back to
9912
- * the developer-provided refresh path. Prevents a wedged HTTP layer from
9913
- * leaving the session with no active refresh mechanism.
9914
- */
9915
- const CREDENTIAL_ACTIVATE_TIMEOUT_MS = 3e4;
9916
- /** JSON-RPC error code for requester validation failure (corrupted auth state). */
9917
- const RPC_ERROR_REQUESTER_VALIDATION_FAILED = -32003;
9918
- /** JSON-RPC error code for invalid params (e.g., missing authentication block). */
9919
- const RPC_ERROR_INVALID_PARAMS = -32602;
9920
- /** JSON-RPC error code for authentication failure (invalid token, missing DPoP, etc.). */
9921
- const RPC_ERROR_AUTHENTICATION_FAILED = -32002;
9922
- /** Default polling interval for RTCPeerConnection.getStats() in milliseconds. */
9923
- const DEFAULT_STATS_POLLING_INTERVAL_MS = 1e3;
9924
- /** Number of initial samples used to build a baseline for spike detection. */
9925
- const DEFAULT_STATS_BASELINE_SAMPLES = 10;
9926
- /** Duration in ms with no inbound audio packets before emitting a critical issue. */
9927
- const DEFAULT_STATS_NO_PACKET_THRESHOLD_MS = 2e3;
9928
- /** Multiplier applied to baseline RTT to detect a warning-level RTT spike. */
9929
- const DEFAULT_STATS_RTT_SPIKE_MULTIPLIER = 3;
9930
- /** Packet loss fraction (0-1) above which a warning is emitted. */
9931
- const DEFAULT_STATS_PACKET_LOSS_THRESHOLD = .05;
9932
- /** Multiplier applied to baseline jitter to detect a jitter spike. */
9933
- const DEFAULT_STATS_JITTER_SPIKE_MULTIPLIER = 4;
9934
- /** Number of seconds of metrics history to retain. */
9935
- const DEFAULT_STATS_HISTORY_SIZE = 30;
9936
- /** Maximum keyframe requests allowed within a single burst window. */
9937
- const DEFAULT_KEYFRAME_MAX_BURST$1 = 3;
9938
- /** Duration of the keyframe burst window in milliseconds. */
9939
- const DEFAULT_KEYFRAME_BURST_WINDOW_MS$1 = 3e3;
9940
- /** Cooldown period in ms after burst limit is reached before allowing more keyframes. */
9941
- const DEFAULT_KEYFRAME_COOLDOWN_MS$1 = 1e4;
9942
- /** Minimum time between re-INVITE attempts in milliseconds. */
9943
- const DEFAULT_REINVITE_DEBOUNCE_TIME_MS = 1e4;
9944
- /** Maximum number of re-INVITE attempts per call. */
9945
- const DEFAULT_REINVITE_MAX_ATTEMPTS = 3;
9946
- /** Timeout for a single re-INVITE attempt in milliseconds. */
9947
- const DEFAULT_REINVITE_TIMEOUT_MS = 5e3;
9948
- /** Debounce window in ms to collapse multiple detection signals into one trigger. */
9949
- const DEFAULT_RECOVERY_DEBOUNCE_TIME_MS = 2e3;
9950
- /** Cooldown period in ms between recovery attempts. */
9951
- const DEFAULT_RECOVERY_COOLDOWN_MS = 1e4;
9952
- /** Grace period in ms before treating ICE 'disconnected' as a failure. */
9953
- const DEFAULT_ICE_DISCONNECTED_GRACE_PERIOD_MS = 3e3;
9954
- /** Timeout for a single ICE restart attempt in milliseconds. */
9955
- const DEFAULT_ICE_RESTART_TIMEOUT_MS$1 = 5e3;
9956
- /** Maximum recovery attempts before emitting 'max_attempts_reached'. */
9957
- const DEFAULT_MAX_RECOVERY_ATTEMPTS = 3;
9958
- /** Upper bound in ms for waiting on iceGatheringState === 'complete' after an ICE restart. */
9959
- const ICE_GATHERING_COMPLETE_TIMEOUT_MS = 1e4;
9960
- /** Upper bound in ms for waiting on RTCPeerConnection.connectionState === 'connected' after a recovery ICE restart. */
9961
- const PEER_CONNECTION_RECOVERY_WAIT_MS = 5e3;
9962
- /** Polling interval in ms while waiting for RTCPeerConnection.connectionState to transition. */
9963
- const PEER_CONNECTION_RECOVERY_POLL_MS = 100;
9964
- /** Polling interval for LocalAudioPipeline.level$ (ms). ~30fps is smooth for meters. */
9965
- const AUDIO_LEVEL_POLL_INTERVAL_MS = 33;
9966
- /** RMS level threshold (0..1) above which the local participant is considered speaking. */
9967
- const VAD_THRESHOLD = .03;
9968
- /** Hold window in ms below the threshold before speaking$ flips back to false. */
9969
- const VAD_HOLD_MS = 250;
9970
- /** Whether to persist device selections to storage by default. */
9971
- const DEFAULT_PERSIST_DEVICE_SELECTION = true;
9972
- /** Whether to auto-apply device changes to active calls by default. */
9973
- const DEFAULT_SYNC_DEVICES_TO_ACTIVE_CALLS = true;
9974
- /** Storage keys for persisted device selections. */
9975
- const DEVICE_STORAGE_KEY_AUDIO_INPUT = "sw:device:audioinput";
9976
- const DEVICE_STORAGE_KEY_AUDIO_OUTPUT = "sw:device:audiooutput";
9977
- const DEVICE_STORAGE_KEY_VIDEO_INPUT = "sw:device:videoinput";
9978
- /** Whether to auto-mute video when the tab becomes hidden. */
9979
- const DEFAULT_AUTO_MUTE_VIDEO_ON_HIDDEN = false;
9980
- /** Whether to re-enumerate devices when the page becomes visible. */
9981
- const DEFAULT_REFRESH_DEVICES_ON_VISIBLE = true;
9982
- /** Whether to check peer connection health when the page becomes visible. */
9983
- const DEFAULT_CHECK_CONNECTION_ON_VISIBLE = true;
9984
- /** Whether automatic video degradation on low bandwidth is enabled. */
9985
- const DEFAULT_ENABLE_AUTO_DEGRADATION = true;
9986
- /** Bitrate in kbps below which video is automatically disabled. */
9987
- const DEFAULT_DEGRADATION_BITRATE_THRESHOLD_KBPS = 150;
9988
- /** Bitrate in kbps above which video is automatically re-enabled (hysteresis). */
9989
- const DEFAULT_DEGRADATION_RECOVERY_THRESHOLD_KBPS = 300;
9990
- /** Whether relay-only escalation is enabled as a last-resort recovery tier. */
9991
- const DEFAULT_ENABLE_RELAY_FALLBACK = true;
9992
- /** Whether to listen for browser online/offline/connection events. */
9993
- const DEFAULT_ENABLE_NETWORK_CHANGE_DETECTION = true;
9994
- /** Whether to intercept server-sent media-timeout hangups and attempt recovery. */
9995
- const DEFAULT_ENABLE_SERVER_HANGUP_INTERCEPTION = true;
9996
- /** Default video track constraints applied when video is enabled without explicit constraints. */
9997
- const DEFAULT_VIDEO_CONSTRAINTS = {
9998
- width: { ideal: 1280 },
9999
- height: { ideal: 720 },
10000
- aspectRatio: 16 / 9
10001
- };
10002
- /** Whether stereo Opus is enabled by default. */
10003
- const DEFAULT_STEREO_AUDIO = false;
10004
- /** Max average bitrate for stereo Opus in bits per second. */
10005
- const DEFAULT_STEREO_MAX_AVERAGE_BITRATE = 51e4;
10006
-
10007
10040
  //#endregion
10008
10041
  //#region src/utils/time.ts
10009
10042
  function fromSecToMs(seconds) {
@@ -13367,7 +13400,9 @@ const DEFAULT_MEMBER_CAPABILITIES = {
13367
13400
  position: false,
13368
13401
  meta: false,
13369
13402
  remove: false,
13370
- audioFlags: false
13403
+ audioFlags: false,
13404
+ denoise: false,
13405
+ lowbitrate: false
13371
13406
  };
13372
13407
  /**
13373
13408
  * Default call capabilities with no permissions
@@ -13440,7 +13475,9 @@ function computeMemberCapabilities(flags, memberType) {
13440
13475
  position: hasBooleanCapability(flags, memberType, null, "position"),
13441
13476
  meta: hasBooleanCapability(flags, memberType, null, "meta"),
13442
13477
  remove: hasBooleanCapability(flags, memberType, null, "remove"),
13443
- 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")
13444
13481
  };
13445
13482
  }
13446
13483
  /**
@@ -13823,6 +13860,10 @@ var Participant = class extends Destroyable {
13823
13860
  get nodeId() {
13824
13861
  return this._state$.value.node_id;
13825
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
+ }
13826
13867
  /** @internal */
13827
13868
  get value() {
13828
13869
  return this._state$.value;
@@ -13884,8 +13925,9 @@ var Participant = class extends Destroyable {
13884
13925
  noise_suppression: !this.noiseSuppression
13885
13926
  });
13886
13927
  }
13928
+ /** Toggles low-bitrate mode for this participant's media. */
13887
13929
  async toggleLowbitrate() {
13888
- throw new UnimplementedError();
13930
+ await this.executeMethod(this.id, "call.lowbitrate.set", { lowbitrate: !this.lowbitrate });
13889
13931
  }
13890
13932
  /**
13891
13933
  * Adjusts the **conference-only** microphone energy gate / sensitivity level
@@ -13932,10 +13974,27 @@ var Participant = class extends Destroyable {
13932
13974
  }
13933
13975
  /**
13934
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
+ *
13935
13985
  * @param value - The {@link VideoPosition} to assign (e.g. `'auto'`, `'reserved-0'`).
13936
13986
  */
13937
13987
  async setPosition(value) {
13938
- 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
+ }] });
13939
13998
  }
13940
13999
  /** Removes this participant from the call. */
13941
14000
  async remove() {
@@ -14025,12 +14084,21 @@ var SelfParticipant = class extends Participant {
14025
14084
  noise_suppression: true
14026
14085
  });
14027
14086
  }
14028
- /** 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
+ */
14029
14096
  async startScreenShare() {
14030
14097
  try {
14031
14098
  await this.vertoManager.addScreenMedia();
14032
14099
  } catch (error) {
14033
14100
  logger$24.error("[Participant.startScreenShare] Screen share error:", error);
14101
+ throw error;
14034
14102
  }
14035
14103
  }
14036
14104
  /** Observable of the current screen share status. */
@@ -14045,12 +14113,20 @@ var SelfParticipant = class extends Participant {
14045
14113
  async stopScreenShare() {
14046
14114
  return this.vertoManager.removeScreenMedia();
14047
14115
  }
14048
- /** 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
+ */
14049
14124
  async addAdditionalDevice(options) {
14050
14125
  try {
14051
14126
  await this.vertoManager.addInputDevice(options);
14052
14127
  } catch (error) {
14053
- logger$24.error("[Participant.startScreenShare] Screen share error:", error);
14128
+ logger$24.error("[Participant.addAdditionalDevice] Additional device error:", error);
14129
+ throw error;
14054
14130
  }
14055
14131
  }
14056
14132
  /** Removes an additional media input device by ID. */
@@ -15682,7 +15758,7 @@ var RTCPeerConnectionController = class extends Destroyable {
15682
15758
  logger$17.debug(`[RTCPeerConnectionController] ${kind} input device selected:`, deviceInfo?.label);
15683
15759
  } catch (error) {
15684
15760
  logger$17.error(`[RTCPeerConnectionController] Failed to select ${kind} input device:`, error);
15685
- this._errors$.next(toError(error));
15761
+ this._errors$.next(new MediaTrackError("updateSelectedInputDevice", kind, error));
15686
15762
  throw error;
15687
15763
  }
15688
15764
  };
@@ -15904,7 +15980,7 @@ var RTCPeerConnectionController = class extends Destroyable {
15904
15980
  case "main":
15905
15981
  default: return {
15906
15982
  ...options,
15907
- offerToReceiveAudio: true,
15983
+ offerToReceiveAudio: this.options.receiveAudio ?? true,
15908
15984
  offerToReceiveVideo: this.options.receiveVideo ?? Boolean(this.inputVideoDeviceConstraints)
15909
15985
  };
15910
15986
  }
@@ -16058,13 +16134,14 @@ var RTCPeerConnectionController = class extends Destroyable {
16058
16134
  */
16059
16135
  async acceptInbound(mediaOverrides) {
16060
16136
  if (mediaOverrides) {
16061
- const { audio, video, receiveAudio, receiveVideo } = mediaOverrides;
16137
+ const { audio, video, receiveAudio, receiveVideo, fallbackToReceiveOnly } = mediaOverrides;
16062
16138
  this.options = {
16063
16139
  ...this.options,
16064
16140
  ...audio !== void 0 ? { audio } : {},
16065
16141
  ...video !== void 0 ? { video } : {},
16066
16142
  ...receiveAudio !== void 0 ? { receiveAudio } : {},
16067
- ...receiveVideo !== void 0 ? { receiveVideo } : {}
16143
+ ...receiveVideo !== void 0 ? { receiveVideo } : {},
16144
+ ...fallbackToReceiveOnly !== void 0 ? { fallbackToReceiveOnly } : {}
16068
16145
  };
16069
16146
  this.transceiverController?.updateOptions({
16070
16147
  receiveAudio: this.receiveAudio,
@@ -16221,7 +16298,19 @@ var RTCPeerConnectionController = class extends Destroyable {
16221
16298
  }
16222
16299
  async setupLocalTracks() {
16223
16300
  logger$17.debug("[RTCPeerConnectionController] Setting up local tracks/transceivers.");
16224
- const localStream = this.localStream ?? await this.localStreamController.buildLocalStream();
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
+ }
16225
16314
  if (this.transceiverController?.useAddStream ?? false) {
16226
16315
  logger$17.warn("[RTCPeerConnectionController] Using deprecated addStream API to add local stream.");
16227
16316
  this.peerConnection?.addStream(localStream);
@@ -16248,6 +16337,48 @@ var RTCPeerConnectionController = class extends Destroyable {
16248
16337
  }
16249
16338
  }
16250
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
+ }
16251
16382
  async getUserMedia(constraints) {
16252
16383
  return (this.options.webRTCApiProvider?.mediaDevices ?? navigator.mediaDevices).getUserMedia(constraints);
16253
16384
  }
@@ -16284,7 +16415,7 @@ var RTCPeerConnectionController = class extends Destroyable {
16284
16415
  stream = await this.getUserMedia({ audio: constraints });
16285
16416
  } catch (error) {
16286
16417
  logger$17.error("[RTCPeerConnectionController] Failed to re-acquire mic for pipeline restore:", error);
16287
- this._errors$.next(toError(error));
16418
+ this._errors$.next(new MediaTrackError("restoreAudioPipelineInput", "audio", error));
16288
16419
  return;
16289
16420
  }
16290
16421
  const newTrack = stream.getAudioTracks().at(0);
@@ -16346,7 +16477,7 @@ var RTCPeerConnectionController = class extends Destroyable {
16346
16477
  logger$17.debug(`[RTCPeerConnectionController] ${track.kind} track added:`, track.id);
16347
16478
  } catch (error) {
16348
16479
  logger$17.error(`[RTCPeerConnectionController] Failed to add ${track.kind} track:`, error);
16349
- this._errors$.next(toError(error));
16480
+ this._errors$.next(new MediaTrackError("addLocalTrack", track.kind, error));
16350
16481
  throw error;
16351
16482
  }
16352
16483
  }
@@ -16371,7 +16502,7 @@ var RTCPeerConnectionController = class extends Destroyable {
16371
16502
  logger$17.debug(`[RTCPeerConnectionController] ${sender.track?.kind} track removed:`, trackId);
16372
16503
  } catch (error) {
16373
16504
  logger$17.error(`[RTCPeerConnectionController] Failed to remove ${sender.track?.kind} track:`, error);
16374
- this._errors$.next(toError(error));
16505
+ this._errors$.next(new MediaTrackError("removeLocalTrack", sender.track?.kind ?? "unknown", error));
16375
16506
  throw error;
16376
16507
  }
16377
16508
  }
@@ -16481,7 +16612,11 @@ function isVertoInviteMessage(value) {
16481
16612
  const msg = value;
16482
16613
  return msg.method === "verto.invite" && isObject(msg.params) && hasProperty(msg.params, "sdp") && hasProperty(msg.params, "callID");
16483
16614
  }
16484
- 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) {
16485
16620
  if (!isVertoMethodMessage(value)) return false;
16486
16621
  return value.method === "verto.bye";
16487
16622
  }
@@ -16501,6 +16636,14 @@ function isVertoMediaParamsInnerParams(value) {
16501
16636
  function isVertoPingInnerParams(value) {
16502
16637
  return isObject(value) && hasProperty(value, "jsonrpc") && value.jsonrpc === "2.0" && hasProperty(value, "method") && value.method === "verto.ping";
16503
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
+ }
16504
16647
 
16505
16648
  //#endregion
16506
16649
  //#region src/managers/VertoManager.ts
@@ -16839,7 +16982,7 @@ var WebRTCVertoManager = class extends VertoManager {
16839
16982
  return this.cachedObservable("vertoMediaParams$", () => this.webRtcCallSession.webrtcMessages$.pipe(filterAs(isVertoMediaParamsInnerParams, "params"), (0, import_cjs$15.takeUntil)(this.destroyed$)));
16840
16983
  }
16841
16984
  get vertoBye$() {
16842
- 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$)));
16843
16986
  }
16844
16987
  get vertoAttach$() {
16845
16988
  return this.cachedObservable("vertoAttach$", () => this.webRtcCallSession.webrtcMessages$.pipe(filterAs(isVertoAttachMessage, "params"), (0, import_cjs$15.takeUntil)(this.destroyed$)));
@@ -16968,6 +17111,7 @@ var WebRTCVertoManager = class extends VertoManager {
16968
17111
  inputVideoStream: options.inputVideoStream,
16969
17112
  receiveAudio: options.receiveAudio,
16970
17113
  receiveVideo: options.receiveVideo,
17114
+ fallbackToReceiveOnly: options.fallbackToReceiveOnly,
16971
17115
  webRTCApiProvider: this.webRTCApiProvider,
16972
17116
  preferredVideoCodecs: options.preferredVideoCodecs,
16973
17117
  preferredAudioCodecs: options.preferredAudioCodecs,
@@ -16992,7 +17136,7 @@ var WebRTCVertoManager = class extends VertoManager {
16992
17136
  logger$16.debug("[WebRTCManager] Inbound answer handler aborted (destroyed).");
16993
17137
  return;
16994
17138
  }
16995
- if (isVertoByeMessage(vertoByeOrAccepted)) {
17139
+ if (isVertoByeInboundParamsGuard(vertoByeOrAccepted)) {
16996
17140
  logger$16.info("[WebRTCManager] Inbound call ended by remote before answer.");
16997
17141
  this.callSession?.destroy();
16998
17142
  } else if (!vertoByeOrAccepted) {
@@ -17094,7 +17238,7 @@ var WebRTCVertoManager = class extends VertoManager {
17094
17238
  logger$16.debug("[WebRTCManager] Destroyed while waiting for call acceptance");
17095
17239
  return;
17096
17240
  }
17097
- if (isVertoByeMessage(vertoByeOrAccepted)) {
17241
+ if (isVertoByeInboundParamsGuard(vertoByeOrAccepted)) {
17098
17242
  logger$16.info("[WebRTCManager] Call ended before answer was sent.");
17099
17243
  this.callSession?.destroy();
17100
17244
  } else if (!vertoByeOrAccepted) {
@@ -17194,9 +17338,11 @@ var WebRTCVertoManager = class extends VertoManager {
17194
17338
  await this.initAdditionalPeerConnection("screenshare", options);
17195
17339
  }
17196
17340
  async initAdditionalPeerConnection(propose, options) {
17341
+ const isScreenShare = propose === "screenshare";
17342
+ let firstPeerConnectionError;
17197
17343
  let rtcPeerConnController = null;
17198
17344
  try {
17199
- this._screenShareStatus$.next("starting");
17345
+ if (isScreenShare) this._screenShareStatus$.next("starting");
17200
17346
  rtcPeerConnController = new RTCPeerConnectionController({
17201
17347
  ...options,
17202
17348
  ...this.RTCPeerConnectionConfig,
@@ -17204,21 +17350,27 @@ var WebRTCVertoManager = class extends VertoManager {
17204
17350
  webRTCApiProvider: this.webRTCApiProvider
17205
17351
  }, void 0, this.deviceController);
17206
17352
  this.setupLocalDescriptionHandler(rtcPeerConnController);
17207
- if (propose === "screenshare") this._screenShareId = rtcPeerConnController.id;
17353
+ if (isScreenShare) this._screenShareId = rtcPeerConnController.id;
17208
17354
  this._rtcPeerConnectionsMap.set(rtcPeerConnController.id, rtcPeerConnController);
17209
17355
  this._rtcPeerConnections$.next(Array.from(this._rtcPeerConnectionsMap.values()));
17210
17356
  this.subscribeTo(rtcPeerConnController.errors$, (error) => {
17211
- this.onError?.(error);
17357
+ firstPeerConnectionError ??= error;
17358
+ this.onError?.(error, { fatal: false });
17212
17359
  });
17213
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$)));
17214
- this._screenShareStatus$.next("started");
17215
- logger$16.info("[WebRTCManager] Screen share started successfully.");
17361
+ if (isScreenShare) this._screenShareStatus$.next("started");
17362
+ logger$16.info(`[WebRTCManager] Additional peer connection connected (${propose}).`);
17216
17363
  return rtcPeerConnController.id;
17217
17364
  } catch (error) {
17218
17365
  logger$16.warn("[WebRTCManager] Error initializing additional peer connection:", error);
17219
- this.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
17220
17366
  if (rtcPeerConnController) rtcPeerConnController.destroy();
17221
- 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 });
17222
17374
  }
17223
17375
  }
17224
17376
  async removeInputDevices(id) {
@@ -18147,6 +18299,12 @@ function mosToQualityLevel(mos) {
18147
18299
  var import_cjs$11 = require_cjs();
18148
18300
  const logger$12 = getLogger();
18149
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";
18307
+ /**
18150
18308
  * Ratio between the critical and warning RTT spike multipliers.
18151
18309
  * Warning threshold = baseline * warningMultiplier (default 3x)
18152
18310
  * Critical threshold = baseline * warningMultiplier * RTT_CRITICAL_TO_WARNING_RATIO
@@ -18353,7 +18511,7 @@ var WebRTCCall = class extends Destroyable {
18353
18511
  * @throws {JSONRPCError} If the RPC call returns an error.
18354
18512
  */
18355
18513
  async executeMethod(target, method, args) {
18356
- const params = this.buildMethodParams(target, args);
18514
+ const params = this.buildMethodParams(target, args, method);
18357
18515
  const request = buildRPCRequest({
18358
18516
  method,
18359
18517
  params
@@ -18367,12 +18525,16 @@ var WebRTCCall = class extends Destroyable {
18367
18525
  throw error;
18368
18526
  }
18369
18527
  }
18370
- buildMethodParams(target, args) {
18528
+ buildMethodParams(target, args, method) {
18371
18529
  const self = {
18372
18530
  node_id: this.nodeId ?? "",
18373
18531
  call_id: this.id,
18374
18532
  member_id: this.vertoManager.selfId ?? ""
18375
18533
  };
18534
+ if (method === POSITION_SET_METHOD) return {
18535
+ ...args,
18536
+ self
18537
+ };
18376
18538
  if (typeof target === "object") return {
18377
18539
  ...args,
18378
18540
  self,
@@ -18938,10 +19100,21 @@ var WebRTCCall = class extends Destroyable {
18938
19100
  return this.deferEmission(this._answered$.asObservable());
18939
19101
  }
18940
19102
  /**
18941
- * 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.
18942
19114
  *
18943
19115
  * @param layout - Layout name (must be one of {@link layouts}).
18944
- * @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.
18945
19118
  * @throws {InvalidParams} If the layout is not in the available {@link layouts}.
18946
19119
  *
18947
19120
  * @example
@@ -18954,10 +19127,17 @@ var WebRTCCall = class extends Destroyable {
18954
19127
  async setLayout(layout, positions) {
18955
19128
  if (!this.layouts.includes(layout)) throw new InvalidParams(`Layout ${layout} is not available in the current call layouts: ${this.layouts.join(", ")}`);
18956
19129
  const selfId = await (0, import_cjs$11.firstValueFrom)(this.selfId$.pipe((0, import_cjs$11.filter)((id) => id !== null)));
18957
- await this.executeMethod(selfId, "call.layout.set", {
18958
- layout,
18959
- positions
18960
- });
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
+ }
18961
19141
  }
18962
19142
  /**
18963
19143
  * Transfers the call to another destination.
@@ -19131,6 +19311,7 @@ function inferCallErrorKind(error) {
19131
19311
  if (error instanceof RPCTimeoutError) return "timeout";
19132
19312
  if (error instanceof JSONRPCError) return "signaling";
19133
19313
  if (error instanceof MediaTrackError) return "media";
19314
+ if (error instanceof MediaAccessError) return "media";
19134
19315
  if (error instanceof WebSocketConnectionError || error instanceof TransportConnectionError) return "network";
19135
19316
  return "internal";
19136
19317
  }
@@ -19147,6 +19328,7 @@ const RECOVERABLE_RPC_CODES = new Set([
19147
19328
  function isFatalError(error) {
19148
19329
  if (error instanceof VertoPongError) return false;
19149
19330
  if (error instanceof MediaTrackError) return false;
19331
+ if (error instanceof MediaAccessError) return error.fatal;
19150
19332
  if (error instanceof RPCTimeoutError) return false;
19151
19333
  if (error instanceof JSONRPCError && RECOVERABLE_RPC_CODES.has(error.code)) return false;
19152
19334
  return true;
@@ -19172,10 +19354,10 @@ var CallFactory = class {
19172
19354
  return {
19173
19355
  vertoManager: new WebRTCVertoManager(callInstance, this.attachManager, this.deviceController, this.webRTCApiProvider, {
19174
19356
  nodeId: options.nodeId,
19175
- onError: (error) => {
19357
+ onError: (error, options$1) => {
19176
19358
  const callError = {
19177
19359
  kind: inferCallErrorKind(error),
19178
- fatal: isFatalError(error),
19360
+ fatal: options$1?.fatal ?? isFatalError(error),
19179
19361
  error,
19180
19362
  callId: callInstance.id
19181
19363
  };
@@ -19638,6 +19820,17 @@ var PendingRPC = class PendingRPC {
19638
19820
  //#region src/managers/ClientSessionManager.ts
19639
19821
  var import_cjs$7 = require_cjs();
19640
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
+ }
19641
19834
  const getAddressSearchURI = (options) => {
19642
19835
  const to = options.to?.split("?")[0];
19643
19836
  const from$9 = options.from?.startsWith("subscriber://") ? options.from.replace("subscriber://", "") : options.from;
@@ -19876,11 +20069,41 @@ var ClientSessionManager = class extends Destroyable {
19876
20069
  }
19877
20070
  } else this._errors$.next(error);
19878
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
+ */
19879
20084
  async cleanupStoredConnectionParams() {
19880
20085
  await this.transport.setProtocol(void 0);
19881
20086
  await this.updateAuthorizationStateInStorage(void 0);
19882
20087
  this._authorization$.next(void 0);
19883
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
+ }
19884
20107
  async updateAuthState(authorization_state) {
19885
20108
  try {
19886
20109
  await this.storage.setItem(this.authorizationStateKey, authorization_state);
@@ -19975,7 +20198,7 @@ var ClientSessionManager = class extends Destroyable {
19975
20198
  async disconnect() {
19976
20199
  this.transport.disconnect();
19977
20200
  this._authState$.next({ kind: "unauthenticated" });
19978
- await this.cleanupStoredConnectionParams();
20201
+ await this.teardownSessionState();
19979
20202
  }
19980
20203
  async createInboundCall(invite) {
19981
20204
  const callSession = await this.createCall({
@@ -20034,7 +20257,7 @@ var ClientSessionManager = class extends Destroyable {
20034
20257
  to: destinationURI,
20035
20258
  ...options
20036
20259
  });
20037
- 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)))));
20038
20261
  this._calls$.next({
20039
20262
  [`${callSession.id}`]: callSession,
20040
20263
  ...this._calls$.value
@@ -21142,6 +21365,10 @@ var TransportManager = class extends Destroyable {
21142
21365
  if (!isSignalwireRequest(message)) return true;
21143
21366
  const eventChannel = message.params.event_channel;
21144
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
+ }
21145
21372
  const currentProtocol = this._currentProtocol;
21146
21373
  if (!currentProtocol) return true;
21147
21374
  if (!eventChannel.includes(currentProtocol)) {
@@ -21742,7 +21969,7 @@ var SignalWire = class extends Destroyable {
21742
21969
  logger$1.warn("[SignalWire] Failed to initialize VisibilityController:", error);
21743
21970
  }
21744
21971
  try {
21745
- this._diagnosticsCollector = new DiagnosticsCollector({ sdkVersion: "4.0.0-rc.0" });
21972
+ this._diagnosticsCollector = new DiagnosticsCollector({ sdkVersion: "4.0.0-rc.2" });
21746
21973
  } catch (error) {
21747
21974
  logger$1.warn("[SignalWire] Failed to initialize DiagnosticsCollector:", error);
21748
21975
  }
@@ -21750,6 +21977,14 @@ var SignalWire = class extends Destroyable {
21750
21977
  /**
21751
21978
  * Disconnects the WebSocket and tears down the current session.
21752
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
+ *
21753
21988
  * The client can be reconnected by calling {@link connect} again,
21754
21989
  * which creates a fresh transport and session.
21755
21990
  */
@@ -22141,12 +22376,20 @@ var SignalWire = class extends Destroyable {
22141
22376
  prefs.preferredVideoInput = null;
22142
22377
  await this._deviceController.clearDeviceState();
22143
22378
  }
22144
- /** 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
+ */
22145
22388
  destroy() {
22146
22389
  this._refreshCoordinator?.destroy();
22147
22390
  this._refreshCoordinator = void 0;
22148
22391
  this._dpopManager?.destroy();
22149
- if (this._attachManager) this._attachManager.detachAll();
22392
+ this._clientSession.teardownSessionState();
22150
22393
  this._transport.destroy();
22151
22394
  this._clientSession.destroy();
22152
22395
  try {
@@ -22261,7 +22504,7 @@ var StaticCredentialProvider = class {
22261
22504
  /**
22262
22505
  * Library version from package.json, injected at build time.
22263
22506
  */
22264
- const version = "4.0.0-rc.0";
22507
+ const version = "4.0.0-rc.2";
22265
22508
  /**
22266
22509
  * Flag indicating the library has been loaded and is ready to use.
22267
22510
  * For UMD builds: `window.SignalWire.ready`
@@ -22283,7 +22526,7 @@ const ready = true;
22283
22526
  */
22284
22527
  const emitReadyEvent = () => {
22285
22528
  if (typeof window !== "undefined") {
22286
- const event = new CustomEvent("signalwire:js:ready", { detail: { version: "4.0.0-rc.0" } });
22529
+ const event = new CustomEvent("signalwire:js:ready", { detail: { version: "4.0.0-rc.2" } });
22287
22530
  window.dispatchEvent(event);
22288
22531
  }
22289
22532
  };
@@ -22307,5 +22550,5 @@ emitReadyEvent();
22307
22550
  if (typeof process === "undefined") globalThis.process = { env: { NODE_ENV: "production" } };
22308
22551
 
22309
22552
  //#endregion
22310
- 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 };
22311
22554
  //# sourceMappingURL=browser.mjs.map