@signalwire/js 4.0.0-beta.7 → 4.0.0-beta.8

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/index.cjs CHANGED
@@ -1,4 +1,4 @@
1
- const require_operators = require('./operators-DT4UB24-.cjs');
1
+ const require_operators = require('./operators-CJEML6aa.cjs');
2
2
  let jwt_decode = require("jwt-decode");
3
3
  let rxjs = require("rxjs");
4
4
  let uuid = require("uuid");
@@ -295,7 +295,7 @@ var PreferencesContainer = class PreferencesContainer {
295
295
  skipDeviceMonitoring: false,
296
296
  savePreferences: false
297
297
  };
298
- this.receiveVideo = true;
298
+ this.receiveVideo = false;
299
299
  this.receiveAudio = true;
300
300
  this.preferredAudioInput = null;
301
301
  this.preferredAudioOutput = null;
@@ -607,7 +607,7 @@ var NavigatorDeviceController = class extends Destroyable {
607
607
  };
608
608
  this._devicesState$ = this.createBehaviorSubject(initialDevicesState);
609
609
  this._selectedDevicesState$ = this.createBehaviorSubject(initialSelectedDevicesState);
610
- this._errors$ = this.createSubject();
610
+ this._errors$ = this.createReplaySubject(1);
611
611
  this.init();
612
612
  }
613
613
  get selectedAudioInputDeviceConstraints() {
@@ -1087,7 +1087,7 @@ const RPCExecute = ({ method, params }) => {
1087
1087
 
1088
1088
  //#endregion
1089
1089
  //#region src/core/RPCMessages/VertoMessages.ts
1090
- const tmpMap = {
1090
+ const SDK_TO_VERTO_FIELD_MAP = {
1091
1091
  id: "callID",
1092
1092
  destinationNumber: "destination_number",
1093
1093
  remoteCallerName: "remote_caller_id_name",
@@ -1096,19 +1096,31 @@ const tmpMap = {
1096
1096
  callerNumber: "caller_id_number",
1097
1097
  fromCallAddressId: "from_fabric_address_id"
1098
1098
  };
1099
+ const EXCLUDED_DIALOG_PARAMS = new Set([
1100
+ "remoteSdp",
1101
+ "localStream",
1102
+ "remoteStream"
1103
+ ]);
1099
1104
  /**
1100
- * Translate SDK fields into verto variables
1105
+ * Translate SDK fields into verto variables.
1106
+ * Returns a new object — the input is never mutated.
1101
1107
  */
1108
+ /** @internal Exported for testing only. */
1102
1109
  const filterVertoParams = (params) => {
1103
- if (Object.prototype.hasOwnProperty.call(params, "dialogParams")) {
1104
- const { remoteSdp, localStream, remoteStream, ...dialogParams } = params.dialogParams;
1105
- for (const key in tmpMap) if (key && Object.prototype.hasOwnProperty.call(dialogParams, key)) {
1106
- dialogParams[tmpMap[key]] = dialogParams[key];
1107
- delete dialogParams[key];
1108
- }
1109
- params.dialogParams = dialogParams;
1110
- }
1111
- return params;
1110
+ if (!Object.prototype.hasOwnProperty.call(params, "dialogParams")) return params;
1111
+ const sourceDialogParams = params.dialogParams;
1112
+ const filteredDialogParams = Object.entries(sourceDialogParams).reduce((acc, [key, value]) => {
1113
+ if (EXCLUDED_DIALOG_PARAMS.has(key)) return acc;
1114
+ const mappedKey = SDK_TO_VERTO_FIELD_MAP[key] ?? key;
1115
+ return {
1116
+ ...acc,
1117
+ [mappedKey]: value
1118
+ };
1119
+ }, {});
1120
+ return {
1121
+ ...params,
1122
+ dialogParams: filteredDialogParams
1123
+ };
1112
1124
  };
1113
1125
  const buildVertoRPCMessage = (method) => {
1114
1126
  return (params = {}) => {
@@ -1759,7 +1771,13 @@ var Participant = class extends Destroyable {
1759
1771
  }
1760
1772
  /** Removes this participant from the call. */
1761
1773
  async remove() {
1762
- await this.executeMethod(this.id, "call.member.remove", {});
1774
+ const state = this._state$.value;
1775
+ const target = {
1776
+ member_id: this.id,
1777
+ call_id: state.call_id ?? "",
1778
+ node_id: state.node_id ?? ""
1779
+ };
1780
+ await this.executeMethod(target, "call.member.remove", {});
1763
1781
  }
1764
1782
  /** Ends the call for this participant. */
1765
1783
  async end() {
@@ -2001,7 +2019,6 @@ var CallEventsManager = class extends Destroyable {
2001
2019
  this.options = options;
2002
2020
  this.callIds = /* @__PURE__ */ new Set();
2003
2021
  this.roomSessionIds = /* @__PURE__ */ new Set();
2004
- this._status$ = this.createBehaviorSubject("trying");
2005
2022
  this._participants$ = this.createBehaviorSubject({});
2006
2023
  this._self$ = this.createBehaviorSubject(null);
2007
2024
  this._sessionState$ = this.createBehaviorSubject(initialSessionState);
@@ -2010,15 +2027,12 @@ var CallEventsManager = class extends Destroyable {
2010
2027
  get participants$() {
2011
2028
  return this.cachedObservable("participants$", () => this._participants$.asObservable().pipe((0, rxjs.map)((participantsRecord) => Object.values(participantsRecord))));
2012
2029
  }
2030
+ get participants() {
2031
+ return Object.values(this._participants$.value);
2032
+ }
2013
2033
  get self$() {
2014
2034
  return this.cachedObservable("self$", () => this._self$.asObservable().pipe(require_operators.filterNull()));
2015
2035
  }
2016
- get status$() {
2017
- return this._status$.asObservable();
2018
- }
2019
- get status() {
2020
- return this._status$.value;
2021
- }
2022
2036
  isRoomSessionIdValid(roomSessionId) {
2023
2037
  return this.roomSessionIds.has(roomSessionId);
2024
2038
  }
@@ -2103,7 +2117,6 @@ var CallEventsManager = class extends Destroyable {
2103
2117
  callId: callJoinedEvent.call_id,
2104
2118
  roomSessionId: callJoinedEvent.room_session_id
2105
2119
  });
2106
- this._status$.next("connected");
2107
2120
  const sessionState = callJoinedEvent.room_session;
2108
2121
  const { capabilities } = callJoinedEvent;
2109
2122
  this.selfId = this.selfId ?? callJoinedEvent.member_id;
@@ -2230,12 +2243,53 @@ var CallEventsManager = class extends Destroyable {
2230
2243
 
2231
2244
  //#endregion
2232
2245
  //#region src/helpers/SDPHelper.ts
2246
+ /** Valid SDP direction attribute values. */
2247
+ const SDP_DIRECTIONS = new Set([
2248
+ "sendrecv",
2249
+ "sendonly",
2250
+ "recvonly",
2251
+ "inactive"
2252
+ ]);
2233
2253
  /**
2234
- * SDPHelper - Utility functions for SDP (Session Description Protocol) parsing and validation.
2254
+ * Extracts the media directions (audio/video) from an SDP string.
2255
+ *
2256
+ * Parses each media section (`m=audio` / `m=video`) and reads the `a=` direction
2257
+ * attribute (`sendrecv`, `sendonly`, `recvonly`, `inactive`).
2258
+ * If no explicit direction attribute is found for a media section, defaults to `sendrecv`
2259
+ * per RFC 4566.
2235
2260
  *
2236
- * This module provides helper functions to analyze and validate SDP content,
2237
- * particularly for ICE candidate validation in WebRTC connections.
2261
+ * @param sdp - The SDP string to parse
2262
+ * @returns The extracted audio and video directions
2263
+ *
2264
+ * @example
2265
+ * ```typescript
2266
+ * const sdp = `v=0\r\nm=audio 9 UDP/TLS/RTP/SAVPF 111\r\na=sendrecv\r\nm=video 9 UDP/TLS/RTP/SAVPF 96\r\na=recvonly`;
2267
+ * extractMediaDirectionsFromSDP(sdp);
2268
+ * // { audio: 'sendrecv', video: 'recvonly' }
2269
+ * ```
2238
2270
  */
2271
+ function extractMediaDirectionsFromSDP(sdp) {
2272
+ const result = {
2273
+ audio: "inactive",
2274
+ video: "inactive"
2275
+ };
2276
+ if (!sdp) return result;
2277
+ const lines = sdp.split(/\r?\n/);
2278
+ let currentMediaKind = null;
2279
+ let currentDirection = null;
2280
+ for (const line of lines) if (line.startsWith("m=")) {
2281
+ if (currentMediaKind) result[currentMediaKind] = currentDirection ?? "sendrecv";
2282
+ if (line.startsWith("m=audio")) currentMediaKind = "audio";
2283
+ else if (line.startsWith("m=video")) currentMediaKind = "video";
2284
+ else currentMediaKind = null;
2285
+ currentDirection = null;
2286
+ } else if (currentMediaKind && line.startsWith("a=")) {
2287
+ const attr = line.substring(2).trim();
2288
+ if (SDP_DIRECTIONS.has(attr)) currentDirection = attr;
2289
+ }
2290
+ if (currentMediaKind) result[currentMediaKind] = currentDirection ?? "sendrecv";
2291
+ return result;
2292
+ }
2239
2293
  /**
2240
2294
  * Validates that an SDP string has at least one non-host ICE candidate
2241
2295
  * for each media section (m= line).
@@ -2536,6 +2590,15 @@ var LocalStreamController = class extends Destroyable {
2536
2590
  track.addEventListener("ended", this.mediaTrackEndedHandler);
2537
2591
  }
2538
2592
  /**
2593
+ * Update the controller options (e.g., when media overrides are applied).
2594
+ */
2595
+ updateOptions(options) {
2596
+ this.options = {
2597
+ ...this.options,
2598
+ ...options
2599
+ };
2600
+ }
2601
+ /**
2539
2602
  * Stop all local tracks and clean up.
2540
2603
  */
2541
2604
  stopAllTracks() {
@@ -2873,11 +2936,12 @@ var RTCPeerConnectionController = class extends Destroyable {
2873
2936
  this._connectionState$ = this.createReplaySubject(1);
2874
2937
  this._signalingState$ = this.createReplaySubject(1);
2875
2938
  this._iceGatheringState$ = this.createReplaySubject(1);
2876
- this._errors$ = this.createSubject();
2939
+ this._errors$ = this.createReplaySubject(1);
2877
2940
  this._iceCandidates$ = this.createReplaySubject(1);
2878
2941
  this._initialized$ = this.createReplaySubject(1);
2879
2942
  this._remoteDescription$ = this.createReplaySubject(1);
2880
2943
  this._remoteStream$ = this.createBehaviorSubject(null);
2944
+ this._remoteOfferMediaDirections = null;
2881
2945
  this.deviceController = deviceController ?? {};
2882
2946
  this.id = options.callId ?? (0, uuid.v4)();
2883
2947
  this._type = remoteSessionDescription ? "answer" : "offer";
@@ -2885,10 +2949,19 @@ var RTCPeerConnectionController = class extends Destroyable {
2885
2949
  type: "offer",
2886
2950
  sdp: remoteSessionDescription
2887
2951
  } : void 0;
2952
+ this._remoteOfferMediaDirections = remoteSessionDescription ? extractMediaDirectionsFromSDP(remoteSessionDescription) : null;
2953
+ const offerDefaults = this._remoteOfferMediaDirections ? {
2954
+ audio: this._remoteOfferMediaDirections.audio.includes("recv"),
2955
+ video: this._remoteOfferMediaDirections.video.includes("recv"),
2956
+ receiveAudio: this._remoteOfferMediaDirections.audio.includes("send"),
2957
+ receiveVideo: this._remoteOfferMediaDirections.video.includes("send")
2958
+ } : {};
2888
2959
  this.options = {
2889
- receiveAudio: options.receiveAudio ?? PreferencesContainer.instance.receiveAudio,
2890
- receiveVideo: options.receiveVideo ?? PreferencesContainer.instance.receiveVideo,
2891
- ...options
2960
+ ...options,
2961
+ audio: options.audio ?? offerDefaults.audio,
2962
+ video: options.video ?? offerDefaults.video,
2963
+ receiveAudio: options.receiveAudio ?? offerDefaults.receiveAudio ?? PreferencesContainer.instance.receiveAudio,
2964
+ receiveVideo: options.receiveVideo ?? offerDefaults.receiveVideo ?? PreferencesContainer.instance.receiveVideo
2892
2965
  };
2893
2966
  this.localStreamController = new LocalStreamController({
2894
2967
  propose: this.propose,
@@ -3033,7 +3106,7 @@ var RTCPeerConnectionController = class extends Destroyable {
3033
3106
  };
3034
3107
  }
3035
3108
  get inputVideoDeviceConstraints() {
3036
- if (this.options.video === false && !this.options.inputVideoDeviceConstraints) return false;
3109
+ if (!this.options.video && !this.options.inputVideoDeviceConstraints) return false;
3037
3110
  return {
3038
3111
  ...this.options.inputVideoDeviceConstraints,
3039
3112
  ...this.deviceController.selectedVideoInputDeviceConstraints
@@ -3055,12 +3128,12 @@ var RTCPeerConnectionController = class extends Destroyable {
3055
3128
  default: return {
3056
3129
  ...options,
3057
3130
  offerToReceiveAudio: true,
3058
- offerToReceiveVideo: Boolean(this.inputVideoDeviceConstraints)
3131
+ offerToReceiveVideo: this.options.receiveVideo ?? Boolean(this.inputVideoDeviceConstraints)
3059
3132
  };
3060
3133
  }
3061
3134
  }
3062
3135
  get answerOptions() {
3063
- return {};
3136
+ return { iceRestart: this.firstSDPExchangeCompleted ? true : void 0 };
3064
3137
  }
3065
3138
  /**
3066
3139
  * Initialize the RTCPeerConnection and setup event listeners.
@@ -3095,11 +3168,15 @@ var RTCPeerConnectionController = class extends Destroyable {
3095
3168
  });
3096
3169
  await this.updateSelectedInputDevice(kind, deviceInfo);
3097
3170
  });
3098
- await this.setupTrackHandling();
3099
- this._initialized$.next(true);
3100
3171
  if (this.type === "answer" && this.sdpInit) {
3172
+ await this.setupRemoteTracks();
3173
+ this._initialized$.next(true);
3101
3174
  this.setupEventListeners();
3102
- await this.handleOfferReceived();
3175
+ this._isNegotiating$.next(true);
3176
+ await this._setRemoteDescription(this.sdpInit);
3177
+ } else {
3178
+ await this.setupTrackHandling();
3179
+ this._initialized$.next(true);
3103
3180
  }
3104
3181
  } catch (error) {
3105
3182
  logger$11.error("[RTCPeerConnectionController] Initialization error:", error);
@@ -3197,6 +3274,35 @@ var RTCPeerConnectionController = class extends Destroyable {
3197
3274
  default:
3198
3275
  }
3199
3276
  }
3277
+ /**
3278
+ * Accept an inbound call by creating the SDP answer.
3279
+ * Optionally override media options before the answer is generated.
3280
+ * Must be called after initialization for inbound (answer-type) connections.
3281
+ */
3282
+ async acceptInbound(mediaOverrides) {
3283
+ if (mediaOverrides) {
3284
+ const { audio, video, receiveAudio, receiveVideo } = mediaOverrides;
3285
+ this.options = {
3286
+ ...this.options,
3287
+ ...audio !== void 0 ? { audio } : {},
3288
+ ...video !== void 0 ? { video } : {},
3289
+ ...receiveAudio !== void 0 ? { receiveAudio } : {},
3290
+ ...receiveVideo !== void 0 ? { receiveVideo } : {}
3291
+ };
3292
+ this.transceiverController?.updateOptions({
3293
+ receiveAudio: this.receiveAudio,
3294
+ receiveVideo: this.receiveVideo
3295
+ });
3296
+ this.localStreamController.updateOptions({
3297
+ inputAudioDeviceConstraints: this.inputAudioDeviceConstraints,
3298
+ inputVideoDeviceConstraints: this.inputVideoDeviceConstraints
3299
+ });
3300
+ }
3301
+ await this.setupLocalTracks();
3302
+ const { answerOptions } = this;
3303
+ logger$11.debug("[RTCPeerConnectionController] Creating inbound answer with options:", answerOptions);
3304
+ await this.createAnswer(answerOptions);
3305
+ }
3200
3306
  async handleOfferReceived() {
3201
3307
  if (!this.sdpInit) throw new require_operators.DependencyError("SDP initialization parameters are not set");
3202
3308
  this._isNegotiating$.next(true);
@@ -3271,37 +3377,29 @@ var RTCPeerConnectionController = class extends Destroyable {
3271
3377
  }
3272
3378
  async setupLocalTracks() {
3273
3379
  logger$11.debug("[RTCPeerConnectionController] Setting up local tracks/transceivers.");
3274
- let { localStream } = this;
3275
- if (!localStream) try {
3276
- localStream = await this.localStreamController.buildLocalStream();
3277
- } catch (error) {
3278
- logger$11.error("[RTCPeerConnectionController] Error building local stream:", error);
3279
- this._errors$.next(error);
3280
- }
3281
- if (localStream) {
3282
- if (this.transceiverController?.useAddStream ?? false) {
3283
- logger$11.warn("[RTCPeerConnectionController] Using deprecated addStream API to add local stream.");
3284
- this.peerConnection?.addStream(localStream);
3285
- if (!this.isNegotiating) {
3286
- logger$11.debug("[RTCPeerConnectionController] Forcing negotiationneeded after local tracks setup.");
3287
- this.negotiationNeeded$.next();
3288
- }
3289
- return;
3380
+ const localStream = this.localStream ?? await this.localStreamController.buildLocalStream();
3381
+ if (this.transceiverController?.useAddStream ?? false) {
3382
+ logger$11.warn("[RTCPeerConnectionController] Using deprecated addStream API to add local stream.");
3383
+ this.peerConnection?.addStream(localStream);
3384
+ if (!this.isNegotiating) {
3385
+ logger$11.debug("[RTCPeerConnectionController] Forcing negotiationneeded after local tracks setup.");
3386
+ this.negotiationNeeded$.next();
3290
3387
  }
3291
- for (const kind of ["audio", "video"]) {
3292
- const tracks = (kind === "audio" ? localStream.getAudioTracks() : localStream.getVideoTracks()).map((track, index) => ({
3293
- index,
3294
- track
3295
- }));
3296
- for (const { index, track } of tracks) {
3297
- this.localStreamController.addTrackEndedListener(track);
3298
- if (this.transceiverController?.useAddTransceivers ?? false) {
3299
- const transceivers = (kind === "audio" ? this.transceiverController?.audioTransceivers : this.transceiverController?.videoTransceivers) ?? [];
3300
- await this.transceiverController?.setupTransceiverSender(track, localStream, transceivers[index]);
3301
- } else {
3302
- logger$11.debug(`[RTCPeerConnectionController] Using addTrack for local ${kind} track:`, track.id);
3303
- this.peerConnection?.addTrack(track, localStream);
3304
- }
3388
+ return;
3389
+ }
3390
+ for (const kind of ["audio", "video"]) {
3391
+ const tracks = (kind === "audio" ? localStream.getAudioTracks() : localStream.getVideoTracks()).map((track, index) => ({
3392
+ index,
3393
+ track
3394
+ }));
3395
+ for (const { index, track } of tracks) {
3396
+ this.localStreamController.addTrackEndedListener(track);
3397
+ if (this.transceiverController?.useAddTransceivers ?? false) {
3398
+ const transceivers = (kind === "audio" ? this.transceiverController?.audioTransceivers : this.transceiverController?.videoTransceivers) ?? [];
3399
+ await this.transceiverController?.setupTransceiverSender(track, localStream, transceivers[index]);
3400
+ } else {
3401
+ logger$11.debug(`[RTCPeerConnectionController] Using addTrack for local ${kind} track:`, track.id);
3402
+ this.peerConnection?.addTrack(track, localStream);
3305
3403
  }
3306
3404
  }
3307
3405
  }
@@ -3318,7 +3416,12 @@ var RTCPeerConnectionController = class extends Destroyable {
3318
3416
  if (!this.peerConnection) throw new require_operators.DependencyError("RTCPeerConnection is not initialized");
3319
3417
  this.peerConnection.ontrack = (event) => {
3320
3418
  logger$11.debug("[RTCPeerConnectionController] Remote track received:", event.track.kind);
3321
- this._remoteStream$.next(event.streams[0]);
3419
+ if (event.streams[0]) this._remoteStream$.next(event.streams[0]);
3420
+ else {
3421
+ const existingTracks = this._remoteStream$.value?.getTracks() ?? [];
3422
+ const newStream = new MediaStream([...existingTracks, event.track]);
3423
+ this._remoteStream$.next(newStream);
3424
+ }
3322
3425
  };
3323
3426
  await this.transceiverController?.setupRemoteTransceivers(this.type);
3324
3427
  }
@@ -3417,7 +3520,7 @@ var RTCPeerConnectionController = class extends Destroyable {
3417
3520
  });
3418
3521
  }
3419
3522
  get mediaDirections() {
3420
- return this.transceiverController?.getMediaDirections() ?? {
3523
+ return this.transceiverController?.getMediaDirections() ?? this._remoteOfferMediaDirections ?? {
3421
3524
  audio: "inactive",
3422
3525
  video: "inactive"
3423
3526
  };
@@ -3576,6 +3679,7 @@ var WebRTCVertoManager = class extends VertoManager {
3576
3679
  });
3577
3680
  this.subscribeTo(this.vertoAnswer$, (event) => {
3578
3681
  logger$10.debug("[WebRTCManager] Received Verto answer event:", event);
3682
+ this._signalingStatus$.next("connecting");
3579
3683
  const { sdp, callID } = event;
3580
3684
  this._rtcPeerConnectionsMap.get(callID)?.updateAnswerStatus({
3581
3685
  status: "received",
@@ -3647,13 +3751,13 @@ var WebRTCVertoManager = class extends VertoManager {
3647
3751
  if (response.error) {
3648
3752
  const error = new require_operators.JSONRPCError(response.error.code, response.error.message, response.error.data);
3649
3753
  this.onError?.(error);
3650
- throw error;
3754
+ return response;
3651
3755
  }
3652
3756
  const innerResult = require_operators.getValueFrom(response, "result.result");
3653
3757
  if (innerResult?.error) {
3654
3758
  const error = new require_operators.JSONRPCError(innerResult.error.code, innerResult.error.message, innerResult.error.data);
3655
3759
  this.onError?.(error);
3656
- throw error;
3760
+ return response;
3657
3761
  }
3658
3762
  return response;
3659
3763
  }
@@ -3673,7 +3777,7 @@ var WebRTCVertoManager = class extends VertoManager {
3673
3777
  }
3674
3778
  } catch (error) {
3675
3779
  logger$10.error(`[WebRTCManager] Error sending Verto ${vertoMethod}:`, error);
3676
- throw error;
3780
+ this.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
3677
3781
  }
3678
3782
  }
3679
3783
  async processModifyResponse(response, rtcPeerConnController) {
@@ -3687,12 +3791,14 @@ var WebRTCVertoManager = class extends VertoManager {
3687
3791
  });
3688
3792
  } catch (error) {
3689
3793
  logger$10.warn("[WebRTCManager] Error processing modify response:", error);
3690
- this.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
3794
+ const modifyError = error instanceof Error ? error : new Error(String(error), { cause: error });
3795
+ this.onError?.(modifyError);
3691
3796
  }
3692
3797
  }
3693
3798
  }
3694
3799
  processInviteResponse(response, rtcPeerConnController) {
3695
3800
  if (!response.error && require_operators.getValueFrom(response, "result.result.result.message") === "CALL CREATED") {
3801
+ this._signalingStatus$.next("trying");
3696
3802
  this._nodeId$.next(require_operators.getValueFrom(response, "result.node_id") ?? null);
3697
3803
  const memberId = require_operators.getValueFrom(response, "result.result.result.memberID") ?? null;
3698
3804
  const callId = require_operators.getValueFrom(response, "result.result.result.callID") ?? null;
@@ -3747,6 +3853,36 @@ var WebRTCVertoManager = class extends VertoManager {
3747
3853
  this.subscribeTo(rtcPeerConnController.errors$, (error) => {
3748
3854
  this.onError?.(error);
3749
3855
  });
3856
+ if (options.initOffer) this.handleInboundAnswer(rtcPeerConnController);
3857
+ }
3858
+ async handleInboundAnswer(rtcPeerConnController) {
3859
+ logger$10.debug("[WebRTCManager] Waiting for inbound call to be accepted or rejected");
3860
+ const vertoByeOrAccepted = await (0, rxjs.firstValueFrom)((0, rxjs.race)(this.vertoBye$, this.webRtcCallSession.answered$).pipe((0, rxjs.takeUntil)(this.destroyed$))).catch(() => null);
3861
+ if (vertoByeOrAccepted === null) {
3862
+ logger$10.debug("[WebRTCManager] Inbound answer handler aborted (destroyed).");
3863
+ return;
3864
+ }
3865
+ if (isVertoByeMessage(vertoByeOrAccepted)) {
3866
+ logger$10.info("[WebRTCManager] Inbound call ended by remote before answer.");
3867
+ this.callSession?.destroy();
3868
+ } else if (!vertoByeOrAccepted) {
3869
+ logger$10.info("[WebRTCManager] Inbound call rejected by user.");
3870
+ try {
3871
+ await this.bye("USER_BUSY");
3872
+ } finally {
3873
+ this._signalingStatus$.next("disconnected");
3874
+ this.callSession?.destroy();
3875
+ }
3876
+ } else {
3877
+ logger$10.debug("[WebRTCManager] Inbound call accepted, creating SDP answer");
3878
+ const answerOptions = this.webRtcCallSession.answerMediaOptions;
3879
+ try {
3880
+ await rtcPeerConnController.acceptInbound(answerOptions);
3881
+ } catch (error) {
3882
+ logger$10.error("[WebRTCManager] Error creating inbound answer:", error);
3883
+ this.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
3884
+ }
3885
+ }
3750
3886
  }
3751
3887
  setupVertoAttachHandler() {
3752
3888
  this.subscribeTo(this.vertoAttach$, async (vertoAttach) => {
@@ -3764,7 +3900,7 @@ var WebRTCVertoManager = class extends VertoManager {
3764
3900
  });
3765
3901
  }
3766
3902
  initObservables(rtcPeerConnController) {
3767
- this.mediaDirections$ = rtcPeerConnController.connectionState$.pipe((0, rxjs.filter)((state) => state === "connected"), (0, rxjs.takeUntil)(this.destroyed$), (0, rxjs.map)(() => rtcPeerConnController.mediaDirections));
3903
+ this.mediaDirections$ = rtcPeerConnController.connectionState$.pipe((0, rxjs.filter)((state) => state === "connected"), (0, rxjs.map)(() => rtcPeerConnController.mediaDirections), (0, rxjs.startWith)(rtcPeerConnController.mediaDirections), (0, rxjs.takeUntil)(this.destroyed$));
3768
3904
  this.localStream$ = rtcPeerConnController.localStream$.pipe(require_operators.filterNull(), (0, rxjs.takeUntil)(this.destroyed$));
3769
3905
  this.remoteStream$ = rtcPeerConnController.remoteStream$.pipe(require_operators.filterNull(), (0, rxjs.takeUntil)(this.destroyed$));
3770
3906
  }
@@ -3780,7 +3916,6 @@ var WebRTCVertoManager = class extends VertoManager {
3780
3916
  });
3781
3917
  this.sendLocalDescriptionOnceAccepted(vertoMessageRequest, rtcPeerConnController);
3782
3918
  } else if (initial) {
3783
- this._signalingStatus$.next("trying");
3784
3919
  const vertoMessageRequest = VertoInvite({
3785
3920
  dialogParams,
3786
3921
  sdp
@@ -4086,8 +4221,8 @@ var WebRTCCall = class extends Destroyable {
4086
4221
  this.clientSession = clientSession;
4087
4222
  this.options = options;
4088
4223
  this.address = address;
4089
- this.participantsMap = /* @__PURE__ */ new Map();
4090
- this._errors$ = this.createSubject();
4224
+ this._errors$ = this.createReplaySubject(1);
4225
+ this._lastMergedStatus = "new";
4091
4226
  this._answered$ = this.createReplaySubject();
4092
4227
  this._holdState = false;
4093
4228
  this._userVariables$ = this.createBehaviorSubject({ ...PreferencesContainer.instance.userVariables });
@@ -4108,8 +4243,10 @@ var WebRTCCall = class extends Destroyable {
4108
4243
  const managers = initialization.initializeManagers(this);
4109
4244
  this.vertoManager = managers.vertoManager;
4110
4245
  this.callEventsManager = managers.callEventsManager;
4111
- if (options.initOffer) this._status$ = this.createBehaviorSubject("ringing");
4112
- else this._status$ = this.createBehaviorSubject("new");
4246
+ if (options.initOffer) {
4247
+ this._status$ = this.createBehaviorSubject("ringing");
4248
+ this._lastMergedStatus = "ringing";
4249
+ } else this._status$ = this.createBehaviorSubject("new");
4113
4250
  const { deviceController } = initialization;
4114
4251
  this.participantFactory = new ParticipantFactory(this.executeMethod.bind(this), this.vertoManager, deviceController);
4115
4252
  }
@@ -4117,9 +4254,17 @@ var WebRTCCall = class extends Destroyable {
4117
4254
  get errors$() {
4118
4255
  return this._errors$.asObservable();
4119
4256
  }
4120
- /** @internal Push an error to the call's error stream. */
4121
- emitError(error) {
4122
- this._errors$.next(error);
4257
+ /**
4258
+ * @internal Push an error to the call's error stream.
4259
+ * Fatal errors automatically transition the call to `'failed'` and destroy it.
4260
+ */
4261
+ emitError(callError) {
4262
+ if (this._status$.value === "destroyed" || this._status$.value === "failed") return;
4263
+ this._errors$.next(callError);
4264
+ if (callError.fatal) {
4265
+ this._status$.next("failed");
4266
+ this.destroy();
4267
+ }
4123
4268
  }
4124
4269
  /** Whether this call is `'inbound'` or `'outbound'`. */
4125
4270
  get direction() {
@@ -4159,7 +4304,7 @@ var WebRTCCall = class extends Destroyable {
4159
4304
  }
4160
4305
  /** Current snapshot of all participants in the call. */
4161
4306
  get participants() {
4162
- return Array.from(this.participantsMap.values());
4307
+ return this.callEventsManager.participants;
4163
4308
  }
4164
4309
  /** The local participant, or `null` if not yet joined. */
4165
4310
  get self() {
@@ -4168,7 +4313,6 @@ var WebRTCCall = class extends Destroyable {
4168
4313
  async toggleLock() {
4169
4314
  const method = this.locked ? "call.unlock" : "call.lock";
4170
4315
  await this.executeMethod(this.selfId ?? "", method, {});
4171
- throw new require_operators.UnimplementedError();
4172
4316
  }
4173
4317
  async toggleHold() {
4174
4318
  if (this._holdState) await this.vertoManager.unhold();
@@ -4210,25 +4354,31 @@ var WebRTCCall = class extends Destroyable {
4210
4354
  }
4211
4355
  }
4212
4356
  buildMethodParams(target, args) {
4213
- const reference = {
4214
- node_id: this.nodeId,
4215
- call_id: this.id
4357
+ const self = {
4358
+ node_id: this.nodeId ?? "",
4359
+ call_id: this.id,
4360
+ member_id: this.vertoManager.selfId ?? ""
4361
+ };
4362
+ if (typeof target === "object") return {
4363
+ ...args,
4364
+ self,
4365
+ targets: [target]
4216
4366
  };
4217
4367
  return {
4218
4368
  ...args,
4219
- self: {
4220
- ...reference,
4221
- member_id: this.vertoManager.selfId
4222
- },
4369
+ self,
4223
4370
  target: {
4224
- ...reference,
4371
+ node_id: this.nodeId ?? "",
4372
+ call_id: this.id,
4225
4373
  member_id: target
4226
4374
  }
4227
4375
  };
4228
4376
  }
4229
4377
  /** Observable of the current call status (e.g. `'ringing'`, `'connected'`). */
4230
4378
  get status$() {
4231
- return this.cachedObservable("status$", () => (0, rxjs.merge)(this._status$.asObservable(), this.vertoManager.signalingStatus$));
4379
+ return this.cachedObservable("status$", () => (0, rxjs.merge)(this._status$.asObservable(), this.vertoManager.signalingStatus$).pipe((0, rxjs.distinctUntilChanged)(), (0, rxjs.tap)((status) => {
4380
+ this._lastMergedStatus = status;
4381
+ })));
4232
4382
  }
4233
4383
  /** Observable of the participants list, emits on join/leave/update. */
4234
4384
  get participants$() {
@@ -4268,7 +4418,7 @@ var WebRTCCall = class extends Destroyable {
4268
4418
  }
4269
4419
  /** Current call status. */
4270
4420
  get status() {
4271
- return this._status$.value;
4421
+ return this._lastMergedStatus;
4272
4422
  }
4273
4423
  /** Whether the call is currently being recorded. */
4274
4424
  get recording() {
@@ -4445,10 +4595,15 @@ var WebRTCCall = class extends Destroyable {
4445
4595
  async sendDigits(dtmf) {
4446
4596
  return this.vertoManager.sendDigits(dtmf);
4447
4597
  }
4448
- /** Accepts an inbound call. */
4449
- answer() {
4598
+ /** Accepts an inbound call, optionally overriding media options for the answer. */
4599
+ answer(options) {
4600
+ this._answerMediaOptions = options;
4450
4601
  this._answered$.next(true);
4451
4602
  }
4603
+ /** Media options provided when answering. Used internally by the VertoManager. */
4604
+ get answerMediaOptions() {
4605
+ return this._answerMediaOptions;
4606
+ }
4452
4607
  /** Rejects an inbound call. */
4453
4608
  reject() {
4454
4609
  this._answered$.next(false);
@@ -4476,10 +4631,10 @@ var WebRTCCall = class extends Destroyable {
4476
4631
  }
4477
4632
  /** Destroys the call, releasing all resources and subscriptions. */
4478
4633
  destroy() {
4634
+ if (this._status$.value === "destroyed") return;
4479
4635
  this._status$.next("destroyed");
4480
4636
  this.vertoManager.destroy();
4481
4637
  this.callEventsManager.destroy();
4482
- this.participantsMap.clear();
4483
4638
  super.destroy();
4484
4639
  }
4485
4640
  };
@@ -4487,6 +4642,23 @@ var WebRTCCall = class extends Destroyable {
4487
4642
  //#endregion
4488
4643
  //#region src/managers/CallFactory.ts
4489
4644
  /**
4645
+ * Infers the semantic error category from a raw Error thrown by VertoManager
4646
+ * or an RTCPeerConnection layer.
4647
+ */
4648
+ function inferCallErrorKind(error) {
4649
+ if (error instanceof require_operators.RPCTimeoutError) return "timeout";
4650
+ if (error instanceof require_operators.JSONRPCError) return "signaling";
4651
+ if (error instanceof require_operators.MediaTrackError) return "media";
4652
+ if (error instanceof require_operators.WebSocketConnectionError || error instanceof require_operators.TransportConnectionError) return "network";
4653
+ return "internal";
4654
+ }
4655
+ /** Determines whether an error should be fatal (destroy the call). */
4656
+ function isFatalError(error) {
4657
+ if (error instanceof require_operators.VertoPongError) return false;
4658
+ if (error instanceof require_operators.MediaTrackError) return false;
4659
+ return true;
4660
+ }
4661
+ /**
4490
4662
  * Factory for creating WebRTCCall instances with proper manager wiring.
4491
4663
  * Eliminates circular dependencies by centralizing Call and Manager creation.
4492
4664
  */
@@ -4507,7 +4679,13 @@ var CallFactory = class {
4507
4679
  vertoManager: new WebRTCVertoManager(callInstance, this.attachManager, this.deviceController, this.webRTCApiProvider, {
4508
4680
  nodeId: options.nodeId,
4509
4681
  onError: (error) => {
4510
- callInstance.emitError(error);
4682
+ const callError = {
4683
+ kind: inferCallErrorKind(error),
4684
+ fatal: isFatalError(error),
4685
+ error,
4686
+ callId: callInstance.id
4687
+ };
4688
+ callInstance.emitError(callError);
4511
4689
  }
4512
4690
  }),
4513
4691
  callEventsManager: new CallEventsManager(callInstance)
@@ -4884,9 +5062,15 @@ var PendingRPC = class PendingRPC {
4884
5062
  return () => signal.removeEventListener("abort", abortHandler);
4885
5063
  }) : rxjs.NEVER).subscribe({
4886
5064
  next: (response) => {
4887
- logger$7.debug(`[PendingRPC(${this.id}) request:${request.id}] Resolving promise with response:`, response);
4888
5065
  isSettled = true;
4889
- resolve(response);
5066
+ if (response.error) {
5067
+ const rpcError = new require_operators.RPCError(response.error.code, request.id, response.error.message, response.error.data);
5068
+ logger$7.debug(`[PendingRPC(${this.id}) request:${request.id}] Rejecting promise with RPC error:`, rpcError);
5069
+ reject(rpcError);
5070
+ } else {
5071
+ logger$7.debug(`[PendingRPC(${this.id}) request:${request.id}] Resolving promise with response:`, response);
5072
+ resolve(response);
5073
+ }
4890
5074
  subscription.unsubscribe();
4891
5075
  },
4892
5076
  error: (error) => {
@@ -4942,7 +5126,7 @@ var ClientSessionManager = class extends Destroyable {
4942
5126
  revision: 0
4943
5127
  };
4944
5128
  this._authorization$ = this.createBehaviorSubject(void 0);
4945
- this._errors$ = this.createSubject();
5129
+ this._errors$ = this.createReplaySubject(1);
4946
5130
  this._authenticated$ = this.createBehaviorSubject(false);
4947
5131
  this._subscriberInfo$ = this.createBehaviorSubject(null);
4948
5132
  this._calls$ = this.createBehaviorSubject({});
@@ -5212,12 +5396,13 @@ var ClientSessionManager = class extends Destroyable {
5212
5396
  }
5213
5397
  async createOutboundCall(destination, options = {}) {
5214
5398
  const destinationURI = destination instanceof Address ? destination.defaultChannel : destination;
5399
+ let callSession;
5215
5400
  try {
5216
- const callSession = await this.createCall({
5401
+ callSession = await this.createCall({
5217
5402
  to: destinationURI,
5218
5403
  ...options
5219
5404
  });
5220
- await (0, rxjs.firstValueFrom)(callSession.selfId$.pipe((0, rxjs.filter)((id) => Boolean(id)), (0, rxjs.take)(1), (0, rxjs.timeout)(this.callCreateTimeout)));
5405
+ await (0, rxjs.firstValueFrom)((0, rxjs.race)(callSession.selfId$.pipe((0, rxjs.filter)((id) => Boolean(id)), (0, rxjs.take)(1), (0, rxjs.timeout)(this.callCreateTimeout)), callSession.errors$.pipe((0, rxjs.take)(1), (0, rxjs.switchMap)((callError) => (0, rxjs.throwError)(() => callError.error)))));
5221
5406
  this._calls$.next({
5222
5407
  [`${callSession.id}`]: callSession,
5223
5408
  ...this._calls$.value
@@ -5225,7 +5410,8 @@ var ClientSessionManager = class extends Destroyable {
5225
5410
  return callSession;
5226
5411
  } catch (error) {
5227
5412
  logger$6.error("[Session] Error creating outbound call:", error);
5228
- const callError = new require_operators.CallCreateError("Call create timeout", error);
5413
+ callSession?.destroy();
5414
+ const callError = new require_operators.CallCreateError(error instanceof rxjs.TimeoutError ? "Call create timeout" : "Call creation failed", error, "outbound");
5229
5415
  this._errors$.next(callError);
5230
5416
  throw callError;
5231
5417
  }
@@ -5251,7 +5437,7 @@ var ClientSessionManager = class extends Destroyable {
5251
5437
  return callSession;
5252
5438
  } catch (error) {
5253
5439
  logger$6.error("[Session] Error creating call session:", error);
5254
- throw new require_operators.CallCreateError("Call create error", error);
5440
+ throw new require_operators.CallCreateError("Call create error", error, options.initOffer ? "inbound" : "outbound");
5255
5441
  }
5256
5442
  }
5257
5443
  destroy() {
@@ -5497,7 +5683,7 @@ var WebSocketController = class WebSocketController extends Destroyable {
5497
5683
  this.shouldReconnect = false;
5498
5684
  this._status$ = this.createBehaviorSubject("disconnected");
5499
5685
  this._incomingMessages$ = this.createSubject();
5500
- this._errors$ = this.createSubject();
5686
+ this._errors$ = this.createReplaySubject(1);
5501
5687
  this.reconnectDelayMin = options.reconnectDelayMin ?? WebSocketController.DEFAULT_RECONNECT_DELAY_MIN_MS;
5502
5688
  this.reconnectDelayMax = options.reconnectDelayMax ?? WebSocketController.DEFAULT_RECONNECT_DELAY_MAX_MS;
5503
5689
  this.connectionTimeout = options.connectionTimeout ?? WebSocketController.DEFAULT_CONNECTION_TIMEOUT_MS;
@@ -5822,7 +6008,8 @@ const buildOptionsFromDestination = (destination) => {
5822
6008
  const channel = new URLSearchParams(queryString).get("channel");
5823
6009
  if (channel === "video") return {
5824
6010
  audio: true,
5825
- video: true
6011
+ video: true,
6012
+ receiveVideo: true
5826
6013
  };
5827
6014
  else if (channel === "audio") return {
5828
6015
  audio: true,
@@ -5852,7 +6039,7 @@ var SignalWire = class extends Destroyable {
5852
6039
  this._directory$ = this.createBehaviorSubject(void 0);
5853
6040
  this._isConnected$ = this.createBehaviorSubject(false);
5854
6041
  this._isRegistered$ = this.createBehaviorSubject(false);
5855
- this._errors$ = this.createSubject();
6042
+ this._errors$ = this.createReplaySubject(1);
5856
6043
  this._options = {};
5857
6044
  this._deps = new DependencyContainer();
5858
6045
  this._options = {
@@ -5929,7 +6116,12 @@ var SignalWire = class extends Destroyable {
5929
6116
  this._subscriber$.next(new Subscriber(this._deps.http));
5930
6117
  if (!this._options.skipConnection) await this.connect();
5931
6118
  if (!this._options.reconnectAttachedCalls && this._attachManager) await this._attachManager.flush();
5932
- if (!this._options.skipRegister) this.register();
6119
+ if (!this._options.skipRegister) try {
6120
+ await this.register();
6121
+ } catch (error) {
6122
+ logger$1.error("[SignalWire] Registration failed:", error);
6123
+ this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
6124
+ }
5933
6125
  this.handleAttachments();
5934
6126
  }
5935
6127
  async handleAttachments() {
@@ -6092,8 +6284,22 @@ var SignalWire = class extends Destroyable {
6092
6284
  }));
6093
6285
  this._isRegistered$.next(true);
6094
6286
  } catch (error) {
6095
- logger$1.error("[SignalWire] Failed to register subscriber:", error);
6287
+ logger$1.debug("[SignalWire] Failed to register subscriber, trying reauthentication...");
6288
+ if (this._deps.credential.token) this._clientSession.reauthenticate(this._deps.credential.token).then(async () => {
6289
+ logger$1.debug("[SignalWire] Reauthentication successful, retrying register()");
6290
+ await this._transport.execute(RPCExecute({
6291
+ method: "subscriber.online",
6292
+ params: {}
6293
+ }));
6294
+ this._isRegistered$.next(true);
6295
+ }).catch((reauthError) => {
6296
+ logger$1.error("[SignalWire] Reauthentication failed during register():", reauthError);
6297
+ const registerError = new require_operators.InvalidCredentialsError("Failed to register subscriber, and reauthentication attempt also failed. Please check your credentials.", { cause: reauthError instanceof Error ? reauthError : new Error(String(reauthError), { cause: reauthError }) });
6298
+ this._errors$.next(registerError);
6299
+ throw registerError;
6300
+ });
6096
6301
  this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
6302
+ throw error;
6097
6303
  }
6098
6304
  }
6099
6305
  /** Unregisters the subscriber, going offline for inbound calls. */